Decision records
ADR 0126 — Unicode lexical scoring, prompt overrides, and time zones on schedules (R19)
- Status: Accepted
- Date: 2026-10-10
- Closes: R19 (PR #554, shipped without a record; this ADR is the retroactive governance entry)
- Builds on: ADR 0123 (
planGoal, plan-graph prompts), ADR 0085 (taint, the untrusted-content note), ADR 0125 (the tool-search tokenizer was already Unicode-aware), T1.2 (schedule$and the cronTimerService). - Placement: 1P, additive.
KERNEL_ABI,EVENT_SCHEMAand every SPI are unchanged; no syscall, no event kind. One optional field,ScheduleDefinition.timeZone, is added to an authoring type.
Context
Three English-only assumptions sat in 1P code. The keyword scorers behind CapabilityCatalog.find and SkillRegistry.find
split on ASCII \W+, so “Rechnung prüfen” lost its non-ASCII letters and “請求書” produced no terms. The fixed English prompt
text of PlannerRuntime, IntentRuntime and planGoal could not be replaced. And schedule$ read its five cron fields on
the UTC clock only, so “09:00 on weekdays” in Berlin drifted by an hour twice a year.
Decision
D1 — Unicode tokenizer (lexicalTerms). The two scorers tokenize with NFKC normalization, then runs of letters, numbers
and marks in any script. Scripts written without spaces (Han, Hiragana, Katakana, Thai, Lao, Khmer, Myanmar) become overlapping
character bigrams; there is no dictionary segmenter and no dependency. The memory and tool-search tokenizers were already
Unicode-aware and are deliberately untouched (memory’s feeds persisted FTS indexes).
D2 — Replay argument. For ASCII input the new tokenizer is byte-identical to the old one (underscore stays a word
character; the length-over-2 filter and the term cap are unchanged), so no existing selection changes. Where a result could
change (a non-ASCII query), nothing replays the scorer: agentLoop$ records its skill selection as a decision (the #skills
decision), so a replay serves the recorded selection and runs no scoring. A tokenizer change therefore cannot diverge a recorded
execution.
D3 — Prompt overrides, English by default. new PlannerRuntime({ prompts: { system, user } }),
new IntentRuntime({ prompts: { system, user } }), and planGoal(ctx, { prompts }) / makeReviser({ prompts }) with
PlannerPrompts (system, maxSteps, capabilities, goal, rejected). Absent, the English text is used unchanged. A
replacement system must still ask for the reply shape the parser reads (a JSON plan, or a bare capability id). The
untrusted-content note is ALWAYS appended to the plan-graph system prompt, whatever the override: it is a security boundary
(ADR 0085), not wording, and a locale must not be able to drop it.
D4 — Time zones on schedules. schedule$({ timeZone }) (also ScheduleDefinition.timeZone and
sentinelPreset({ timeZone })) takes an IANA name. The five cron fields are read on that zone’s wall clock, computed with
Intl.DateTimeFormat. Default is UTC, so every existing schedule is unchanged. An unknown zone throws when schedule$ is
called, and parseCron / TimerService re-check it. The zone is data on the schedule, not the node’s, so every node computes
the same occurrences. The occurrence id stays the UTC epoch ms, so an occurrence’s execution id (scheduleId@occurrence)
is unambiguous across the DST fold and its start stays idempotent.
D5 — The DST rule. A fixed wall time runs once per day it matches:
- Skipped wall time (spring forward): it runs once, shifted forward by the gap (
30 2 * * *inAmerica/New_Yorkon 2026-03-08 runs at 03:30 EDT). - Repeated wall time (fall back): it runs once, at its FIRST occurrence (
30 1 * * *on 2026-11-01 runs at 01:30 EDT and not again at 01:30 EST).
A consequence is that an hourly or sub-hourly expression (0 * * * *, */15 * * * *) in a DST zone silently loses the
repeated hour’s second pass. Fixed-interval schedules belong in UTC; schedule$ warns once when it sees the combination.
Consequences
- Non-English queries now match; ASCII behaviour and recorded executions are unchanged. Bigrams over-match short CJK queries
slightly; ranking beyond term overlap stays the caller’s concern (the
ToolRanker/ embedding path). - A locale can reword the planner and intent prompts, but cannot remove the untrusted-content note.
- Schedules in a DST zone are correct for daily and fixed-time expressions, and have one documented, signalled limit (repeated hour, sub-hourly).
- No kernel, ABI, schema or SPI change; the registry entry is
unicode-locale(L2).
Source: docs/adr/0126-unicode-lexical-scoring-prompt-overrides-and-schedule-time-zones.md in the loom repository.