`isInheritedSeq` had no production caller — only its own tests — so it was
a public core export shaped by nothing. A bracket owner reads the boundary
positionally; the helper belongs with the compaction seam, where a real
consumer decides its signature.
Also condense the `session/inherited` and `lastActivityTime` docs.
A plugin owning a standalone open/close bracket cannot tell a dead marker
from a live one: an unmatched `compact/start` reads identically whether the
previous writer died mid-compaction or a compaction is running now.
`Session.firstLiveSeq` already holds that answer exactly, but only in memory.
Append the log-only `session/inherited` event at that seq from the seeded
constructor — the single waist all six seeded-start paths pass through
(resume, configured startup on a persisted id, `sessions.fork()`, a subagent
fork child, `adopt()`'s live prefix, and a bare seeded `create`). Read it
through the new `isInheritedSeq(events, seq)`.
The constructor placement means persistence needs no changes: the marker is
already in `events` when a backend captures the creation seed, so it rides
the ordinary seed path with no load-time write. It also covers fork, where
the inherited bracket's owner may still be running — the case a
persistence-layer boundary could not reach.
Activity ordering excludes the boundary through `lastActivityTime()`, since
lazy resume makes browsing a pickup and the three call sites would otherwise
float every opened session to the top of a picker or list.
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.
SessionTitleInvalidError narrows the one rename failure that blames the
input; the fallback-unpin append extracts to appendFallback beside
ensureFallback's guarded twin; a deferred-provider test proves rename
supersedes ACTIVE generation; the invariant companion enforces
messageSeqs-empty iff user-source on every appended session/title event
(tsconfig gains the session-title invariant path); SessionTitleEventData
field docs state the third source kind and the empty-seqs rule, mirrored
into the bilingual core-data-structures page; the note qualifies the
refresh unpin as conditional on a derivable replacement.
sessionTitle.rename appends a user-source session/title event; onUserMessage
skips scheduling while a user title stands, and an explicit refresh is the
deliberate unpin (provider regeneration, or a re-derived fallback when no
provider is registered).
- resolveLlmRoute: reuse the yml pi-ai row for providers it already routes
(DUPLICATE_ADAPTER boot failure) and detect an unset model by origin, not
by comparison against one deployment default; covered by a new spec.
- LlmService.resolveModelInfoFor preserves (and validates) modality
metadata, arming the host image preflight for exact-route resolution.
- session.selectModel refuses a text-only target once the session log
carries an image on any replayed route; an accepted switch would strand
every later turn with no in-product recovery.
- The composer no longer gates image intake on the handshake activeModel
snapshot (wrong authority for a per-session decision); the host preflight
plus the error strip own capability, deployment limits stay client-side.
- InputHub shell teardown releases the scope's draft images (File objects
and object URLs leaked for the page lifetime).
- session.prompt image parts carry optional alt into the durable block;
ImageBlock documents assistant-side rendering as forward compatibility.
- Assembled built-client lane apps/web/tests/image-display.snapshot.ts pins
the history galleries over the authorized attachment route, the lightbox,
and the composer paste rail; the attachment rail is an accessible group.
- Docs: validateImage on the seam page, fixture byte metadata matches its
PNG, and the Agent Note claims now match the shipped coverage.
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.
New credentials data-structure page (type-equiv manifested), group README,
rewritten llm-deepseek/llm-pi-ai READMEs (dynamic configuration, dict
profiles, credential chain), capability-seams/service-role registration,
Agent Note (bilingual), demo compositions mounting settings-local +
credentials-local with no inline key plumbing, installSettingsSection
consumer helper on the settings seam (deduplicating both adapters' wiring),
jscpd symmetry markers for the provider twins, runtime-closure additions for
python/sdk-runtime, and doc-budget ceilings AGENTS.md 1750→1755 /
packages/README.md 850→865 for the structural one-line group rows.
Conflicts were the parallel-edit set: packages/README group table (kept
master's session-title wording, re-added the settings row condensed),
doc-budgets AGENTS.md ceiling (took master's 1750), and regenerated
graph/catalog docs plus re-recorded translation pairs. packages/README.md
ceiling 845 -> 850: the table legitimately gained one row for the new
settings group and the row is already minimal.
Confirmed and fixed, each with a regression test that failed first:
- Concurrent writes to different namespaces lost whole sections on disk
(each persist rendered the full document from a stale text): the local
provider serializes render->write->rename->text-commit on one internal
persist chain shared by every namespace queue.
- One throwing settings/updated listener starved the rest (cordis emit
stops at the first throw): commit fans out per listener via
events.dispatch, contains individual failures, and rethrows the first
INVARIANT-coded error only after every listener ran.
- Write queues ignored fiber/service lifecycle: the base init now
registers a teardown that refuses new writes and drains queued chains;
queued tasks re-verify service liveness and namespace ownership before
running and again before committing, so a registrant disposed
mid-flight is never notified and a disposed service never commits.
- Async watcher invocations could interleave (a slow stale call applied
last): each watcher carries a serialized invocation chain — one call
at a time, in commit order; JSDoc/doc pages state the async timing.
- update/replace borrowed the caller's object until the queued task ran:
inputs are structured-clone snapshotted at call time; non-cloneable
plain objects reject with a typed error.
- Composition guard now proves the documented fallback: the consumer
uses the optional scoped-inject shape and boots both with the settings
entry (hot publish) and without it (entry-config resolution, no scope).
- core-data-structures index: settings.md row added to the sub-page
table in core.md/core.zh.md.
Both packages hold per-file 100% coverage across repeated runs.
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.