Repository navigation
refactor: one home per rule — the day's one read model, one Decode Start, one Launch Mode, and nine more deepenings - #638
Merged
Merged
Conversation
"What kind of process is this?" was answered by four predicates in ten files (isRunningTests, usesScratchData, isHarnessLaunch and raw CommandLine.arguments checks), and the 17 harness flags were written twice: in TesseractApp.harnessFlags and again in the init's dispatch chain with their labels and log subdirectories. setup() skipped the owner's background work for only 3 of the 17 harnesses (the DFlash2 guard landed after a Companion moment loaded a second model beside a ruler run and deadlocked MLX), and harness launches still opened the main window. The Launch Mode (Core/LaunchMode.swift) decides once, from the arguments and environment, whether the process is the owner's launch, a scratch launch, the test host or a harness, and answers what it may do: keep off the owner's data, run the owner's background work, which window to open at launch, post notifications, enforce a single instance, migrate the owner's data, play sounds. Callers ask an answer, never the arguments. One harness table (App/Harness.swift) holds each harness's flag, label, log subdirectory, failure exit code and run; its order is precedence, so --paro-parity-bench still wins over --benchmark, which still exits 0 on failure. Shared code (StorageEnvironment) asks the same decision from the environment alone, which decides first. Behaviour changes, pinned in LaunchModeTests: - every harness skips setup()'s background work (was 3 of 17); - a harness opens no window and restores none, so it installs no menu bar item and never opens the Welcome window on an un-onboarded Mac; - a harness no longer runs the Companion's retired-data clean at launch. Docs: ADR-0095, an ADR-0073 amendment, CONTEXT (Launch Mode, Harness), ARCHITECTURE and docs/testing.md.
One step's life was spread over the plan and six string-keyed collections in DayState (cuedSteps, startedSteps, smallStarts, startedMinutes, putOff, cutShort) plus cueOnPanel, keyed "id@epoch" for a start's marks, "id@epoch+minutes" for an end's check-in and by task id for what was put off or cut. Every new per-step fact had to be threaded through the decoder, relaunched, rolledOver, follow, the Companion going off and a card taking the panel; the repeating must-do fix rewrote the maps by string-prefix surgery; Today rebuilt the engine's key from a Timeline row; tests filled three or four maps to set up a started step. The plan now holds Steps (Engine/Step.swift): a task's slot and its life, each part bound to the start, the end or the task, the binding read off the old keys. Moving is one operation (Step.reslot, move, resize) that resets what the old start or end held and keeps what the task carries; the cue on the panel is a CueOnPanel (task and phase), and taking it down unanswered is DayState.uncuePanel. A day-state.json saved with the maps is read once into its steps; only steps are written. Behaviour change (ADR-0091), pinned by aStepTakenOffThePlanAndPutBackStartsAfresh: a task taken off today's plan and put back is a fresh step, its put-off count begun again (the map kept it by task for the rest of the day). The marks of a slot a step has left are no longer kept; only the cue reaction's secondsToReact read them, so a re-plan that moves a step while its cue is still on the panel leaves that one reaction without a time. Trace vocabulary and values are otherwise unchanged.
Each hotkey id ("tts", "agent", "capture", "appshot", ...) was written at
registration in the container and again at rebind in App Bindings, and
HotkeyManager.updateRegisteredHotkey returned silently on an unknown id,
so a typo turned live rebinding off while the five rebind tests (which
only recorded effect calls) still passed. Six updateXHotkey effects were
one-line pass-throughs, the Hotkeys pane restated three defaults the
Settings Catalogue owns, and the ADR-0082 interrupt rule lived untested
in two container closures.
The Hotkey Table (App/HotkeyTable.swift) has one row per global hotkey:
the setting that holds its combo, its default read from the catalogue,
its label and help, and what its press, release, spoiled press and ⇧ tap
do, by the name of an action the container supplies. HotkeyTable binds
every row at launch through the registrar seam (HotkeyManager in the
app, an in-memory peer in the tests) and re-binds a row when its setting
changes to a different combo. The pane lists the same rows. The interrupt
keys are marked on their rows and one named action applies the rule.
App Bindings loses the hotkey subscriptions and currentDictationHotkey;
HotkeyManager loses registerHotkey, updateRegisteredHotkey,
unregisterHotkey (already dead) and its id constants. A press now reads
whether a voice session is active without building one (and the chat
session with it).
Behaviour change, pinned in HotkeyTableTests: every row of the Hotkeys
pane offers "Reset to <default>" once its combo differs from the
catalogue's default; before, only Fix Last Take, Capture to Reminders and
Appshot did.
Tests: HotkeyTableTests (every row bound from its setting, each key does
what its row says, a changed setting moves its key, an unchanged combo is
never re-bound, the interrupt keys in a voice session, defaults from the
catalogue) replace the five AppBindingsTests rebind tests.
Docs: CONTEXT (Hotkey Table; App Bindings loses hotkey rebinding),
ARCHITECTURE, docs/testing.md, an ADR-0082 amendment.
…ry generation path Six decode starts each decided where the KV Scheme converts, by four mechanisms, and each handed the decoded array to its owner, badged a speculative arm and started the token loop by hand; 27e4951 was a start that forgot to convert. The Decode Start (ADR-0093, the module ADR-0087 decision 8 names) is now the one place: a path names its Decode Handover (standard, speculative, preparing, or the Prefill Strategy route), and the handover fixes the Model Session verb, where the scheme converts and which part of the KV plan each verb still applies. It reads the converted array back, gives it to a leased leaf's cache or a new owner, emits the badge right before prefillFinished (where every speculative path emitted it) and starts the loop through one iterator enum, absorbing KeyedDecodeIterator and SpeculativeDecodeIterator.startGeneration. No verb is added (ADR-0016) and engagement stays in Speculation.plan (ADR-0079). RequestFacts.decodeParameters goes, the scheme string is read back in one place (KVScheme(parameters)), and the logit seam loses its pathQuantizesKVUpFront flag: its one carve-out lives in PrefillStrategy, the only place it applies. Behaviour is unchanged: every path keeps its conversion point, error scopes, progress-event order and loop-start position. The loaded-model gates on qwen3.5-4b-paro, qwen3.8-27b with DFlash2, and qwen3.8-27b under turbo8v4 match main check for check and acceptance line for acceptance line, hybrid-cache-correctness matches bitwise, and the masked diagnostics differ only in the Speculative Canonical Prefill's scheduling race. DecodeStartTests is one table of handover x scheme x penalties over the toy Model Session; it replaces the per-path wiring tests.
The Agenda's snapshot and the day's state were projected into DayFacts three ways (the engine, a SwiftUI body on Today, the Jarvis Panel from a card's placements), and every reader rebuilt the facts and the timeline and copied bits of state and settings. On top of them "the step under way" had four rules that disagreed: the engine's focus and the menu-bar clock said a step the owner started, the Now Card showed any event first, the Day Line tinted every row spanning now, so a step started inside the owner's own block was the step for the engine and the block on the Now Card. Free and busy time was computed three ways, "a time to leave still applies" was written eight times, and the evening window, the Area name and Waiting on You twice each. Timeline(snapshot:state:) (Features/Companion/Day/, moved out of Today/ with DayFacts, whose init(snapshot:state:) is now the one projection) answers what every surface asks: the Day Line and tomorrow, the step under way (UnderWay: a started step, then a meeting, then a block, then a task whose slot is now), the next step, when to leave, free time and the Inbox's offers, Waiting on You, the menu bar's clock. Today reads it in one call (CompanionRuntime.timeline(at:)), NowCardBuilder decides on it, the panel lists a plan's steps from it, the engine reads its focus, next step and tomorrow from it. DayFacts+Time classifies time once (a meeting: other people or a place; a block of the owner's own; a time to leave that stands while its event does), and the Day Line's free time, a step's boundaries and the card parser's placement check are named compositions of it, each keeping its own grid and strictness. The evening window (DaySettings.isEvening), the Area name and Waiting on You (WaitingOnYou) have one home each. Behaviour changes (ADR-0091), each pinned by a test: - A step the owner started inside a block of their own is the step on the Now Card, as it was for the engine and the menu bar (aStepStartedInsideABlockReadsTheSameEverywhere). - The Day Line tints one row, the step under way (it tinted every row spanning now, a tomorrow row in the small hours too). - A meeting under way inside a block is the step on the Now Card, whichever started first (aMeetingInsideABlockIsTheStepTheDayIsOn). - The Jarvis Panel lists a Morning Plan's steps from the day's plan as it stands, not the card's copy; a step already started is not offered to start again (thePlansStepsAreTheTasksPlannedAmongTheEvents). Tests: TimelineBuilderTests became TimelineTests over a DayFixture snapshot and state, with the step under way's decision table and the one test reading a started step inside a block on the Now Card, the Day Line, the menu bar and the engine; NowCardTests builds every card from a snapshot and state; the freeGaps and InboxSlot.suggest tests on the old seams are gone, their rules tested through the Timeline.
… session The Voice Capture Session was deep, but every dependency it took was optional with a nil default, so each surface restated the whole recipe: the Onboarding Tour built its Voice Input without the Learned Words, the Capture Dump or the Proofread Pass (ADR-0085 §2 says every caller gains them); the proofread closure and the language read were hand-built three times; the Live Preview re-composed the cleanup → Learned Words pipeline in DictationCoordinator and reached past the session to the mic and the recognizer, so ADR-0086 §3 was enforced by two copies; "Insert anyway" re-implemented history, catch counting and the paste; and "is a take open" was a hand list of surfaces (#636 had to add Today's). Voice Capture (Core/VoiceCaptureSession.swift) is built once in the container from the shared mic and recognizer, the Capture Dump and its switch, the Learned Words, the Proofread Pass and the language, every one required, and is the only way to make a session. The session owns the take: one text pipeline for both the Live Preview (start(preview:) runs the pump) and the final pass, the Proofread Pass as the .proofreading phase, "insert anyway" (commitRejectedTake through the surface's own commit), and whether its take is open (phase), which Voice Capture answers across every session for the Models section. Surfaces keep their commit, presentation, sounds and errors. One hold-to-talk modifier and one "spoken words join typed words" rule replace the copies. Behaviour changes, each pinned by a test: - the Onboarding Tour's takes gain the Learned Words, the Capture Dump and the Proofread Pass (AgentVoiceInputControllerTests); - the Models section reads one open-take fact: a voice session's take shows Listening/Transcribing (was "Voice session" throughout), and any surface's Proofread Pass shows Proofreading, not only dictation's (VoiceCaptureSessionTests); - "Insert anyway" commits like any take: its history entry keeps the take's length instead of 0 s (DictationCoordinatorTests); - spoken words join the agent composer's text after its trailing spaces and newlines are trimmed, like every other field (AgentVoiceInputControllerTests). Tests: VoiceCaptureSessionTests run through the module (plus the preview and the take reading the same, insert anyway, one open take across surfaces); AgentVoiceInputControllerTests (every stage, the joining rule) lose the no-dependencies case; DictationCoordinatorTests build their coordinator the container's way; MenuModelsSectionTests lose the two hand-list tests from #636. Docs: ADR-0096, amendments to ADR-0034, ADR-0085 and ADR-0086, CONTEXT (Voice Capture; Voice Capture Session, Voice Input and Proofread Pass updated), ARCHITECTURE, docs/testing.md.
…nd frees them "Which models are loaded" was derived twice — by the menu's Models section (a 113-line derivation in the container) and by LoadedModels on the Models page — with different reads of the language model's state; the page read a @published flag on the container for the embedder. The arbiter mirrored residency for 2 of the 6 models while Offload freed 3, and the recall embedder was never unloaded. "The usable directory for a model" was spelled out at each loader (isDownloaded ? modelPath : nil). Model Residency (App/ModelResidency.swift) takes one row per model's holder (DependencyContainer+ModelResidency.swift): the model it holds, whether it is loaded and what it is doing, and how to free it. The menu's Models section, the Models page's in-memory mark and Offload Model all read it. Offload frees every holder that lets go of its model at once; the language model goes after the running generation through InferenceArbiter.offloadLLM, and dictation's Whisper stays (ADR-0025). The arbiter loses ModelSlot, loadedSlots and its voice closures; the embedder gets its own holder (RecallEmbedder) with a loaded mirror kept by its own load and unload. ModelDownloadManager.usableDirectory(for:) is the one answer every loader asks (the proofreader, the embedder, the arbiter, the vision probe), and sizeOnDisk(of:) the one size read. The usable directory lives with the download manager rather than the residency module: it already owns where a model's files go and whether they are complete, and the phone shares it. Behaviour changes, pinned in ModelResidencyTests: - Offload Model also frees the recall embedder (unloading it is safe: a running embed keeps its own reference; the next recall loads it again in about 0.4 s), and frees the proofreader at once instead of after the language model's generation; - the Offload item is enabled while any model it can free is loaded, the proofreader or the embedder alone included; - Clear Disk Cache offloads the same way before it wipes, so it frees the proofreader and the embedder too. Tests: ModelResidencyTests (rows are what holders report, the draft listed only while loaded, the page's mark, Offload frees everything unloadable and leaves Whisper, when Offload is offered, the usable directory) replace LoadedModelsTests. Docs: an ADR-0081 amendment (Model Residency; As built moves), CONTEXT (Model Residency; Inference Arbiter updated), ARCHITECTURE, docs/testing.md.
…Residual Prefill extends it Whether a snapshot can seed a prefill was spelled five times (the planner, the Leaf Admission Builder, the boundary route, the speculative pass, salvage) and two producers restored and extended a prefix their own way, the speculative pass on raw context.model outside the session verbs (ADR-0078 follow-up 4). Only the keyed turn dropped a snapshot whose restore threw. PrefillPlanner.usableRestore is now the one rule (ADR-0087 decision 7, ADR-0094): the restore point or why it is unusable, for a residual that may carry an Image Span or text only; all five callers read it. ResidualPrefill does everything after it for the boundary route and the speculative pass: restore through the Backing Leaf, reserve the path, prefill a range through session.prefill from the seeded Position Anchor, match a request-local view. Its failed-restore policy drops the snapshot through the manager in one MainActor hop (where the keyed turn re-plans its checkpoints) and logs the one unrestorable-snapshot line, for all three restorers. Declared behaviour changes, each pinned by a test: a failed restore in the boundary route or the speculative pass now drops the snapshot and logs restore/unrestorable-snapshot before the producer's unchanged line; the speculative pass prefills each chunk through the Model Session's prefill verb, so an MLX failure throws (speculativePrefill/prefill-threw) instead of ending the process and the pool is cleared between chunks. It enters as many sessions as before. The e2e's diagnostics match the previous commit apart from the speculative scheduling race.
Today's buttons wrote Reminders themselves (a tick on the Day Line, the Now Card's Done, the Inbox's ring went straight to the Agenda), while a card's Done and a cue's Did it went through the engine. The runtime, whose header said it only does the I/O, held the rules for dates (the owner's tomorrow, a time of day kept on a move), the engine held the rules for series (Later or Let go on a repeating leftover skips to tomorrow, Move to Today leaves a repeating reminder alone), and views read the real clock. Every owner action now comes in through runtime.act and is decided by the engine: a tick on Today is CardAction.tick, as a card's Done is itemDone. The change it asks of Reminders is an AgendaMutation, and the Agenda makes it (Agenda.apply, Agenda+Changes.swift) with Reminders' rules for dates and series in that one place; the runtime only performs. Today's minute, the leftovers' Another Day, the composer's confirmation and the Jarvis Panel's words read the runtime's clock. DayWalkTests walks the day over the in-memory Agenda, which makes each change the engine asks for, instead of a hand-written copy of it. The panel keeps what it shows and the engine which cue it put there (so one taken down unanswered is cued again); the panel's unused shownCardID is gone. Behaviour is unchanged where the owner can reach it. The trace keeps its labels (agenda.changed source "today" for a tick on Today). One trace change in a path Today never offers: Move to Today on a repeating reminder now records card.reaction "today" while the Agenda leaves the series alone (the engine used to drop it silently). Tests: the series rules moved from engine tables to the Agenda through the in-memory store (TodayRedatingTests: aRepeatingLeftoverSkipsToTomorrowInsteadOfLosingItsSeries, moveToTodayLeavesARepeatingReminderAlone), and a tick on Today is pinned end to end (aTickOnTodayGoesThroughTheEngineToReminders).
The Cache Claim's typed check-out outcome was flattened at plan application into a `restoreMode` string plus four loose values (the copy, the wait, the handoff, a fallback flag), and each report projected them again its own way: the `restored` memory phase, the `lookup` event, the generation value, the Leaf Store's context, and the `leafStore` report the drive patched afterwards. The copies had already drifted: the stored leaf's source string-matched "failedCopy" as a copy, and the trace re-derives the restored offset from the lookup reason. Plan application now yields one `TurnRestore` (cold, handoff, copy, or failed then cold), and every report reads it: its `mode` is the wire string, its `memoryFacts` the restored phase's facts, and the lookup line, the `leafStore` fields, the trace's emitted-path fields and the reserve's leaf source all take it in place of the string. No line changes here; the two drifted projections are fixed in the next two commits, each declared on its own. `TurnRestoreTests` tables the projections without running a completion. The per-turn report grown from the trace accumulator, the claim reporting rewinds into it, the unkeyed trivial report and one home for the handoff scope are left undone and recorded in ADR-0069's amendment.
Since a planned restore that yields no cache runs the turn cold (ADR-0069's 2026-10-03 amendment), its leaf is no copy of a body the tree already counts. The stored-source rule still string-matched `failedCopy` as `copy`, so the `leafStore` event, the `recordingRequest` phase and the trace said `copy`, and the Active-Inference Reserve did not price the next lane for a deep-copied leaf (capturesByCopy=false on an image turn that copied its live cache). Declared one-row change: on a `failedCopy` turn whose leaf is stored on the live or direct path, the source now reads `handoff` when the capture moved the generation's objects and `live` when it deep-copied them, as a cold turn's does. Nothing else changes; `copyReason` and `copyRefusal` keep the claim's answer. Pinned by TurnRestoreTests.aFailedRestoresLeafReadsLikeAColdTurns and by the fallback suite's text (handoff) and image (live) turns.
The trace record derived the restored offset from the lookup reason alone, so a hit the turn never restored recorded the hit's offset beside `hitTokens=0`: a turn whose planned restore fell back cold, and a hit the plan ran cold because the usable-restore rule refused it (one covering the whole prompt). Those records also carried a rewind block and counted as hits in the cache corpus. Declared one-row change: the accumulator's rule now reads the Turn Restore, so the restored offset is the hit's offset on a handoff or a copy and 0 when the generation began from an empty cache. Only the Completion Trace Log changes, on those two kinds of record; no diagnostics line does. Pinned by CompletionTraceAccumulatorTests.restoredOffsetIsWhatTheTurnRestored and aFallbackTurnsRecordRestoredNothing.
Reminders keeps a repeating reminder as one item for its series. Ticked
off, the occurrence stays behind as a done copy with an id of its own,
and the series moves on under its id. The Agenda passed that straight
through. So the engine matched the copy by title and moved what the day
held of the task to the copy's id (followDoneOccurrences,
DayState.follow), Tomorrow took today's star off the series by hand, and
taking a tick back reached the wrong reminder.
The day now holds a repeating task by its series' id, and the Agenda
answers for its occurrence that day. Agenda+Occurrences holds one rule
that matches a copy to its series, the day's occurrence (open, or done
and when), and taking an occurrence back. The must-do, a step's run, the
Day Line's row and Today's Chat ask it. Taking an occurrence back deletes
the copy first, then puts the series back on its date, so a failure on
the way leaves at most one open reminder. The in-memory store also names
an event by its start, as EventKit does, so a moved event gets a new id
in the tests too. EventKit's behaviour was read from Apple's guide and
headers (ADR-0092). The copy is undocumented, so it is matched by what
it is.
Behaviour changes, each pinned by a test:
- The Undo of a tick on a repeating task leaves Reminders as it was: the
series back on the occurrence's date, the copy gone, one open
reminder. Before, it reopened the series, which was already open, and
the occurrence stayed done
(takenBackTheOccurrenceLeavesRemindersAsItWas, "undo").
- Unticking a repeating task done today, by the task (a cue's Undo,
Today's ring) or by its copy, does the same. Before, the series was
left as it was, or the copy was reopened beside it, leaving two open
reminders ("the task", "the copy").
- So a must-do taken back is no longer done
(aMustDoTakenBackIsNotDoneAfterAll).
- Today's Chat's Morning Plan shows a repeating task ticked off as done.
Before, it showed the series open, and its ring ticked off tomorrow's
occurrence (theMorningPlanShowsTodaysOccurrenceDone).
- In the in-memory store, a moved event gets a new id, as on the Mac
(AgendaToolsTests). Only the tests see this.
What the 2026-10-10 amendment promised still holds, now pinned through
the Agenda over the in-memory store as well as from snapshots
(DayEngineRepeatingTaskTests).
Docs: ADR-0092; ADR-0080 amended (the 2026-10-10 mechanism superseded);
CONTEXT.md (Occurrence; Agenda, Must-do); ARCHITECTURE.md;
docs/testing.md.
A step's marks belong to its slot (ADR-0091). So when a new plan moves a step while its cue is still on the panel, the owner's answer to that cue is traced without secondsToReact. The step value's commit (ab85996) listed this change, but no test held it.
…ew follow-ups A two-axis review of the branch found one bug and two behaviour changes the spec didn't allow: - A session's start that failed after advancing its epoch left the phase at the previous take's .transcribing: that take now returns superseded and never reset it, so the Models section read "Transcribing" until the surface's next take. The failed start now sets the session idle (VoiceCaptureSessionTests). - Clear Disk Cache had come to offload every model through Model Residency. It frees the voice and the language model again, as it always has; the voice's part of Offload is one helper, guarded as the arbiter guarded it (a voice still loading finishes). - The Hotkeys pane had come to offer "Reset to <default>" on every row. A row says whether Settings offers its reset, and the same three do (HotkeyTableTests). Follow-ups: ModelResidencyTests check the app's real holders list every model; LaunchModeTests pin the kept precedence of the rotated-checkpoint parity harness after --benchmark; the holders' state cascades share one ModelActivityRow.State.of; View.holdToTalk moves to its own file; App Bindings' input is isLLMLoaded (no slots left); a Commit is called words, not take, beside onTake; the duplicate test-host check in TelemetryEnvironmentTests goes (LaunchModeTests owns it). Docs: the scratch launch no longer marks onboarding done (docs/testing.md), the usable directory's reach is stated exactly (Whisper keeps its contract, the voice engine its catalog spec), ADR-0096's As built names the residency files, ADR-0095 names the harnesses that win over --benchmark, and the Operation Guard is the session's.
No caller has read it since the Agenda landed in #601; the tools speak AgendaTime.clock and the day forms.
Plan application held the manager's order by comment: plan checkpoints only after Snapshot Resolution and only with the hit's alignment (`alignTo: resolved.alignmentLookup`), then, after a failed restore, drop the snapshot and plan again in the same hop through a generic closure. About ten members were non-private only so tests could call them, and `LookupResult.restoreCache()` was a dead duplicate of the session's restore verb. A keyed turn's `resolve(planningFrom:for:)` now resolves and plans in its own hop and returns a `PlannedResolution`; dropping the turn's unrestorable snapshot returns the re-plan from the same hop, so the failed-restore policy loses its generic follow-up. Checkpoint planning and the manager's own steps (promote, restored refs, chain-prefix bodies, the alignment rule, eviction, pressure, the Budget Floor) are private, and the suites drive the ones they need through `testSeam()`, listed in scripts/test-only-code.allow. The dead restore is deleted. Same hops, same order, no diagnostics line changes (ADR-0087's amendment records what stays with its one caller).
…commit The gates ran on c25f74a against main (fa438b3), as ADR-0078's As built does: the 4B and 27B prefix-cache e2e (bf16 and turbo8v4) with their diagnostics diffed under ADR-0093's masking, hybrid-cache-correctness, the Completion Trace Log records of each run, and the DFlash2 bench's full-stream run from each commit's build. A fresh main run settles that the Speculative Canonical Prefill's scheduling race follows the machine's load, not the change. ADR-0094 holds the record; ADR-0069's and ADR-0087's amendments and ADR-0093 point at it.
…ates stop exiting 141 dev.sh's find_app and bench.sh's resolve_app read BUILT_PRODUCTS_DIR with an awk that exits at the first match. xcodebuild, still writing, then dies of SIGPIPE, and set -o pipefail made that the script's exit: the loaded-model gates (scripts/dev.sh prefix-cache-e2e, hybrid-cache- correctness) failed with 141 depending on timing. Both scripts also took "the" DerivedData as the first tesseract-* folder in a glob, so with several worktrees a stale-module clean could empty another checkout's precompiled modules, and the configuration stamp landed in another checkout's folder. scripts/lib/checkout.sh holds the two answers once: checkout_derived_data (the folder whose info.plist names this project, test.sh's rule, which it now sources) and built_products_dir (awk reads xcodebuild's whole output). test.sh, dev.sh and bench.sh source it. Behaviour change: scripts/dev.sh clean removes this checkout's DerivedData only; it removed every checkout's, other worktrees' builds included.
ADR-0064 decision 7 scopes the handoff to text-only key spaces over unquantized KV, off the MTP path, and three places spelled it: the check-out's image and quantized rungs, the Leaf Admission's move of a finished turn, and the Leaf Store's copy reason. A fourth reader would have spelled it again. `CacheClaim.HandoffExclusion.of(textOnly:kvBits:arm:)` is now the rule: the first reason a turn is outside the scope, or nil when its cache may move. The check-out asks it without an arm (MTP engages only on a turn that restores nothing), the admission with the decoding arm, and the Leaf Store reads its copy reason. No line changes; an MTP turn's copied leaf still reports no copy reason, which the next commit declares.
The handoff scope leaves the MTP path out (ADR-0064 decision 7), so an MTP-decoded turn's live leaf is copied, yet the Leaf Store gave no reason for the copy: the one copied leaf with no label. With the scope as one rule, the Leaf Store reads it with the turn's decoding arm and reports the rule's reason. Declared one-row change: on a turn the MTP drafter decoded, the `leafStore` event gains `copyReason=mtp`, the `recordingRequest` phase's `leafCopyReason` reads `mtp` where it read `none`, and the trace's emitted-path `copyReason` reads `mtp`. No other turn changes. Pinned by the MTP row of CacheClaimTests.theHandoffScopeIsOneRule; no toy session engages MTP end to end, so the pin is the rule's.
… leafStore The Turn Restore was one value, but each output took it from wherever it happened to be (the generation value, the Leaf Admission's context, the leafStore report the drive patched), the trace tally was threaded from the drive into the Leaf Store phase, and the one-leafStore rule was a flag the drive had to set on the claim before the phase (#589), because the claim built a leafStore report of its own for a rewind. Each request's Cache Claim now keeps a Turn Report. Plan application files the Turn Restore in it once; the reserve's source, the phase's report, the recordingRequest phase and the trace record read it there. It carries the trace tally, and it owns the turn's one leafStore event: the phase begins it as its first step and logs its report through it, and the claim reports each Leaf Rewind to it instead of building a report, so the claim no longer knows the Leaf Store phase exists. No line changes: every line is written at the step it was, through the same request context. TurnReportTests drives the report's interface; the cancelled-turn replay cases and the keyed sequencing suite check it through real turns.
The Agenda found the copy to take back by what it looks like, and matched it by the day it was done. Two things went wrong: - An occurrence overdue from yesterday, ticked off late, moves a daily series on only to today, so the match failed. Unticking the copy then reopened it beside the series (two open reminders), the tick's Undo re-dated the series and left the copy done, and a cue's Undo did nothing. - A one-off of a series' name in its list, such as "Call mom" today beside a weekly "Call mom", looked like a copy. Unticking it deleted it and moved the series, and the untick's Undo ticked off the series. Deleting now goes by what the Agenda knows, not by what a reminder looks like. Right after its own tick of a repeating reminder, the Agenda reads what the tick left (the series moved on, and the copy done since the tick and due on the occurrence's day) and keeps it in memory and in the tick's Undo (TickedOccurrence). Taking that occurrence back deletes the copy and puts the series back on its date, and only while both are as the tick left them. Every other untick opens again only the reminder it names, even if a copy is then open beside its series: data safety before tidiness. What a copy looks like (isDoneOccurrence) now compares dues: due before the series' next date. Reading whether today's occurrence is done still goes by it, for ticks on the phone, and changes nothing. Behaviour changes, each pinned by a test (DayEngineRepeatingTaskTests): - An overdue occurrence ticked off late is taken back like today's, by Undo, by unticking the task, or by unticking the copy (takenBackTheOccurrenceLeavesRemindersAsItWas, overdue: true). - A one-off of a series' name is only ever opened again, and its untick's Undo ticks the one-off off again (aOneOffOfTheSeriesNameIsOnlyEverOpenedAgain). - A copy ticked off elsewhere is only opened again, beside its series, as on main (aCopyTickedOffElsewhereIsOnlyOpenedAgain). - A series moved since its tick is left where it is, and the copy is only opened again (anOccurrenceChangedSinceItsTickIsOnlyOpenedAgain). Docs: ADR-0092 (decisions 2 and 3, the option dropped, the risks left), the ADR-0080 amendment, CONTEXT.md Occurrence, ARCHITECTURE.md, docs/testing.md.
…ts a passed chunk run Two findings from review of ADR-0094's Residual Prefill. The declared change "a chunk's MLX failure throws instead of ending the process" held only for the closing flush: ResidualPrefill.prefill hard-coded `.pipelined`, whose per-chunk asyncEval reaches MLX's process-fatal handler outside `withError`. The eval policy is now the caller's: the speculative pass evaluates each chunk `.checkedSynchronous` (it forwards one chunk per call, so it gives up no pipelining) and the boundary route keeps the `.pipelined` it had. And the pass changed when a preemption lands: a cancel arriving while a chunk waited for its session now threw at the prefill's own per-chunk check, so the pass settled one chunk short where the old loop on container.perform ran that chunk. The chunk now runs in a detached task, the claim's conclusion's way, so the loop's check is again the one cancellation point. Tests: one checked prefill verb per chunk (which is also what returns the pool between chunks), a chunk that throws ends the pass with nothing admitted, and a preemption while a chunk waits for its session still runs that chunk (a mutation without the shield fails it). The toy records each prefill's eval policy and counts session requests.
Three assertions passed on wrong output (REVIEW.md). The Jarvis Panel's "nothing to start" checks used allSatisfy, which is true of an empty list. The Now Card's free-slot check compared through map(...) != false, which is true of nil. They now pin the steps listed, in order, with no task to start (an unplanned day, a plan already started), and the first free half hour after the way to the 1:1: 13:45.
Today's Start now started a step before cutting it clear of the next meeting, while a cue's Start cut it first and then started it. A step started from Today just before a meeting was therefore timed against the minutes offered, and the plan's pace read the meeting's cut as the owner being quick (60 planned, 25 taken). Both now go through one operation (DayEngine.startNow): cut clear of the next meeting, then started. Step.started(at:timed:) is the engine's one word for a start, not only the fixtures'. AgendaMutation's tick repeated complete and reopen apart from the trace's source. The three are now one case, setDone(reminderID:done: onToday:). Its source stays "today" for a tick on Today and "card" otherwise. Declared and pinned what 01 and 03 changed without saying: - The Now Card leads with a step the owner started over a meeting it runs into, and over an earlier task, not started, whose slot holds now too. It showed the meeting, and otherwise offered the earlier task's Start now (aStartedStepLeadsTheNowCardOverAMeetingAndAnEarlierTask). - Move to Today on a repeating reminder is traced as a card reaction, and the Agenda leaves the series alone (moveToTodayIsTracedForARepeatingReminderToo). Behaviour change, pinned: a step started from Today before a meeting is timed against the minutes the meeting leaves it (startNowOnTodayBeforeAMeetingEndsBeforeIt). Docs: ADR-0091 (starting as one operation, the two Now Card cases, the timing), CONTEXT.md (Time to leave, the glossary's word for what the code calls a standing departure).
…ed-restore policy Review of 05-08 found two remaining duplications. The Position Anchor seed was spelled three times (the Residual Prefill's base and both of plan application's restore shapes), and the keyed plan unpacked the usable restore's point into `.restore(cacheOffset:anchorDelta:)`. The plan now carries the `RestorePoint` whole, and the point's `initialState(seedsPositionAnchor:)` is the one rule. The failed-restore policy had two overloads repeating the corrupt-layers check, the MainActor hop and the skip line, and its typed failure carried a `drop` nobody read. One implementation now takes an optional `PlannedResolution` (the keyed turn's re-plan), the manager's drop has one public entry, and `Failure.unrestorable` carries only the error. No line changes; same hops and order.
…ther Day The engine still worked on a step by hand. About ten plan.firstIndex(where:) lookups were each followed by setting a field (cuedAt, checkedInAt, putOff, cutMinutes, isSmall), and the arithmetic of "15 more min" and of going on after a meeting lived in the engine. A slot's end, start plus minutes, was written out about ten times. The step now changes through operations on the value: cue(at:), checkIn(at:), putOffOnce(), cut(to:), resume(at:minimum:) and extend(by:from:late:). The engine reaches them through one accessor, DayState.changeStep(_:_:). A slot's end is one sum (Placement.end, and TimelineTask.end for a row with a time), used by Step, StepCue, the Timeline and the Now Card. A leftover's Another Day was a menu written twice: once on the Now Card, and once in Today's Chat, which also worked out the owner's day from the clock in its view. It is now one view (AnotherDayMenu), and its days come from the day model (DayFacts.anotherDays: the day after tomorrow to a week on, from the owner's today), pinned by anotherDayIsTheDayAfterTomorrowToAWeekOn. Behaviour unchanged. Docs: ADR-0091 (the step's operations and its one end).
…off facts TurnRestore.Handoff re-declared three of CacheClaim.Handoff's fields, and plan application copied them across field by field. The claim's handoff now carries the cache objects beside one `TurnRestore.Handoff` (the lease, the rewind backup's bytes, the wait), which plan application files as the turn's restore unchanged. No line changes.
The engine decided a repeating reminder's Later by reading the repeat flag itself, while the Agenda held every other series rule, and Agenda.apply(.dueAt) had no series guard. A Later that reached the Agenda for a repeating reminder (a stale snapshot) would have given the whole series a new time. The rule now lives with the Agenda's date and series rules (Agenda+Changes): - AgendaSnapshot.isSeries says a reminder is one item for its whole series. The engine asks it where it would give a one-off a new time, and places a step in today's plan instead, because the plan is the engine's to change. - Agenda.apply(.dueAt) never re-times a series (aRepeatingReminderIsNeverGivenANewTime). - dueOn's rule is now written down: a repeating reminder moved to another day skips to it at its time, as to tomorrow. Behaviour unchanged where the owner can reach it. Docs: the ADR-0080 one-door amendment, CONTEXT.md Agenda.
The claim's rewind report was built from the lease alone and made up a Pending-Payload Wait of zero. The claim now keeps the handoff it reported at check-out with the leased leaf and reports it to the Turn Report, which renders the turn's real handoff. Declared one-row change: on a turn that ends before the Leaf Store phase after a wait that ended in a handoff, the `leafStore path=rewind` event gains `copyWaitMs`, as the turn's `lookup` line already carries. No other line changes. Pinned by TurnReportTests.aRewindAfterAWaitedHandoffReportsTheWait.
…cription
VoiceCaptureSession.cancel() told the shared recognizer to cancel
whenever the session had a task in flight, which includes while its own
take was proofreading or committing. TranscriptionEngine cancels the one
transcription it is running, whichever session started it: the capture
panel's take proofreading, the owner dictating, releasing and pressing
Escape lost dictation's words. The race predates the Voice Capture
module, against its own rule ("a panel closing must not cut off another
surface's take"). The session now counts its own calls into the
recognizer and cancels it only while one of them runs.
Also, HotkeyTable.stop() was app code only its tests called. The
followers hold the table weakly and stop in its isolated deinit, so the
tests need no stop and it is gone.
Tests: VoiceCaptureSessionTests pin a cancel while proofreading leaving
another session's transcription to commit, and a cancel while
transcribing still cancelling its own.
Docs: ADR-0096 (Consequences), docs/testing.md.
…ywhere "The usable directory for a model" was still spelled by hand in the server's integration snapshot (which also ran its own vision probe instead of the memoized isVisionCapable), the Server Configuration's preserve-thinking scan, the phone's voice (modelPath(for:)!), the benchmark runner, and the voice engine, which rebuilt modelPath's folder rule because its @sendable closure can't call the main-actor download manager. The folder rule is now one pure, nonisolated function, ModelDefinition.directory(forRepo:pathPrefix:in:), with the entry's own directory(in:) on top. The download manager's modelPath, usableDirectory, status checks, downloads and deletes apply it, through directory(for:); the voice engine's off-main closure applies it to the spec it shares with its entry; the benchmark runner and the phone's voice read the same rule (the phone through its download manager's directory(for:), no force unwrap). The integration snapshot asks downloads.isVisionCapable, and the preserve-thinking scan asks usableDirectory(for:). cacheSubdirectory is gone. Whisper's dictation load keeps WhisperModelContract: it is stricter than its entry's completeness, and the shared catalog can't see it. No behaviour change: every folder is the same path. The integration snapshot's vision answer is now the memoized one (a known answer is kept for the id), as everywhere else. Tests: IntegrationSnapshotBuilderTests check the snapshot carries the catalog's vision verdict (the probe stays pinned in ModelCatalogTests; the directory-probing cases go); the catalog tests read the folder rule. The iOS target builds (xcodebuild -scheme tesseract-ios, Debug, generic/platform=iOS). Docs: CONTEXT (Usable Directory), ARCHITECTURE, the ADR-0081 amendment.
…all start Review found a Refused Bequest in the Decode Start: a `.prefillStrategy` start refused `cacheOwner`, refused a token recorder and required an empty cache, three runtime traps for a type distinction, and the Raw Generation Start kept a dummy `var cache = []` for it. The route keeps no array of the app's, so it now starts through `PrefillStrategyDecodeStart`, whose interface is only the build and the loop: misuse cannot compile. The Decode Start keeps the three handovers whose cache the app holds, and its three app-loop starts fold into one generic call badged with the start's arm. Tests: the table's Prefill Strategy rows run through the new start, and both speculative kinds over turbo8v4 now pin the read-back of the iterator's converted array. No line changes.
…; names match the glossary
Review's small items: `cumulativeCountersResetForTesting` and
`setSnapshotHydratingForTesting` were still manager members beside the
`testSeam()` the other test-only steps moved behind, so they are seam
members now (listed in scripts/test-only-code.allow). Three names drifted
from CONTEXT's _Avoid_ list ("the restore guard", "restore outcome", a
failure called `restored`), and `applyMemoryPressure`'s doc still said
tests could call it as-is. No behaviour changes.
The Unkeyed Completion's generation value carried a stand-in `lookupReason: .missNoEntries` for a lookup it never made, so anything that read it (the trace's restored-offset rule, two log lines) was told a miss happened. Its lookup reason is now absent, and its Turn Report stays trivial: no restore filed, no trace record, and the phase's `unkeyed-completion` skip as its one leafStore. No diagnostics line changes. Two Log.agent lines (not diagnostics events) now print the request's cache reason where they printed the lookup reason: `unkeyed(<reason>)` instead of the stand-in on an unkeyed turn, the same text on a keyed one. The drive's remaining keyed-or-unkeyed branches read Request Keying's outcome, not the report, and stay; ADR-0069's amendment records why.
…s that say what they answer The review found rules written twice and names that answered another question: - SpeechState.isSpeaking is the one "the voice is speaking" rule. The menu bar's glyph and the Models section's Voice row each switched over the same eight cases. - ModelDefinition.displayName(of:) is the one "the catalog's name, or the id" fallback, for the Models section, dictation's history and correction pairs, the onboarding chapters, the agent composer and the benchmark runner. - MenuBarManager.isModelLoaded is canOffload: since Model Residency it asks whether Offload Model would free anything, not whether a model is loaded. - Whether a hotkey has a Settings section of its own is its row's (HotkeyRow.hasOwnSettingsSection), not a case the Hotkeys pane filters out by name. - VoiceCaptureSession.commitRejectedTake returns the commit's Result. It can only commit or fail; the six-case Outcome it returned left its caller matching cases it never produced. No behaviour changes. Docs: CONTEXT.md (Hotkey Table).
…the take's own app The review found four changes that landed without a declaration, one of them without a test: - ADR-0095: a test host or a scratch launch whose arguments carry a harness flag no longer runs the harness: the environment decides first (LaunchModeTests.theEnvironmentDecidesBeforeTheArguments). Before, the dispatch chain read only the arguments. - ADR-0096: "Insert anyway" records the rejected take's own app. Before, it recorded the app the session's latest take started in, so a take begun and let go after the rejection moved it. - ADR-0096: spoken words join the agent composer's text with the typed words trimmed at both ends, as the other fields already did (AgentVoiceInputControllerTests.spokenWordsJoinTypedWords). - ADR-0081: "InferenceArbiter: unloaded TTS" is gone, and the offload's gate failure logs as "InferenceArbiter.offloadLLM: the LLM gate failed" (it said offloadAllModels). Tests: DictationCoordinatorTests pin "Insert anyway" keeping the take's own app after another take began in another app; the test fails with the old choice. Docs: docs/testing.md.
…rom the Turn Restore The lookup event took the Turn Restore apart into five loose values at its one call site, and the leaf's reported copy reason (the claim's on a copy restore, else why the leaf itself was copied) was spelled three times: the leafStore event, the recordingRequest phase and the trace record. The lookup event now takes the Turn Restore itself, and the leafStore report's `reportedCopyReason` and `recordingFacts` are the one projection the other two read. No line changes.
… rule DFlash2Support spelled the draft's folder, "incoai_Qwen3.8-27B-DFlash2", the way the catalog's folder rule gives it, because the catalog is MainActor-isolated and the support enum is not. The folder rule is nonisolated now, so the support keeps only the draft's repo, which a test pins to the catalog entry, and finds the folder through ModelDefinition.directory(forRepo:pathPrefix:in:). The folder is the same. Tests: DFlash2SupportTests pin the repo to the catalog entry, and the detection to the folder the owner's download is in. Docs: CONTEXT.md (Usable Directory), ARCHITECTURE.md, the ADR-0081 amendment.
…and its Pending Row `.agentEnd` lowers a run's busy flag one hop after the event, before the run's body returns and its task releases the gate. A send in that window sets the flag for a newer run; the older run's task then cleared it, fired the settle (the Chat Session handed the newer, still queued message back to the composer) and dropped the newer run's task handle, so a cancel missed it. DayThreadPicturesTests.theOwnersTurnSeesThePictureAndTheNextTurnReadsTheLine failed on that now and then under load: its second wait saw the flag the first run lowered and read one generation instead of two. Each run takes a number; only the newest run's task clears the flag, settles and drops the handle. A failure still reports from either. Pinned by AgentRunControllerTests.anOlderRunSettlingLeavesTheNewerRunItsFlagAndItsSettle, which fails without the fix (the flag lowered, the settle fired).
The follow-up commits on top of a888a3e ran the 4B prefix-cache e2e after every step that could move a line (eight runs, 0 of 1,959 in-order lines differing each time), and the 27B DFlash2 e2e and hybrid-cache-correctness on the last code commit, against references captured on a888a3e. ADR-0094's As built records them.
The 27B e2e under turbo8v4 on the last code commit matches main check for check and acceptance line for acceptance line, with main's own requestP3 failure on both sides.
2 of 5 tasks
spokvulcan
added a commit
that referenced
this pull request
Oct 11, 2026
…ds the build's index, Swift formats on write (#640) ## Summary These are the seven environment fixes from the retrospective of #638's overnight run, where three agents refactored in parallel on one Mac. ```diff proving a server refactor ("every diagnostics line stays", ADR-0078 d8) - scratch scripts per refactor; dev.sh gates kill every Tesseract Agent; captures are slices of the owner's diagnostics + scripts/gate.sh <run-folder> <flag> on the base and on the change, then scripts/gate-compare.py → SAME / DIFFERENT a member only tests call, whose name another member shares (stop, started) - invisible to the test-only scan; review caught it + the scan's index pass reports it as Type.member, from the build's index swift-format - check.sh / the commit stops on "files were modified by this hook", 3–6 times per agent + the file is formatted as it's written (PostToolUse hook) --bench-model qwen3.8-27b - missingDirectory(./qwen3.8-27b), a second into the run + "No model folder at …/qwen3.8-27b. --bench-model takes a model folder: pass --bench-model-id qwen3.8-27b for the catalog's." ``` ```text scripts/ ├── gate.sh # one gate → its own folder: report + diagnostics + traces (TESSERACT_TELEMETRY_DIR) ├── gate-compare.py # ADR-0078's masked comparison: checks, decode lines, diagnostics, traces; exit 0 = same ├── lib/app.sh # a run (waited for) vs an opened app (asked to quit): nothing kills another checkout's run ├── check-test-only-code.py # + the index pass (libIndexStore): Type.member, overrides and witnesses left out ├── hooks/format-swift.py # PostToolUse: swift-format the Swift file just written └── dev.sh # gates go through gate.sh; `run` quits only the open app; prune-worktrees [--yes] ``` Also in this PR: - **REVIEW.md:** an undeclared behaviour change in a `refactor:` PR is Important. The "assertions that pass on wrong output" rule now names `allSatisfy` over a list that can be empty, and `!= false` on an optional. - **CLAUDE.md:** a pointer to the gate set, and the stuck-subagent rule now names a signal you can observe: the subagent's worktree. ## Evidence - **Whole target:** 3,705 tests, 3,698 passed, 0 failed, 7 skipped (three dead members' tests went with them). `scripts/check.sh origin/main` passes, index pass included. - **`gate-compare.py` on last night's captures** (the 4B e2e, merged build against the server track's build) prints `SAME`: - 38/38 checks on both sides; - check and decode lines identical; - 0 of 1,959 in-order diagnostics lines differ; - 6 scheduling lines print but don't fail. With one check flipped and one `leafStore` field changed, it prints `DIFFERENT` and exits 1. - **Index pass:** about 3 s. It found 25 declarations the name pass couldn't see: - 18 test seams (e.g. three `flushForTesting`, `CaptureDumpStore.flush`, the PrefixCacheManager `TestSeam.*`), now listed with reasons; - 7 that looked dead, each triaged in the last commit: - `SSDEnduranceAccumulator.persistNow` is now called at termination. Its doc said shutdown writes the ledger, and nothing did. - `ImagePreviewFileCache.materialize`, `VisionPrefixMemoryGuard.rejection` (the single-shot guard the windowed continuation replaced), `PromptCacheEventDisplay.symbol` and `MenuBarClockText.spoken` are deleted with their tests. - `EmittedPathIndex.lookup` stays as a test seam. - `AgentSystemPromptInspector.fetchRawSystemPrompt` stays as planned: #184 kept the inspector "for a future settings surface". A protocol witness whose conformance sits elsewhere (`SSDSnapshotStore.loadSyncPrefix`) is left to the name pass. - **`prune-worktrees` dry run on this Mac:** lists 7 worktrees main already holds, about 15 GB with their builds. It never lists the main checkout; an early draft did, and the dry run caught it. - **Not run end to end yet:** `gate.sh` on a real gate. The owner's app was open, and gates now refuse to run beside an open app. ## Merge Danger **Door:** two-way. This is scripts, docs and one small app change: the telemetry seam honours `TESSERACT_TELEMETRY_DIR`. **Blast Radius:** tooling. What changes for whoever uses it: - `scripts/dev.sh run` quits the open app (any checkout's) instead of every instance. - `bench.sh` and `parity-ab.sh` wait for runs and stop at an open app instead of killing it. - `dev.sh`'s gate reports move to the benchmark directory's `runs/<gate>-<time>/`. - The commit hook's test-only scan takes about 2 s more when the build's index is fresh. - Every Claude Code session in this repo now formats Swift files on write. - Quitting the app writes the SSD endurance ledger once more, so the last 5 s of counters aren't lost. ## Docs impact - [ ] New or changed domain term → updated `CONTEXT.md` - [ ] Structural change (new module, moved seam, renamed file) → updated `ARCHITECTURE.md` - [ ] Durable, surprising decision with a real trade-off → added an ADR in `docs/adr/` - [x] Test workflow change → updated `docs/testing.md` and `docs/prefix-cache-testing.md` ("Prove a refactor") - [x] Also `REVIEW.md` and `CLAUDE.md`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The twelve deepenings from the 2026-10-10 architecture review are all in this PR. Each rule that was written in several places now has one home, behind a small interface that the tests go through.
Two fixes surfaced while reviewing the branch, each in its own commit:
AgentRunController: an older run's settling lowered a newer run's busy flag and handed its queued message back to the composer. This madeDayThreadPicturesTestsflaky.scripts/lib/checkout.sh):dev.sh's loaded-model gates no longer exit 141 on SIGPIPE, and no script touches another worktree's DerivedData.Declared behaviour changes (each pinned by a test; details in each commit and ADR)
Companion (ADR-0091, ADR-0092):
Server (ADR-0093, ADR-0094, ADR-0069 and ADR-0087 amendments):
restore/unrestorable-snapshotline.prefill-threwinstead of ending the process.handoff/live, notcopy. The trace recordsrestoredOffset=0when nothing was restored.copyReason=mtp.copyWaitMs.Composition (ADR-0095, ADR-0096, ADR-0081 amendment):
scripts/dev.sh cleanremoves this checkout's DerivedData only.Evidence
Unit tests,
scripts/test.sh:fa438b35): the whole target green.scripts/check.sh fa438b35passes over the whole branch, and so does thetesseract-iosbuild.The new tests that fail without their fix include:
Loaded-model gates (MacBook Pro M3 Max, 48 GB, Debug), using ADR-0078's method: diagnostics masked for ids and timings, other-task lines compared as multisets.
fa438b35prefix-cache-e2e, qwen3.5-4b-paroturbo8v4(server track)requestP3, a main bug)hybrid-cache-correctnessmaxAbsDiff=0.0)dflash2-bench --bench-check(server track)The only diagnostics differences, every run, are the Speculative Canonical Prefill's scheduling race. It runs both ways and follows the machine's load: a fresh main run logged
not-idle20 times against 25 in the earlier main runs.Merge Danger
Door: two-way, with two one-way edges:
Blast Radius: wide. It covers:
The gates above cover the server. The rest is unit tests through the new interfaces.
Left for later
Each item is recorded in its ADR:
scripts/bench.shkeeps its own list of harness log folders.--rotated-checkpoint-paritystill loses to--benchmark(ADR-0095).CapturePanelControllerandCompanionVoiceSessionControllerstill have no suites.Docs impact
CONTEXT.md(Timeline, Step, Step under way, Meeting, Block, Occurrence, Time to leave, Usable Restore, Residual Prefill, Decode Start, Decode Handover, Turn Restore, Turn Report, Launch Mode, Harness, Hotkey Table, Voice Capture, Model Residency, Usable Directory)ARCHITECTURE.mddocs/testing.mdanddocs/prefix-cache-testing.md