- `SessionSummary.updatedAt`'s wire doc still said "Persisted file mtime",
which stopped being true for attached sessions.
- The core invariant let `session/inherited` fall through the merge-extensible
default. It is core-owned, so it gets an explicit case; an unbalanced seed
legally places it inside an open turn, which the relation permits.
- The Agent Note claimed the boundary reaches disk via `live.pending`/
`scheduleDrain`. Verified false: the constructor append precedes `enter()`,
so it never publishes on `session/event` and rides the creation seed instead.
Attaching is therefore a write where none happened before — recorded, since
only `load()` stays a pure read.
- The deferred-index proposal asserted this change documented the cold-mtime
skew on `dsh-host-apiproxy`. It did not; the README entry now exists.
- `firstLiveSeq`'s firehose gap runs through its own seq, not below it.
- The boundary is not always at `firstLiveSeq` (the idempotence guard), so
consumers scan for the last one.
- `lastActivityTime` excludes by type, so a pickup time still leaks onto a
synthetic closer when a boundary ends an open turn. Documented.
- Pin the fork claim end-to-end: a child inherits a still-running parent's
open bracket below its own boundary, while the parent has none. Fails if the
write moves back to the load path.
- Fix the telemetry title that contradicted its own assertions.
The `/status` call site cannot be pinned the way the other two are: the
command appends its own `command/run` before rendering, so the boundary is
never the log tail there. Its fixture now at least renders over a
boundary-bearing log.
Conflict resolutions:
- `session.list`: master's projection columns fold into the PR's cancellable,
batched `listVisibleSessionSummaries`, which `session.search` shares as its
visibility baseline; master's goal helpers stay beside it.
- Client sessions face: master narrowed `ctx.sessions` to `ISessions`, so the
search verb and its protocol-constant bound are declared there and the
test-runtime double implements them (recorded, empty page unless a scenario
stubs hits).
- `WorkspaceBrowser`: master's per-row Rename wiring rides the PR's search
results view; the tree keeps the PR's query-free derivations.
- `dsh web` bin: the PR's shutdown-handlers-before-readiness order with
master's boot-time LAN address snapshot.
- `session-query-sqlite`: master's `SCHEMA_VERSION` 7 stands; the PR's bump
carried no schema change.
- Specs: master wraps assistant/steering message payloads and requires an
`application/json` carrier request, so the search fixtures and tests follow.
- Web aria goldens keep master's recording plus the PR's search placeholder;
the navigation-panes inventory keeps master's terminal-card golden next to
the PR's search-results golden.
Conflicts, all in files this branch and master both touched:
- `chat/helpers.ts` — kept both sides. Master replaced the inline env
filter in `gitBranch` with `scrubbedParentEnv()`; this branch added the
surface-marker and compaction-source imports.
- `docs/module-graph.md` — regenerated. Both sides added a `tui` edge
(master `subprocess`, this branch `compact`); the generated row now
carries both.
- Three `.i18n.yaml` pairing records — re-recorded from the merged files.
Blob hashes cannot be hand-merged, and both sides' prose survives on
each side of every pair.
`docs/cordis-catalog/services.md` also regenerated for shifted source
lines and master's new `sessionTitle.rename` entry.
New unary method in RpcMethodMap with the title-invalid error code; the impl
resolves the agent (cold sessions resume first) and delegates to
ctx.sessionTitle.rename, returning the normalized title plus its event seq so
clients settle the title projection cell ahead of the push frame. session.fork
stays on the reserved-seam list.
Review follow-ups on the append-origin transcript projection.
The live/replay equivalence claim was stated unconditionally but does not
cover `tool/call`: only replay re-derives call pairing, because a call
event carries no `surfaceOp` of its own and inherits transcript
membership from the `assistant/message` that advertised it — which the
live listener has necessarily just rendered. Narrow the claim in the TUI
README and Agent Note, and record at `rebuildTranscript` why the filter
is replay-only rather than a missing live branch.
Add `surface-replayed-compaction`: the three existing fixtures all come
from the live path, leaving the resume case the bug report leads with
pinned only by a unit test. The new checkpoint mounts with the
replacement already stored and records byte-identical to
`surface-after-compaction-wide`, so the two fixtures now pin the
equivalence they assert. The shared fixture appends move into
`appendPreCompactionLog` / `appendCompactionCheckpoint`.
`MESSAGE_TYPES` is not "human message event types" — it includes
`assistant/message`. Say what the code distinguishes (append-origin
conversation messages vs. model-only replacement copies) at the const,
the `paginate` and `session.history` JSDoc, the apiproxy README, and the
Agent Note.
Also: spell the replace shape as `Extract<SurfaceOp, { op: 'replace' }>`
for symmetry with the module's two other uses; document why
`isCompactCheckpoint` keeps a replacement check that is redundant at both
call sites; say that Ctrl+R toggles reasoning, which rebuilds the
transcript; and qualify "the sole source of derived history" as derived
*model* history now that the transcript is the other projection.
The terminal and history pagination both treated the model-visible surface as
the human transcript. A landed compaction replacement therefore erased the
conversation it summarized — messages the reader had already seen — and a
model-only replacement copy consumed a page's `maxMessages` quota, which could
also split a compaction's provenance from the replacement citing it.
`dsh-session` now exports the marker split `isAppendSurfaceEvent` /
`isReplacementSurfaceEvent`. The terminal replays append-origin surface events,
keeps a shadowed step's tool cards paired through its append-origin assistant
message, and renders one dim marker where a compaction landed; the checkpoint is
recognized through the compaction seam's `isCompactCheckpointSource` contract,
not the shape of the replacement. `session.history` counts only append-origin
human messages. Everything model-facing keeps reading `session.surface`.
Shared-surface conflicts resolve as unions: the fixture serves all five
projection keys (title/todos/permissions/plan/goal) with the /permission
and /plan command mirrors side by side, the connection specs assert the
five-key baseline and the shifted approval/question replay indices, and
the cli roster/deps, tsconfig aggregate, and README allowlist carry both
lines' rows. Plan-side content lands verbatim from master.
One end-state consolidation both branches half-did: with questions
(ui-question) and approvals (ApprovalPanel) each owning a composer
takeover, PendingCard retires outright — ChatView renders no pending
placeholder, the card component and its specs go, and both README halves
state the takeover-only contract.
Union resolutions throughout — the fixture serves both the goal and plan
projection units (catalog gains /goal beside /plan; the retired
goal-fixture sample command yields to the real goal mirror), the mux
baseline spec expects all four unit frames, and the tsconfig paths /
Model Experience allowlist carry both domains' outlets.
Shared-surface conflicts resolve as unions: the fixture serves all four
projection keys (title/todos/permissions/goal) with both the /permission
and /goal command mirrors (the goal-fixture placeholder retires with
master), apps/cli carries both lines' dependency additions, and the README
Model Experience allowlist keeps both entries. The connection specs assert
the four-key baseline and the shifted approval/question replay indices;
the module graph regenerates over the merged dependency set.
Three ui-conversation spec conflicts resolve to master's SlotTestRuntime
rewrites. Adaptation to the new outward session face: ISession gains the
command verb (the composer chip and the /permission picker submit through
it), FixtureSession grows the matching fail-loud stub plus the
waitingApproval summary default, and the picker reads the projection
through projections.faceOf (the ProjectionsFace shape) instead of the
retired store getter.
The summary blank bit switches from log emptiness to the absence of any
turn/start: standalone plugin events — command lifecycle records,
plan/mode, session titles, goal metadata — no longer surface a fresh
session in lists or steal the New Session view. Running /plan (or /goal)
on a blank session keeps it blank and reusable; the first accepted
prompt's turn clears it. Both carriers share one predicate (summarize +
the host/session-added frame); the cold path keeps its constant false
with the index-read rationale; the client mirror already flips only on
prompt acceptance and needed no change.
Review finding (PR #791): the column carried bare values (no seq), so the
client could not seed its value store without risking a stale list block
outranking newer push frames — and nothing consumed the column at all,
leaving cold titles absent after a restart. SessionSummary.projections is
now the same SessionProjectionsBlock as the history tail (values +
asOfSeq; attached rows cut the live registry, cold rows serve the cache's
identity-checked cachedSnapshot whose asOfSeq is the lowest served-row
watermark). SessionManager.refreshList seeds each row's block into the
per-session projection store via per-key apply — partial-baseline
semantics: an absent key never clears, and higher-seq-wins keeps stale
list blocks beneath push frames and tail baselines — so cold titles
surface in the sidebar without opening a session.
SessionSummary grows an optional projections column (whole value per key,
same passthrough posture as the history-tail block): attached rows cut the
live registry watermark cache; cold rows view the persisted projection
cache's stored rows via the new registry viewCheckpoint face (version-
matching keys only, zero I/O) — the RFC's motivating scenario, every
session's title across a listing without loading one event log. The column
is fail-soft and absence-coded: no registry, no cache row, or a throwing
read serve the row without the column, never breaking the listing.
The session.permissions/setPermission unary pair, the PermissionOption wire
DTO, the client Session wrappers, and the fixture/fake mirrors all leave the
wire: the read side moves to the 'permissions' session projection and the
write side moves to the /permission command in follow-up commits, so the
web protocol gains no permission methods at all.
The pendingSwitches + prompt-submit flush + hasOpenTurn move also goes.
Knob events no longer need turn enclosure: the persistence scanner keeps
standalone events after the last turn/end as part of the preserved prefix
(remove-synthetic-log-only-turns), none of the three knob invariants demand
an open turn, and the setters append bare events. An idle switch commits
immediately; hasOpenTurn stays a user-approval private fold (its audit pair
is the one contract that still requires enclosure).
The old PermissionSelect chip and its mount-time fetch die with the RPCs
(the resident composer broke the mount-once assumption); the projection-fed
replacement lands with the Access seat swap.
39 conflicts resolved per the reattachment plan (missions worktree-projbiz
0728-1859): baseline wins for deleted packages (host/runtime, old ui/acp,
ui-sidebar Rows/tree) and retired specs; unions for wire-layer exports and
client summary fields; the approval takeover, waitingApprovals tracking, and
PendingApproval domain face carry over onto the master structure. The two
new host specs follow the runtime->apiproxy rename. Dead PR-side wiring
(ConversationInjected permissions/setPermission spread, InputBar controls
prop, boot.ts sandbox composition) resolves to master and its replacement
lands in follow-up commits.
Conflict rulings follow the projection-reattach plan:
- host/runtime package (deleted on master): take master; the PR's boot
composition moves to the cordis.yml roster and its goals handlers will be
re-landed in dsh-host-apiproxy; the session.prompt slash interception and
its spec are dropped entirely (superseded by command.execute + command/run
logging).
- client core (rewritten on master): take master; the PR's Session goal
fields/methods, ConversationSnapshot.goal, goalActions injection, and the
hard-mounted GoalBar are all superseded by the 'goal' session projection
(useProjection) and will return as the ui-goal plugin.
- wire contract: union of master's workspace/command/skill domains and the
PR's goal domain, minus goal.get (the read side is the projection block +
session/projection frames; six mutation RPCs stay).
- GoalBar component and spec leave ui-conversation (they re-land in the new
ui-goal package); IconSparkle16 stays in ui-conversation chat.
- The web-slash-command-dispatch note documents the dropped interception and
is removed; the goal-bar note will be rewritten for the projection model.
- pnpm-lock.yaml taken from master (reinstall recomputes).
Sweep the silently auto-merged leftovers of the superseded #587 wire layer:
the PlanModeState interface and planMode/setPlanMode entries in the sessions
API and rpc map, the fixture plan RPC spec, the composer.controls README
paragraph and spec assertions, the old-format plan-mode snapshot, and the
two Agent Notes written against the RPC design (to be rewritten for the
projection reattachment). ChatView/PendingCard keep the approval-only
pending rendering; QuestionComposer improvements ride along unchanged.
Resolution follows the reattachment plan: the #587 wire layer (planMode/
setPlanMode RPC, prompt.planMode, client Session projection fences) is
dropped in favor of the session-projection base now on master; master
structure wins in all shared files. Kept from the PR side: the approval-only
pending filter in ChatView (questions render solely through the composer
takeover) and the auto-merged QuestionComposer improvements. The deleted
host/runtime package and retired test carriers are removed with master.
- host trio kept on the merged api-proxy: session.models (provider-grouped
advisory directory), session.selectModel (validated provider, advisory
model), installAgentLlmTarget threaded through create/resume/ensureSession;
the gateway declares the llm inject it reads
- history no longer piggybacks modelTarget: the current target travels on
session.models alone (the /model popup is the sole consumer)
- new @deepseek-ai/dsh-client-ui-model plugin: /model popupSelect over the
wire — options load the directory (group label in the detail column,
provider-local failures listed inline), onSelect routes selectModel;
failures ride the popup shell's error/retry surface
- ModelSelector package, conversation.composer.control slot, and the
Session-side modelSelection state machine are removed: model selection
belongs to the /model popup; the named conversation.input.model seat
stays empty until a control-seat entry is designed for it
The client's todos projection derived only from the paged display window,
so reopening a session whose last todo/write preceded the tail page showed
an empty plan until the user paged back — session-level state cannot be
reconstructed from an arbitrary window. The host owns the full log, so the
tail history response now attaches todos (latest todo/write backscan, the
same posture as the view pairing); installWindow seeds it, window rebuilds
preserve it, and any in-window or live write keeps overwriting it. The
fixture mirrors the host; docs and both Agent Notes record the mechanism.
The web host now composes the sandboxed product path (sandbox-local +
sandbox-policy behind bash-sandbox/fs-sandbox, with user-approval and
permission on top); BootHostOptions.sandbox carries the deployment
defaults (workspace-write + ask).
createApiProxy owns the approval pending registry: a ctx.approval ask
becomes an answerable approval/requested mux frame with a stable rpcId,
replayed verbatim on every mux open until settled; respond routes by the
echoed rpcId, validates the ApprovalResponsePayload audit correlation,
and broadcasts approval/resolved; the ask's abort signal withdraws the
question as cancelled.
session.permissions / session.setPermission project ctx.permission into
a protocol-owned PermissionOption select; idle switches are held
last-write-wins and
flushed into the next prompted turn (the ACP bridge's anchoring
pattern). The shared hasOpenTurn fold moved to dsh-session,
deduplicating the private copies in user-approval, the ACP bridge, and
the proxy.
Client, per the designer draft: a pending approval takes over the
composer (ApprovalPanel replaces the InputBar — amber strip,
justification headline, paired command, one-shot refuse/allow, keyed by
rpcId so a queued second approval remounts live; the resolved frame
restores the composer); the sidebar session row shows an amber
waiting-approval dot that outranks the running ring (manager-tracked
approvalId set, idempotent under mux-open replays, cleared per
connection generation, lit for uninstantiated sessions too); the
permission selector is a composer bottom-row chip over an invisible
native select, with a presentation-only title-case transform
(workspace-write renders as Workspace Write; wire names untouched). Question placeholders stay in the message flow. The
connection fixture mirrors the host behavior for keyless browser
acceptance.