Two lifecycle holes the registry and the consumer helper left open.
`AdapterRegistrationHandle.replace` had no liveness guard: after the handle's
disposer ran, a replace put routes back into the registry with nothing left to
release them, so the adapter leaked permanently. `owned` being empty cannot
carry that fact, because `replace([])` is the legal empty-section state, so the
disposer records it explicitly.
`installSettingsSection`'s watcher lacked the guard its own disposer carries:
a stored change landing while the consumer unloads reached `onChange`, which
re-registers routes against a fiber whose resources are being released.
Also documents `withFileLock` in the atomic-write README (it claimed one
export), records the age-based lock takeover as a known limitation, and lists
ctx.settings and ctx.credentials in the architecture capability table.
The `readDenyPaths` policy field shipped in the previous commit broke Linux
confinement outright. bwrap has to create the `/dev/null` bind's mount point
inside a tree its own profile has already made read-only, so it refused the
entire confinement whenever the parent directory was absent — every host that
has not stored a credential yet, including a fresh install:
bwrap: Can't mkdir parents for /home/runner/.dsh/.env: Read-only file system
which the executor correctly classifies as SANDBOX_UNAVAILABLE, so every
confined bash call failed closed. Landlock cannot subtract from its own `/`
read grant, so it reported `partial` enforcement on every confined call for a
file it never hid, with no way to switch the denial off (schemastery fills an
omitted array with `[]`, so empty and omitted were indistinguishable).
A protection that breaks confinement where it works and misreports it where it
does not is worse than a documented absence. Revert the field, both expressible
backends, the enforcement downgrade, and the policy default; state the residue
plainly in the credentials-local READMEs — file mode stops other OS users, not
the model — and keep the OS-keychain provider recorded as the real answer.
The narrower discipline stands: no surface hoists the credential document into
`process.env`, and the model is never handed a resolved path to it.
Both provider READMEs state what actually holds: credentials-local now
documents the physical-line editor, the read-modify-write under the
writer lock, and a Security boundary section saying plainly that the file
mode stops other OS users and not the model. sandbox-policy documents
readDenyPaths and its per-backend enforcement. The llm READMEs carry the
registration handle, pi-ai's credential-miss semantics, and DeepSeek's
same-generation snapshot; app-boot and the CLI README stop describing
$DSH_HOME/.env as an environment layer.
A new Agent Note records the round (and the prior seam note cross-links
it); the sandbox and core catalog pages gain readDenyPaths and
AdapterRegistrationHandle with their manifest entries. The headless
missing-credential snapshot re-records for the reworded guidance, pi-ai
gains the Loader-composition guard its twin already had, and the
deliberate provider symmetry is marked for the clone detector.
Documentation only; no behavior change.
`firstLiveSeq`'s JSDoc still stated the boundary sits at that seq
unconditionally. Verified reachable on a plain production path: pick up a log,
do no work, pick it up again — the seed already ends in a boundary, so it is
not re-marked and `events[firstLiveSeq]` is undefined. Both that claim and the
firehose-gap sentence are now conditional, with the locate rule ("scan for the
last boundary") matching what types.ts already said.
`repair.ts`'s header claimed this module supplies the boundary. It does not —
the constructor does; this module supplies the activity read that must skip it.
Reworded, and it now names the closer timestamp leak, which is the one real
coupling that justifies `lastActivityTime` living beside the repair synthesis.
Recorded that `Session`'s constructor is the boundary's only legitimate writer,
since the invariant companion constrains nothing and a plugin-side append would
silently turn live brackets below it into dead history.
The seam README states the JSON-shaped write boundary, watch-disposer
quiescence, async listener containment, and the drained teardown; the
provider README rewrites Behavior around the operation chain,
read-modify-write, writer lock, ready reconcile, and leaf-level YAML
diffs, and updates Known Limitations to the residual guarantees.
A new Agent Note records the round's decisions and supersedes the
original note's deferred-lockfile alternative (cross-linked in place).
Chinese counterparts updated pair-by-pair (three briefed minimal
updates, one whole-document translation); type-equiv, config, cordis,
and module-graph catalogs re-recorded.
- `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.
`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.