An omitted `todos` on a tail request was treated as "no projection carried"
and preserved the prior value. Every installWindow caller is a tail request
(doOpen, its gap re-pull, repairGap; loadOlder prepends without it), which the
host answers with the full-log projection or omits only when the log holds no
todo/write — so the field's absence is the authoritative empty list. A live
write whose host crashed before persisting therefore left the rolled-back plan
on screen indefinitely; the assignment now clears it on the next open or
resync. Widened the parameter to an explicit `| undefined` so the two meanings
cannot be conflated again, and updated the JSDoc at both declaring seams plus
the bilingual README/note pair.
The automation-only ACP bridge deliberately omits todo presentation (its
edge test asserts plan updates are omitted; the todo-write tool note records
the mapping's retirement). Chinese counterpart updated, pair re-recorded;
the TodoPanel header comment drops the same claim.
Regenerate event-producer-consumer.md after the queued-frame steering removal;
re-record the bilingual consistency records the master merge left stale; and
restore the eight archived notes the agent-loop rename had swept up (archived
notes are frozen — their archival-time path references stand).
The master merge committed the i18n.yaml with unresolved conflict hunks
(carried over from the adapt branch's own master merge); re-recorded via
verify-translation-pairing --write. 518 pairs consistent.
The session/queued frame no longer carries steering — AgentMessage no longer
has the field. The client derives it from the same ordered turn boundaries the
host saw (a frame arriving while a turn is open joined the steering FIFO).
- TodoPanel mounts through a 'conversation.input.dock' list entry
(todoDockEntry, QueueDock posture, order -1 above the queue rows) instead
of a ConversationRoot hardcode; the inner component is unchanged and takes
useSession from the dock entry's standard kit.
- The verify-todo-display.mjs chromium probe is replaced by an assembled
keyless snapshot (apps/web/tests/todo-display.snapshot.ts, the
code-mode-fixture idiom) pinning the TodoRow summary/state, the dock
panel content, and the collapse round-trip over built bundles.
- Fake snapshots across specs gain the todos field; bilingual note/READMEs
updated for the dock mount and the snapshot.
The interrupted pre-commit hook ran eslint --fix while client lib/types
were stale, which stripped two deliberate 'as' casts from master's
tests; one fails typecheck under exactOptionalPropertyTypes without it.
Restore both files to master's content.
- Extract the shared New Session action into WorkspacesService.startSession
(sidebar button and workspace browser both delegate; recent-Workspace
targeting and the no-workspace clear live in one place).
- Fold the chip-insertion transaction shared by insert-ref and paste-upgrade
into one InputMachine helper.
- Share the fixture's session-not-found guard across the sessionId-addressed
catalog routes.
- Drop the AppFrame baselines-ready loading gate (user ruling: the bare
status line reads worse than the shell's own pending rendering); both
column occupants mount from first paint.
- Mount WorkspacesService.startInitialSelection in the runtime apply (the
one-shot baseline follower shipped in 98633b5aa without a caller): a
restored current session wins, an explicit clear stays cleared, a failed
connect retries on the next baseline projection.
- Cover the policy in client-apply and the assembled workspace-flow
snapshot; startup now lands in the recent Workspace's blank session, so
the draft-carry scenario starts from the hero directly.
- Bring docs along: startup-selection paragraphs in the session-scope RFC
note (both languages), bilingual README pairs for the four new client
packages, doc-graph regeneration with client-declared events exempt from
the dispatcher requirement (client dispatch sites are structurally
invisible to the host-side ts.Program), and pairing re-records.
- Keep ConversationSession mounted for blank sessions (chrome-less) so the
draft-persistence mirror stays bound in the hero; hero typing reaches the
chat store again.
- Restore the baselines-ready gate in AppFrame: empty boot snapshots no
longer flash the New Workspace hero before either baseline lands.
- Commit ordinary sends through the machine (send-committed event +
Shell.commitSend): undo can no longer resurrect already-sent content on
the default-sink path.
- Give the production InputMachine a real wall clock so the typing-run
merge window actually expires.
- Coalesce concurrent connectWorkspace creates per workspace: the summary
has no cwd until the host frame lands, so a second New Session inside
that window minted a duplicate hidden blank session.
Test-side catch-up with the session-maybe conversation architecture: the
provide channel's descriptor shape and maybeProvideInfo in fakes, the shared
chat-store handle asserted on conversation.session (the session-maybe shell
carries no store), startSession fakes exposing the workspace list snapshot,
strict session slots declining (not throwing) without a session, AppFrame's
removed empty seat and loading gate, and the hero draft asserted on the
machine (the chat-store mirror binds with ConversationSession). Plus three
lint fixes (max-len split, boolean-compare, arrow-parens/unbound-method).
eslint --fix autofixes plus manual repairs: max-len line splits
(fake-api handlers, notifier/slots JSDoc, spec signatures), charAt over
non-null-asserted indexing in slash detect/menu cores, Array.from for
code-point capping, typeof assertions for unbound-method in specs,
generic getByRole for the send-button cast, effect disposer void-wrap in
command register, and dropped unused type imports.
inspect() awaited an in-flight retirement drain unconditionally before entering
serialize(), so a slow drain pinned a cancelled inspect until it finished, past
the documented cancellation boundary. Race the retirement wait against the
signal with observeQueuedAbort, matching serialize()'s queued-read behaviour.
Adds a regression that cancels an inspect while a gated retirement is pending
and asserts prompt rejection without a backend read.
After an agent-loop-only reload the server keeps its SessionRecord but the
record's agent is no longer registered. followup() no longer throws for a
detached agent, so a prompt would run against a zombie session and still report
accepted. Validate the record against the live registry before delivery, as the
ACP bridge does. Adds a regression that detaches the agent and asserts rejection.
agent/disposed only cleared the status line, so an agent-loop-only reload that
disposed the agent while the TUI stayed mounted left the local disposed flag
false. Since retained agents accept deliveries after detachment, later input
drove a zombie agent/session. Set disposed on agent/disposed so dispatchMessage
reports it. Adds a regression that sends after disposal and asserts no delivery.
Resolve merge by regenerating cordis/config catalogs and the doc graphs from
the merged tree, and re-record the tools README and tools doc bilingual pair
hashes so the pairing gate matches the merged content.
The mount-local baseline guard was seeded from "a baseline already exists in
the log", which a resumed session and a hot plugin remount both satisfy. That
made a resume skip its baseline, so offline AGENTS.md edits or removals never
reached the first resumed request — violating the documented resume contract.
Distinguish the two by agent/session-start: a startup or resume emits it before
the first step, while a remount attaches to an already-live session and never
witnesses it. Only a remount (no witnessed start, baseline already logged)
keeps the single logged baseline and skips; a resume falls through and
re-composes from current files. Adds a regression that resumes a session with
an offline baseline edit and asserts the fresh baseline reflects it.
cancel() emitted agent/cancel-requested whenever queued or steering work
existed, even under keepInbox with no active turn — a call the contract
documents as a no-op. Consumers could misread that notification as a real
cancellation. Emit only when the call actually aborts the active turn or
discards pending work, matching the "effective call" contract.
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
The reference-admission discard listener matched on followup()'s returned id,
but an agent/inbox/enqueue listener that synchronously cancels emits
agent/inbox/discard before followup() returns to assign that id. The match
then missed, leaking both the submit and discard listeners plus the attached
context per referenced prompt. Match on the content reference instead — the
same value send() carries onto the message, known before followup() runs, and
symmetric with the submit wrapper's content check.
The prior "ordinary allowed path" test passed only because the fake agent
returned a fixed 'stub' id that collided with the id the test constructed;
it now releases each wrapper through its own allowed admission, and a new
regression drives the synchronous-discard timing directly.
goal-session rides retry turns and survives admission failures. A
recovery policy closes a goal round's failed turn and reopens its
history under a retry trigger; the attempt now adopts that turn and
drops the failed turn's provisional reason, so the round settles from
the retry's own outcome instead of blocking an armed goal with
turn-error after a successful response. A downstream admission hook
that throws (rather than blocks) used to strand the queued reservation
forever; the listener now clears a still-turnless matching attempt on
the rejection path and reschedules the round.
agent-loop contains a persistently rejecting step close in the catch
path the same way the finally contains the turn close, so the
post-finally tail always publishes the terminal status — previously a
double veto escaped run(), leaving status at running while whenIdle()
resolved. The whenIdle catch arm is annotated as the backstop it now
is: every driver rejection path is contained today.
workspace-context folds an already-appended baseline from the session
log when the plugin is hot-remounted over a live session, instead of
injecting a duplicate from its fresh mount-local guard.
The TUI's reference-admission discard listener installs before
followup(): admission runs synchronously inside it on the common path,
so a listener installed afterwards missed its own cleanup and leaked
one callback per referenced prompt.