Review finding, pinned red-first: the SDK accepts
processor.maxExportBatchSize <= 0 (or fractional), but its shutdown
drain then splices empty batches without consuming the queue —
disposing telemetry hangs forever whenever records are queued. The
constructor now rejects a non-positive-integer batch size before
building the SDK processor, per the misconfiguration-fails-loud rule;
everything else in the processor block remains the SDK's verbatim
passthrough.
Review finding: synthetic turn closers written by SessionPersistence
.load() at crash-resume sit below firstLiveSeq and are never exported,
leaving the remote turn unbalanced. Kept deliberately — exporting a
synthetic closer cannot complete a turn whose real tail records died
in the crashed process's queue; it can only make an incomplete turn
look closed. The OTel README now states the receiver rule (a
never-closed turn on a resumed stream marks the previous process dying
inside it; a later clean shutdown marker attests only to the resumed
process's exit), and the revival Agent Note records why the repair
suffix stays local.
A cursor-less adoption (process restart + resume, fork, seam-module
reload) replayed the session's full log from seq 0, re-exporting
history that already left the process — a resume re-billed its entire
stored log on every restart, and a fork re-shipped the parent's prefix
under the child's id, doubling query-time counts on OTLP backends with
no native ingest dedupe.
dsh-session now exposes the fact the constructor already validated but
discarded: Session.firstLiveSeq, the constructor-seed length — the
first seq appended in this process. header.seedLength cannot serve
here: it is the durable fork-lineage boundary, and a resumed session's
constructor seed is its full stored log while the header keeps the
original fork value (llm-replay and session-query-sqlite depend on
that meaning). Constructor seeds also never publish on the
session/event firehose, so adoption replaying them was inconsistent
with the system's own publication semantics.
Adoption's cursor-less fallback starts at firstLiveSeq; seed events
still feed the chunk projection, so mid-step continuations re-drop
after a resume. Fork streams are no longer self-contained: records now
carry session.seed_length (with the existing session.parent_id) so
receivers stitch the child's stream onto the parent's. Accepted cost,
consistent with at-most-once delivery and recorded in the revival
Agent Note: a resume no longer backfills records a previous process
failed to deliver — a deployment with that requirement needs the
deferred outbox, not replay.
Pinned red-first: seeded adoption exports nothing (assertion reversed
from the prior seed-readback test, obsolete behavior changed with its
test), resume-shaped seed rebuilds the projection without exporting,
and fork records carry the stitch attributes.
Three review rounds each found a new silent-loss path in the same
wrapper state (dispose racing an in-flight flush, overlapping hints
displacing the retained promise, the provider's fixed 30s flush timeout
rejecting while the processor still drains). Every path exists only
because forwarding the seam's turn-boundary hint to forceFlush() made
this backend the process's second flusher against undocumented SDK
internals from the upstream experimental tree.
The backend now implements no flush(): the batch processor is the only
flusher, its scheduledDelayMillis (already deployment-tunable through
the processor passthrough) governs export cadence, and shutdown()'s
drain is complete by construction. The two race-pin tests collapse into
one dispose-during-in-flight-batch drain pin; the seam's optional
flush() contract now tells implementers they own the concurrent-flush/
shutdown interaction. Removal rationale and the reinstatement trigger
(a stated turn-boundary latency requirement scheduledDelayMillis cannot
meet — and then via the processor's own forceFlush(), never the
provider's timeout-wrapped one) are recorded in the revival Agent Note,
both languages.
Resolutions: regenerate docs/capability-seams.md (the only conflict);
master widened the bilingual pairing scope to package READMEs, so this
merge adds the Chinese counterparts and i18n records for the three
telemetry READMEs and patches packages/README.zh.md with the telemetry
group row.
ui-sidebar shell landed on master (#643: geometry-only shell, the
browsing region moved to the sidebar.workspaces slot); this branch's
sidebar.settings foot seat re-applies on top — the hardcoded foot row
becomes the seat, the seat rides the railIn crossfade, and the shell
spec's renderSlot stub key-splits region vs settings.
The shell is now a pure composition face: no dictionaries, no locale
dependency, and three new chrome content seats (settings.trigger /
settings.header / settings.close) whose slot content also carries the
accessible names (trigger text, dialog aria-labelledby, visually hidden
close label). ui-settings-general returns as the owner of copy that
belongs to no single feature: chrome content, the General section with
its item slot, and the settings dictionaries. Slot types split homes —
trigger/header/close/section live in the shell contract; the
settings.general.item entry moves to the locale package (the common
dependency of every item registrant), with ui-theme consuming it
through a re-export seam; the verbatim duplicate merges are gone and
the dependency graph is a clean DAG.
deferRegistration() stays the shipped form; the note pins the follow-up
direction (bridge declarations into slot:<name> services, migrate
registrants to nested ctx.inject, delete the helper) and the boundaries
a separate PR must settle.
The five settings-surface registrants carried near-identical
spec-check/ledger-judge/subscribe scaffolding (three jscpd clones);
ui-slots now owns deferRegistration() — ledger-judged presence, refresh
for registrant-localized labels, one-call disposal — and every
registrant shrinks to its registration body.
The ui-layout apply bench provides a real LocaleService before the
theme plugin boots (ui-theme now injects slots/locale to register its
Appearance row); drop locale's unused clsx dependency.
Locale and ui-theme apply coverage on a real Context + SlotCore:
dictionary assembly, declaration-aware registration both ways,
inject-time getter re-sync, service write-back through the event flow,
HMR collapse recovery, and teardown reclamation. Four settings-surface
packages sit at full per-file coverage.
Generated docs follow the ui-models rename and the removed
ui-settings-general package; the English note side picks up the
feature-owner self-registration doctrine.
Settings collaboration direction (recorded in the note): the shell only
provides composition faces — feature plugins register themselves. The
General section moves into the ui-settings shell (order 0, skeleton
rows) and declares the settings.general.item list slot; locale registers
the Language row and ui-theme the Appearance row (each with its own
store mirror, dictionaries, and ledger-judged deferral); the
ui-settings-general package is gone. ui-settings-models becomes
ui-models — a feature package that contributes its Settings section
rather than a settings-owned satellite. The item-slot SlotMap entry is
authored in the ui-settings contract and repeated verbatim in
locale/ui-theme (reference-cycle avoidance; declaration merging keeps
the copies identical).