Merge branch 'worktree/process-plugin-streaming-72f9b3' into worktree/job-kill-delivery

Carries the session seq-brand master sync forward; unions the seq/offset
imports with this branch's jobs augmentation in commands.ts and
regenerates the catalogs and zh ports over the merged source.
This commit is contained in:
Yichen Jiang committed 2026-09-01 22:20:21 +08:00
commit 374baa2621
356 files changed
+5933 -2844

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
2026-06-20-branded-ids.md: 954fd89aa229ba587cd1293973b4038cfeb20473
2026-06-20-branded-ids.zh.md: 0dd761da2e5b5fc3e864fe03c250b9781be9ee59
2026-06-20-branded-ids.md: 1f579a7afb7ac5f7facd6c5e8040d5df719c4bed
2026-06-20-branded-ids.zh.md: eaf032027f2f61bec9e4ab624622a5012c97e9b9
@@ -51,7 +51,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
- **`ModelId`** (`GenerateOptions.model`, the `LlmRuntime` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused.
- **`ToolName`** (the `ToolRuntime` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand.
- **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything.
- **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded<string>` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low.
- **Other numeric ordinals** — the [Session sequence and log-offset decision](2026-08-31-session-sequence-and-log-offset-brands.md) brands event identities and log gaps because they cross persistence and reference seams. Turn and step numbers remain plain numbers: they are payload-local ordinals and are not interchangeable with Session event positions.
- **Validated construction** — `brandString<T>()` performs no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a runtime-behavior change with its own design (what is "malformed"? what happens on failure?) and belongs in its own decision.
## Verification
@@ -51,7 +51,7 @@ const owner = brandString<OwnerToken>('session-1')
- **`ModelId`**(`GenerateOptions.model`,`LlmRuntime` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本决策的影响范围而暂不纳入。
- **`ToolName`**(`ToolRuntime` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。
- **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。
- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded<string>` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。
- **其他数值序号**:[Session 序列号与日志偏移决策](2026-08-31-session-sequence-and-log-offset-brands.zh.md)会为事件身份与日志间隙加 brand,因为它们跨越 persistence 与引用 seam。turn 与 step number 保持普通 number:它们是 payload-local ordinal,不会与 Session 事件位置互换。
- **带校验的构造**:`brandString<T>()` 不执行运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它属于运行时行为变更,有自己的设计问题(什么算「格式错误」?失败时会怎样?),应在独立决策中处理。
## 验证
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
2026-07-30-session-end-seed-log-boundary.md: 1c5a8097a6b901f133205dcd52d674a8e3594b28
2026-07-30-session-end-seed-log-boundary.zh.md: ea3549543229a15d0fba7ad0316a8683c674a558
2026-07-30-session-end-seed-log-boundary.md: c6ed3911a797480804d064273922d85412664c79
2026-07-30-session-end-seed-log-boundary.zh.md: 1e9517f9a5aed819fdaff6194ab952c322c85b82
@@ -40,7 +40,7 @@ The predicate holds for a bracket *this* session inherited, not as a liveness si
**A boundary appended at loop start.** The loop calls `resumeWith`, so it covers the resume paths, but it misses `fork()` and `adopt()` entirely, and the event would have to fire on `'startup'` — the source a fork child publishes — so `SessionStartSource` would stop discriminating. It also publishes the session before the marker is appended, so a `session/created` listener could observe a seeded log with no boundary.
**Reusing `header.seedLength`.** It is the durable *fork-lineage* boundary and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
**Reusing `Session.inheritedEventCount`.** It is the durable *fork-lineage* cut and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
**Crash repair closing `compaction/*` alongside turn boundaries.** Rejected: it moves every plugin's bracket semantics into core's repair pass, and core cannot know what closing another package's bracket should record.
@@ -40,7 +40,7 @@ Status: implemented
**在 loop 启动时追加边界。** loop 调用 `resumeWith`,因此覆盖恢复路径,但完全漏掉 `fork()` 与 `adopt()`,而且事件不得不在 `'startup'` 上触发——那是 fork 子会话发布的来源——于是 `SessionStartSource` 将不再具有区分力。它还会在追加标记之前就发布会话,因此 `session/created` 监听方可能观察到一份没有边界的带种子日志。
**复用 `header.seedLength`。** 它是持久的 *fork 血缘*边界,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
**复用 `Session.inheritedEventCount`。** 它是持久的 *fork 血缘* cut,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
**让崩溃修复连同轮次边界一起关闭 `compaction/*`。** 否决:这会把每个插件的括号语义搬进核心的修复流程,而核心无法知道关闭另一个包的括号应该记录什么。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md
2026-08-06-subagent-list-identity-projection.md: aeed828530f615b1bb4958a360b5ba4db543f714
2026-08-06-subagent-list-identity-projection.zh.md: b2b64eaa7c06b738734a7b975adb5948704465bd
2026-08-06-subagent-list-identity-projection.md: cbb15696314930acfaf20ba8651699c53c5dbde2
2026-08-06-subagent-list-identity-projection.zh.md: dbb62dbfc6bb6ca3ab63504de1ba8dd35327bbbf
@@ -14,16 +14,16 @@ The root cause is that the [durable-subagent-catalog decision](../feature/2026-0
## Decision
mode and label are folded by the new `subagent` projection unit (pure identity, two arms), and the unit is the sole authority over the fold rules; `listChildren` no longer depends on session-query — enumeration is a subagent-owned live-preferred merge, and value retrieval walks a three-rung compute-and-discard ladder: a live child synchronously reads the registry's existing watermark cache (zero log reads); a cold child first asks the optional `sessionProjectionCache` checkpoint, and a served identity that passes the seq gate is final; otherwise it pays one full `persistence.inspect` read plus a fold through the registered `subagent` unit. No index, no cache of its own, no write-back.
mode and label are folded by the `subagent` projection unit (pure identity, two arms), and the unit is the sole authority over the fold rules. Enumeration uses the shared Session query corpus, while value retrieval walks a three-rung compute-and-discard ladder: a live child synchronously reads the registry's existing watermark cache (zero log reads); an unseeded cold child may use the optional `sessionProjectionCache` checkpoint because its exact inherited cut is known to be zero; every seeded child and every cache miss pays one body-bearing Session observation plus a fold through the registered `subagent` unit. No index, no cache of its own, no list-side write-back.
There are three families of escape from the per-child scan: promote mode/label into the header (the write path pays); build a durable derivation for the projection (a checkpoint ladder, or values landed during query-index rebuild with read-side reconciliation); or compute at read time (live from the watermark cache, cold from one full read). This note takes the third. "Values landed with the query index" was retired wholesale: query infrastructure was forced to learn domain vocabulary while the sole consumer is satisfied by read-time computation — the live child's zero reads come for free from session-projection's existing watermark cache, and the cold child's single full read is explicitly accepted as compute-and-discard. The first two routes and the retirement rationale are detailed under Alternatives considered.
Key points:
- **The subagent list does not depend on session-query**: enumeration is completed by a subagent-owned live-preferred merge, and mode/label is retrieved through `ctx.sessionProjections`; deployments without a query backend list as usual.
- **Value retrieval is a three-rung compute-and-discard ladder**: a live child reads `sessionProjections.snapshot(session, ['subagent'])` (the registry's existing watermark cache, zero log reads); a cold child first reads the optional `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`, using the non-null identity directly when it passes the seq gate (`seq >= seedLength ?? 0`); otherwise it pays one full Session observation plus a fold through the registered `subagent` unit; beyond that, absent is absent — no cache of its own, no write-back, no index.
- **The subagent list uses the Session query corpus for enumeration and body-bearing observations**: mode/label still comes through `ctx.sessionProjections`, and the list owns no descriptor parser or domain index.
- **Value retrieval is a three-rung compute-and-discard ladder**: a live child reads `sessionProjections.snapshot(session, ['subagent'])` (the registry's existing watermark cache, zero log reads); an unseeded cold child may read `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`; a seeded child or cache miss pays one Session observation carrying `inheritedEventCount` plus a fold through the registered `subagent` unit. Beyond that, absent is absent — no cache of its own, no list-side write-back, no index.
- **The `subagent` projection unit is the sole authority over the fold rules**: live and cold snapshots both run the one registered unit; no second copy of descriptor-interpretation logic exists.
- **The header, the descriptor (v2), session-persistence, session-projection(-cache), and session-query(-sqlite) are all untouched**; pre-existing data acquires exact values through one `inspect` computation the first time it is listed — no degraded unknown state, no migration.
- **The descriptor (v2) remains untouched**. Session, persistence, projection cache, and query now carry the exact inherited cut separately from the logical header; pre-existing data acquires exact values through one body-bearing observation when listing cannot prove a zero cut — no degraded unknown state and no durable format migration.
Relationship to existing notes:
@@ -36,8 +36,8 @@ It hangs beside the existing `subagentTiming` ([projection.ts](../../../../packa
```ts ignore-check
export type SubagentIdentityProjection =
| { mode: 'one-shot'; label?: string; seq: number }
| { mode: 'continuable'; label: string; seq: number }
| { mode: 'one-shot'; label?: string; seq: SessionSeq }
| { mode: 'continuable'; label: string; seq: SessionSeq }
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionStateMap {
@@ -52,19 +52,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
- The projection is pure identity, and **the projection system has no failure channel**: a unit never throws; a corrupt payload or an unrecognized version folds exactly like a log with no descriptor at all. The host checkpoint state is the serializable wrapper `{ identity?: SubagentIdentityProjection }`; absence is `{}`. Its client view is the non-optional `SubagentIdentityProjection | null` entry. `null` passes JSON losslessly, so a pushed reset replaces a stale identity instead of being dropped by stringify. The judging discipline: consuming surfaces treat null and an absent client key alike as no value. How "computed to nothing" is presented is the consumer's own business (see the `listChildren` four-state mapping below).
- Label strength is decided by the descriptor schema: a continuable's label is mandatory at parse, a one-shot's was always optional; the mode/label discriminant matches the child row's strong contract below exactly (the row carries no `seq` — it is the projection's internal own-suffix proof).
- The identity carries `seq`: the seq of the `subagent/descriptor` event it was folded from, mandatory on both arms and absent on the null sentinel — `seq >= header.seedLength ?? 0` proves the identity was folded from the child's own suffix rather than a fork seed's replayed ancestor descriptor. The unit maps the wrapper's validated identity to its client wire view and is checkpointed like every unit (the `persist` opt-in is gone); its `stateVersion` is 2, bumped when `seq` was added. Existing older checkpoint rows are invalidated by version mismatch per the registry contract, falling to the authoritative refold.
- The identity carries branded `seq`: the seq of the `subagent/descriptor` event it was folded from, mandatory on both arms and absent on the null sentinel. A live Session checks it through `isOwnSeq()`; a cold body-bearing observation compares it with `inheritedEventCount`. Header-only seeded candidates skip the cache because the header intentionally exposes no integer cut; unseeded candidates know the cut is zero. The unit maps the wrapper's validated identity to its client wire view and is checkpointed like every unit (the `persist` opt-in is gone); its `stateVersion` is 2, bumped when `seq` was added. Existing older checkpoint rows are invalidated by version mismatch per the registry contract, falling to the authoritative refold.
- Fold rule: `subagent/descriptor` is last-wins, under the same descriptor-reset discipline as `subagentTiming` — ancestor descriptors in the fork prefix are overridden by the session's own descriptor. A corrupt or unrecognized-version payload is last-wins all the same: it resets to the null sentinel rather than keeping the prior identity, so a fork of a healthy ancestor does not inherit an identity its own descriptor cannot stand up.
### Enumeration: subagent-owned live-preferred merge
### Enumeration: query corpus with live preference
`listChildren`'s ([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts)) enumeration goes through no query service: the two sources `ctx.sessions.list()` and `ctx.get('sessionPersistence')?.list()` merge by id, with a live record overriding the same-id persisted record wholesale and no header consistency check. Everything enumeration needs is header facts:
`listChildren` ([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts)) asks `sessionQuery.listSessions()` for the canonical live-preferred corpus, then pairs each listed id with `ctx.sessions.get(id)` when a live Session exists. The live header overrides the listed header for that id. Everything enumeration needs is header facts:
- Filtering: `header.origin === 'subagent' && header.parentSession === parentSessionId`.
- `hasChildren`: the same merged material, looked at one level down — a direct descendant exists with `origin === 'subagent'` whose `parentSession` is that child.
- `activity`: a live record is `running`; one present only in persistence is `inactive`.
- Ordering: `createdAt` ascending, then child id ascending (matching the old contract).
- **Absent persistence degrades to live-only enumeration, not an error**: in a deployment without persistence, a cold child could not be resumed anyway, and listing live children remains meaningful. (Contrast: the old implementation rejected wholesale when sessionQuery was missing.)
- A persistence listing failure fails the whole enumeration; per-child isolation applies only to the per-child cold reads.
- An absent `sessionQuery` service fails with `SUBAGENT_CONTROL_QUERY_UNAVAILABLE`; the shared query corpus owns whether a deployment can enumerate live-only or persisted Sessions.
- A query-corpus failure fails the whole enumeration; per-child isolation applies only to per-child cold observations.
### Value retrieval: the three-rung compute-and-discard ladder
@@ -73,20 +73,20 @@ For each enumerated child, mode/label retrieval walks a three-rung ladder — co
| Rung | Read | Cost |
| --- | --- | --- |
| 1: live child | `ctx.sessionProjections.snapshot(session, ['subagent'])` | Zero log reads — the registry's existing watermark cache, synchronous retrieval |
| 2: cold child, cache hit | The optional `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`, used directly only when the non-null identity satisfies `identity.seq >= header.seedLength ?? 0` — an own descriptor is immutable once appended, and the seq gate proves the value was folded from the child's own suffix, regardless of the row's watermark | Zero log reads |
| 3: cold child, fallback | One full `persistence.inspect(id)` read + a fold through the registered `subagent` unit | One full read computed per listing |
| 2: unseeded cold child, cache hit | The optional `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`; every valid seq is owned when the exact cut is zero | Zero log reads |
| 3: seeded child or cold fallback | One body-bearing `sessionQuery.observeSession(id)` + the registered `subagent` projection, with `inheritedEventCount` available for the own-suffix check | One full read computed per listing |
- Error contract: `sessionProjections` is a required injection — `SubagentRuntime` declares it in its inject set, so a deployment without the registry never activates the service (or the loop), and `listChildren` is unreachable rather than served degraded rows ([mandatory-seam note](2026-08-19-session-projection-mandatory-seam.md)); the loud runtime check and `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` are deleted with it. The session store keeps the explicit posture: an absent `ctx.get('sessions')` (a strict global read, never the caller-scope-bound property proxy) fails with `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`. apiproxy's dedicated `PROJECTIONS_UNAVAILABLE` wire face is deleted along with the code; `SESSION_STORE_UNAVAILABLE` goes through the generic internal fallback — apiproxy's composition injects `sessions` itself, so that error is unreachable in its deployment, and a dedicated mapping would violate the need principle. `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` is deleted along with the session-query dependency.
- The cache is a purely optional acceleration layer: an absent service is skipped on a null check — no error code, no part in configuration validation (in contrast to `sessionProjections`, a required injection). Anything the second rung throws (including a poisoned unit row in the cache detonating `viewCheckpoint`) silently falls to the third rung — the cache is derived data, so its faults never produce a `corrupt` verdict; the final judgment belongs to the authoritative refold. A row whose checkpoint cut predates the descriptor naturally lacks the `subagent` key and falls through automatically, with no special-casing; a null sentinel in the row does not count either — it falls to the third rung for the authoritative refold's verdict. A count/interval checkpoint inside the creation window can land a fork seed's replayed ancestor identity in the row — the ancestor's seq falls inside the seed range, the seq gate rejects it, and it likewise falls to the third rung's verdict.
- Error contract: `sessionProjections`, the Session store, and `sessionQuery` are required runtime services for listing. Their explicit failures are `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`, `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`, and `SUBAGENT_CONTROL_QUERY_UNAVAILABLE`; no empty result disguises a missing classification or corpus capability.
- The cache is a purely optional acceleration layer: an absent service is skipped on a null check — no error code, no part in configuration validation (in contrast to `sessionProjections`, a required injection). A seeded header skips this rung because it cannot supply the cache identity's exact cut without a body read. For an unseeded child, anything the second rung throws (including a poisoned unit row detonating `viewCheckpoint`) silently falls to the third rung — the cache is derived data, so its faults never produce a `corrupt` verdict; the final judgment belongs to the authoritative refold. A row whose checkpoint cut predates the descriptor, an absent key, or a null sentinel likewise falls through.
- Per-child isolation: a single child's failed cold full read only turns that row into an `unavailable` diagnostic, naturally retried on the next listing, without affecting siblings (see the four-state mapping).
- The cold path's lifecycle witness: preparation's result must still point at the lifecycle that was enumerated — the witness field set is the same seven fields as the old SOURCE_CONFLICT check (version, id, createdAt, cwd, parentSession, seedLength, delegationDepth); a session deleted and republished under the same id degrades to a `corrupt` row in the old parent's catalog, leaking nothing of the new owner's child.
- The cold path's lifecycle witness: the observation must still point at the lifecycle that was enumerated. The witness fields are version, id, createdAt, cwd, parentSession, isSeeded, delegationDepth, origin, and agentPreset; a Session deleted and republished under the same id degrades to a `corrupt` row in the old parent's catalog, leaking nothing of the new owner's child.
- Cold-read concurrency is bounded by the constant 4 — it constrains a read-only scan of local media, not deployment behavior; when a networked persistence backend appears, it is promoted to a validated `Config` field.
- The cold-read cost, recorded honestly: only with the cache unmounted or missed does a cold child pay one full read per listing, at a cost proportional to its transcript size; the settled stance is compute-and-discard, and no cache of its own is built. The full read goes through `inspect()` into the [Session preparation](2026-08-05-session-preparation.md) cold read, so short-term repeated reads of the same id can hit its LRU for reuse, but listing does not depend on this. A live child reads zero log throughout.
- The cold-read cost, recorded honestly: every seeded child and every unseeded cache miss pays one full query observation per listing, at a cost proportional to its transcript size; the settled stance is compute-and-discard, and no cache of its own is built. The observation may reuse the query/persistence preparation layer, but listing does not depend on that optimization. A live child reads zero log throughout.
- Cancellation: the caller's signal is checked before and after each persistence read, and a read that settles only after abort is rejected, normalized to the stable error code `CANCELLED`.
### Authority model
- The session log is the sole authority; this design adds no derived persistence of any kind — no index values, no checkpoints of its own, no in-process memo; the `sessionProjectionCache` checkpoint the second rung reads is an existing composition item's derived data, which this design only reads and never writes. Values are computed on read and discarded, and a value's freshness is exactly the live state or persisted revision at the moment of the read (an own descriptor is immutable once appended — a cached identity past the seq gate has no staleness problem; the gate guards against seed-replayed ancestor identities).
- The session log is the sole authority; this design adds no domain index, checkpoint of its own, or in-process memo. The `sessionProjectionCache` checkpoint the second rung reads is an existing composition item's derived data, which the list only reads. Values are computed on read and discarded. Seeded candidates use a body-bearing observation to classify the identity against the exact cut; unseeded cached identities need no seq gate because every valid seq is owned.
- The Session and persistence write paths are entirely unaware of listing and projection consumption: no event-listener write-back, no fold-on-write.
- Enumeration and value retrieval constitute no second authorization source and make no unpublished child visible — the two sources see only published live records and durably written persisted records, consistent with the rule the durable-subagent-catalog note laid down for derived read surfaces.
@@ -127,25 +127,24 @@ For each enumerated child, the ladder's result maps to a row through four states
Known boundary deviations (deliberately accepted, recorded with this note):
- A fork child that died in its publication window, with an ancestor descriptor in its seed, gets the ancestor identity from last-wins and wrongly surfaces as a child row; resume still fails against the own-suffix fold authority (`NOT_RESUMABLE`). The old implementation omitted it via `seedLength` filtering; the projection unit cannot see the header, and this debris-grade deviation is accepted (`subagentTiming` has the same kind of pre-existing exposure).
- Multiple descriptors in the own suffix: the old implementation judged corrupt; last-wins now takes the final one (the provider contract guarantees exactly one anyway).
- A live/persisted header conflict: the old implementation made it per-child corrupt; enumeration now prefers live with no consistency check, the conflict goes unnoticed, and the live record forms the row.
- A source-read failure on damaged storage (e.g. a bad surface rejected by the cold full read): the old implementation mapped it to per-child `corrupt`; it is now uniformly an `unavailable` row (the read side cannot tell the causes apart).
- An unknown parent: the old implementation threw not-found through session-query ('parent session … was not found'); the subagent-owned merge now yields an empty subset for a nonexistent parent, enumeration returns an empty list, and later operations on the wire land as child-level subagent-not-found — a silent change of semantics and wording, recorded as explicitly accepted.
- Rung 2's later-event window: a cache row lands right after the first own descriptor, the log then appends a second own descriptor (or a malformed payload setting the null sentinel), and the process crashes before the next checkpoint — from then on a cold listing's rung 2, admitted by the seq≥seedLength gate, keeps serving the row's old identity (the first own descriptor's value), diverging from the authoritative refold (last-wins, the second), and a rung-2 hit triggers no refold, so nothing notices. Three boundaries: ① the precondition is a second own descriptor on the same child, violating the establishing provider's append-exactly-once contract — corruption-class data, same family and source as the multi-descriptor deviation; ② it takes both "corruption + a crash missing every checkpoint (the two mandatory points, turn/end and disposal, and the count/interval throttle points all unmet)" at once; ③ a healthy child (exactly one own descriptor) is unaffected — what the seq gate admits is precisely the only true identity. Self-healing: any live run of that child (the turn/end mandatory checkpoint) or any moment that triggers cache.write overwrites the whole row with a fresh fold (whole-record replace), and rung 2 serves correctly from then on; the authoritative paths (the rung-3 refold, the live snapshot, the resume fold) are correct from the start, and the divergence exists only in listing reads while the child stays cold and the row is never rewritten. The mechanical fixes were not taken: gate reconciliation would need the log-end seq, unavailable to a zero-read cold path; a cache row carrying the revision is an opaque token, incomparable and a cross-domain schema change — filed as accepted under the "the cache is never authoritative" doctrine.
- Rung 2's later-event window applies only to an unseeded child: a cache row lands right after the first descriptor, the log then appends a second descriptor (or a malformed payload setting the null sentinel), and the process crashes before the next checkpoint. Cold listing can keep serving the old identity until a live run or cache write replaces the row. The precondition violates the provider's append-exactly-once contract and also requires missing every mandatory checkpoint; healthy children are unaffected. Seeded children never take rung 2 without the body-owned cut.
Consuming surfaces: diagnostic handling across wire, tool, and GUI **stays entirely as it was, zero changes** (the `list_agents` description and output schema are untouched; the plugin's load requirement changes — `sessionQuery` dropped from inject, `sessionProjections` added as a required injection). The only behavioral changes are in apiproxy: on the route segment, the `hasSubagentDescriptor()` scan is deleted and `hasSubagentOwner` looks only at `header.origin` — pre-#1569 data without `origin` is no longer recognized as a subagent owner; it never entered the catalog anyway, and the pre-release stance accepts this; and `subagents.history` is aligned with `session.history`'s source — a live child served from in-memory events and the registry's watermark snapshot, a cold child from `inspectServable` reading persistence directly with a detached fold, no query service involved, the SESSION_QUERY_* error arms retired with it, and the wire shape unchanged (the `history` JSDoc wording becomes the live in-memory snapshot / cold persisted log dual arm).
Consuming surfaces keep the same row and diagnostic wire shape. `list_agents` reaches the required query corpus plus projection registry; live identities come from the registry snapshot and cold identities from cache or query observation. Host ownership still uses `header.origin`, and history uses the shared live/cold Session query sources; no consumer parses descriptor events independently.
### Change footprint
| Area | Files | Change |
| --- | --- | --- |
| subagent | projection.ts, projection-types.ts, index.ts | New client-visible `subagent` unit and its registration |
| subagent | list-children.ts and its types | Rewritten as subagent-owned enumeration plus the projection-ladder four-state mapping; the session-query dependency, per-child event reads, and in-place classification machinery deleted; error code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` deleted, and `sessionProjections` becomes a required injection (no projection error code remains); new optional dependency dsh-session-projection-cache (pure read acceleration, skipped when absent) |
| host/apiproxy | api-proxy.ts | `hasSubagentDescriptor` deleted; the owner check looks only at `header.origin`; `subagents.history` shares `session.history`'s source — live from in-memory events and the registry's watermark snapshot, cold from `inspectServable` reading persistence directly with a detached fold, no query service, the SESSION_QUERY_* error arms and the dedicated `PROJECTIONS_UNAVAILABLE` wire face retired with it |
| tool | tool-subagent-control/list-agents.ts | Load requirement narrowed (`sessionQuery` dropped from inject); model-visible schema, description, and rendering unchanged |
| subagent | list-children.ts and its types | Query-corpus enumeration plus the projection-ladder four-state mapping; required projections/query services and optional projection-cache acceleration |
| host/apiproxy | Session controller/query integration | Owner checks use `header.origin`; live/cold history and listing consume the shared query and projection sources |
| tool | tool-subagent-control/list-agents.ts | Model-visible schema, description, and rendering remain unchanged |
| wire/client | api/subagents.ts, runtime sessions/service.ts, GUI | Types, row shape, and diagnostic handling **unchanged**; api/subagents.ts only reworded the `history` JSDoc to the dual arm |
| core/session, session-persistence, session-projection(-cache), session-query(-sqlite) | — | **Zero changes** |
| core/session, session-persistence, session-projection(-cache), session-query(-sqlite) | body-bearing cut and branded seq plumbing | Logical headers expose `isSeeded`; Session, persistence observations, cache identity, and query records carry exact `inheritedEventCount` separately |
## Alternatives considered
@@ -169,20 +168,20 @@ Consuming surfaces: diagnostic handling across wire, tool, and GUI **stays entir
## Verification
`packages/subagent/subagent/tests/list-children.spec.ts` is rewritten to this contract: live-only listing without persistence, query services, or the continuation runtime; without the registry the service never activates (the mandatory seam — a `setup` variant asserting `ctx.get('subagents')` stays undefined); a live child incurs zero `inspect` throughout while a cold child incurs exactly one per listing; multiple descriptors resolve last-wins to the final one; corrupt payloads and unknown versions fold to `corrupt`; a cold-read failure maps to `unavailable` and retries on the next listing; the ancestor descriptor in a fork seed forms a row under that identity (pinning deviation one); ordinary forks and descendants without a subagent origin neither enter the list nor count toward `hasChildren`; `createdAt`-then-id ordering; an unmounted provider does not affect listing; compacted and uncompacted twins list identically; the three cases of pre-abort, persistence listing, and cold-read cancellation all normalize to `CANCELLED`; the empty list and stable error codes (`SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` for an absent store). Second-rung cases: an own-seq identity used directly with zero `inspect`, a fork seed's ancestor identity (seq inside the seed range) rejected by the gate and falling through, an in-row identity absence (null sentinel or absent key) falling through, an absent cache service falling through, and a poisoned cache row silently falling through to the refold; cold-path lifecycle tampering degrades to `corrupt` field by witness field (`it.each` over the seven). The `tool-subagent-control` list-agents tests are updated for the narrowed load requirement; `optional-session-query.spec.ts` is deleted with the dependency it guarded; the existing keyless snapshots (`subagent-list-agents` among others) are unchanged, pinning that the healthy path's wire and model-visible surfaces did not move; a new keyless snapshot, `subagent-diagnostic` (examples/headless-agent), pins the four-state mapping's diagnostic classification — the model-visible changes such as descriptor-less settled debris becoming a `corrupt` row.
`packages/subagent/subagent/tests/list-children.spec.ts` pins this contract: live identity checks use `Session.isOwnSeq()`; an unseeded cold identity may use the cache at cut zero; seeded candidates skip that cache rung and use an observation carrying `inheritedEventCount`; ancestor identities fail the own-suffix check; absent, null, poisoned, and unavailable cache/observation cases fall through or produce the documented diagnostic; lifecycle tampering degrades to `corrupt` across the complete witness field set. The existing keyless snapshots keep the healthy wire and model-visible surfaces fixed, while `subagent-diagnostic` pins diagnostic classification.
## Consequences
- Listing a live child reads zero log throughout; with the cache unmounted or missed, a cold child pays one full `inspect` read per listing, at a cost proportional to its transcript size and repeated with listing frequency — compute-and-discard is the settled stance: no cache of its own is built, nothing is written back, and short-term repeated full reads of the same id can hit the preparation-phase LRU, though listing does not depend on it.
- The subagent list no longer requires a query backend: both pure-live and persistence-less deployments can list; `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` is gone, loading the `list_agents` plugin no longer requires `sessionQuery`, and `sessionProjections` becomes a required injection of `SubagentRuntime` — a deployment without the projection registry never activates the service (the mandatory seam).
- The subagent list requires the Session query corpus and projection registry; missing services fail explicitly instead of producing incomplete rows. The optional projection cache changes only the number of body reads.
- Identity interpretation exists only in the single unit registered with the registry: the list's three-rung ladder and GUI history's cold read use its live, cached, or observed wire snapshots, and no hand-written bypass fold exists; if some future consuming surface bypasses the unit with a hand-written fold, values will drift across read faces — a discipline this design requires be maintained, not a mechanical guarantee.
- Per-child isolation is back: a single child's cold-read failure loses only that row and healthy siblings are unaffected; a persistence listing failure still fails the whole enumeration.
- The diagnostic and enumeration semantics leaves six boundary deviations (a stillborn fork surfacing under its ancestor's identity, multiple descriptors resolving to the last, header conflicts going unnoticed, damaged-source read failures shifting from `corrupt` to `unavailable`, an unknown parent yielding an empty list instead of not-found, and rung 2's later-event window); the full semantics is in the known-boundary-deviations list; the first four are display or classification deviations on debris-grade data, the unknown-parent one is a silent query-semantics change, and the rung-2 window is a self-healing cache-serving divergence under the double condition of corruption plus a crash; resume authorization is unaffected throughout, all explicitly accepted.
- The diagnostic and enumeration semantics leaves five boundary deviations (multiple descriptors resolving to the last, header conflicts going unnoticed, damaged-source read failures changing classification, an unknown parent yielding an empty list instead of not-found, and the unseeded rung-2 later-event window). Seeded ancestor identities are no longer a deviation because body-bearing reads compare them with `inheritedEventCount`; resume authorization remains unaffected.
- Pre-#1569 data without `origin` is no longer recognized as a subagent owner; it never entered the catalog anyway, and pre-release carries no compatibility promise.
## Related
- [Durable subagent catalog and list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md) — partially superseded by this note: the descriptor remains the durable authority for mode/label and the fold input, while the list's enumeration and value retrieval move to the subagent-owned merge plus the projection ladder.
- [Durable subagent catalog and list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md) — partially superseded by this note: the descriptor remains the durable authority for mode/label and the fold input, while value retrieval moves to the projection ladder over the shared query corpus.
- [Session projections and command lifecycle logging](../../proposed/architecture/2026-07-27-session-projection-and-command-log.md) — the authority for the registry contract; this note adds the `subagent` identity unit and consumes its live and cold wire snapshots.
- [Session projection state and client views](2026-08-19-session-projection-state-and-client-views.md) — the state/client split; both `subagent` and `subagentTiming` provide client wire views.
- [Session projections as a required seam](2026-08-19-session-projection-mandatory-seam.md) — `sessionProjections` becomes a required injection; the list's error contract follows it (registry absence is an activation-time failure, and the projection error code is deleted).
@@ -14,16 +14,16 @@ Status: implemented
## 决策
mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威;`listChildren` 不再依赖 session-query——枚举是 subagent 自管的 live-preferred 合并,取值走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);cold child 先问可选的 `sessionProjectionCache` checkpoint,取到过 seq 门的身份即定值;否则一次 `persistence.inspect` 整读加经注册的 `subagent` unit 折叠。无索引、不自建缓存、无回写。
mode 与 label 由 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威。枚举使用共享 Session query corpus,取值则走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);unseeded cold child 可以使用可选 `sessionProjectionCache` checkpoint,因为其精确 inherited cut 已知为零;每个 seeded child 与每次 cache miss 都执行一次含正文的 Session observation,再经注册的 `subagent` unit 折叠。无索引、不自建缓存、列表侧无回写。
消除逐 child 扫描的出路有三类:把 mode/label 提升进 header(写路承担);为投影建持久派生(checkpoint 阶梯,或随查询索引重建落值、读端对账);读时现算(live 走水位缓存,cold 一次整读)。本记录取第三条。「值随查询索引落库」已整体退役:查询基础设施被迫认识领域词汇,而唯一消费方读时现算即可满足——live child 的零读由 session-projection 既有水位缓存白拿,cold child 的一次整读被「算完即止」显式接受。前两条与退役理由详见考虑过的替代方案一节。
要点:
- **subagent 列表不依赖 session-query**:枚举由 subagent 自管的 live-preferred 合并完成,mode/label 经 `ctx.sessionProjections` 取值;没有 query backend 的部署照常列表。
- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);cold child 先读可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份通过 seq 门(`seq >= seedLength ?? 0`)即直接使用;否则执行一次完整 Session 观察,再经注册的 `subagent` unit 折叠;再没有就没有——不自建缓存、无回写、无索引。
- **subagent 列表使用 Session query corpus 完成枚举与含正文 observation**:mode/label 仍经 `ctx.sessionProjections` 获取,列表不拥有 descriptor parser 或领域索引。
- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);unseeded cold child 可读 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;seeded child 或 cache miss 执行一次携带 `inheritedEventCount` 的 Session observation,再经注册的 `subagent` unit 折叠。再没有就没有——不自建缓存、列表侧无回写、无索引。
- **`subagent` projection unit 是折叠规则唯一权威**:live 与 cold 快照都运行同一份已注册 unit,不存在第二份描述符解释逻辑。
- **header、描述符(v2)、session-persistence、session-projection(-cache)、session-query(-sqlite) 全部零改动**;存量数据第一次被列表时一次 `inspect` 现算获得精确值,无 unknown 降级态、无迁移。
- **描述符(v2)保持不变**。Session、persistence、projection cache 与 query 在 logical header 之外单独携带精确 inherited cut;listing 无法证明 cut 为零时,存量数据经一次含正文 observation 获得精确值——无 unknown 降级态,也无持久格式迁移。
与既有记录的关系:
@@ -36,8 +36,8 @@ mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠
```ts ignore-check
export type SubagentIdentityProjection =
| { mode: 'one-shot'; label?: string; seq: number }
| { mode: 'continuable'; label: string; seq: number }
| { mode: 'one-shot'; label?: string; seq: SessionSeq }
| { mode: 'continuable'; label: string; seq: SessionSeq }
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionStateMap {
@@ -52,19 +52,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
- 投影是纯身份,**projection 体系不做失败通道**:unit 永不抛错;载荷损坏、版本不认识与整日志没有描述符一样。host checkpoint 状态使用可序列化的包装 `{ identity?: SubagentIdentityProjection }`,缺席为 `{}`;客户端 view 则是非可选的 `SubagentIdentityProjection | null` 条目。`null` 完好通过 JSON,因此推送 reset 会替换旧身份,而不会被 stringify 丢掉。判定纪律:消费面把 null 与客户端 key 缺席一律视为无值。「算出来没有」如何呈现是消费方自己的事(见下文 `listChildren` 四态映射)。
- label 强度由描述符 schema 决定:continuable 的 label 解析强制必有,one-shot 的本就可选;mode/label 判别与下文 child 行的强约定完全一致(行不携带 `seq`——它是投影内部的 own-suffix 证明)。
- 身份携带 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无——`seq >= header.seedLength ?? 0` 证明身份折叠自 child 自身后缀,而非 fork 种子回放的祖先描述符。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
- 身份携带品牌化 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无。live Session 通过 `isOwnSeq()` 检查它;cold 含正文 observation 则与 `inheritedEventCount` 比较。仅 header 的 seeded candidate 会跳过 cache,因为 header 有意不暴露整数 cut;unseeded candidate 知道 cut 为零。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
- 折叠规则:`subagent/descriptor` last-wins,与 `subagentTiming` 同一条 descriptor-reset 纪律——fork 前缀里的祖先描述符被自身描述符覆盖。损坏或版本不认识的载荷同样 last-wins:重置为 null 哨兵而非保留先前身份,健康祖先的 fork 不会继承自身描述符立不住的身份。
### 枚举:subagent 自管 live-preferred 合并
### 枚举:query corpus 与 live preference
`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))的枚举不经任何查询服务:`ctx.sessions.list()` 与 `ctx.get('sessionPersistence')?.list()` 两个来源按 id 合并,live 记录整条覆盖同 id 持久化记录、不做 header 一致性校验。枚举所需全部是 header 事实:
`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))通过 `sessionQuery.listSessions()` 取得 canonical live-preferred corpus,再把每个 listed id 与可能存在的 `ctx.sessions.get(id)` 配对;同 id 存在 live Session 时使用 live header。枚举所需全部是 header 事实:
- 过滤:`header.origin === 'subagent' && header.parentSession === parentSessionId`。
- `hasChildren`:同一份合并材料向下看一层——存在 `origin === 'subagent'` 且 `parentSession` 为该 child 的直接后代。
- `activity`:live 记录为 `running`,仅存在于持久化的为 `inactive`。
- 排序:`createdAt` 升序、再按 child id 升序(与旧约定一致)。
- **persistence 缺席退为 live-only 枚举,不报错**:没有 persistence 的部署,cold child 本就无法 resume,列出 live child 仍然有意义。(对照:旧实现在 sessionQuery 缺失时整体拒绝。)
- persistence 列表失败使整次枚举失败;per-child 隔离只作用于逐 child 的冷读。
- `sessionQuery` 服务缺席时以 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 失败;共享 query corpus 负责决定部署能枚举 live-only 还是持久化 Session。
- query corpus 失败使整次枚举失败;per-child 隔离只适用于逐 child cold observation。
### 取值:三级「算完即止」阶梯
@@ -73,20 +73,20 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
| 级 | 读法 | 成本 |
| --- | --- | --- |
| 1:live child | `ctx.sessionProjections.snapshot(session, ['subagent'])` | 零日志读——注册表既有水位缓存,同步取值 |
| 2:cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份满足 `identity.seq >= header.seedLength ?? 0` 才直接使用——own descriptor 一经追加不可变,seq 门证明该值折叠自 child 自身后缀,无视行水位 | 零日志读 |
| 3:cold child,兜底 | `persistence.inspect(id)` 整读 + 经注册的 `subagent` unit 折叠 | 每次列表一次整读现算 |
| 2:unseeded cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;精确 cut 为零时,每个合法 seq 都归 child 自有 | 零日志读 |
| 3:seeded child 或 cold 兜底 | 一次含正文 `sessionQuery.observeSession(id)` 加已注册的 `subagent` projection,使用 `inheritedEventCount` 做 own-suffix 检查 | 每次列表一次整读现算 |
- 错误约定:`sessionProjections` 是必需注入——`SubagentRuntime` 在 inject 集里声明它,没有 registry 的部署根本无法激活服务(与 loop),`listChildren` 不可达,而不是供出降级行([mandatory-seam 记录](2026-08-19-session-projection-mandatory-seam.zh.md));响亮运行时检查与 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 随之删除。会话存储保留显式姿态:`ctx.get('sessions')`(严格全局读取,不走调用方作用域的属性代理)缺席以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 失败。apiproxy 为 `PROJECTIONS_UNAVAILABLE` 设的专门 wire 脸随码删除;`SESSION_STORE_UNAVAILABLE` 走通用 internal 兜底——apiproxy 组合自身就 inject `sessions`,该错误在其部署不可达,专门映射违反 need 原则。`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 已随 session-query 依赖删除。
- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。第二级任何抛错(包括缓存内任一 unit 行中毒使 `viewCheckpoint` 引爆)静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 切面早于描述符的行,`subagent` key 天然缺席,自动落底,无特判;行里的 null 哨兵同样不作数——一律落第三级,由权威重折裁决。创建窗口内的 count/interval checkpoint 可能把 fork 种子回放的祖先身份落进行——祖先 seq 落在 seed 区间,被 seq 门拒绝,同样落第三级裁决。
- 错误约定:`sessionProjections`、Session store 与 `sessionQuery` 都是 listing 所需的 runtime service。三者分别以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`、`SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 与 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 显式失败;缺失分类或 corpus 能力不会伪装成空结果。
- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。seeded header 会跳过该级,因为不读取正文就无法提供 cache identity 所需的精确 cut。对 unseeded child,第二级任何抛错(包括中毒 unit 行引爆 `viewCheckpoint`)都会静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 早于 descriptor、key 缺席或 null 哨兵也都会落底。
- per-child 隔离:单 child 的 cold 整读失败只使该行成为 `unavailable` diagnostic,下次列表自然重试,不影响 sibling(见四态映射)。
- 冷路径的生命周期见证:preparation 的结果必须仍指向枚举时的那个生命周期——见证字段集与旧 SOURCE_CONFLICT 检查同款七字段(version、id、createdAt、cwd、parentSession、seedLength、delegationDepth);同 id 删除后重新发布的会话对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
- 冷路径的生命周期见证:observation 必须仍指向枚举时的那个生命周期。见证字段为 version、id、createdAt、cwd、parentSession、isSeeded、delegationDepth、origin 与 agentPreset;同 id 删除后重新发布的 Session 对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
- 冷读并发以常数 4 有界——它约束的是本地介质的一次只读扫描而非部署行为;出现联网 persistence backend 时提升为验证过的 `Config` 字段。
- 冷读成本如实记录:cache 未挂载或未命中时,cold child 每次列表才付一次整读,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。整读经 `inspect()` 走 [Session 准备阶段](2026-08-05-session-preparation.zh.md)的冷读,同 id 短期重复读取可命中其 LRU 复用,但列表不依赖此。live child 全程零日志读。
- 冷读成本如实记录:每个 seeded child 与每次 unseeded cache miss 都会在每次列表时支付一次完整 query observation,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。observation 可以复用 query/persistence preparation 层,但列表不依赖该优化。live child 全程零日志读。
- 取消:每次 persistence 读前后检查调用方 signal,abort 之后才结算的读拒绝归一化为稳定错误码 `CANCELLED`。
### 权威模型
- session log 是唯一权威;本方案不新增任何派生持久化——没有索引值、没有自己的 checkpoint、没有进程 memo;第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,本方案只读不写。取值现算现弃,值的新鲜度就是读取时点的 live 状态或持久化 revision(own descriptor 一经追加不可变——缓存身份过 seq 门后无陈旧性问题,门防的是种子回放的祖先身份)。
- session log 是唯一权威;本方案不新增领域索引、自有 checkpoint 或进程 memo。第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,列表只读。取值现算现弃;seeded candidate 用含正文 observation 按精确 cut 分类,unseeded cached identity 无需 seq 门,因为每个合法 seq 都归自身所有。
- Session 与 persistence 写路完全不感知列表与投影消费:没有事件监听回写,没有写时折叠。
- 枚举与取值不构成第二个鉴权来源,也不让尚未发布的 child 可见——两个来源只见已发布的 live 记录与已落盘的持久化记录,与 durable-subagent-catalog 记录对派生读面立下的规则一致。
@@ -127,25 +127,24 @@ export type SubagentListEntry =
已知边界偏差(有意接受,随本记录留档):
- 死于发布窗口的 fork child,seed 里若有祖先描述符,last-wins 会给出祖先身份,误现为 child 行;恢复仍按 own-suffix 折叠权威失败(`NOT_RESUMABLE`)。旧实现靠 `seedLength` 过滤将其 omit;projection unit 看不到 header,接受此残骸级偏差(`subagentTiming` 有同类既有暴露)。
- own suffix 出现多个描述符,旧实现判 corrupt,现 last-wins 取末者(提供方约定本就保证恰一)。
- live/persisted header 冲突,旧实现是 per-child corrupt;现枚举 live 优先、不做一致性校验,冲突不再被察觉,以 live 记录成行。
- 损坏存储的源读失败(如坏 surface 被冷读整读拒收),旧实现映射 per-child `corrupt`,现统一成 `unavailable` 行(读侧无从区分成因)。
- 未知 parent,旧实现经 session-query 抛 not-found(「parent session … was not found」);现自管合并对不存在的 parent 得到空子集,枚举返回空列表,wire 上后续操作落到 child 级 subagent-not-found——语义与文案的静默变化,显式接受。
- rung 2 的更晚事件窗口:cache 行恰在首个自有描述符之后落盘,日志随后追加第二个自有描述符(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃——此后冷列表的 rung 2 凭 seq≥seedLength 门持续供出行内旧身份(第一个自有描述符的值),与权威重折(last-wins 第二个)分歧,且 rung 2 命中期间不触发重折、无从察觉。边界三条:①前提是同一 child 出现第二个自有描述符,违反建档提供方「恰追加一次」约定,属损坏类数据,与多描述符偏差同族同源;②需「损坏 + 崩溃错过 checkpoint(turn/end 与 disposal 两个 mandatory 点及 count/interval 节流点全部未及)」双条件同时成立;③健康 child(恰一自有描述符)不受影响——seq 门放行的正是唯一真身份。自愈条件:该 child 任一次 live 运行(turn/end mandatory checkpoint)或任何触发 cache.write 的时点,都会以新 fold 整行覆写(whole-record replace),rung 2 随即供正;权威路径(rung 3 重折、live snapshot、resume 折叠)自始正确,分歧只存在于持续冷、行未再更新期间的列表读。机制修法不采:gate 对账需知日志末端 seq,冷路径零读不可得;cache 行携 revision 是 opaque token,无法比较且跨域改 schema——按「cache 永不为权威」总纲归档为接受项。
- rung 2 的更晚事件窗口只适用于 unseeded child:cache 行恰在首个 descriptor 后落盘,日志随后追加第二个 descriptor(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃。cold listing 可能持续供出旧身份,直到一次 live 运行或 cache write 替换该行。其前提违反 provider 的「恰追加一次」约定,并且还需错过所有 mandatory checkpoint;健康 child 不受影响。seeded child 没有 body-owned cut 时绝不进入 rung 2。
消费面:wire、tool、GUI 的 diagnostic 处理**全部保持原状零改动**(`list_agents` 的 description 与 output schema 未动;该插件的加载要求变化——inject 去掉 `sessionQuery`、新增必需注入 `sessionProjections`)。行为上动的只有 apiproxy:路由段的 `hasSubagentDescriptor()` 扫描已删除,`hasSubagentOwner` 只看 `header.origin`——pre-#1569 的无 `origin` 存量不再被认作 subagent 属主,其本就不进目录,pre-release 立场接受;`subagents.history` 与 `session.history` 同源对齐——live child 用内存事件与注册表水位快照,cold child 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂随之退役,wire 形状不变(`history` 的 JSDoc 措辞改为 live 内存快照/cold 持久日志双臂)。
消费面保持相同的 row 与 diagnostic wire 形状。`list_agents` 使用必需的 query corpus 与 projection registry;live identity 来自 registry snapshot,cold identity 来自 cache 或 query observation。Host ownership 仍使用 `header.origin`,history 使用共享的 live/cold Session query source;没有消费方独立解析 descriptor event。
### 改动落点
| 区域 | 文件 | 改动 |
| --- | --- | --- |
| subagent | projection.ts、projection-types.ts、index.ts | 新客户端可见 `subagent` unit 与注册 |
| subagent | list-children.ts 及类型 | 重写为自管枚举 + 投影阶梯四态映射;删 session-query 依赖、逐 child 事件读取与就地分类机器;错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 删除,`sessionProjections` 转为必需注入(不再存在投影错误码);新增可选依赖 dsh-session-projection-cache(纯加速读取,缺席跳过) |
| host/apiproxy | api-proxy.ts | 删 `hasSubagentDescriptor`,属主判定只看 `header.origin`;`subagents.history` 与 `session.history` 同源——live 用内存事件与注册表水位快照,cold 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂与 `PROJECTIONS_UNAVAILABLE` 专门 wire 脸随之退役 |
| tool | tool-subagent-control/list-agents.ts | 加载要求收窄(inject 去 `sessionQuery`);model-visible schema、描述与渲染零改动 |
| subagent | list-children.ts 及类型 | query-corpus 枚举加 projection 阶梯四态映射;必需 projections/query service 与可选 projection-cache 加速 |
| host/apiproxy | Session controller/query integration | owner 检查使用 `header.origin`;live/cold history 与 listing 消费共享 query 和 projection source |
| tool | tool-subagent-control/list-agents.ts | model-visible schema、描述与渲染保持不变 |
| wire/client | api/subagents.ts、runtime sessions/service.ts、GUI | 类型、行形状与 diagnostic 处理**零改动**;api/subagents.ts 仅 `history` 的 JSDoc 措辞改为双臂 |
| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | — | **零改动** |
| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | 含正文 cut 与品牌化 seq 传递 | Logical header 暴露 `isSeeded`;Session、persistence observation、cache identity 与 query record 单独携带精确 `inheritedEventCount` |
## 考虑过的替代方案
@@ -169,20 +168,20 @@ export type SubagentListEntry =
## 验证
`packages/subagent/subagent/tests/list-children.spec.ts` 重写为本约定:无 persistence、query 服务与继续运行时的 live-only 列表;registry 缺席时服务根本不激活(mandatory seam——`setup` 变体断言 `ctx.get('subagents')` 保持 undefined);live child 全程零 `inspect`、cold child 每次列表恰一次;多描述符 last-wins 取末者;损坏载荷与未知版本折为 `corrupt`;冷读失败映射 `unavailable` 且下次列表重试;fork seed 里的祖先描述符按该身份成行(偏差一钉住);普通 fork 与无 subagent origin 的后代不入列也不计入 `hasChildren`;`createdAt`→id 排序;提供方未挂载不影响列表;压缩与未压缩孪生一致;预中止、持久化列表与冷读取消三例归一 `CANCELLED`;空列表与稳定错误码(存储缺席时 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`)。第二级例:own-seq 身份直用零 `inspect`、fork 种子祖先身份(seq 落在 seed 区间)被门拒绝落底、行内无身份(null 哨兵或 key 缺席)落底、cache 服务缺席落底、缓存行中毒静默落底重折;冷路径 lifecycle 篡改按见证七字段逐一(`it.each`)降级为 `corrupt`。`tool-subagent-control` 的 list-agents 测试随加载要求收窄更新;`optional-session-query.spec.ts` 随依赖消失删除;既有无密钥快照(`subagent-list-agents` 等)零变化,钉住健康路径的 wire 与 model-visible 面不变;新增无密钥快照 `subagent-diagnostic`(examples/headless-agent)钉住四态映射的诊断分类——descriptor-less 定局残骸成 `corrupt` 行等模型可见变化。
`packages/subagent/subagent/tests/list-children.spec.ts` 固定本约定:live identity 通过 `Session.isOwnSeq()` 检查;unseeded cold identity 可在 cut 零时使用 cache;seeded candidate 跳过该 cache rung,转而使用携带 `inheritedEventCount` 的 observation;祖先 identity 无法通过 own-suffix 检查;缺席、null、中毒与不可用的 cache/observation 会按约定落底或产生 diagnostic;lifecycle 篡改按完整见证字段集降级为 `corrupt`。既有无密钥快照保持健康 wire 与 model-visible 面不变,`subagent-diagnostic` 则固定诊断分类。
## 后果
- live child 的列表全程零日志读;cold child 在 cache 未挂载或未命中时每次列表一次 `inspect` 整读,成本与其 transcript 大小成正比、随列表频率重复——定案「算完即止」,不自建缓存、不回写,同 id 短期重复整读可命中准备阶段 LRU 但列表不依赖它。
- subagent 列表不再要求 query backend:纯 live 与无 persistence 的部署都能列表;`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 消失,`list_agents` 插件加载不再要求 `sessionQuery`,而 `sessionProjections` 转为 `SubagentRuntime` 的必需注入——没有投影 registry 的部署根本不会激活服务(mandatory seam)。
- subagent 列表要求 Session query corpus 与 projection registry;服务缺失会显式失败,而不是供出不完整 row。可选 projection cache 只改变正文读取次数。
- 身份解释只存在于 registry 注册的一份 unit:列表三级阶梯与 GUI history 冷读使用其 live、cached 或 observed wire 快照,不存在手写旁路折叠;若未来某消费面绕开该 unit 手写折叠,各读面的值将漂移——这是本设计要求维持的纪律,不是机制保证。
- per-child 隔离回归:单 child 冷读失败只损失该行,healthy sibling 不受影响;persistence 列表失败仍使整次枚举失败。
- 诊断与枚举语义留下六处边界偏差(stillborn fork 祖先身份误现、多描述符取末者、header 冲突不再被察觉、损坏源读失败由 `corrupt` 转 `unavailable`、未知 parent 由 not-found 改为空列表、rung 2 更晚事件窗口),完整语义见已知边界偏差清单;前四处为残骸级数据的展示或分类偏差,未知 parent 一处是查询语义的静默变化,rung 2 窗口一处是损坏加崩溃双条件下可自愈的缓存供值分歧;恢复鉴权均不受影响,显式接受。
- 诊断与枚举语义留下五处边界偏差(多描述符取末者、header 冲突不再被察觉、损坏源读失败改变分类、未知 parent 由 not-found 改为空列表、unseeded rung 2 更晚事件窗口)。seeded 祖先 identity 已不再构成偏差,因为含正文读取会把它与 `inheritedEventCount` 比较;恢复鉴权始终不受影响。
- pre-#1569 的无 `origin` 存量不再被认作 subagent 属主;其本就不进目录,pre-release 无兼容承诺。
## 相关
- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,列表的枚举与取值改为自管合并加投影阶梯。
- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,取值改为共享 query corpus 上的 projection 阶梯。
- [session projections 与命令生命周期日志](../../proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——registry 约定的权威;本记录为其新增 `subagent` 身份 unit,并消费其 live 与 cold wire 快照。
- [session projection 状态与客户端视图](2026-08-19-session-projection-state-and-client-views.zh.md)——state/client 拆分;`subagent` 与 `subagentTiming` 都提供客户端 wire view。
- [session projections 作为必需接缝](2026-08-19-session-projection-mandatory-seam.zh.md)——`sessionProjections` 转为必需注入;列表的错误约定随其变化(registry 缺席是激活期失败,投影错误码删除)。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.md
2026-08-31-session-sequence-and-log-offset-brands.md: 0057b2b6c95391abbb4ec2464ee58b54e2fe24f2
2026-08-31-session-sequence-and-log-offset-brands.zh.md: f51ad5953cab34294a53db6e3c91884292497faf
@@ -0,0 +1,45 @@
# Agent Note: Distinguish Session event identities from log offsets
Status: implemented
English | [中文](2026-08-31-session-sequence-and-log-offset-brands.zh.md)
## Problem
Session positions used one structural `number` type for two incompatible meanings. An event reference names an existing row, while a prefix length, next append position, or read cut names a gap and may equal the event count. The compiler therefore accepted an offset where an event identity was required and could not expose a missed sequence-field migration.
`SessionHeader.seedLength` also mixed a v0 storage coordinate into metadata used by body-free readers. Listing needs to know whether a Session has fork lineage, but only a reader that holds the event body can interpret the exact inherited prefix length.
## Decision
`@deepseek-ai/dsh-brand` exports the erased numeric primitive `BrandedNumber<B>` and the runtime-identity helper `brandNumber()`. `@deepseek-ai/dsh-session` owns two validated brands: `SessionSeq` names one existing event and `SessionLogOffset` names a log gap, prefix length, or read offset. `SessionSeqCursor = SessionSeq | -1` represents an inclusive watermark before or after the first event, and `OptionalSessionSeq = SessionSeq | null` represents an event identity whose absence is data.
`SessionEvent.seq`, surface replacement endpoints, provenance, and owner payload fields that identify Session events use `SessionSeq`. `Session.seq`, `Session.firstLiveSeq`, `Session.inheritedEventCount`, body-read offsets, and inherited prefix cuts use `SessionLogOffset`. Arithmetic returns an ordinary number and re-enters either domain through its validating constructor.
The logical `SessionHeader` carries `isSeeded: boolean` and no numeric seed cut. Body-bearing storage values and observations carry `inheritedEventCount` beside the header; `Session.ownEvents()` and `Session.isOwnSeq()` hide the comparison from ordinary consumers. A seeded constructor requires an explicit seed and exact cut, including an empty seed with cut zero, because constructor input may contain child-owned setup events after the inherited prefix.
The v0 JSONL header remains byte-compatible: absent `seedLength` decodes to `isSeeded: false` with cut zero, while present zero or nonzero values decode to `isSeeded: true` with the exact cut. Header-only listing translates only the presence bit. API, SDK, DeepSeek, telemetry, query-row, and JSON representations continue to carry ordinary numbers; their owning adapters validate and brand values when they enter same-process domain code.
## Admission and ownership
Domain constructors reject negative, fractional, non-finite, and unsafe integer values. Parsers validate a raw number once and retain the parsed object where the brand does not require a runtime wrapper. A compile-time brand does not discover unknown numeric fields in an external event; a format migration still needs an exhaustive owner disposition and must refuse schemas it cannot safely rewrite.
`session/end-seed` remains a lifecycle marker, not the source of the inherited cut. Every constructor restore appends or retains that marker, including unseeded replay, so projections and cold readers receive `inheritedEventCount` explicitly instead of scanning the log.
## Alternatives considered
**Keep every position as `number`.** Rejected because event identities, counts, and cursors cross package and persistence seams frequently enough that accidental interchange is a migration risk, not a local arithmetic convenience.
**Use one branded Session position for identities and offsets.** Rejected because it would again permit `eventCount` or `fromSeq` where an existing event is required and would force the `-1` and `null` sentinels into unrelated operations.
**Derive the inherited cut from `session/end-seed`.** Rejected because the marker records constructor lifecycle, not only fork lineage, and a constructor seed may contain child-owned events after the inherited prefix.
## Consequences
Sequence-bearing code now states whether a number identifies an event or a gap. Header-only readers receive stable lineage metadata without opening the body, while persistence, projection, query, and authorization paths retain the exact cut they need. The on-disk v0 format and public numeric wires do not change.
The cost is explicit conversion at durable and wire parsers and a separate exact-cut field on body-bearing observations. Projection-cache identity includes the lineage bit and exact cut, so its disposable storage domain advances and older rows rebuild on demand; body-free readers skip seeded cache hints when they do not hold the cut. Turn numbers, step numbers, message-list indexes, workflow member ordinals, token counts, and unrelated numeric domains remain plain numbers because they do not identify Session events.
## Testing
Type assertions pin that `SessionSeq` and `SessionLogOffset` are not interchangeable. Runtime suites cover constructor validation, mixed inherited and child-owned seeds, empty seeds, `ownEvents()` and `isOwnSeq()`, v0 JSONL absent/zero/nonzero headers in plain and Zstandard encodings, header-only listing, cold prepare and reopen, query and projection cuts, and unchanged numeric wire values.
@@ -0,0 +1,45 @@
# Agent Note: 区分 Session 事件身份与日志偏移
Status: implemented
[English](2026-08-31-session-sequence-and-log-offset-brands.md) | 中文
## Problem
Session 位置曾用同一个结构化 `number` 类型表达两种不兼容的含义。事件引用指向一条已存在的记录,而前缀长度、下一追加位置或读取切点指向记录间隙,并且可以等于事件总数。因此,编译器会在需要事件身份的位置接受偏移,也无法暴露迁移时漏改的序号字段。
`SessionHeader.seedLength` 还把 v0 存储坐标混入了无须读取正文的 metadata consumer。列表只需要知道 Session 是否有 fork lineage,只有同时持有事件正文的读取方才能解释精确的继承前缀长度。
## Decision
`@deepseek-ai/dsh-brand` 导出编译后消失的数值原语 `BrandedNumber<B>` 与运行时保持原值的 helper `brandNumber()`。`@deepseek-ai/dsh-session` 拥有两个经验证的 brand:`SessionSeq` 指明一条已存在事件,`SessionLogOffset` 指明日志间隙、前缀长度或读取偏移。`SessionSeqCursor = SessionSeq | -1` 表达首条事件之前或之后的闭区间 watermark,`OptionalSessionSeq = SessionSeq | null` 表达允许以缺失为数据的事件身份。
`SessionEvent.seq`、surface 替换端点、provenance 以及 owner payload 中指向 Session 事件的字段使用 `SessionSeq`。`Session.seq`、`Session.firstLiveSeq`、`Session.inheritedEventCount`、带正文读取的偏移与继承前缀切点使用 `SessionLogOffset`。算术结果恢复为普通 number,并通过对应的验证构造函数重新进入任一领域。
逻辑 `SessionHeader` 携带 `isSeeded: boolean`,不携带数值 seed cut。包含正文的存储值和 observation 在 header 旁携带 `inheritedEventCount`;`Session.ownEvents()` 与 `Session.isOwnSeq()` 向普通 consumer 隐藏比较。seeded constructor 必须显式提供 seed 与精确 cut,包括 cut 为零的空 seed,因为 constructor 输入可能在继承前缀之后还包含 child-owned setup event。
v0 JSONL header 保持字节兼容:缺少 `seedLength` 时解码为 `isSeeded: false` 和零 cut,存在零或非零值时解码为 `isSeeded: true` 和对应精确 cut。仅 header 的 listing 只转换字段是否存在。API、SDK、DeepSeek、telemetry、query row 与 JSON 表示继续携带普通 number;由它们各自的 adapter 在值进入同进程 domain code 时完成验证与 brand。
## Admission and ownership
Domain constructor 拒绝负数、小数、非有限值与非安全整数。parser 验证原始 number 一次;brand 不需要运行时 wrapper 时,保留原解析对象。编译期 brand 无法发现外部事件里的未知数值字段;格式迁移仍须获得穷尽的 owner disposition,并拒绝无法安全改写的 schema。
`session/end-seed` 仍是 lifecycle marker,不是继承 cut 的来源。每次 constructor restore 都会追加或保留该 marker,unseeded replay 也一样,因此 projection 与 cold reader 会显式接收 `inheritedEventCount`,而不是扫描日志。
## Alternatives considered
**继续让所有位置都使用 `number`。** 拒绝,因为事件身份、计数与 cursor 已频繁跨越 package 与 persistence seam,意外混用是迁移风险,而不是局部算术便利。
**用同一个 branded Session position 表达身份和偏移。** 拒绝,因为这样仍会在需要已存在事件的位置接受 `eventCount` 或 `fromSeq`,还会迫使 `-1` 与 `null` sentinel 进入互不相关的操作。
**从 `session/end-seed` 推导继承 cut。** 拒绝,因为该 marker 记录 constructor lifecycle,并不只记录 fork lineage,而且 constructor seed 可以在继承前缀之后包含 child-owned event。
## Consequences
携带序号的代码会明确说明一个 number 指向事件还是间隙。仅 header 的 reader 无须打开正文即可取得稳定 lineage metadata,persistence、projection、query 与 authorization path 则保留所需的精确 cut。磁盘 v0 格式与公共数值 wire 不变。
代价是在 durable 与 wire parser 处显式转换,并让含正文 observation 携带独立的精确 cut 字段。Projection cache identity 包含 lineage bit 与精确 cut,因此其可丢弃的 storage domain 会推进,旧 row 按需重建;不持有 cut 的仅 header reader 会跳过 seeded cache hint。turn number、step number、message-list index、workflow member ordinal、token count 与无关数值领域保持普通 number,因为它们不指向 Session 事件。
## Testing
类型断言钉住 `SessionSeq` 与 `SessionLogOffset` 不可互换。运行时 suite 覆盖 constructor 验证、混合继承与 child-owned seed、空 seed、`ownEvents()` 与 `isOwnSeq()`、plain 与 Zstandard 编码中的 v0 JSONL 缺失/零/非零 header、仅 header 的 listing、cold prepare 与 reopen、query 和 projection cut,以及不变的数值 wire 值。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
2026-06-30-session-store-fork-api.md: ff7617f4bb306926a7b0782751ae82f6ef0c6371
2026-06-30-session-store-fork-api.zh.md: ecef7ba2985321677b29d3fc7692a8dcb7afb2b6
2026-06-30-session-store-fork-api.md: a2d169a36a8a0b624abef98377d644d713e5d64c
2026-06-30-session-store-fork-api.zh.md: 5ebf05e677f2d3676f860220b1cf9ff960565402
@@ -20,11 +20,11 @@ The store exposes one operation:
type SessionForkSource = Session | SessionId
class SessionStore extends Service {
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
}
```
`boundary` is the inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, and sets `seedLength` to the copied prefix length. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
`boundary` is the branded inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, sets logical `isSeeded: true`, and supplies the copied prefix length separately as `inheritedEventCount`. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
An empty prefix is forkable; any non-empty boundary must be a safe existing sequence outside an open turn. Typed errors distinguish missing sources, stale objects, duplicate child ids, invalid boundaries, and prefixes ending during execution. Broader log validation and crash repair remain with their existing owners.
@@ -44,6 +44,6 @@ The Host creates the child through the agent registry with the selected seed and
## Consequences
The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header.
The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so the JSONL backend persists that seed once and preserves logical `parentSession` / `isSeeded` plus the separate exact cut (encoded as v0 physical `seedLength`).
This decision excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has protocol and snapshot coverage; this Agent Note adds no ACP wire behavior, so no ACP snapshot is required. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md); focused store, Host, carrier, and client tests pin the boundary and reconciliation contracts, while the real Chromium scenario pins the assembled message action and lineage tree.
@@ -20,11 +20,11 @@ store 暴露一个操作:
type SessionForkSource = Session | SessionId
class SessionStore extends Service {
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
}
```
`boundary` 是要复制到的源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,并将 `seedLength` 设为已复制前缀的长度。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
`boundary` 是要复制到的品牌化源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,设置 logical `isSeeded: true`,并把复制前缀的长度单独作为 `inheritedEventCount` 传入。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
空前缀可以被 fork;任何非空边界都必须是位于开放轮次之外且安全、已存在的序号。类型化的错误区分源缺失、对象陈旧、子 id 重复、边界无效和前缀结束于执行过程中等情况。更广泛的日志校验与崩溃恢复仍由其现有的负责方处理。
@@ -44,6 +44,6 @@ Host 通过 agent(智能体)注册表,以选定的种子和谱系创建子
## 后果
公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此现有后端只需持久化该种子一次,并在 header 中保存 `parentSession`/`seedLength`。
公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此 JSONL 后端只需持久化该种子一次,并保留 logical `parentSession`/`isSeeded` 与单独的精确 cut(编码为 v0 物理 `seedLength`)。
本决策排除 ACP(Agent Client Protocol)`session/fork`、对未加载的已持久化会话执行 fork、面向模型的工具,以及 subagent 重构。如果未来添加 ACP 方法,应在具备协议与快照覆盖后才声明支持该能力;本 Agent Note 不添加任何 ACP 协议行为,因此不需要 ACP 快照。fork 子会话的回放仍由现有的[种子边界测试 Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.zh.md)覆盖;store、Host、载体与客户端的专项测试固定边界和对账约定,真实 Chromium 场景则固定组装后的消息操作与谱系树。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md
2026-07-21-continuable-background-subagents.md: e1adf8efa19cce8e0632170886770b8d12bc9e3d
2026-07-21-continuable-background-subagents.zh.md: f62c314320a1dace43f3028b68a1bb7d65203e84
2026-07-21-continuable-background-subagents.md: 24ab64879fa18f5de9952af2dba321e2cae17477
2026-07-21-continuable-background-subagents.zh.md: 0cf06f7fe30d0953bb6e7f1a4c96527fa181805a
@@ -71,9 +71,9 @@ Human input uses the same `followup` operation. The UI may display the child tra
### Durable child handle and cold resume
The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor in the child's own suffix (after `seedLength`, so a fork seed cannot leak an ancestor's descriptor) and its header identifies the caller as the direct parent.
The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor at or after its exact `inheritedEventCount`, so a fork seed cannot leak an ancestor's descriptor, and its header identifies the caller as the direct parent.
The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `seedLength`, and `delegationDepth`, while the persisted child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `isSeeded`, and `delegationDepth`; body-bearing persistence metadata owns the exact `inheritedEventCount`, while the child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
Cold resume cannot depend on an optional method of `SubagentRun`, because that run has been disposed and is not retained across process restart. A run represents one disposable activation and exposes only activation-scoped operations. `SubagentRun.steer?()` names the confirmed live-only capability so it cannot be confused with service orchestration or the model-facing tool.
@@ -71,9 +71,9 @@ durable child Session
### 持久化 child handle 与从持久化存储恢复
继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在该 child 自身的后缀中(`seedLength` 之后,因此 fork seed 不会泄露祖先的描述符)得到受支持的描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在其精确 `inheritedEventCount` 位置或之后得到受支持的描述符,从而阻止 fork seed 泄露祖先描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`seedLength` 和 `delegationDepth` 的权威信息,持久化 child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`isSeeded` 和 `delegationDepth` 的权威信息;含正文的持久化 metadata 拥有精确 `inheritedEventCount`,child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
从持久化存储恢复不能依赖 `SubagentRun` 的可选方法,因为该 run 已被 dispose,并且进程重启后不会保留。run 表示一次可 dispose 的激活,只暴露作用于当前激活的操作。`SubagentRun.steer?()` 这一名称明确指代提供确认语义且仅适用于在线消息的功能,以免该功能与服务编排或面向模型的工具混淆。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md
2026-07-23-session-telemetry-otel-revival.md: 836531f605eee5c0dcdf108e9ee2b2744d6f72aa
2026-07-23-session-telemetry-otel-revival.zh.md: 9874598a58ff91bd2cc5ef58c771744d3f3d9610
2026-07-23-session-telemetry-otel-revival.md: 1110db4b0dfcfd68bde97f9964383ddd041f328b
2026-07-23-session-telemetry-otel-revival.zh.md: 6c046c5f963760638deacacda51f8d94a13e0efd
@@ -28,7 +28,7 @@ The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry
**Map onto OTel spans (GenAI semantic conventions) instead of logs.** Rejected for this revival: the branch implementation's log mapping is reviewed and shipped-shaped; the span model is lossy for forkable, interruptible sessions and belongs to a future consumer with real span queries to serve.
**Full-log replay when no handoff cursor survived (re-export constructor seeds).** Shipped in the first revival round, then narrowed: adoption now replays from the session's construction boundary (`Session.firstLiveSeq`, the constructor-seed length — a fact the session already validated but did not expose; `header.seedLength` cannot serve, it is the durable fork-lineage value and a resumed session's constructor seed is its full stored log). A resumed session's history already shipped from the previous process under the same id, and a fork's inherited prefix already shipped in the parent's stream — re-exporting either re-billed every resume for its full history and doubled query-time counts on OTLP backends with no native ingest dedupe. Receivers stitch fork lineage via `session.parent_id` + `session.seed_length`. What the narrowing gives up, consistently with the at-most-once stance: a resume no longer backfills records the previous process failed to deliver (telemetry unmounted then, or queued at crash) — the full replay's only real benefit, bought at the common case's expense. A deployment that states a backfill requirement needs the deferred outbox above, not replay. The boundary also swallows the synthetic turn closers `SessionPersistence.load()` writes when repairing a crash-interrupted log (they sit below `firstLiveSeq` despite never existing in the previous process) — deliberate, not incidental: exporting a synthetic closer cannot complete the remote turn whose real tail records died in the crashed process's queue, it can only make an incomplete turn look closed. The wire stream stays faithful to what the crashed process actually shipped; receivers read a never-closed turn on a resumed stream as "the previous process died inside it" (the OTel README states the rule), and a later clean `shutdown` marker attests only to the resumed process's exit. Threading the pre-repair boundary through load/prepare so repairs export as live events would couple three packages to un-ship that signal.
**Full-log replay when no handoff cursor survived (re-export constructor seeds).** Shipped in the first revival round, then narrowed: adoption now replays from the session's construction boundary (`Session.firstLiveSeq`, the constructor-seed length; `Session.inheritedEventCount` cannot serve because it is the durable fork-lineage cut and a resumed session's constructor seed is its full stored log). A resumed session's history already shipped from the previous process under the same id, and a fork's inherited prefix already shipped in the parent's stream — re-exporting either re-billed every resume for its full history and doubled query-time counts on OTLP backends with no native ingest dedupe. Receivers stitch fork lineage via `session.parent_id` + `session.seed_length`. What the narrowing gives up, consistently with the at-most-once stance: a resume no longer backfills records the previous process failed to deliver (telemetry unmounted then, or queued at crash) — the full replay's only real benefit, bought at the common case's expense. A deployment that states a backfill requirement needs the deferred outbox above, not replay. The boundary also swallows the synthetic turn closers `SessionPersistence.load()` writes when repairing a crash-interrupted log (they sit below `firstLiveSeq` despite never existing in the previous process) — deliberate, not incidental: exporting a synthetic closer cannot complete the remote turn whose real tail records died in the crashed process's queue, it can only make an incomplete turn look closed. The wire stream stays faithful to what the crashed process actually shipped; receivers read a never-closed turn on a resumed stream as "the previous process died inside it" (the OTel README states the rule), and a later clean `shutdown` marker attests only to the resumed process's exit. Threading the pre-repair boundary through load/prepare so repairs export as live events would couple three packages to un-ship that signal.
**Forwarding the seam's turn-boundary `flush()` hint to the OTel provider's `forceFlush()`.** Shipped in the first revival round, then removed: three distinct silent-loss paths shared the wrapper state — a dispose racing an in-flight flush (the SDK's concurrent-flush guard makes shutdown's internal drain skip), overlapping hints displacing the retained promise, and the provider's fixed 30-second flush timeout rejecting while the processor still drains. Every path exists only because the forwarding made this backend the process's second flusher against undocumented SDK internals from the upstream experimental tree; with no `flush()` implemented, 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. Reinstate only if a deployment states a turn-boundary latency requirement `scheduledDelayMillis` cannot meet — and then by calling the retained `BatchLogRecordProcessor`'s own `forceFlush()`, never the provider's timeout-wrapped one.
@@ -29,7 +29,7 @@ Status: implemented
**映射到 OTel span(GenAI 语义约定)而非日志。** 本次复活否决:分支实现的日志映射已经过评审、形态可交付;span 模型对可 fork、可中断的会话有损,留给将来真正有 span 查询需求的消费方。
**handoff 游标未存活时全量回放日志(重新导出构造函数种子)。** 首轮复活曾交付此方案,其后收窄:接管操作现在从会话的构造边界起回放(`Session.firstLiveSeq`,即构造函数种子长度,这一事实会话早已校验过却未曾暴露;`header.seedLength` 不能胜任:它是持久保存的 fork 谱系(lineage)值,而恢复会话的构造函数种子是其完整的已存储日志)。恢复会话的历史已由上一个进程以同一 id 发出,fork 继承的前缀也已在父会话的流中发出;再次导出任何一者,都会让每次恢复为其完整历史重复付费,并在没有原生摄取去重的 OTLP 后端上使查询时的计数翻倍。接收端基于 `session.parent_id` + `session.seed_length` 拼接 fork 谱系。此次收窄放弃的内容与至多一次立场一致:恢复不再回填上一个进程未能投递的记录(彼时遥测未挂载,或崩溃时仍在队列中)——这本是全量回放唯一的真实收益,代价却由常见情形承担。提出回填要求的部署需要的是上文已推迟的 outbox,而不是回放。该边界同样吞掉 `SessionPersistence.load()` 修复被崩溃打断的日志时写入的合成轮次关闭事件(它们落在 `firstLiveSeq` 之前,尽管在上一个进程中从未存在过)。这是有意为之,而非附带效果:远端轮次的真实尾部记录已随崩溃进程的队列一同消亡,导出合成关闭事件无法补全该轮次,只会让一个未完成的轮次看起来已经关闭。导出的流忠实于崩溃进程实际发出的内容;接收端会把恢复后的流中一个从未关闭的轮次读作「上一个进程死在了该轮次之内」(OTel README 陈述了这条规则),其后干净的 `shutdown` 标记也只证明恢复后进程自身的退出。若为让修复以实时事件的身份导出而将修复前边界贯穿 load/prepare 传递,将使三个包相互耦合,只为抹除这一信号。
**handoff 游标未存活时全量回放日志(重新导出构造函数种子)。** 首轮复活曾交付此方案,其后收窄:接管操作现在从会话的构造边界起回放(`Session.firstLiveSeq`,即构造函数种子长度;`Session.inheritedEventCount` 不能胜任,因为它是持久保存的 fork 谱系 cut,而恢复会话的构造函数种子是其完整的已存储日志)。恢复会话的历史已由上一个进程以同一 id 发出,fork 继承的前缀也已在父会话的流中发出;再次导出任何一者,都会让每次恢复为其完整历史重复付费,并在没有原生摄取去重的 OTLP 后端上使查询时的计数翻倍。接收端基于 `session.parent_id` + `session.seed_length` 拼接 fork 谱系。此次收窄放弃的内容与至多一次立场一致:恢复不再回填上一个进程未能投递的记录(彼时遥测未挂载,或崩溃时仍在队列中)——这本是全量回放唯一的真实收益,代价却由常见情形承担。提出回填要求的部署需要的是上文已推迟的 outbox,而不是回放。该边界同样吞掉 `SessionPersistence.load()` 修复被崩溃打断的日志时写入的合成轮次关闭事件(它们落在 `firstLiveSeq` 之前,尽管在上一个进程中从未存在过)。这是有意为之,而非附带效果:远端轮次的真实尾部记录已随崩溃进程的队列一同消亡,导出合成关闭事件无法补全该轮次,只会让一个未完成的轮次看起来已经关闭。导出的流忠实于崩溃进程实际发出的内容;接收端会把恢复后的流中一个从未关闭的轮次读作「上一个进程死在了该轮次之内」(OTel README 陈述了这条规则),其后干净的 `shutdown` 标记也只证明恢复后进程自身的退出。若为让修复以实时事件的身份导出而将修复前边界贯穿 load/prepare 传递,将使三个包相互耦合,只为抹除这一信号。
**将 seam 的轮次边界 `flush()` 提示转发到 OTel 提供方的 `forceFlush()`。** 首轮复活曾交付此转发,其后移除:三条不同的静默丢失路径共用同一份包装层状态——dispose 与进行中的 flush 之间的竞态(SDK 的并发 flush 防护会令 shutdown 的内部排空被跳过)、相互重叠的提示顶掉留存的 promise、以及提供方固定的 30 秒 flush 超时在批处理器仍在排空时便 reject。这些路径存在的唯一原因,是该转发让这个后端成为进程内第二个执行 flush 的组件,面对的还是上游实验性(experimental)源码树中未见诸文档的 SDK 内部行为;不实现 `flush()` 时,批处理器就是唯一执行 flush 的组件,其 `scheduledDelayMillis`(已可由部署方经 `processor` passthrough 调优)决定导出节奏,`shutdown()` 的排空从构造上就是完整的。仅当某个部署提出 `scheduledDelayMillis` 无法满足的轮次边界延迟要求时才恢复此转发——且届时应调用留存的 `BatchLogRecordProcessor` 自身的 `forceFlush()`,绝不调用提供方那个带超时包装的版本。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
2026-07-25-subagent-policy-inheritance.md: 34751a4e29e48c84d37425857b8b1b56c8d866eb
2026-07-25-subagent-policy-inheritance.zh.md: 3d5a6c7a88579583a0ab027c52dd3063eed0a8b2
2026-07-25-subagent-policy-inheritance.md: 6df9ad53f8588018fca53ebae3a9dfba0d4382de
2026-07-25-subagent-policy-inheritance.zh.md: f3f7328d558ea55379cb95e53bb010f9f2d2c792
@@ -12,7 +12,7 @@ Sandbox and approval overrides are per-session log folds. An in-process subagent
The delegation boundary snapshots `sandboxPolicy.overrideOf(parent.session)` before its first await, through the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`), which the one-shot driver and the [continuable start](2026-08-10-continuable-subagent-policy-inheritance.md) both call. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The sandbox-policy service is optional, and only the explicit session override is copied, never deployment defaults or one-shot grants. The approval policy is not inherited: the same capture pins every child to `'never'` — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` after the constructor seed, while `Session.inheritedEventCount` keeps the exact fork-prefix length, so the inherited facts follow fork history and reach telemetry when the child is announced without changing its lineage cut. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
Ordinary session appends validate the inherited events before publication, and persistence captures the complete unpublished log when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The `source: 'delegation'` marker lets approval narration distinguish inheritance from a child-side user switch.
@@ -12,7 +12,7 @@ Status: implemented
委派边界在第一次 await 之前,经由共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`)对 `sandboxPolicy.overrideOf(parent.session)` 获取快照;一次性驱动器与[可继续启动](2026-08-10-continuable-subagent-policy-inheritance.zh.md)都会调用这些辅助函数。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。沙箱策略服务为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承:同一次捕获会把每个子 agent 钉定为 `'never'`——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.zh.md)取代了本 note 原先的审批覆盖项继承。
每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已将 `Session.firstLiveSeq` 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已把 `Session.firstLiveSeq` 固定在 constructor seed 之后,而 `Session.inheritedEventCount` 保留精确的 fork 前缀长度,因此继承事实会排在 fork 历史之后,并在子 agent 公布时进入遥测,却不改变其谱系 cut。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
普通的会话追加会在发布前校验继承事件,持久化层则在会话公布时捕获完整的未发布日志。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。`source: 'delegation'` 标记让审批叙述能够区分继承与子 agent 侧的用户切换。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md
2026-08-05-durable-web-schedule.md: bac4a5cfd8965032dad2cf689ca42b1f8e5da1e3
2026-08-05-durable-web-schedule.zh.md: 266a90d3a56caccda56be9fe648c059939b8155f
2026-08-05-durable-web-schedule.md: 1e7a343472cce638a7d1476871cec221fa9fd889
2026-08-05-durable-web-schedule.zh.md: 50f3347a2c312e3c1a2ea28c582a277cc93a614e
@@ -22,11 +22,11 @@ The user-visible boundary is `session-local`: the original Session runs an on-ti
| Due while busy | Active create remains in the fold | Owner waits for idle maintenance, queues one follow-up, then appends dispatch | A later ordinary conversation turn |
| Several Every records are overdue | Each active record retains its earliest unaccepted anchor-aligned target | One decision selects each record's latest occurrence and advances it past now | One ordinary follow-up containing one occurrence per record |
| Process stopped or Session cold | Active create remains persisted | No timer or background scan; resume rebuilds the owner | Future target waits; overdue target is attempted |
| Fork | Parent events remain in the inherited prefix | Child fold starts at `seedLength` | Parent work does not become active in the child |
| Fork | Parent events remain in the inherited prefix | Child fold starts at the exact `inheritedEventCount` | Parent work does not become active in the child |
### Session-log authority and tools
The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after `SessionHeader.seedLength`.
The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after the `inheritedEventCount` passed into projection initialization.
When `ctx.sessionProjections` exists, Schedule registers a strict unit that uses the same transition and publishes the complete active `ScheduleRecord[]`; the shared [projection state decision](../architecture/2026-08-19-session-projection-state-and-client-views.md) owns its initialization and restore contract. Corrupt durable input fails the existing read path rather than yielding a partial array. The browser-safe record vocabulary is exposed through the type-only `@deepseek-ai/dsh-schedule/client` subpath.
@@ -22,11 +22,11 @@ Status: implemented
| 到期时繁忙 | 活动 create 仍在 fold 中 | owner 等待 idle maintenance,排入一个 follow-up,再追加 dispatch | 后续一个普通对话轮次 |
| 多条 Every 记录逾期 | 每条活动记录都保留最早一个尚未接受且与锚点对齐的目标 | 一次决策选择每条记录的最新发生时点,并将其推进到当前时刻之后 | 一个普通 follow-up,其中每条记录各有一个发生时点 |
| 进程停止或 Session cold | 活动 create 仍在 persistence 中 | 不存在 timer 或后台扫描;resume 重建 owner | 未来目标继续等待;overdue 目标会被尝试 |
| fork | 父 event 留在继承前缀 | child fold 从 `seedLength` 开始 | 父工作不会在 child 中变为活动状态 |
| fork | 父 event 留在继承前缀 | child fold 从精确 `inheritedEventCount` 开始 | 父工作不会在 child 中变为活动状态 |
### Session 日志权威与工具
版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠 `SessionHeader.seedLength` 位置及其后的 event。
版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠传入 projection 初始化的 `inheritedEventCount` 位置及其后的 event。
`ctx.sessionProjections` 存在时,Schedule 会注册一个复用同一 transition 的严格单元,并发布完整的活动 `ScheduleRecord[]`;共享的 [projection state 决策](../architecture/2026-08-19-session-projection-state-and-client-views.zh.md)拥有其初始化与 restore 约定。损坏的持久输入会使既有读取路径失败,而不会产生部分数组。浏览器安全的记录词汇通过纯类型子路径 `@deepseek-ai/dsh-schedule/client` 暴露。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
2026-06-22-fork-child-replay-seed-boundary.md: cf4a974035b38ee61e4c2d1cab34776b2ad186c8
2026-06-22-fork-child-replay-seed-boundary.zh.md: bcc60a583a5f9a4d1fd10ec16e90c49c879d295c
2026-06-22-fork-child-replay-seed-boundary.md: 1fefc3546ebe029ba95318dad3ac2d463a3fe497
2026-06-22-fork-child-replay-seed-boundary.zh.md: bb43eacf075318dc0101fca2cacb45a03d847764
@@ -18,19 +18,19 @@ Deriving the child script from the whole fork-child log therefore replays the **
Record where a session's **inherited** prefix ends, persist it, and have the replay harness derive a child's script from its **own** events only.
### 1. `seedLength` on the session header
### 1. Lineage metadata and an exact body-owned cut
`SessionHeader` gains an optional `seedLength: number` — how many leading events were inherited via a seed rather than produced by this session. The fork backend stamps it (= the seeded-prefix length) when it creates the child; a fresh spawn leaves it absent (≡ 0). It is threaded through `CreateSessionOptions.meta` (and `CreateAgentOptions.meta`), set in `SessionStore.prepare`.
`SessionHeader.isSeeded` records whether a Session has inherited lineage without exposing a body coordinate to header-only readers. The exact leading-event count is the separately branded `SessionLogOffset` `inheritedEventCount`; a fork supplies both `isSeeded: true` and the copied-prefix length, while a fresh spawn supplies an unseeded header and cut zero. The cut travels through `CreateSessionOptions`, `CreateAgentOptions`, persistence inspection, and restored Session state.
`seedLength` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the persisted `seedLength` back from the loaded header instead. (Same shape as `createdAt`, which is also explicitly preserved on reconstruction rather than re-defaulted to now.)
`inheritedEventCount` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the decoded cut beside the logical header instead.
### 2. JSONL round-trips it
JSONL stores `seedLength` on the header line (`toHeaderLine`/`fromHeaderLine`) and returns it through the shared persistence contract.
The v0 JSONL header keeps its optional numeric `seedLength` for byte compatibility. `toHeaderLine` / `fromHeaderLine` translate it to and from logical `isSeeded` plus the exact `inheritedEventCount`, which the shared body-bearing persistence values return separately.
### 3. Replay derives a child script after the boundary
`dsh-llm-replay`'s `parseSessionHeader` now also reads `seedLength` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(seedLength)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child `seedLength` is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
`dsh-llm-replay`'s private v0 parser reads physical `seedLength` into `inheritedEventCount` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(inheritedEventCount)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child the cut is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
This closes the routing correctness gap, and two recorded fork scenarios exercise it end to end — see [Record fork and mixed spawn+fork snapshot scenarios](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md).
@@ -40,5 +40,5 @@ This closes the routing correctness gap, and two recorded fork scenarios exercis
## Consequences
- A new persisted header field spans core and the JSONL provider; the subsystems catalog (`persistence.md`) is updated in the same change (its `SessionHeader` / `CreateSessionOptions` `type-equiv` blocks).
- Spawn replay is unchanged (`seedLength` 0). Fork replay now routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.
- The lineage bit spans logical Session metadata while the exact cut spans only body-bearing core, persistence, query, and replay values; the v0 physical header remains unchanged.
- Spawn replay is unchanged (cut 0). Fork replay routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.
@@ -18,19 +18,19 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/test-support/llm-
记录会话**继承**前缀的结束位置,将其持久化,并让回放 harness 仅从子会话**自身**的事件推导脚本。
### 1. 会话头部的 `seedLength`
### 1. 谱系 metadata 与正文拥有的精确 cut
`SessionHeader` 新增可选字段 `seedLength: number`——表示有多少前导事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= 播种前缀的长度);全新的 spawn 子会话不设置(等同于 0)。它通过 `CreateSessionOptions.meta`(及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置。
`SessionHeader.isSeeded` 记录 Session 是否具有继承谱系,而不向仅 header 的 reader 暴露正文坐标。精确的前导事件数量是单独品牌化为 `SessionLogOffset` 的 `inheritedEventCount`;fork 同时提供 `isSeeded: true` 与复制前缀的长度,全新的 spawn 则提供 unseeded header 与零 cut。该 cut 经 `CreateSessionOptions`、`CreateAgentOptions`、持久化 inspection 与恢复后的 Session 状态传递。
`seedLength` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为从加载的 header 中取回持久化的 `seedLength`。(做法与 `createdAt` 相同:恢复时显式保留,而非重新默认为当前时间。)
`inheritedEventCount` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为在 logical header 之外传递解码后的 cut。
### 2. JSONL 完整往返
JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),并通过共享持久化约定返回它。
v0 JSONL header 为保持字节兼容而继续携带可选数值 `seedLength`。`toHeaderLine`/`fromHeaderLine` 在它与 logical `isSeeded` 加精确 `inheritedEventCount` 之间转换,共享的含正文持久化值再单独返回该 cut。
### 3. 回放从边界之后推导子会话脚本
`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 `seedLength` 为 0,此操作是空操作,spawn 场景逐字节不变。
`dsh-llm-replay` 的私有 v0 parser 把物理 `seedLength` 读入 `inheritedEventCount`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(inheritedEventCount)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 cut 为 0,此操作是空操作,spawn 场景逐字节不变。
这弥补了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md)。
@@ -40,5 +40,5 @@ JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),
## 后果
- core 与 JSONL provider 新增一个持久化 header 字段;子系统目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。
- spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。
- 谱系 bit 横跨 logical Session metadata,精确 cut 则只横跨含正文的 core、持久化、query 与 replay 值;v0 物理 header 保持不变。
- spawn 回放不变(cut 为 0)。fork 回放将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。
@@ -5,7 +5,7 @@ import { Context } from '@deepseek-ai/cordis'
import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage, ToolCallId , createMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
@@ -29,17 +29,18 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
id: sessionId,
createdAt: 1,
cwd,
isSeeded: false,
delegationDepth: 0,
}
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: 1, time: 11, data: createUserMessage({
{ type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({
content: [{ type: 'text', text: 'Perform one side-effecting remote mutation.' }], source: { kind: 'user' },
}), surfaceOp: 'append' },
{ type: 'step/start', seq: 2, time: 12, data: { turn: 1, step: 1 } },
{ type: 'step/start', seq: SessionSeq(2), time: 12, data: { turn: 1, step: 1 } },
{
type: 'assistant/message',
seq: 3,
seq: SessionSeq(3),
time: 13,
data: {
turn: 1,
@@ -57,7 +58,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
},
{
type: 'tool/call',
seq: 4,
seq: SessionSeq(4),
time: 14,
data: {
turn: 1,
@@ -13,6 +13,7 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-l
import SessionStore, {
SESSION_FORMAT_VERSION,
SessionId,
SessionSeq,
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
@@ -32,7 +33,7 @@ async function seedSession(root: string, cwd: string, version: number, events: S
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd }
const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd, isSeeded: false }
try {
await ctx.sessionPersistence.create(meta)
await ctx.sessionPersistence.append(sessionId, events)
@@ -46,8 +47,8 @@ async function seedSession(root: string, cwd: string, version: number, events: S
function closedTurn(): SessionEvent[] {
return [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
{ type: 'turn/end', seq: SessionSeq(1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
]
}
@@ -92,7 +93,7 @@ describe('session format guard through the assembled app', () => {
prepare: async (runCwd) => {
sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION, [
...closedTurn(),
{ type: 'future/event', seq: 2, time: 3, data: { payload: 1 } } as unknown as SessionEvent,
{ type: 'future/event', seq: SessionSeq(2), time: 3, data: { payload: 1 } } as unknown as SessionEvent,
])
},
})
@@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis'
import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
@@ -40,12 +40,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
id: parentId,
createdAt: 1,
cwd,
isSeeded: false,
delegationDepth: 0,
}
const parentEvents: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
{ type: 'turn/end', seq: 2, time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
{ type: 'turn/end', seq: SessionSeq(2), time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
]
const childMeta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
@@ -53,12 +54,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
createdAt: 2,
cwd,
parentSession: parentId,
isSeeded: false,
origin: 'subagent',
delegationDepth: 1,
}
const childEvents: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 20, data: { turn: 1 } },
{ type: 'turn/end', seq: 1, time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
{ type: 'turn/start', seq: SessionSeq(0), time: 20, data: { turn: 1 } },
{ type: 'turn/end', seq: SessionSeq(1), time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
]
try {
await ctx.sessionPersistence.create(parentMeta)
@@ -10,7 +10,7 @@ import { Context } from '@deepseek-ai/cordis'
import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
@@ -36,15 +36,16 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
id: sessionId,
createdAt: 1,
cwd,
isSeeded: false,
delegationDepth: 0,
}
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
{ type: 'sandbox/mode', seq: 2, time: 12, data: { mode: 'read-only' } },
{ type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
{ type: 'sandbox/mode', seq: SessionSeq(2), time: 12, data: { mode: 'read-only' } },
{
type: 'request/header',
seq: 3,
seq: SessionSeq(3),
time: 13,
data: {
header: {
@@ -57,7 +58,7 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
reason: 'initial',
},
},
{ type: 'turn/end', seq: 4, time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'turn/end', seq: SessionSeq(4), time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
]
try {
await ctx.sessionPersistence.create(meta)
@@ -14,6 +14,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SESSION_FORMAT_VERSION,
SessionId,
SessionSeq,
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
@@ -53,6 +54,7 @@ async function seedVisibleBaseline(
id: sessionId,
createdAt: 1,
cwd,
isSeeded: false,
delegationDepth: 0,
}
const files = options.files ?? [{ name: 'AGENTS.md', content: oldInstruction }]
@@ -69,17 +71,17 @@ async function seedVisibleBaseline(
: { instructionFileCandidates: options.instructionFileCandidates },
})
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
{ type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
{
type: 'user/message',
seq: 1,
seq: SessionSeq(1),
time: 11,
data: createUserMessage({ content: [{ type: 'text', text: 'Remember the workspace instruction.' }], source: { kind: 'user' } }),
surfaceOp: 'append',
},
{
type: 'user/message',
seq: 2,
seq: SessionSeq(2),
time: 12,
data: createUserMessage({
content: [{ type: 'text', text: baseline.text }],
@@ -98,7 +100,7 @@ async function seedVisibleBaseline(
}),
surfaceOp: 'append',
},
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'turn/end', seq: SessionSeq(3), time: 13, data: { turn: 1, reason: { kind: 'completed' } } },
]
try {
await ctx.sessionPersistence.create(meta)
+6 -4
View File
@@ -6,7 +6,7 @@ import { dirname, join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { SessionId } from '@deepseek-ai/dsh-session'
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
@@ -655,9 +655,11 @@ describe('a forked session', () => {
const inherited = ctx.sessionProjections.stateOf(parent.agent.session, 'agentPreset') ?? undefined
const child = await ctx.agents.create({
sessionId: SessionId('preset-fork-child'),
seed: [],
inheritedEventCount: SessionLogOffset(0),
meta: {
parentSession: SessionId('preset-fork-parent'),
seedLength: 0,
isSeeded: true,
...inherited === undefined ? {} : { agentPreset: inherited },
},
setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
@@ -685,7 +687,7 @@ describe('a delegated child', () => {
// Exactly what an in-process subagent driver's creation window does.
const child = await parent.agent.ctx.agents.create({
sessionId: SessionId('preset-child'),
meta: childSessionMeta(parent.agent, 1, 0),
meta: childSessionMeta(parent.agent, 1, false),
setup: (agentCtx) => {
applyChildComposition(agentCtx, parent.agent, {})
},
@@ -711,7 +713,7 @@ describe('a delegated child', () => {
await ctx.agentPresets.recompose(parent.agent.ctx, 'minimal')
const child = await parent.agent.ctx.agents.create({
sessionId: SessionId('preset-child-switch'),
meta: childSessionMeta(parent.agent, 1, 0),
meta: childSessionMeta(parent.agent, 1, false),
setup: (agentCtx) => {
applyChildComposition(agentCtx, parent.agent, {})
},
+11 -7
View File
@@ -15,8 +15,9 @@ import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { MessageId } from '@deepseek-ai/dsh-llm'
import {
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionHeader, type SessionId,
SESSION_FORMAT_VERSION, SessionId as sessionId, SessionSeq, type SessionHeader, type SessionId,
} from '@deepseek-ai/dsh-session'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import {
@@ -116,6 +117,7 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
createdAt,
cwd: scaffold.workspaceCwd,
parentSession: parentId,
isSeeded: false,
origin: 'subagent',
delegationDepth: 1,
agentPreset: 'minimal',
@@ -124,15 +126,17 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
await scaffold.ctx.sessionPersistence.append(childId, [
{
type: 'turn/start',
seq: 0,
seq: SessionSeq(0),
time: createdAt,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
data: { turn: 1 },
},
{
type: 'user/message',
seq: 1,
seq: SessionSeq(1),
time: createdAt + 1,
data: {
id: MessageId(`legacy-message:${childId}:1`),
role: 'user',
content: [{ type: 'text', text: 'Check the session-header action order.' }],
source: { kind: 'user' },
},
@@ -140,7 +144,7 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
},
{
type: 'subagent/descriptor',
seq: 2,
seq: SessionSeq(2),
time: createdAt + 2,
data: snapshotSubagentDescriptor({
mode: 'one-shot', provider: 'spawn', label: 'header order probe',
@@ -148,11 +152,11 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
},
{
type: 'turn/end',
seq: 3,
seq: SessionSeq(3),
time: createdAt + 3,
data: { turn: 1, reason: { kind: 'completed' } },
},
] as SessionEvent[])
])
}
/**
+1 -1
View File
@@ -311,7 +311,7 @@ describe('web e2e: long Chat interaction contract', () => {
const child = scaffold.ctx.agents.list()
.find(agent => agent.session.header.parentSession === SessionId(SESSION_ID))
if (child === undefined) throw new Error('message branch did not create a child session')
expect(child.session.header.seedLength).toBe(boundary.seq + 1)
expect(child.session.inheritedEventCount).toBe(boundary.seq + 1)
expect(child.session.snapshotEvents().some(event => carries(event, branchAssistantMarker))).toBe(true)
expect(child.session.snapshotEvents().some(event => carries(event, FIXTURE.markers.user(BRANCH_TURN + 1)))).toBe(false)
expect(child.session.snapshotEvents().some(event => carries(event, FIXTURE.markers.user(FIXTURE.turns)))).toBe(false)
@@ -591,6 +591,18 @@ describe('web e2e: long Chat scroll contract', () => {
await expect.poll(() => tooltip.count(), { timeout: 15_000 }).toBe(1)
expect(await tooltip.textContent()).toContain(HISTORY_FIXTURE.markers.user(1))
expect(await tooltip.textContent()).toContain(HISTORY_FIXTURE.markers.assistant(1))
const transcriptLayers = await tooltip.evaluate((preview) => {
const railSlot = preview.closest('nav')?.parentElement
const codeBanner = document.querySelector<HTMLElement>('.md-code-block > :first-child')
if (!(railSlot instanceof HTMLElement) || codeBanner === null) {
throw new Error('turn preview or code-block banner stacking context is unavailable')
}
return {
codeBanner: Number(getComputedStyle(codeBanner).zIndex),
rail: Number(getComputedStyle(railSlot).zIndex),
}
})
expect(transcriptLayers.rail).toBeGreaterThan(transcriptLayers.codeBanner)
await world.page.keyboard.press('Enter')
// The jump pages history in and lands on turn 1: its mark flips to the
+2 -2
View File
@@ -17,7 +17,7 @@ import {
createUserMessage,
} from '@deepseek-ai/dsh-llm'
import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionSeq } from '@deepseek-ai/dsh-session'
import {
SESSION_FORMAT_VERSION,
Session,
@@ -187,7 +187,7 @@ function text(value: string): { type: 'text'; text: string }[] {
return [{ type: 'text', text: value }]
}
function appendTitle(session: Session, title: string, messageSeq: number): void {
function appendTitle(session: Session, title: string, messageSeq: SessionSeq): void {
session.append('session/title', {
title,
messageSeqs: [messageSeq],
+2 -1
View File
@@ -12,6 +12,7 @@ import {
SESSION_FORMAT_VERSION,
Session,
SessionId,
SessionSeq,
} from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-reference/types'
import type {} from '@deepseek-ai/dsh-session-title'
@@ -80,7 +81,7 @@ function targetSessionFixture(): string {
references: [{
sessionId: SOURCE_SESSION_ID,
label: 'Research notes',
capturedThroughSeq: 4,
capturedThroughSeq: SessionSeq(4),
compacted: false,
originalMessages: 2,
retainedMessages: 2,
+17 -2
View File
@@ -67,6 +67,7 @@ import SessionStore, {
packChunkRuns,
SESSION_FORMAT_VERSION,
SessionId,
SessionSeq,
type Session,
type SessionEvent,
type SessionHeader,
@@ -801,8 +802,20 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
* in-memory record-mode harvest, so the on-disk zstd default never matters.
*/
function rawSessionLog(session: Session): string {
const header = session.header
return [
JSON.stringify({ type: 'session', ...session.header }),
JSON.stringify({
type: 'session',
version: header.version,
id: header.id,
createdAt: header.createdAt,
...header.cwd === undefined ? {} : { cwd: header.cwd },
...header.parentSession === undefined ? {} : { parentSession: header.parentSession },
...header.isSeeded ? { seedLength: Number(session.inheritedEventCount) } : {},
...header.origin === undefined ? {} : { origin: header.origin },
...header.delegationDepth === undefined ? {} : { delegationDepth: header.delegationDepth },
...header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset },
}),
...packChunkRuns(session.snapshotEvents()).map(record => JSON.stringify(record)),
'',
].join('\n')
@@ -1030,6 +1043,7 @@ export async function seedSession(
id: SessionId(id),
createdAt: Date.now() - 60_000,
cwd: scaffold.workspaceCwd,
isSeeded: false,
delegationDepth: 0,
...agentPreset === undefined ? {} : { agentPreset },
}
@@ -1054,11 +1068,12 @@ export async function seedBlankSession(
id: SessionId(id),
createdAt: Date.now() - 60_000,
cwd,
isSeeded: false,
delegationDepth: 0,
}
await persistSeedSession(scaffold, meta, [{
type: 'session/end-seed',
seq: 0,
seq: SessionSeq(0),
time: meta.createdAt,
data: {},
}])
+7 -3
View File
@@ -10,7 +10,7 @@ import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
import { composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import { SessionId, SessionLogOffset, type SessionEvent } from '@deepseek-ai/dsh-session'
import {
ScheduleId,
createEveryScheduleRecord,
@@ -615,8 +615,12 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
await workspace.attachSession(CATALOG_SESSION_ID)
// Seed the zero-I/O list view before the Session is opened.
const catalog = await scaffold.ctx.sessionPersistence.readFrom(CATALOG_SESSION_ID, 0)
scaffold.ctx.sessionProjectionCache.coldSnapshot(catalog.meta, catalog.events)
const catalog = await scaffold.ctx.sessionPersistence.readFrom(CATALOG_SESSION_ID, SessionLogOffset(0))
scaffold.ctx.sessionProjectionCache.coldSnapshot(
catalog.meta,
catalog.inheritedEventCount,
catalog.events,
)
browser = await chromium.launch()
page = await browser.newPage({
+26 -18
View File
@@ -5,8 +5,9 @@ import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { MessageId } from '@deepseek-ai/dsh-llm'
import {
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionHeader, type SessionId,
SESSION_FORMAT_VERSION, SessionId as sessionId, SessionLogOffset, SessionSeq, type SessionEvent, type SessionHeader, type SessionId,
} from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-agent'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
@@ -66,9 +67,10 @@ async function waitForAgentToSettle(scaffold: WebScaffold, id: SessionId): Promi
async function waitForCacheRow(
scaffold: WebScaffold,
header: SessionHeader,
inheritedEventCount: SessionLogOffset,
): Promise<void> {
const deadline = Date.now() + 10_000
while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header) === undefined) {
while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header, inheritedEventCount) === undefined) {
if (Date.now() >= deadline) throw new Error(`cache row for "${header.id}" did not land`)
await new Promise<void>(resolve => setTimeout(resolve, 10))
}
@@ -136,6 +138,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
createdAt: oneShotAt,
cwd: scaffold.workspaceCwd,
parentSession: parent.id,
isSeeded: false,
origin: 'subagent',
delegationDepth: 1,
}
@@ -143,15 +146,17 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
const oneShotEvents = [
{
type: 'turn/start',
seq: 0,
seq: SessionSeq(0),
time: oneShotAt,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
data: { turn: 1 },
},
{
type: 'user/message',
seq: 1,
seq: SessionSeq(1),
time: oneShotAt + 1,
data: {
id: MessageId(`legacy-message:${oneShotId}:1`),
role: 'user',
content: [{ type: 'text', text: 'Review the event sourcing explanation.' }],
source: { kind: 'user' },
},
@@ -159,7 +164,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
},
{
type: 'subagent/descriptor',
seq: 2,
seq: SessionSeq(2),
time: oneShotAt + 2,
data: snapshotSubagentDescriptor({
mode: 'one-shot', provider: 'spawn', label: ONE_SHOT_LABEL,
@@ -167,14 +172,14 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
},
{
type: 'turn/end',
seq: 3,
seq: SessionSeq(3),
time: oneShotAt + oneShotDurationMs,
data: { turn: 1, reason: { kind: 'completed' } },
},
] as SessionEvent[]
] satisfies SessionEvent[]
await scaffold.ctx.sessionPersistence.append(oneShotId, oneShotEvents)
scaffold.ctx.sessionProjectionCache.coldSnapshot(oneShotHeader, oneShotEvents)
await waitForCacheRow(scaffold, oneShotHeader)
scaffold.ctx.sessionProjectionCache.coldSnapshot(oneShotHeader, SessionLogOffset(0), oneShotEvents)
await waitForCacheRow(scaffold, oneShotHeader, SessionLogOffset(0))
grandchildId = sessionId('recorded-grandchild')
const authoredAt = Date.now()
const grandchildHeader: SessionHeader = {
@@ -183,6 +188,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
createdAt: authoredAt,
cwd: scaffold.workspaceCwd,
parentSession: childId,
isSeeded: false,
origin: 'subagent',
delegationDepth: 2,
}
@@ -190,15 +196,17 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
const grandchildEvents = [
{
type: 'turn/start',
seq: 0,
seq: SessionSeq(0),
time: authoredAt,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
data: { turn: 1 },
},
{
type: 'user/message',
seq: 1,
seq: SessionSeq(1),
time: authoredAt + 1,
data: {
id: MessageId(`legacy-message:${grandchildId}:1`),
role: 'user',
content: [{ type: 'text', text: NESTED_PROMPT }],
source: { kind: 'user' },
},
@@ -206,7 +214,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
},
{
type: 'subagent/descriptor',
seq: 2,
seq: SessionSeq(2),
time: authoredAt + 2,
data: snapshotSubagentDescriptor({
mode: 'continuable', provider: 'spawn', label: NESTED_LABEL,
@@ -214,14 +222,14 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
},
{
type: 'turn/end',
seq: 3,
seq: SessionSeq(3),
time: authoredAt + 3,
data: { turn: 1, reason: { kind: 'completed' } },
},
] as SessionEvent[]
] satisfies SessionEvent[]
await scaffold.ctx.sessionPersistence.append(grandchildId, grandchildEvents)
scaffold.ctx.sessionProjectionCache.coldSnapshot(grandchildHeader, grandchildEvents)
await waitForCacheRow(scaffold, grandchildHeader)
scaffold.ctx.sessionProjectionCache.coldSnapshot(grandchildHeader, SessionLogOffset(0), grandchildEvents)
await waitForCacheRow(scaffold, grandchildHeader, SessionLogOffset(0))
expect(scaffold.ctx.agents.get(childId)).toBeUndefined()
expect(scaffold.ctx.agents.get(oneShotId)).toBeUndefined()
expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined()
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: aead833370a0301ed878fb0eaad1a9a8492cab02
config-catalog.zh.md: 35e6af02276273efd4b457a060ac55d6541c0965
config-catalog.md: 15c9daf2e6e2817d4ca9d28b486e45fe08e47987
config-catalog.zh.md: 89cd06451d325239f988db72bff9978e8a01909b
+15 -15
View File
@@ -219,7 +219,7 @@ export interface Config {
}
```
Source: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts)
Source: [`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts)
<a id="deepseek-aidsh-api-settings-controller"></a>
@@ -461,7 +461,7 @@ export interface ToolResultPruneConfig {
}
```
Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
<a id="deepseek-aidsh-cordis-host-runner"></a>
@@ -767,7 +767,7 @@ export interface Config {
}
```
Source: [`packages/goal/goal/src/index.ts:171`](../packages/goal/goal/src/index.ts)
Source: [`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
<a id="deepseek-aidsh-headless"></a>
@@ -783,7 +783,7 @@ export interface Config {
}
```
Source: [`packages/bundle/headless/src/index.ts:33`](../packages/bundle/headless/src/index.ts)
Source: [`packages/bundle/headless/src/index.ts:34`](../packages/bundle/headless/src/index.ts)
<a id="deepseek-aidsh-hooks-claude-code"></a>
@@ -1363,7 +1363,7 @@ export interface ReplayModelConfig {
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
Source: [`packages/test-support/llm-replay/src/index.ts:919`](../packages/test-support/llm-replay/src/index.ts)
Source: [`packages/test-support/llm-replay/src/index.ts:924`](../packages/test-support/llm-replay/src/index.ts)
<a id="deepseek-aidsh-llm-retry"></a>
@@ -1507,7 +1507,7 @@ export interface Config {
}
```
Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
Source: [`packages/feedback/message-feedback/src/index.ts:50`](../packages/feedback/message-feedback/src/index.ts)
<a id="deepseek-aidsh-permission-presets"></a>
@@ -1808,7 +1808,7 @@ export interface Config {
}
```
Source: [`packages/session/session-log-deepseek/src/index.ts:23`](../packages/session/session-log-deepseek/src/index.ts)
Source: [`packages/session/session-log-deepseek/src/index.ts:36`](../packages/session/session-log-deepseek/src/index.ts)
<a id="deepseek-aidsh-session-log-export"></a>
@@ -1866,7 +1866,7 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
Source: [`packages/session/session-persistence-jsonl/src/index.ts:62`](../packages/session/session-persistence-jsonl/src/index.ts)
Source: [`packages/session/session-persistence-jsonl/src/index.ts:70`](../packages/session/session-persistence-jsonl/src/index.ts)
<a id="deepseek-aidsh-session-projection-cache"></a>
@@ -1890,7 +1890,7 @@ export interface Config {
}
```
Source: [`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
Source: [`packages/session/session-projection-cache/src/index.ts:55`](../packages/session/session-projection-cache/src/index.ts)
<a id="deepseek-aidsh-session-query-sqlite"></a>
@@ -1936,7 +1936,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
Depends on: [`SessionQueryConfig`](../packages/session-query/session-query/src/index.ts)
Source: [`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
Source: [`packages/session-query/session-query-sqlite/src/index.ts:96`](../packages/session-query/session-query-sqlite/src/index.ts)
<a id="deepseek-aidsh-session-reference"></a>
@@ -2022,7 +2022,7 @@ export interface Config {
}
```
Source: [`packages/session/session-title/src/index.ts:55`](../packages/session/session-title/src/index.ts)
Source: [`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
<a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
@@ -2591,7 +2591,7 @@ export interface Config {
}
```
Source: [`packages/context/time-context/src/index.ts:48`](../packages/context/time-context/src/index.ts)
Source: [`packages/context/time-context/src/index.ts:49`](../packages/context/time-context/src/index.ts)
<a id="deepseek-aidsh-tmux-context"></a>
@@ -2620,7 +2620,7 @@ Requires: `sessionProjections`
export type TokenMeterConfig = Record<string, never>
```
Source: [`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
Source: [`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
<a id="deepseek-aidsh-tool-bash"></a>
@@ -2985,7 +2985,7 @@ export interface Config {
Depends on: [`AgentOptions`](subsystems/core.md)
Source: [`packages/subagent/tool-subagent/src/index.ts:46`](../packages/subagent/tool-subagent/src/index.ts)
Source: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts)
<a id="deepseek-aidsh-tool-terminal"></a>
@@ -3162,7 +3162,7 @@ export interface Config {
export type ApprovalPolicy = 'ask' | 'never'
```
Source: [`packages/interaction/user-approval/src/index.ts:126`](../packages/interaction/user-approval/src/index.ts)
Source: [`packages/interaction/user-approval/src/index.ts:127`](../packages/interaction/user-approval/src/index.ts)
<a id="deepseek-aidsh-web"></a>
+11 -11
View File
@@ -221,7 +221,7 @@ export interface Config {
}
```
来源:[`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts)
来源:[`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts)
<a id="deepseek-aidsh-api-settings-controller"></a>
@@ -463,7 +463,7 @@ export interface ToolResultPruneConfig {
}
```
来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
<a id="deepseek-aidsh-cordis-host-runner"></a>
@@ -769,7 +769,7 @@ export interface Config {
}
```
来源:[`packages/goal/goal/src/index.ts:171`](../packages/goal/goal/src/index.ts)
来源:[`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
<a id="deepseek-aidsh-headless"></a>
@@ -1365,7 +1365,7 @@ export interface ReplayModelConfig {
依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
来源:[`packages/test-support/llm-replay/src/index.ts:919`](../packages/test-support/llm-replay/src/index.ts)
来源:[`packages/test-support/llm-replay/src/index.ts:924`](../packages/test-support/llm-replay/src/index.ts)
<a id="deepseek-aidsh-llm-retry"></a>
@@ -1509,7 +1509,7 @@ export interface Config {
}
```
来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
来源:[`packages/feedback/message-feedback/src/index.ts:50`](../packages/feedback/message-feedback/src/index.ts)
<a id="deepseek-aidsh-permission-presets"></a>
@@ -1810,7 +1810,7 @@ export interface Config {
}
```
来源:[`packages/session/session-log-deepseek/src/index.ts:23`](../packages/session/session-log-deepseek/src/index.ts)
来源:[`packages/session/session-log-deepseek/src/index.ts:36`](../packages/session/session-log-deepseek/src/index.ts)
<a id="deepseek-aidsh-session-log-export"></a>
@@ -1868,7 +1868,7 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
来源:[`packages/session/session-persistence-jsonl/src/index.ts:62`](../packages/session/session-persistence-jsonl/src/index.ts)
来源:[`packages/session/session-persistence-jsonl/src/index.ts:70`](../packages/session/session-persistence-jsonl/src/index.ts)
<a id="deepseek-aidsh-session-projection-cache"></a>
@@ -1892,7 +1892,7 @@ export interface Config {
}
```
来源:[`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
来源:[`packages/session/session-projection-cache/src/index.ts:55`](../packages/session/session-projection-cache/src/index.ts)
<a id="deepseek-aidsh-session-query-sqlite"></a>
@@ -1938,7 +1938,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
依赖:[`SessionQueryConfig`](../packages/session-query/session-query/src/index.ts)
来源:[`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
来源:[`packages/session-query/session-query-sqlite/src/index.ts:96`](../packages/session-query/session-query-sqlite/src/index.ts)
<a id="deepseek-aidsh-session-reference"></a>
@@ -2024,7 +2024,7 @@ export interface Config {
}
```
来源:[`packages/session/session-title/src/index.ts:55`](../packages/session/session-title/src/index.ts)
来源:[`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
<a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
@@ -2622,7 +2622,7 @@ export interface Config {
export type TokenMeterConfig = Record<string, never>
```
来源:[`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
来源:[`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
<a id="deepseek-aidsh-tool-bash"></a>
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: 2fd8d0331fe71ff886b29fa76e340bc5ea7aa2b1
event-producer-consumer.zh.md: 839185241171d1ecc3ebc9041707e55c904ee138
event-producer-consumer.md: 409f7c14b2d2fc042f4100f7d767bb493d0e1d73
event-producer-consumer.zh.md: 41ea2a736a39362e621e93fa2cc68db79005c091
+7 -7
View File
@@ -21,14 +21,14 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:579`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:585`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:592`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:598`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:625`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
@@ -45,7 +45,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:62`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:83`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+9 -9
View File
@@ -23,22 +23,22 @@
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:579`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:585`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:592`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:598`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:625`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:398`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:368`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:96`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:84`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
@@ -47,7 +47,7 @@
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:62`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:83`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: fd5b3a4a377069fb7b1fbc37b78d11ec037073a4
persistence-catalog.zh.md: 9d8fc88a44da1620e2de9aa066b9d6c6204b29b1
persistence-catalog.md: 2fb2b020841a42da6f7505928a87868fc43bc559
persistence-catalog.zh.md: 4014fd2c10e20193ea68a5f6c761c99b4569b032
+32 -32
View File
@@ -40,7 +40,7 @@ export type SurfaceEventType =
*/
export type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
| { op: 'replace'; start: SessionSeq; end: SessionSeq }
/**
* One immutable entry in the session log.
@@ -59,7 +59,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
seq: SessionSeq
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
@@ -83,14 +83,14 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
* provider stream; when the field is absent, the event does not record which
* earlier events produced the message.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
}[T]
```
Sources: [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:359`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:373`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:402`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:434`](../packages/core/session/src/types.ts)
## Events
@@ -200,7 +200,7 @@ Source: [`packages/interaction/user-approval/src/types.ts:55`](../packages/inter
}
```
Source: [`packages/interaction/user-approval/src/index.ts:32`](../packages/interaction/user-approval/src/index.ts)
Source: [`packages/interaction/user-approval/src/index.ts:33`](../packages/interaction/user-approval/src/index.ts)
### `assistant/*`
@@ -215,7 +215,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:32`](../packages/inter
Types: [StreamChunk](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
<a id="assistantmessage--surface"></a>
@@ -237,7 +237,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/
Types: [TokenUsage](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts)
### `command/*`
@@ -256,11 +256,11 @@ Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/
commandId: CommandId
kind: 'success' | 'error'
text?: string
sourceEventSeq?: number
sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq
}
```
Source: [`packages/interaction/commands/src/types.ts:103`](../packages/interaction/commands/src/types.ts)
Source: [`packages/interaction/commands/src/types.ts:104`](../packages/interaction/commands/src/types.ts)
<a id="commandrun--log-only"></a>
@@ -280,7 +280,7 @@ Source: [`packages/interaction/commands/src/types.ts:103`](../packages/interacti
'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
```
Source: [`packages/interaction/commands/src/types.ts:96`](../packages/interaction/commands/src/types.ts)
Source: [`packages/interaction/commands/src/types.ts:97`](../packages/interaction/commands/src/types.ts)
### `compaction/*`
@@ -296,7 +296,7 @@ Source: [`packages/interaction/commands/src/types.ts:96`](../packages/interactio
'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string }
```
Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
Source: [`packages/compaction/compaction/src/types.ts:72`](../packages/compaction/compaction/src/types.ts)
<a id="compactionprune--log-only"></a>
@@ -314,15 +314,15 @@ Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compactio
*/
'compaction/prune': {
/** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
shadowedRange: { start: number; end: number }
shadowedRange: { start: SessionSeq; end: SessionSeq }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
/** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
shadowedTokenCount: number
}
```
Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
Source: [`packages/compaction/compaction/src/types.ts:82`](../packages/compaction/compaction/src/types.ts)
<a id="compactionstart--log-only"></a>
@@ -337,7 +337,7 @@ Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compactio
'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null }
```
Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
Source: [`packages/compaction/compaction/src/types.ts:24`](../packages/compaction/compaction/src/types.ts)
<a id="compactionsummary--log-only"></a>
@@ -357,8 +357,8 @@ Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compactio
compactionId: CompactionId
sourceCommandId?: CommandId
summary: ContentBlock[]
shadowedRange: { start: number; end: number }
shadowedSeqs: number[]
shadowedRange: { start: SessionSeq; end: SessionSeq }
shadowedSeqs: SessionSeq[]
shadowedTokenCount: number
/** The provider route that wrote the summary. */
provider: string
@@ -391,7 +391,7 @@ Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compactio
Types: [ContentBlock](subsystems/core.md) · [TokenUsage](subsystems/llm-streaming.md)
Source: [`packages/compaction/compaction/src/types.ts:33`](../packages/compaction/compaction/src/types.ts)
Source: [`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts)
### `feedback/*`
@@ -563,7 +563,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:46`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:339`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -582,7 +582,7 @@ Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/
}
```
Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:329`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -657,7 +657,7 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
'session/end-seed': Record<string, never>
```
Source: [`packages/core/session/src/types.ts:319`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:362`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -673,7 +673,7 @@ Source: [`packages/core/session/src/types.ts:319`](../packages/core/session/src/
Types: [SessionTitleEventData](subsystems/session-title.md)
Source: [`packages/session/session-title/src/index.ts:76`](../packages/session/session-title/src/index.ts)
Source: [`packages/session/session-title/src/index.ts:77`](../packages/session/session-title/src/index.ts)
<a id="sessiontitle-llm-request--log-only"></a>
@@ -686,7 +686,7 @@ Source: [`packages/session/session-title/src/index.ts:76`](../packages/session/s
Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md)
Source: [`packages/session/session-title-llm/src/index.ts:44`](../packages/session/session-title-llm/src/index.ts)
Source: [`packages/session/session-title-llm/src/index.ts:45`](../packages/session/session-title-llm/src/index.ts)
### `session-log-deepseek/*`
@@ -700,11 +700,11 @@ Source: [`packages/session/session-title-llm/src/index.ts:44`](../packages/sessi
/** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */
sessionId: import('@deepseek-ai/dsh-session/types').SessionId
/** Last canonical event included in the accepted request. */
throughSeq: number
throughSeq: import('@deepseek-ai/dsh-session/types').SessionSeq
}
```
Source: [`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts)
Source: [`packages/session/session-log-deepseek/src/types.ts:57`](../packages/session/session-log-deepseek/src/types.ts)
### `step/*`
@@ -717,7 +717,7 @@ Source: [`packages/session/session-log-deepseek/src/types.ts:26`](../packages/se
'step/end': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
<a id="stepstart--log-only"></a>
@@ -728,7 +728,7 @@ Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -859,7 +859,7 @@ Source: [`packages/todo/tool-todo/src/types.ts:31`](../packages/todo/tool-todo/s
Types: [ToolCallId](subsystems/core.md)
Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:306`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -934,7 +934,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
@@ -1014,7 +1014,7 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow
Types: [TurnEndReason](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
<a id="turnstart--log-only"></a>
@@ -1030,7 +1030,7 @@ Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/
'turn/start': { turn: number }
```
Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
### `user/*`
@@ -1049,7 +1049,7 @@ Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/
'user/message': UserMessage
```
Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:287`](../packages/core/session/src/types.ts)
### `web/*`
+31 -31
View File
@@ -42,7 +42,7 @@ export type SurfaceEventType =
*/
export type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
| { op: 'replace'; start: SessionSeq; end: SessionSeq }
/**
* One immutable entry in the session log.
@@ -61,7 +61,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
seq: SessionSeq
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
@@ -85,14 +85,14 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
* provider stream; when the field is absent, the event does not record which
* earlier events produced the message.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
}[T]
```
来源:[`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:359`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:373`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:402`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:434`](../packages/core/session/src/types.ts)
## 事件
@@ -217,7 +217,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[StreamChunk](subsystems/llm-streaming.zh.md)
来源:[`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
<a id="assistantmessage--surface"></a>
@@ -239,7 +239,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[TokenUsage](subsystems/llm-streaming.zh.md)
来源:[`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts)
### `command/*`
@@ -258,11 +258,11 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
commandId: CommandId
kind: 'success' | 'error'
text?: string
sourceEventSeq?: number
sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq
}
```
来源:[`packages/interaction/commands/src/types.ts:103`](../packages/interaction/commands/src/types.ts)
来源:[`packages/interaction/commands/src/types.ts:104`](../packages/interaction/commands/src/types.ts)
<a id="commandrun--log-only"></a>
@@ -282,7 +282,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
```
来源:[`packages/interaction/commands/src/types.ts:96`](../packages/interaction/commands/src/types.ts)
来源:[`packages/interaction/commands/src/types.ts:97`](../packages/interaction/commands/src/types.ts)
### `compaction/*`
@@ -298,7 +298,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string }
```
来源:[`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
来源:[`packages/compaction/compaction/src/types.ts:72`](../packages/compaction/compaction/src/types.ts)
<a id="compactionprune--log-only"></a>
@@ -316,15 +316,15 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
*/
'compaction/prune': {
/** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
shadowedRange: { start: number; end: number }
shadowedRange: { start: SessionSeq; end: SessionSeq }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
/** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
shadowedTokenCount: number
}
```
来源:[`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
来源:[`packages/compaction/compaction/src/types.ts:82`](../packages/compaction/compaction/src/types.ts)
<a id="compactionstart--log-only"></a>
@@ -339,7 +339,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null }
```
来源:[`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
来源:[`packages/compaction/compaction/src/types.ts:24`](../packages/compaction/compaction/src/types.ts)
<a id="compactionsummary--log-only"></a>
@@ -359,8 +359,8 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
compactionId: CompactionId
sourceCommandId?: CommandId
summary: ContentBlock[]
shadowedRange: { start: number; end: number }
shadowedSeqs: number[]
shadowedRange: { start: SessionSeq; end: SessionSeq }
shadowedSeqs: SessionSeq[]
shadowedTokenCount: number
/** The provider route that wrote the summary. */
provider: string
@@ -393,7 +393,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[ContentBlock](subsystems/core.zh.md) · [TokenUsage](subsystems/llm-streaming.zh.md)
来源:[`packages/compaction/compaction/src/types.ts:33`](../packages/compaction/compaction/src/types.ts)
来源:[`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts)
### `feedback/*`
@@ -565,7 +565,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'request/context': RequestContext
```
来源:[`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:339`](../packages/core/session/src/types.ts)
<a id="requestheader--log-only"></a>
@@ -584,7 +584,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:329`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -659,7 +659,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'session/end-seed': Record<string, never>
```
来源:[`packages/core/session/src/types.ts:319`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:362`](../packages/core/session/src/types.ts)
<a id="sessiontitle--log-only"></a>
@@ -675,7 +675,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[SessionTitleEventData](subsystems/session-title.zh.md)
来源:[`packages/session/session-title/src/index.ts:76`](../packages/session/session-title/src/index.ts)
来源:[`packages/session/session-title/src/index.ts:77`](../packages/session/session-title/src/index.ts)
<a id="sessiontitle-llm-request--log-only"></a>
@@ -688,7 +688,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md)
来源:[`packages/session/session-title-llm/src/index.ts:44`](../packages/session/session-title-llm/src/index.ts)
来源:[`packages/session/session-title-llm/src/index.ts:45`](../packages/session/session-title-llm/src/index.ts)
### `session-log-deepseek/*`
@@ -702,11 +702,11 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
/** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */
sessionId: import('@deepseek-ai/dsh-session/types').SessionId
/** Last canonical event included in the accepted request. */
throughSeq: number
throughSeq: import('@deepseek-ai/dsh-session/types').SessionSeq
}
```
来源:[`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts)
来源:[`packages/session/session-log-deepseek/src/types.ts:57`](../packages/session/session-log-deepseek/src/types.ts)
### `step/*`
@@ -719,7 +719,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'step/end': { turn: number; step: number }
```
来源:[`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
<a id="stepstart--log-only"></a>
@@ -730,7 +730,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'step/start': { turn: number; step: number }
```
来源:[`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -861,7 +861,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[ToolCallId](subsystems/core.zh.md)
来源:[`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:306`](../packages/core/session/src/types.ts)
<a id="toolcode-dispatch--log-only"></a>
@@ -936,7 +936,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts)
### `tool-workflow/*`
@@ -1016,7 +1016,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[TurnEndReason](subsystems/session.zh.md)
来源:[`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
<a id="turnstart--log-only"></a>
@@ -1032,7 +1032,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'turn/start': { turn: number }
```
来源:[`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
### `user/*`
@@ -1051,7 +1051,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'user/message': UserMessage
```
来源:[`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
来源:[`packages/core/session/src/types.ts:287`](../packages/core/session/src/types.ts)
### `web/*`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/commands.md
commands.md: f3760558b04253701915a240375f13e096bc0c51
commands.zh.md: 2b4377b76b55203d84a956f66879ab2c50a9fd23
commands.md: 46d3b736afd3a72d1119c9744cd592a86b9cbc29
commands.zh.md: db8d6e722e3a1742887aaa4023ab67b9ed914552
+1 -1
View File
@@ -83,7 +83,7 @@ type CommandResult =
readonly kind: 'success'
readonly text?: string
/** Earlier authoritative domain event that owns a richer presentation. */
readonly sourceEventSeq?: number
readonly sourceEventSeq?: SessionSeq
}
| { readonly kind: 'error'; readonly text: string }
```
+1 -1
View File
@@ -83,7 +83,7 @@ type CommandResult =
readonly kind: 'success'
readonly text?: string
/** Earlier authoritative domain event that owns a richer presentation. */
readonly sourceEventSeq?: number
readonly sourceEventSeq?: SessionSeq
}
| { readonly kind: 'error'; readonly text: string }
```
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/compaction.md
compaction.md: 03642b32cc3cb3caf743d908e9c988c1a3c8fa1e
compaction.zh.md: af37c4824cc4b01515400049525fbd7b91d018f3
compaction.md: b49957a5f476a02ccd12b791f287a9675073c0ae
compaction.zh.md: 4b4c6845b8ff48bce019b352a811f968628cbdbf
+9 -9
View File
@@ -34,11 +34,11 @@ interface CompactionResult {
/** Human command that initiated this compaction, when it was manual. */
sourceCommandId?: CommandId
/** The seq of the appended `compaction/start` event. */
startSeq: number
startSeq: SessionSeq
/** The seq of the appended `compaction/summary` event. */
summarySeq: number
summarySeq: SessionSeq
/** The seq of the appended `compaction/end` event. */
endSeq: number
endSeq: SessionSeq
/** The summary content blocks produced by the backend. */
summary: ContentBlock[]
/**
@@ -49,9 +49,9 @@ interface CompactionResult {
* can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
* authoritative set of shadowed nodes, in surface order.
*/
shadowedRange: { start: number; end: number }
shadowedRange: { start: SessionSeq; end: SessionSeq }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
/** Estimated token count of the shadowed content. */
shadowedTokenCount: number
}
@@ -95,9 +95,9 @@ The optional tool-result pruning service reports each durable content replacemen
/** Cited source event and size accounting for one landed surface replacement. */
interface PrunedEntry {
/** Full-fidelity tool-result event shadowed by the replacement. */
readonly originalSeq: number
readonly originalSeq: SessionSeq
/** Newly appended pruned tool-result event. */
readonly replacementSeq: number
readonly replacementSeq: SessionSeq
/** Tool call shared by the original and replacement. */
readonly callId: ToolCallId
/** Original text size in Unicode code points. */
@@ -187,10 +187,10 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
* @returns the appended event seqs, summary, replaced range, and token accounting.
*/
abstract compactRegion( start: number, end: number, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
abstract compactRegion( start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
```
Types: [CommandId](commands.md)
Types: [CommandId](commands.md) · [SessionSeq](session.md)
Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts)
+9 -9
View File
@@ -34,11 +34,11 @@ interface CompactionResult {
/** Human command that initiated this compaction, when it was manual. */
sourceCommandId?: CommandId
/** The seq of the appended `compaction/start` event. */
startSeq: number
startSeq: SessionSeq
/** The seq of the appended `compaction/summary` event. */
summarySeq: number
summarySeq: SessionSeq
/** The seq of the appended `compaction/end` event. */
endSeq: number
endSeq: SessionSeq
/** The summary content blocks produced by the backend. */
summary: ContentBlock[]
/**
@@ -49,9 +49,9 @@ interface CompactionResult {
* can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
* authoritative set of shadowed nodes, in surface order.
*/
shadowedRange: { start: number; end: number }
shadowedRange: { start: SessionSeq; end: SessionSeq }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
/** Estimated token count of the shadowed content. */
shadowedTokenCount: number
}
@@ -95,9 +95,9 @@ type ManualCompactionErrorCode =
/** Cited source event and size accounting for one landed surface replacement. */
interface PrunedEntry {
/** Full-fidelity tool-result event shadowed by the replacement. */
readonly originalSeq: number
readonly originalSeq: SessionSeq
/** Newly appended pruned tool-result event. */
readonly replacementSeq: number
readonly replacementSeq: SessionSeq
/** Tool call shared by the original and replacement. */
readonly callId: ToolCallId
/** Original text size in Unicode code points. */
@@ -187,10 +187,10 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
* @returns the appended event seqs, summary, replaced range, and token accounting.
*/
abstract compactRegion( start: number, end: number, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
abstract compactRegion( start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
```
Types: [CommandId](commands.zh.md)
Types: [CommandId](commands.zh.md) · [SessionSeq](session.zh.md)
Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/core.md
core.md: 124d30a5abeddd81edd2eb03a9bb213487b6a38e
core.zh.md: c2d67eabb3b223d3f1d20c42f01280f16656ebef
core.md: a59a692573c2e2756e60626268314a8ade6c546e
core.zh.md: acea4966a30ab03c24f1aea9bcd2d33a6271a28d
+1 -1
View File
@@ -46,7 +46,7 @@ interface AgentHandle {
}
```
`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
+1 -1
View File
@@ -48,7 +48,7 @@ interface AgentHandle {
}
```
`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/persistence.md
persistence.md: cd7eaa126ea16b17526223498d1fa1591792747c
persistence.zh.md: c96aa87f738e5607c699dfa97e6d2b237767b76d
persistence.md: b61ddee8d12854b330b967a09a1a685d763f0041
persistence.zh.md: 08cc35d987360138ec096b70f8c6b11b5cba4c55
+55 -20
View File
@@ -40,7 +40,7 @@ interface SessionLocation {
## `SessionHeader` — metadata beside the log
Per-session metadata travels **separately** from the event log: format version, cwd, lineage, and the seed boundary are storage concerns, not conversation events, so they stay out of `SessionEventMap` and never reach `deriveMessages()`. The header is attached to a `Session` via `session.header`.
Per-session metadata travels **separately** from the event log: the header carries format version, cwd, and the `isSeeded` lineage bit, while body-bearing storage values carry the exact inherited cut beside it. Neither belongs to `SessionEventMap` or reaches `deriveMessages()`. The logical header is attached through `session.header`; the Session exposes its cut as `inheritedEventCount`.
Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
@@ -64,10 +64,10 @@ interface SessionHeader {
/** The session this one was forked from (seed lineage), if any. */
readonly parentSession?: SessionId
/**
* How many leading events were inherited through a seed. Persisting this
* boundary lets resume and replay distinguish parent history from child work.
* Whether this Session contains a fork-inherited event prefix. The exact prefix
* length is Session state rather than ordinary header metadata.
*/
readonly seedLength?: number
readonly isSeeded: boolean
/**
* Coarse product classification for a session created as a subagent child.
* This is presentation metadata, not proof that the child is continuable.
@@ -95,7 +95,7 @@ A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupp
## `CreateSessionOptions` — seeding and metadata
Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, the `agentPreset` the agent was composed from, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
Creating a `Session` through the store takes a `seed` (initial replay or fork history), an optional exact `inheritedEventCount`, and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, `parentSession` lineage, `isSeeded` lineage bit, optional coarse `origin`, `delegationDepth`, `agentPreset`, and an existing `createdAt`. A seeded creation requires both an explicit seed and exact cut because child-owned setup events may follow the inherited prefix. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
```ts type-equiv
/**
@@ -107,14 +107,19 @@ interface CreateSessionOptions {
/** Initial replay or fork history supplied at construction. */
readonly seed?: readonly SessionEvent[]
/**
* Storage metadata read once before publication. `seedLength` is explicit
* because a resumed seed contains the full stored log, not only its inherited prefix.
* Exact fork-inherited prefix length when `meta.isSeeded` is true. A
* constructor seed may also contain child-owned setup events after this cut.
*/
readonly inheritedEventCount?: SessionLogOffset
/**
* Storage metadata read once before publication. `isSeeded` marks fork
* lineage; supplying replay history alone does not make it inherited.
*/
readonly meta?: {
readonly cwd?: string
readonly parentSession?: SessionId
readonly createdAt?: number
readonly seedLength?: number
readonly isSeeded?: boolean
readonly origin?: 'subagent'
readonly delegationDepth?: number
readonly agentPreset?: string
@@ -122,7 +127,21 @@ interface CreateSessionOptions {
}
```
Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
Plain replay is `ctx.sessions.create(id, { seed: seedEvents })`; a fork additionally supplies `inheritedEventCount` and `meta.isSeeded: true`. Resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
## `SessionStorageMetadata` — logical header and inherited cut
Every persistence result that reads a Session body carries `SessionStorageMetadata`: the current logical header plus the separately validated inherited-event cut. Header-only listing intentionally returns only `SessionHeader`.
```ts type-equiv
/** Logical Session header paired with its exact inherited cut for body-bearing storage operations. */
interface SessionStorageMetadata {
/** Validated immutable Session header. */
readonly meta: SessionHeader
/** Number of leading events inherited from the Session's fork parent. */
readonly inheritedEventCount: SessionLogOffset
}
```
## `SessionRawArtifact` — verbatim stored artifact text
@@ -130,9 +149,7 @@ A backend's own artifact text for one session, byte-identical to what it durably
```ts type-equiv
/** A backend's own raw artifact text for one session, verbatim. */
interface SessionRawArtifact {
/** The session header parsed from the artifact's own first line. */
readonly meta: SessionHeader
interface SessionRawArtifact extends SessionStorageMetadata {
/** The artifact's base filename on disk, without any physical encoding suffix. */
readonly filename: string
/** The artifact's full text content, decoded from the backend's physical encoding. */
@@ -154,6 +171,8 @@ interface RestoredSessionOptions {
readonly seed: SessionEvent[]
/** Fresh detached storage metadata to validate and freeze in place. */
readonly meta: SessionHeader
/** Exact number of fork-inherited leading events decoded from storage. */
readonly inheritedEventCount: SessionLogOffset
/** Select the persistence ownership-transfer path. */
readonly seedSource: 'persistence'
}
@@ -198,14 +217,26 @@ declare class SessionPreparation implements Disposable {
```ts type-equiv
/** Immutable logical session prepared from persistence or a live owner. */
interface SessionInspection {
/** Validated immutable session metadata. */
readonly meta: SessionHeader
interface SessionInspection extends SessionStorageMetadata {
/** Validated contiguous logical event log. */
readonly events: readonly SessionEvent[]
}
```
## Detached stored-log suffixes
`readFrom` returns a detached `SessionEventSuffix` anchored by the requested `fromSeq`. Its event list may start above zero or be empty, so it is not a complete `SessionInspection` and must not be restored as a whole Session.
```ts type-equiv
/** Detached logical suffix returned by one explicit stored-log offset read. */
interface SessionEventSuffix extends SessionStorageMetadata {
/** First requested log offset; {@link events} contains only seqs at or after it. */
readonly fromSeq: SessionLogOffset
/** Valid contiguous stored events at or after {@link fromSeq}; not a complete Session log when the offset is nonzero. */
readonly events: readonly SessionEvent[]
}
```
## Lightweight source revisions
Consumers of derived state compare a cheap opaque revision before loading a full event log. The persistence backend owns its representation and changes it transactionally with append or mutating load repair; callers compare it only for equality.
@@ -281,8 +312,10 @@ readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | unde
* created-but-never-appended session is absent from {@link list}
* — abandoned sessions leave nothing behind.
* @param meta - the immutable header (id, version, cwd, lineage) to record.
* @param inheritedEventCount - exact fork-inherited prefix length. Required
* for a seeded header and omitted only for an unseeded header.
*/
abstract create(meta: SessionHeader): Promise<void>
abstract create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>
/**
* Ensure a live session has a durable header even when it has no events.
@@ -297,6 +330,8 @@ ensureMaterialized(_session: Session): Promise<void>
* seq contracts: the first event's `seq` MUST equal the stored next-seq
* (after `load` has durably closed any interrupted turn). Rejects non-JSON-
* serializable `event.data` with an error naming the offending event type.
* A seeded session's first materializing batch must reach its complete
* inherited prefix.
* @param id - the session the batch belongs to.
* @param events - the contiguous batch to persist, in seq order.
*/
@@ -371,11 +406,11 @@ abstract borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSes
* forward. The primitive bounds what is returned and refolded, not every
* backend's physical read.
* @param id - the persisted session to read.
* @param fromSeq - first event seq to include; a non-negative safe integer.
* @param fromSeq - first event offset to include.
* @param signal - optional cancellation for queued and backend read work.
* @returns the header and the stored events with `seq >= fromSeq`.
* @returns storage metadata, the requested offset, and stored events with `seq >= fromSeq`.
*/
abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
abstract readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>
/**
* Lightweight listing from metadata, without a full-log parse.
@@ -397,7 +432,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
```
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionId](core.md)
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionId](core.md) · [SessionLogOffset](session.md)
Source: [`packages/session/session-persistence/src/index.ts`](../../packages/session/session-persistence/src/index.ts)
<!-- END GENERATED cordis-surface -->
+55 -20
View File
@@ -40,7 +40,7 @@ interface SessionLocation {
## `SessionHeader`:日志旁的元数据
每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
每个会话的元数据与事件日志**分开**存储:header 携带格式版本、cwd 与 `isSeeded` 谱系 bit,含正文的存储值则在其旁边单独携带精确 inherited cut。二者都不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。logical header 通过 `session.header` 附加,Session 则以 `inheritedEventCount` 暴露其 cut。
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
@@ -64,10 +64,10 @@ interface SessionHeader {
/** The session this one was forked from (seed lineage), if any. */
readonly parentSession?: SessionId
/**
* How many leading events were inherited through a seed. Persisting this
* boundary lets resume and replay distinguish parent history from child work.
* Whether this Session contains a fork-inherited event prefix. The exact prefix
* length is Session state rather than ordinary header metadata.
*/
readonly seedLength?: number
readonly isSeeded: boolean
/**
* Coarse product classification for a session created as a subagent child.
* This is presentation metadata, not proof that the child is continuable.
@@ -95,7 +95,7 @@ interface SessionHeader {
## `CreateSessionOptions`:seed 与元数据
通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 整合进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth`、用于组装该 agent(智能体)的 `agentPreset` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)、可选的精确 `inheritedEventCount` 与 `meta`(store 整合进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`isSeeded` 谱系标记、可选的粗粒度 `origin`、`delegationDepth`、用于组装该 agent(智能体)的 `agentPreset` 以及已有的 `createdAt`。seeded 创建必须同时显式提供 seed 与精确 cut,因为继承前缀之后还可能存在 child-owned setup event。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
```ts type-equiv
/**
@@ -107,14 +107,19 @@ interface CreateSessionOptions {
/** Initial replay or fork history supplied at construction. */
readonly seed?: readonly SessionEvent[]
/**
* Storage metadata read once before publication. `seedLength` is explicit
* because a resumed seed contains the full stored log, not only its inherited prefix.
* Exact fork-inherited prefix length when `meta.isSeeded` is true. A
* constructor seed may also contain child-owned setup events after this cut.
*/
readonly inheritedEventCount?: SessionLogOffset
/**
* Storage metadata read once before publication. `isSeeded` marks fork
* lineage; supplying replay history alone does not make it inherited.
*/
readonly meta?: {
readonly cwd?: string
readonly parentSession?: SessionId
readonly createdAt?: number
readonly seedLength?: number
readonly isSeeded?: boolean
readonly origin?: 'subagent'
readonly delegationDepth?: number
readonly agentPreset?: string
@@ -122,7 +127,21 @@ interface CreateSessionOptions {
}
```
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
因此,普通回放的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;fork 还会提供 `inheritedEventCount` 与 `meta.isSeeded: true`。将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
## `SessionStorageMetadata`:逻辑 header 与继承 cut
每个读取 Session 正文的持久化结果都携带 `SessionStorageMetadata`:当前逻辑 header,以及单独校验的继承事件 cut。仅 header 的列表操作有意只返回 `SessionHeader`。
```ts type-equiv
/** Logical Session header paired with its exact inherited cut for body-bearing storage operations. */
interface SessionStorageMetadata {
/** Validated immutable Session header. */
readonly meta: SessionHeader
/** Number of leading events inherited from the Session's fork parent. */
readonly inheritedEventCount: SessionLogOffset
}
```
## `SessionRawArtifact`——逐字存储工件文本
@@ -130,9 +149,7 @@ interface CreateSessionOptions {
```ts type-equiv
/** A backend's own raw artifact text for one session, verbatim. */
interface SessionRawArtifact {
/** The session header parsed from the artifact's own first line. */
readonly meta: SessionHeader
interface SessionRawArtifact extends SessionStorageMetadata {
/** The artifact's base filename on disk, without any physical encoding suffix. */
readonly filename: string
/** The artifact's full text content, decoded from the backend's physical encoding. */
@@ -154,6 +171,8 @@ interface RestoredSessionOptions {
readonly seed: SessionEvent[]
/** Fresh detached storage metadata to validate and freeze in place. */
readonly meta: SessionHeader
/** Exact number of fork-inherited leading events decoded from storage. */
readonly inheritedEventCount: SessionLogOffset
/** Select the persistence ownership-transfer path. */
readonly seedSource: 'persistence'
}
@@ -198,14 +217,26 @@ declare class SessionPreparation implements Disposable {
```ts type-equiv
/** Immutable logical session prepared from persistence or a live owner. */
interface SessionInspection {
/** Validated immutable session metadata. */
readonly meta: SessionHeader
interface SessionInspection extends SessionStorageMetadata {
/** Validated contiguous logical event log. */
readonly events: readonly SessionEvent[]
}
```
## 分离的持久日志后缀
`readFrom` 返回以请求的 `fromSeq` 为锚点、与其他状态分离的 `SessionEventSuffix`。其事件列表可能从非零位置开始,也可能为空,因此它不是完整的 `SessionInspection`,不得作为完整 Session 恢复。
```ts type-equiv
/** Detached logical suffix returned by one explicit stored-log offset read. */
interface SessionEventSuffix extends SessionStorageMetadata {
/** First requested log offset; {@link events} contains only seqs at or after it. */
readonly fromSeq: SessionLogOffset
/** Valid contiguous stored events at or after {@link fromSeq}; not a complete Session log when the offset is nonzero. */
readonly events: readonly SessionEvent[]
}
```
## 轻量源修订号
派生状态的消费方会在加载完整事件日志之前比较一个低开销的不透明修订号。其表示由持久化后端拥有,并随 append 或会修改数据的 load 修复以事务方式改变;调用方仅比较修订号是否相等。
@@ -281,8 +312,10 @@ readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | unde
* created-but-never-appended session is absent from {@link list}
* — abandoned sessions leave nothing behind.
* @param meta - the immutable header (id, version, cwd, lineage) to record.
* @param inheritedEventCount - exact fork-inherited prefix length. Required
* for a seeded header and omitted only for an unseeded header.
*/
abstract create(meta: SessionHeader): Promise<void>
abstract create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>
/**
* Ensure a live session has a durable header even when it has no events.
@@ -297,6 +330,8 @@ ensureMaterialized(_session: Session): Promise<void>
* seq contracts: the first event's `seq` MUST equal the stored next-seq
* (after `load` has durably closed any interrupted turn). Rejects non-JSON-
* serializable `event.data` with an error naming the offending event type.
* A seeded session's first materializing batch must reach its complete
* inherited prefix.
* @param id - the session the batch belongs to.
* @param events - the contiguous batch to persist, in seq order.
*/
@@ -371,11 +406,11 @@ abstract borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSes
* forward. The primitive bounds what is returned and refolded, not every
* backend's physical read.
* @param id - the persisted session to read.
* @param fromSeq - first event seq to include; a non-negative safe integer.
* @param fromSeq - first event offset to include.
* @param signal - optional cancellation for queued and backend read work.
* @returns the header and the stored events with `seq >= fromSeq`.
* @returns storage metadata, the requested offset, and stored events with `seq >= fromSeq`.
*/
abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
abstract readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>
/**
* Lightweight listing from metadata, without a full-log parse.
@@ -397,7 +432,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
```
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionId](core.zh.md)
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionId](core.zh.md) · [SessionLogOffset](session.zh.md)
Source: [`packages/session/session-persistence/src/index.ts`](../../packages/session/session-persistence/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/schedule.md
schedule.md: f98cf319d588f805e4630ca572f10c3c45e94b79
schedule.zh.md: bee816be9cc49943f5815d4d13869b27e30380d5
schedule.md: 40aa3592dec89fe8daa45beb630076e0476d2b16
schedule.zh.md: 53a36ee9b51545ca74ac6166ef31d1e002d1f1f2
+1 -1
View File
@@ -149,7 +149,7 @@ type ScheduleDispatchChange = OneShotScheduleDispatchChange | EveryScheduleDispa
type ScheduleChange = ScheduleCreateChange | ScheduleDeleteChange | ScheduleDispatchChange
```
The strict decoder and fold reject unknown versions, extra fields, reused ids, mismatched one-shot or Every dispatch shapes, and delete or dispatch transitions against inactive records. A normal Session folds its complete event stream. A fork folds only events at or after `SessionHeader.seedLength`, so it retains history without adopting the parent Session's active reminders. The Schedule projection derives that boundary from the immutable header passed to `init(header)`, uses the shared transition, and persists both active records and used-id history so cached restore preserves strict replay. The `schedule/change` declaration and source location are also indexed in the [persistence catalog](../persistence-catalog.md#schedulechange--log-only).
The strict decoder and fold reject unknown versions, extra fields, reused ids, mismatched one-shot or Every dispatch shapes, and delete or dispatch transitions against inactive records. A normal Session folds its complete event stream. A fork folds only events at or after its exact `inheritedEventCount`, so it retains history without adopting the parent Session's active reminders. Projection initialization receives that cut beside the immutable header, uses the shared transition, and persists both the cut, active records, and used-id history so cached restore preserves strict replay. The `schedule/change` declaration and source location are also indexed in the [persistence catalog](../persistence-catalog.md#schedulechange--log-only).
## Active views and management
+1 -1
View File
@@ -149,7 +149,7 @@ type ScheduleDispatchChange = OneShotScheduleDispatchChange | EveryScheduleDispa
type ScheduleChange = ScheduleCreateChange | ScheduleDeleteChange | ScheduleDispatchChange
```
严格 decoder 与 fold 会拒绝未知版本、额外字段、复用 id、不匹配的一次性提醒或 Every dispatch 形状,以及针对非活动记录的 delete 或 dispatch 转换。普通 Session 折叠完整事件流。fork 只折叠 `SessionHeader.seedLength` 位置及其后的事件,因此保留历史,但不会接管父 Session 的活动提醒。Schedule projection 从传给 `init(header)` 的不可变 header 派生该边界,复用共享 transition,并持久化活动记录与已使用 id 历史,使缓存恢复继续保持严格回放。`schedule/change` 声明和源码位置也编入[持久化目录](../persistence-catalog.zh.md#schedulechange--log-only)。
严格 decoder 与 fold 会拒绝未知版本、额外字段、复用 id、不匹配的一次性提醒或 Every dispatch 形状,以及针对非活动记录的 delete 或 dispatch 转换。普通 Session 折叠完整事件流。fork 只折叠精确 `inheritedEventCount` 位置及其后的事件,因此保留历史,但不会接管父 Session 的活动提醒。Projection 初始化会在不可变 header 旁接收该 cut,复用共享 transition,并持久化 cut、活动记录与已使用 id 历史,使缓存恢复继续保持严格回放。`schedule/change` 声明和源码位置也编入[持久化目录](../persistence-catalog.zh.md#schedulechange--log-only)。
## 活动视图与管理
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
session-projection.md: b5bacc4846a9a9709bb8c102aaa78810a2752110
session-projection.zh.md: 36653721a125ceef13bff8b1a4562e7981a45696
session-projection.md: c8a5d4c1b05d830be3224e2e04db1762fe4b3c8f
session-projection.zh.md: d46c4f1589f57e560d70871d011245a3547c7825
+16 -12
View File
@@ -30,9 +30,10 @@ interface ProjectionDefinition<
/**
* State for the empty log and its immutable Session metadata.
* @param header - immutable metadata for the Session being projected.
* @param inheritedEventCount - exact fork-inherited prefix length.
* @returns the initial state.
*/
init(header: SessionHeader): NoInfer<S>
init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>
/**
* Pure transition: previous state + one committed event → next state. A
* unit uninterested in an event MUST return the same state reference — an
@@ -78,7 +79,7 @@ The whole-value event rule is load-bearing: a state-carrying log event carries t
*/
interface ProjectionSnapshot {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
asOfSeq: SessionSeqCursor
/** Whole current client value per registered key. */
values: Partial<SessionProjectionMap>
}
@@ -94,7 +95,7 @@ type ProjectionChangeListener = (
session: Session,
key: Extract<keyof SessionProjectionMap, string>,
value: unknown,
seq: number,
seq: SessionSeq,
) => void
```
@@ -128,11 +129,13 @@ The persisted projection cache service. Opens the `session_projcache` domain at
* paths (the history tail baseline) supersede these values whenever a
* session is actually opened.
* @param meta - the listed session's header (identity witness; no log read).
* @param inheritedEventCount - exact inherited prefix length that completes
* the checkpoint identity.
* @param keys - optional projection keys required by the caller's audience.
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
*/
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
/**
* Hydrate projection cells for an already-prepared Session without another
@@ -140,11 +143,10 @@ cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjec
* advances every unit to the observation cut. No checkpoint is written
* because the logical observation may contain recovery events not yet durable.
* @param session - exact unpublished Session retained by persistence.
* @param meta - observed lifecycle header.
* @param events - exact logical event prefix represented by the observation.
* @returns all projection values at the event cut.
*/
hydratePrepared( session: Session, meta: SessionHeader, events: readonly SessionEvent[], ): ProjectionSnapshot
hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
/**
* Durably checkpoint one live session NOW (all mandatory points call
@@ -166,13 +168,14 @@ async write(session: Session): Promise<void>
* The caller supplies the complete log in seq order: this service never
* consults the persistence layer.
* @param meta - the stored session header (identity witness).
* @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
* @param events - the session's complete log, in seq order.
* @returns the projection cut at the log end.
*/
coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot
coldSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], ): ProjectionSnapshot
```
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md)
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)
Source: [`packages/session/session-projection-cache/src/index.ts`](../../packages/session/session-projection-cache/src/index.ts)
@@ -272,7 +275,7 @@ checkpoint(session: Session): ProjectionCheckpoint
* when no unit is registered (no read needed — {@link restore} would
* serve empty values regardless).
*/
restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
/**
* View a checkpoint's rows without any log read: for every registered
@@ -306,11 +309,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
* @param events - the stored events with `seq >= baseSeq`, in seq order.
* @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
* @param header - immutable metadata for the Session being restored.
* @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
* refreshed checkpoint rows at that cut, ready for a durable write-back.
*/
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, header: SessionHeader, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
/**
* Restore an exact cut and install its states on the supplied prepared Session.
@@ -322,10 +326,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
* @param baseSeq - first supplied event sequence.
* @returns all projection values at the supplied cut.
*/
hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, ): ProjectionSnapshot
hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
```
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md)
Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)
Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
<!-- END GENERATED cordis-surface -->
+16 -12
View File
@@ -30,9 +30,10 @@ interface ProjectionDefinition<
/**
* State for the empty log and its immutable Session metadata.
* @param header - immutable metadata for the Session being projected.
* @param inheritedEventCount - exact fork-inherited prefix length.
* @returns the initial state.
*/
init(header: SessionHeader): NoInfer<S>
init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>
/**
* Pure transition: previous state + one committed event → next state. A
* unit uninterested in an event MUST return the same state reference — an
@@ -78,7 +79,7 @@ interface ProjectionDefinition<
*/
interface ProjectionSnapshot {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
asOfSeq: SessionSeqCursor
/** Whole current client value per registered key. */
values: Partial<SessionProjectionMap>
}
@@ -94,7 +95,7 @@ type ProjectionChangeListener = (
session: Session,
key: Extract<keyof SessionProjectionMap, string>,
value: unknown,
seq: number,
seq: SessionSeq,
) => void
```
@@ -128,11 +129,13 @@ The persisted projection cache service. Opens the `session_projcache` domain at
* paths (the history tail baseline) supersede these values whenever a
* session is actually opened.
* @param meta - the listed session's header (identity witness; no log read).
* @param inheritedEventCount - exact inherited prefix length that completes
* the checkpoint identity.
* @param keys - optional projection keys required by the caller's audience.
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
*/
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
/**
* Hydrate projection cells for an already-prepared Session without another
@@ -140,11 +143,10 @@ cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjec
* advances every unit to the observation cut. No checkpoint is written
* because the logical observation may contain recovery events not yet durable.
* @param session - exact unpublished Session retained by persistence.
* @param meta - observed lifecycle header.
* @param events - exact logical event prefix represented by the observation.
* @returns all projection values at the event cut.
*/
hydratePrepared( session: Session, meta: SessionHeader, events: readonly SessionEvent[], ): ProjectionSnapshot
hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
/**
* Durably checkpoint one live session NOW (all mandatory points call
@@ -166,13 +168,14 @@ async write(session: Session): Promise<void>
* The caller supplies the complete log in seq order: this service never
* consults the persistence layer.
* @param meta - the stored session header (identity witness).
* @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
* @param events - the session's complete log, in seq order.
* @returns the projection cut at the log end.
*/
coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot
coldSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], ): ProjectionSnapshot
```
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md)
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)
Source: [`packages/session/session-projection-cache/src/index.ts`](../../packages/session/session-projection-cache/src/index.ts)
@@ -272,7 +275,7 @@ checkpoint(session: Session): ProjectionCheckpoint
* when no unit is registered (no read needed — {@link restore} would
* serve empty values regardless).
*/
restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
/**
* View a checkpoint's rows without any log read: for every registered
@@ -306,11 +309,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
* @param events - the stored events with `seq >= baseSeq`, in seq order.
* @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
* @param header - immutable metadata for the Session being restored.
* @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
* refreshed checkpoint rows at that cut, ready for a durable write-back.
*/
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, header: SessionHeader, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
/**
* Restore an exact cut and install its states on the supplied prepared Session.
@@ -322,10 +326,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
* @param baseSeq - first supplied event sequence.
* @returns all projection values at the supplied cut.
*/
hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, ): ProjectionSnapshot
hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
```
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md)
Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)
Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-query.md
session-query.md: 74fb3e15514afd32c594b083732153a1f03b21d5
session-query.zh.md: fee4c29f67bf850fa9ed07d869308844e84b2bb4
session-query.md: 4317fa3d706dd40c596114a396ba746da67a979f
session-query.zh.md: 4b67706aea3d3f73304e4c8ad0c689a4c77a13a1
+17 -11
View File
@@ -34,6 +34,8 @@ interface SessionRecord {
interface SessionLogSnapshot {
/** Cloned session header selected from the same observation as `events`. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Cloned contiguous raw events after persistence repair and replay validation. */
events: SessionEvent[]
}
@@ -44,8 +46,10 @@ interface SessionLogSnapshot {
interface SessionSurfaceSnapshot {
/** Cloned session header selected from the same corpus observation as `events`. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Highest raw-log seq included in the observation, or `null` for an empty log. */
capturedThroughSeq: number | null
capturedThroughSeq: OptionalSessionSeq
/** Cloned current surface events in model-history order. */
events: SurfaceEvent[]
}
@@ -90,7 +94,7 @@ interface SessionEventRecord {
/** Session that owns the event. */
sessionId: SessionId
/** Monotonic event seq within the session. */
seq: number
seq: SessionSeq
/** Discriminant of the session event. */
type: SessionEventType
/** Event timestamp in Unix epoch milliseconds. */
@@ -266,7 +270,7 @@ interface SessionEventReadRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
seq: SessionSeq
/** Number of preceding raw events to include. */
before?: number
/** Number of following raw events to include. */
@@ -279,14 +283,16 @@ interface SessionEventReadRequest {
interface SessionEventWindow {
/** Cloned header for the live-preferred source read. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Full cloned target event. */
target: SessionEvent
/** Full cloned events from `startSeq` through `endSeq`. */
events: SessionEvent[]
/** First seq included in `events`. */
startSeq: number
startSeq: SessionSeq
/** Last seq included in `events`. */
endSeq: number
endSeq: SessionSeq
}
```
@@ -300,7 +306,7 @@ interface SessionEventTraceRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
seq: SessionSeq
}
```
@@ -310,15 +316,15 @@ interface SessionEventTrace {
/** Lightweight target record. */
target: SessionEventRecord
/** Immediate positional replacement event, when the target was shadowed. */
replacedBy?: number
replacedBy?: SessionSeq
/** Positional replacers from the immediate replacement to the final replacement. */
replacementChain: number[]
replacementChain: SessionSeq[]
/** Surface nodes directly removed when the target itself performed a replacement. */
replacedEventSeqs: number[]
replacedEventSeqs: SessionSeq[]
/** Earlier events cited directly as sources, in their recorded order. */
sourceEventSeqs: number[]
sourceEventSeqs: SessionSeq[]
/** Later events that directly cite the target as a source, in log order. */
derivedEventSeqs: number[]
derivedEventSeqs: SessionSeq[]
}
```
+17 -11
View File
@@ -34,6 +34,8 @@ interface SessionRecord {
interface SessionLogSnapshot {
/** Cloned session header selected from the same observation as `events`. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Cloned contiguous raw events after persistence repair and replay validation. */
events: SessionEvent[]
}
@@ -44,8 +46,10 @@ interface SessionLogSnapshot {
interface SessionSurfaceSnapshot {
/** Cloned session header selected from the same corpus observation as `events`. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Highest raw-log seq included in the observation, or `null` for an empty log. */
capturedThroughSeq: number | null
capturedThroughSeq: OptionalSessionSeq
/** Cloned current surface events in model-history order. */
events: SurfaceEvent[]
}
@@ -90,7 +94,7 @@ interface SessionEventRecord {
/** Session that owns the event. */
sessionId: SessionId
/** Monotonic event seq within the session. */
seq: number
seq: SessionSeq
/** Discriminant of the session event. */
type: SessionEventType
/** Event timestamp in Unix epoch milliseconds. */
@@ -266,7 +270,7 @@ interface SessionEventReadRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
seq: SessionSeq
/** Number of preceding raw events to include. */
before?: number
/** Number of following raw events to include. */
@@ -279,14 +283,16 @@ interface SessionEventReadRequest {
interface SessionEventWindow {
/** Cloned header for the live-preferred source read. */
session: SessionHeader
/** Exact number of fork-inherited events in the observed log. */
inheritedEventCount: SessionLogOffset
/** Full cloned target event. */
target: SessionEvent
/** Full cloned events from `startSeq` through `endSeq`. */
events: SessionEvent[]
/** First seq included in `events`. */
startSeq: number
startSeq: SessionSeq
/** Last seq included in `events`. */
endSeq: number
endSeq: SessionSeq
}
```
@@ -300,7 +306,7 @@ interface SessionEventTraceRequest {
/** Session that owns the target event. */
sessionId: SessionId
/** Target event seq. */
seq: number
seq: SessionSeq
}
```
@@ -310,15 +316,15 @@ interface SessionEventTrace {
/** Lightweight target record. */
target: SessionEventRecord
/** Immediate positional replacement event, when the target was shadowed. */
replacedBy?: number
replacedBy?: SessionSeq
/** Positional replacers from the immediate replacement to the final replacement. */
replacementChain: number[]
replacementChain: SessionSeq[]
/** Surface nodes directly removed when the target itself performed a replacement. */
replacedEventSeqs: number[]
replacedEventSeqs: SessionSeq[]
/** Earlier events cited directly as sources, in their recorded order. */
sourceEventSeqs: number[]
sourceEventSeqs: SessionSeq[]
/** Later events that directly cite the target as a source, in log order. */
derivedEventSeqs: number[]
derivedEventSeqs: SessionSeq[]
}
```
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-title.md
session-title.md: 05d20c2d7b278dd0663e50d86f1cd22b64852e81
session-title.zh.md: 2eaa296871bd90e1708f2cf27298b7bbc1efa2c0
session-title.md: 20ed32b4a08127e7193433673d22f0dea3a90c46
session-title.zh.md: c01d5d4bf177169069bbe0a39ef533ada0f7c36a
+5 -5
View File
@@ -46,7 +46,7 @@ interface SessionTitleEventData {
/** Normalized non-empty title text. */
readonly title: string
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
readonly messageSeqs: number[]
readonly messageSeqs: SessionSeq[]
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
readonly source: SessionTitleSource
}
@@ -56,7 +56,7 @@ interface SessionTitleEventData {
/** Latest folded title plus the title event's durable envelope facts. */
interface SessionTitleSnapshot extends SessionTitleEventData {
/** Seq of the latest `session/title` event. */
readonly eventSeq: number
readonly eventSeq: SessionSeq
/** Timestamp of the latest `session/title` event. */
readonly updatedAt: number
}
@@ -72,7 +72,7 @@ interface SessionTitleLlmRequestEventData {
/** Registered title-provider identity responsible for the request. */
readonly titleProvider: SessionTitleProviderId
/** Exact human `user/message` seqs represented in `messages`. */
readonly messageSeqs: number[]
readonly messageSeqs: SessionSeq[]
/** Exact auxiliary LLM route. */
readonly route: SessionTitleModelProvenance
/** Exact auxiliary system prompt. */
@@ -92,7 +92,7 @@ The service snapshots eligible messages through one revision. A provider returns
/** One eligible human text message exposed to title providers. */
interface SessionTitleUserMessage {
/** Source `user/message` event seq. */
readonly seq: number
readonly seq: SessionSeq
/** Exact concatenated text-block content. */
readonly text: string
}
@@ -123,7 +123,7 @@ interface SessionTitleProviderResult {
/** Proposed title text. */
readonly title: string
/** Exact seqs from `request.messages` used by this result. */
readonly messageSeqs: readonly number[]
readonly messageSeqs: readonly SessionSeq[]
/** Auxiliary LLM route, when generation used a model. */
readonly model?: SessionTitleModelProvenance
}
+5 -5
View File
@@ -46,7 +46,7 @@ interface SessionTitleEventData {
/** Normalized non-empty title text. */
readonly title: string
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
readonly messageSeqs: number[]
readonly messageSeqs: SessionSeq[]
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
readonly source: SessionTitleSource
}
@@ -56,7 +56,7 @@ interface SessionTitleEventData {
/** Latest folded title plus the title event's durable envelope facts. */
interface SessionTitleSnapshot extends SessionTitleEventData {
/** Seq of the latest `session/title` event. */
readonly eventSeq: number
readonly eventSeq: SessionSeq
/** Timestamp of the latest `session/title` event. */
readonly updatedAt: number
}
@@ -72,7 +72,7 @@ interface SessionTitleLlmRequestEventData {
/** Registered title-provider identity responsible for the request. */
readonly titleProvider: SessionTitleProviderId
/** Exact human `user/message` seqs represented in `messages`. */
readonly messageSeqs: number[]
readonly messageSeqs: SessionSeq[]
/** Exact auxiliary LLM route. */
readonly route: SessionTitleModelProvenance
/** Exact auxiliary system prompt. */
@@ -92,7 +92,7 @@ interface SessionTitleLlmRequestEventData {
/** One eligible human text message exposed to title providers. */
interface SessionTitleUserMessage {
/** Source `user/message` event seq. */
readonly seq: number
readonly seq: SessionSeq
/** Exact concatenated text-block content. */
readonly text: string
}
@@ -123,7 +123,7 @@ interface SessionTitleProviderResult {
/** Proposed title text. */
readonly title: string
/** Exact seqs from `request.messages` used by this result. */
readonly messageSeqs: readonly number[]
readonly messageSeqs: readonly SessionSeq[]
/** Auxiliary LLM route, when generation used a model. */
readonly model?: SessionTitleModelProvenance
}
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session.md
session.md: 3a293832264c467c846c350264d0bbd6146839ea
session.zh.md: 6ac06d33dfea84eb9eab04914607e444fe39afc4
session.md: f3400faedec4daaa40d5013bb7f492a3084d4e7a
session.zh.md: 5b34ff3156daafb4a617bf557b5a237e17554d02
+75 -25
View File
@@ -179,6 +179,28 @@ interface RequestContext {
A proper discriminated union over `type` (not independent `type`/`data` unions), so `switch (event.type)` narrows `event.data` without casts. `seq` is the monotonic position in the log (`seq = log.length`); `time` is epoch ms.
```ts type-equiv
/** Sequence number of one existing event in a Session log. */
type SessionSeq = BrandedNumber<'SessionSeq'>
```
```ts type-equiv
/** A Session log gap, prefix length, or read offset, which may equal the event count. */
type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
```
```ts type-equiv
/** Inclusive Session event watermark, or `-1` before any event exists. */
type SessionSeqCursor = SessionSeq | -1
```
```ts type-equiv
/** One existing Session event position, or explicit absence. */
type OptionalSessionSeq = SessionSeq | null
```
`SessionSeq(value)` and `SessionLogOffset(value)` admit only non-negative safe integers and reject negative zero. They add compile-time brands without changing the serialized number; arithmetic returns an ordinary `number` that callers must admit again through the constructor for its intended role.
```ts type-equiv
/**
* One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
seq: SessionSeq
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
* provider stream; when the field is absent, the event does not record which
* earlier events produced the message.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
@@ -268,7 +290,7 @@ type SurfaceEventType =
*/
type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
| { op: 'replace'; start: SessionSeq; end: SessionSeq }
```
`'append'` is the normal tail-append path. `replace` shadows surface entries from `start` through `end` inclusive (both must be valid surface seqs; `start === end` replaces a single entry) and inserts the new event in their place.
@@ -288,7 +310,7 @@ interface SurfaceIntent {
* absent, the event does not record which earlier events produced the message.
* Other surface events require a non-empty set when this field is present.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
}
```
@@ -306,7 +328,7 @@ Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the f
/** Readonly live projection of the message-producing session events. */
interface SessionSurface {
/** Current surface event sequences in model-visible order. */
readonly nodes: readonly number[]
readonly nodes: readonly SessionSeq[]
/** Monotonic count of committed positional replacements. */
readonly replaceGeneration: number
}
@@ -320,13 +342,13 @@ interface SessionSurface {
/** One replacement operation observed while folding a session surface. */
interface SurfaceFoldReplacement {
/** Seq of the event that replaced the prior surface range. */
seq: number
seq: SessionSeq
/** Declared inclusive start seq of the replaced surface range. */
start: number
start: SessionSeq
/** Declared inclusive end seq of the replaced surface range. */
end: number
end: SessionSeq
/** Actual surface entries removed by the operation, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
}
```
@@ -334,7 +356,7 @@ interface SurfaceFoldReplacement {
/** Complete result of replaying the surface operations in a session log. */
interface SurfaceFoldResult {
/** Current surface event sequences in model-visible order. */
nodes: number[]
nodes: SessionSeq[]
/** Replacement operations in event order. */
replacements: SurfaceFoldReplacement[]
}
@@ -358,13 +380,15 @@ declare class Session {
get surface(): SessionSurface;
/**
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
* and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
* `Session` is created without a store-owned header, a minimal header is
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
* `session.header` is always present. Kept out of the event log — it is a
* storage concern, not replayable conversation state.
*/
readonly header: SessionHeader;
/** Number of leading events inherited from this Session's fork parent. */
readonly inheritedEventCount: SessionLogOffset;
/** The session identity, derived from its durable header's single copy. */
get id(): SessionId;
/**
@@ -373,9 +397,9 @@ declare class Session {
* construction — replay, fork, or resume — and were never published on the
* `session/event` firehose (constructor seeds do not emit), so consumers
* that replay the log as a publication substitute (telemetry adoption)
* start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
* boundary: a resumed session's constructor seed is its full stored log,
* while its header keeps the original fork value — this field is the
* start here. Distinct from {@link inheritedEventCount}, the DURABLE
* fork-lineage cut: a resumed session's constructor seed is its full stored
* log, while the inherited count keeps the original fork value — this field is the
* in-process construction fact.
*
* Not persisted itself: a seeded session projects it into the log as the
@@ -389,16 +413,22 @@ declare class Session {
* store attaches and therefore does not publish either. Otherwise this seq
* holds an ordinary published write.
*/
readonly firstLiveSeq: number;
readonly firstLiveSeq: SessionLogOffset;
/**
* Create a detached session by validating and snapshotting borrowed seed
* events and storage metadata.
* @param id - session identity.
* @param seed - optional borrowed replay or fork events.
* @param header - optional borrowed storage metadata.
* @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
* @returns a detached session.
*/
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
static create(
id: SessionId,
seed?: readonly SessionEvent[],
header?: SessionHeader,
inheritedEventCount?: SessionLogOffset,
): Session;
/**
* Restore a detached session by taking ownership of fresh persistence values.
* The storage format, event envelopes, sequence continuity, surface transitions,
@@ -406,15 +436,21 @@ declare class Session {
* @param id - restored session identity.
* @param seed - fresh detached events whose ownership is transferred.
* @param header - fresh detached metadata whose ownership is transferred.
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
* @returns a restored detached session.
*/
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
static fromRestore(
id: SessionId,
seed: readonly SessionEvent[],
header: SessionHeader,
inheritedEventCount: SessionLogOffset,
): Session;
/**
* Return the immutable event stored at one exact sequence number.
* @param seq - event sequence number.
* @returns the accepted event, or undefined when the log does not contain it.
*/
eventAt(seq: number): SessionEvent | undefined;
eventAt(seq: SessionSeq): SessionEvent | undefined;
/**
* Materialize an immutable snapshot of a half-open event sequence range.
* A full current snapshot is reused until the next append; every previously
@@ -423,9 +459,23 @@ declare class Session {
* @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
* @returns a frozen array of the selected deeply frozen events.
*/
snapshotEvents(fromSeq: number = 0, toSeqExclusive: number = this.log.length): readonly SessionEvent[];
snapshotEvents(
fromSeq: SessionLogOffset = SessionLogOffset(0),
toSeqExclusive: SessionLogOffset = this.seq,
): readonly SessionEvent[];
/**
* Return this Session's events after its fork-inherited prefix.
* @returns a fresh array containing child-owned events in log order.
*/
ownEvents(): readonly SessionEvent[];
/**
* Whether one existing event position is outside the fork-inherited prefix.
* @param seq - event position in this Session.
* @returns true when the event belongs to this Session rather than its parent.
*/
isOwnSeq(seq: SessionSeq): boolean;
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
get seq(): number;
get seq(): SessionLogOffset;
/**
* Append one typed event to the log and synchronously notify observers via
* the store-owned, module-private publication hooks. The hot path never blocks
@@ -525,7 +575,7 @@ Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and
`ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API:
- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `boundary` seq (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events plus child metadata (`parentSession`, `seedLength`, and inherited `cwd`).
- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `SessionSeq` boundary (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events, `parentSession`, `isSeeded: true`, the exact `inheritedEventCount`, and inherited `cwd`.
An explicit `boundary` lets callers fork from any stable between-turn position, including a previous `turn/end` or a later standalone log-only event, even if the source has newer events or an open current turn. The API rejects a prefix that ends inside an open turn instead of clipping silently. Broader execution-relation sanity stays in the existing `dsh-invariants` plugin and persistence repair path rather than being duplicated in `fork()`. `dsh-subagent-fork-in-process` keeps its completed-prefix clipping because tool-time delegation usually starts while the parent turn is open; ordinary session branching should make the requested boundary explicit.
@@ -574,7 +624,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
## The end-seed boundary: `session/end-seed`
A seeded session — resume, fork, or replay — appends this log-only event immediately after its constructor seed, as its first live write. Events before it have smaller seq values and came from the seed. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
A Session constructed with an explicit seed — restore, fork, or replay — appends this log-only event immediately after that constructor seed, as its first live write. Events before it have smaller seq values and came through construction. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. It does not define fork ownership; `isSeeded` plus `inheritedEventCount` do. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
An explicitly supplied empty seed writes `session/end-seed` at seq 0, which distinguishes an empty resumed session from a fresh one. A seed already ending in `session/end-seed` is not re-marked, so reopening an untouched session does not grow its log per pickup. Locate the LAST `session/end-seed` in stored history rather than assuming one exists at `firstLiveSeq`: after a pickup with no work, the event has a smaller seq than the next lifecycle's `firstLiveSeq`.
@@ -630,7 +680,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
* @param signal - optional caller cancellation for persistence reads.
* @returns the current attached state or persisted header and event prefix.
*/
inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }>
inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
/**
* Read all visible Session rows without resuming an Agent.
@@ -770,7 +820,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
@Remote({ mode: 'stream' }) observeJob(request: SessionObserveJobRequest, signal: AbortSignal): AsyncIterable<SessionObserveJobFrame>
```
Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
Types: [SessionId](core.md) · [SessionInspection](persistence.md) · [SessionSearchRequest](session-query.md)
Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
@@ -903,7 +953,7 @@ list(): Session[]
* `SessionStore`'s id policy.
* @returns The created live child session.
*/
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
```
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
+75 -25
View File
@@ -179,6 +179,28 @@ interface RequestContext {
基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
```ts type-equiv
/** Sequence number of one existing event in a Session log. */
type SessionSeq = BrandedNumber<'SessionSeq'>
```
```ts type-equiv
/** A Session log gap, prefix length, or read offset, which may equal the event count. */
type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
```
```ts type-equiv
/** Inclusive Session event watermark, or `-1` before any event exists. */
type SessionSeqCursor = SessionSeq | -1
```
```ts type-equiv
/** One existing Session event position, or explicit absence. */
type OptionalSessionSeq = SessionSeq | null
```
`SessionSeq(value)` 与 `SessionLogOffset(value)` 只接纳非负安全整数,并拒绝负零。它们仅添加编译期品牌,不改变序列化后的数值;算术会返回普通 `number`,调用方必须按结果的预期角色通过对应构造器重新接纳。
```ts type-equiv
/**
* One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
seq: SessionSeq
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
* provider stream; when the field is absent, the event does not record which
* earlier events produced the message.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
@@ -270,7 +292,7 @@ type SurfaceEventType =
*/
type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
| { op: 'replace'; start: SessionSeq; end: SessionSeq }
```
`'append'` 是常规的尾部追加路径。`replace` 会遮蔽从 `start` 到 `end`(含两端)的 surface 条目(两者都必须是有效的 surface seq;`start === end` 时仅替换单个条目),并在原位置插入新事件。
@@ -290,7 +312,7 @@ interface SurfaceIntent {
* absent, the event does not record which earlier events produced the message.
* Other surface events require a non-empty set when this field is present.
*/
sourceEventSeqs?: number[]
sourceEventSeqs?: SessionSeq[]
}
```
@@ -308,7 +330,7 @@ interface SurfaceIntent {
/** Readonly live projection of the message-producing session events. */
interface SessionSurface {
/** Current surface event sequences in model-visible order. */
readonly nodes: readonly number[]
readonly nodes: readonly SessionSeq[]
/** Monotonic count of committed positional replacements. */
readonly replaceGeneration: number
}
@@ -322,13 +344,13 @@ interface SessionSurface {
/** One replacement operation observed while folding a session surface. */
interface SurfaceFoldReplacement {
/** Seq of the event that replaced the prior surface range. */
seq: number
seq: SessionSeq
/** Declared inclusive start seq of the replaced surface range. */
start: number
start: SessionSeq
/** Declared inclusive end seq of the replaced surface range. */
end: number
end: SessionSeq
/** Actual surface entries removed by the operation, in surface order. */
shadowedSeqs: number[]
shadowedSeqs: SessionSeq[]
}
```
@@ -336,7 +358,7 @@ interface SurfaceFoldReplacement {
/** Complete result of replaying the surface operations in a session log. */
interface SurfaceFoldResult {
/** Current surface event sequences in model-visible order. */
nodes: number[]
nodes: SessionSeq[]
/** Replacement operations in event order. */
replacements: SurfaceFoldReplacement[]
}
@@ -360,13 +382,15 @@ declare class Session {
get surface(): SessionSurface;
/**
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
* and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
* `Session` is created without a store-owned header, a minimal header is
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
* `session.header` is always present. Kept out of the event log — it is a
* storage concern, not replayable conversation state.
*/
readonly header: SessionHeader;
/** Number of leading events inherited from this Session's fork parent. */
readonly inheritedEventCount: SessionLogOffset;
/** The session identity, derived from its durable header's single copy. */
get id(): SessionId;
/**
@@ -375,9 +399,9 @@ declare class Session {
* construction — replay, fork, or resume — and were never published on the
* `session/event` firehose (constructor seeds do not emit), so consumers
* that replay the log as a publication substitute (telemetry adoption)
* start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
* boundary: a resumed session's constructor seed is its full stored log,
* while its header keeps the original fork value — this field is the
* start here. Distinct from {@link inheritedEventCount}, the DURABLE
* fork-lineage cut: a resumed session's constructor seed is its full stored
* log, while the inherited count keeps the original fork value — this field is the
* in-process construction fact.
*
* Not persisted itself: a seeded session projects it into the log as the
@@ -391,16 +415,22 @@ declare class Session {
* store attaches and therefore does not publish either. Otherwise this seq
* holds an ordinary published write.
*/
readonly firstLiveSeq: number;
readonly firstLiveSeq: SessionLogOffset;
/**
* Create a detached session by validating and snapshotting borrowed seed
* events and storage metadata.
* @param id - session identity.
* @param seed - optional borrowed replay or fork events.
* @param header - optional borrowed storage metadata.
* @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
* @returns a detached session.
*/
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
static create(
id: SessionId,
seed?: readonly SessionEvent[],
header?: SessionHeader,
inheritedEventCount?: SessionLogOffset,
): Session;
/**
* Restore a detached session by taking ownership of fresh persistence values.
* The storage format, event envelopes, sequence continuity, surface transitions,
@@ -408,15 +438,21 @@ declare class Session {
* @param id - restored session identity.
* @param seed - fresh detached events whose ownership is transferred.
* @param header - fresh detached metadata whose ownership is transferred.
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
* @returns a restored detached session.
*/
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
static fromRestore(
id: SessionId,
seed: readonly SessionEvent[],
header: SessionHeader,
inheritedEventCount: SessionLogOffset,
): Session;
/**
* Return the immutable event stored at one exact sequence number.
* @param seq - event sequence number.
* @returns the accepted event, or undefined when the log does not contain it.
*/
eventAt(seq: number): SessionEvent | undefined;
eventAt(seq: SessionSeq): SessionEvent | undefined;
/**
* Materialize an immutable snapshot of a half-open event sequence range.
* A full current snapshot is reused until the next append; every previously
@@ -425,9 +461,23 @@ declare class Session {
* @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
* @returns a frozen array of the selected deeply frozen events.
*/
snapshotEvents(fromSeq: number = 0, toSeqExclusive: number = this.log.length): readonly SessionEvent[];
snapshotEvents(
fromSeq: SessionLogOffset = SessionLogOffset(0),
toSeqExclusive: SessionLogOffset = this.seq,
): readonly SessionEvent[];
/**
* Return this Session's events after its fork-inherited prefix.
* @returns a fresh array containing child-owned events in log order.
*/
ownEvents(): readonly SessionEvent[];
/**
* Whether one existing event position is outside the fork-inherited prefix.
* @param seq - event position in this Session.
* @returns true when the event belongs to this Session rather than its parent.
*/
isOwnSeq(seq: SessionSeq): boolean;
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
get seq(): number;
get seq(): SessionLogOffset;
/**
* Append one typed event to the log and synchronously notify observers via
* the store-owned, module-private publication hooks. The hot path never blocks
@@ -527,7 +577,7 @@ declare class Session {
`ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork,`SessionStore` 暴露一个策略 API:
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `SessionSeq` boundary(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的 seed event、`parentSession`、`isSeeded: true`、精确 `inheritedEventCount` 及继承的 `cwd`。
显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。更广泛的执行关系健全性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork-in-process` 保留其已完成前缀截断逻辑,因为工具调用时的委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
@@ -578,7 +628,7 @@ interface TurnEndReasonMap {
## 种子结束边界:`session/end-seed`
带种子的会话(恢复、fork 或回放)紧接构造种子之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且来自种子。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的消费方回答本生命周期的写入从哪里开始,该事件则为只持有存储字节的消费方回答同一问题。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
用显式 seed 构造的 Session(restore、fork 或 replay)会紧接该 constructor seed 之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且经由构造进入。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的 consumer 回答本 lifecycle 的写入从哪里开始,该事件则为只持有存储字节的 consumer 回答同一问题。它不定义 fork ownership;`isSeeded` 与 `inheritedEventCount` 才定义。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
显式传入的空种子会在 seq 0 写入 `session/end-seed`,从而把从空日志恢复的会话与全新会话区分开来。种子本身已以 `session/end-seed` 结尾时不会重复标记,因此重新打开一个未被改动的会话不会每次拾起都增长日志。应定位存储历史中的最后一条 `session/end-seed`,而不是假定 `firstLiveSeq` 处一定有一条:在一次没有产生工作的拾起之后,该事件的 seq 会小于下一个生命周期的 `firstLiveSeq`。
@@ -634,7 +684,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
* @param signal - optional caller cancellation for persistence reads.
* @returns the current attached state or persisted header and event prefix.
*/
inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }>
inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
/**
* Read all visible Session rows without resuming an Agent.
@@ -774,7 +824,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
@Remote({ mode: 'stream' }) observeJob(request: SessionObserveJobRequest, signal: AbortSignal): AsyncIterable<SessionObserveJobFrame>
```
Types: [SessionHeader](persistence.zh.md) · [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md)
Types: [SessionId](core.zh.md) · [SessionInspection](persistence.zh.md) · [SessionSearchRequest](session-query.zh.md)
Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
@@ -907,7 +957,7 @@ list(): Session[]
* `SessionStore`'s id policy.
* @returns The created live child session.
*/
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
```
Types: [CreateSessionOptions](persistence.zh.md) · [PrepareSessionOptions](persistence.zh.md) · [SessionId](core.zh.md)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
subagent.md: 109f8569879c0578f804569d1105c03250ad3bf5
subagent.zh.md: 649a3796f9f199306e45ea9653bb38df8683a518
subagent.md: 5369ed85b5382dcb07df3e458a99124107ad5e51
subagent.zh.md: 7495890531c6faa604a96edf443543840f69ae2c
+1 -1
View File
@@ -258,7 +258,7 @@ interface ContinuableCreateSpec {
The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity).
A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `header.seedLength` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `Session.inheritedEventCount` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. A seeded cold list skips a cache hint until an authoritative observation supplies that exact cut. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries
+1 -1
View File
@@ -258,7 +258,7 @@ interface ContinuableCreateSpec {
描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。
本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`header.seedLength` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`Session.inheritedEventCount` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。seeded cold list 会跳过 cache hint,直到权威 observation 提供该精确 cut。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/token-meter.md
token-meter.md: 9c4a1e4b95ffd84f65f7a73e208be245378a3301
token-meter.zh.md: d9e2e7f773041ccb6d1e4c3cc4d81a342db0cc01
token-meter.md: 2265f5073efbd2953e4f399ad58a026772b60006
token-meter.zh.md: b1d9d66dc804fecace15489e61c1d080c096932d
+2 -2
View File
@@ -12,7 +12,7 @@ Source: [`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter
/** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
interface TokenMeasurement {
/** Number of durable events consumed; equal to the next unread event seq. */
readonly logRevision: number
readonly logRevision: SessionLogOffset
/** Provider or heuristic anchor used for this measurement. */
readonly baseline: TokenMeasurementBaseline
/** Signed repricing of current surface content relative to the baseline anchor. */
@@ -34,7 +34,7 @@ Every measurement resolves the effective envelope's routed provider/model to tha
/** One token-priced node in the current ordered session surface. */
interface TokenSurfaceNode {
/** Durable sequence number of the surface event. */
readonly seq: number
readonly seq: SessionSeq
/**
* Request-pressure tokens for the exact message projected by this node under
* the measured route: image occurrences carry the route's declared visual
+2 -2
View File
@@ -12,7 +12,7 @@
/** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
interface TokenMeasurement {
/** Number of durable events consumed; equal to the next unread event seq. */
readonly logRevision: number
readonly logRevision: SessionLogOffset
/** Provider or heuristic anchor used for this measurement. */
readonly baseline: TokenMeasurementBaseline
/** Signed repricing of current surface content relative to the baseline anchor. */
@@ -34,7 +34,7 @@ interface TokenMeasurement {
/** One token-priced node in the current ordered session surface. */
interface TokenSurfaceNode {
/** Durable sequence number of the surface event. */
readonly seq: number
readonly seq: SessionSeq
/**
* Request-pressure tokens for the exact message projected by this node under
* the measured route: image occurrences carry the route's declared visual
+9 -8
View File
@@ -254,6 +254,7 @@ describe('automation-only ACP bridge', () => {
id: sessionId,
createdAt: 1,
cwd: process.cwd(),
isSeeded: false,
}])
const resume = vi.spyOn(harness.ctx.agents, 'resume')
@@ -363,14 +364,14 @@ describe('automation-only ACP bridge', () => {
const active = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const persistence = harness.ctx.get('sessionPersistence')!
vi.spyOn(persistence, 'list').mockResolvedValue([
{ version: 0, id: SessionId(active.sessionId), createdAt: 9, cwd: process.cwd() },
{ version: 0, id: SessionId('subagent'), createdAt: 8, cwd: '/missing/filter', origin: 'subagent' },
{ version: 0, id: SessionId('fork'), createdAt: 7, cwd: '/missing/filter', parentSession: SessionId('parent') },
{ version: 0, id: SessionId('no-cwd'), createdAt: 6 },
{ version: 0, id: SessionId('relative'), createdAt: 5, cwd: 'relative' },
{ version: 0, id: SessionId('other'), createdAt: 4, cwd: '/missing/other' },
{ version: 0, id: SessionId('valid-b'), createdAt: 3, cwd: '/missing/filter' },
{ version: 0, id: SessionId('valid-a'), createdAt: 3, cwd: '/missing/filter' },
{ version: 0, id: SessionId(active.sessionId), createdAt: 9, cwd: process.cwd(), isSeeded: false },
{ version: 0, id: SessionId('subagent'), createdAt: 8, cwd: '/missing/filter', isSeeded: false, origin: 'subagent' },
{ version: 0, id: SessionId('fork'), createdAt: 7, cwd: '/missing/filter', isSeeded: true, parentSession: SessionId('parent') },
{ version: 0, id: SessionId('no-cwd'), createdAt: 6, isSeeded: false },
{ version: 0, id: SessionId('relative'), createdAt: 5, cwd: 'relative', isSeeded: false },
{ version: 0, id: SessionId('other'), createdAt: 4, cwd: '/missing/other', isSeeded: false },
{ version: 0, id: SessionId('valid-b'), createdAt: 3, cwd: '/missing/filter', isSeeded: false },
{ version: 0, id: SessionId('valid-a'), createdAt: 3, cwd: '/missing/filter', isSeeded: false },
])
await expect(harness.client.listSessions({ cwd: 'relative' })).rejects.toThrow(/absolute path/)
+4 -4
View File
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import type { Context } from '@deepseek-ai/cordis'
import { ToolCallId, MessageId } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { SessionSeq, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
import { assistantUpdates, toolCallUpdate, toolResultUpdate } from '../src/updates.ts'
/** Minimal committed assistant event for pure update projection tests. */
@@ -11,7 +11,7 @@ function assistantEvent(
): SessionEvent<'assistant/message'> {
return {
type: 'assistant/message',
seq: 0,
seq: SessionSeq(0),
time: 0,
data: {
turn: 1,
@@ -57,13 +57,13 @@ describe('standard ACP update projection', () => {
it('preserves malformed tool input and projects a failed result without hidden content', async () => {
const call = toolCallUpdate({
type: 'tool/call',
seq: 0,
seq: SessionSeq(0),
time: 0,
data: { turn: 1, step: 1, callId: ToolCallId('call-bad'), name: 'broken', arguments: '{' },
})
const result = await toolResultUpdate({ get: () => undefined } as unknown as Context, {
type: 'tool/result',
seq: 0,
seq: SessionSeq(0),
time: 0,
data: {
turn: 1,
+8 -3
View File
@@ -9,7 +9,8 @@ import type {
import type {} from '@deepseek-ai/dsh-agent-default-model'
import type {} from '@deepseek-ai/dsh-agent-presets'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import type {} from '@deepseek-ai/dsh-typert-registry'
@@ -112,7 +113,7 @@ export async function inspectApiSession(
ctx: Context,
sessionId: SessionId,
signal?: AbortSignal,
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
): Promise<SessionInspection> {
try {
using observation = await ctx.sessionQuery.observeSession(sessionId, {
...(signal === undefined ? {} : { signal }),
@@ -121,7 +122,11 @@ export async function inspectApiSession(
if (observation.header.cwd === undefined) {
throw new ApiSessionNotFound(`session "${sessionId}" not found`)
}
return { meta: observation.header, events: [...observation.events] }
return {
meta: observation.header,
inheritedEventCount: observation.inheritedEventCount,
events: [...observation.events],
}
} catch (error: unknown) {
if (error instanceof SessionQueryError
&& error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
@@ -9,7 +9,7 @@
*/
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
@@ -121,7 +121,7 @@ export interface ISession {
* @param title - raw title text (the host normalizes acceptance).
* @returns the normalized accepted title and its event seq, or the business error.
*/
rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>>
rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>>
/**
* Extend the history window backwards (older messages pagination).
* @returns completion; failures land in snapshot.openState/loadingOlder.
@@ -135,7 +135,7 @@ export interface ISession {
* @param seq - durable event seq the window must reach (a turn's `turn/start` seq).
* @returns completion once covered, exhausted, superseded, or failed soft.
*/
loadThrough(seq: number): Promise<void>
loadThrough(seq: SessionSeq): Promise<void>
/**
* Execute one slash-command line against this session's agent — pure
* admission semantics (the host executor durably logs the lifecycle).
@@ -3,7 +3,7 @@
// List data never enters zustand; React connects via subscribe/getListSnapshot.
import type { SubagentAddress, SubagentCatalog } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionSeq, type SessionId, type SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type {
SessionControlBaseline,
@@ -26,6 +26,10 @@ import { ProjectionValueStore } from './projection-store.ts'
import { Session } from './session.ts'
import type { SessionRemotes } from './remotes.ts'
function sessionSeqCursor(value: number): SessionSeqCursor {
return value === -1 ? -1 : SessionSeq(value)
}
/**
* List arrival lifecycle, orthogonal to the pull-activity `state` axis:
* `pending` (no successful pull yet — an empty items array means "nothing
@@ -498,7 +502,7 @@ export class SessionManager {
if (block === undefined) continue
const store = this.projectionStore(s.sessionId)
const values = block.values as Record<string, unknown>
for (const key of Object.keys(values)) store.apply(key, values[key], block.asOfSeq)
for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq))
}
} else {
this.listState = 'error'
@@ -590,7 +594,7 @@ export class SessionManager {
* @returns the fork result (the child session id).
*/
async fork(
opts: { sessionId: SessionId; atSeq?: number },
opts: { sessionId: SessionId; atSeq?: SessionSeq },
): Promise<RemoteResult<{ sessionId: SessionId }>> {
const source = this.summaries.find(s => s.sessionId === opts.sessionId)
const result = await this.remote.session.fork({
@@ -661,7 +665,7 @@ export class SessionManager {
return
}
if (frame.type === 'projection') {
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, frame.seq)
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq))
this.notifier.markDirty()
return
}
@@ -688,8 +692,9 @@ export class SessionManager {
for (const [sessionId, block] of Object.entries(baseline.projections)) {
const store = this.projectionStore(sessionId as SessionId)
store.truncate(block.asOfSeq)
store.seed(block)
const asOfSeq = sessionSeqCursor(block.asOfSeq)
store.truncate(asOfSeq)
store.seed({ ...block, asOfSeq })
}
for (const [sessionId, session] of this.sessions) {
session.replaceControl(this.queues.get(sessionId) ?? [])
@@ -708,7 +713,7 @@ export class SessionManager {
if (projections !== undefined) {
const store = this.projectionStore(summary.sessionId)
for (const [key, value] of Object.entries(projections.values)) {
store.apply(key, value, projections.asOfSeq)
store.apply(key, value, sessionSeqCursor(projections.asOfSeq))
}
}
if (summary.origin === 'subagent' && summary.parentSessionId !== undefined) {
@@ -9,6 +9,7 @@
* bare observable faces feed `useProjection` (ui-renderer binds them).
*/
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import { Notifier } from './notifier.ts'
@@ -46,7 +47,7 @@ export type UseProjection = {
*/
export interface ProjectionsBaseline {
/** The consistent-cut seq (equals the window tail seq by construction). */
asOfSeq: number
asOfSeq: SessionSeqCursor
/** Whole current values by key; a registered key absent here means the capability is absent. */
values: Readonly<Record<string, unknown>>
}
@@ -54,7 +55,7 @@ export interface ProjectionsBaseline {
/** One key's row: the latest finished value and the seq it is consistent with. */
interface Row {
value: unknown
seq: number
seq: SessionSeqCursor
}
/** Per-key notification channel: the bare face plus its batching notifier. */
@@ -130,7 +131,7 @@ export class ProjectionValueStore {
* @param value - whole value computed by the host unit.
* @param seq - the unit's watermark at emission.
*/
apply(key: string, value: unknown, seq: number): void {
apply(key: string, value: unknown, seq: SessionSeqCursor): void {
const row = this.rows.get(key)
if (row !== undefined && seq <= row.seq) return // higher seq wins; replays and stale frames drop
this.rows.set(key, { value, seq })
@@ -165,7 +166,7 @@ export class ProjectionValueStore {
* baseline immediately afterward.
* @param lastSeq - highest durable sequence reflected by the baseline.
*/
truncate(lastSeq: number): void {
truncate(lastSeq: SessionSeqCursor): void {
for (const [key, row] of this.rows) {
if (row.seq <= lastSeq) continue
this.rows.delete(key)
@@ -16,7 +16,7 @@
*/
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import { SESSION_SEARCH_RESULT_LIMIT } from '../../types.ts'
@@ -438,7 +438,7 @@ export class ClientSessions implements ISessions {
// Flooring lands inside the anchor's own turn (every turn opens with a
// turn/start), so the host's first-turn/end-at-or-after cut still ends
// on that turn — never clipped back to the previous one.
...(opts.atSeq === undefined ? {} : { atSeq: Math.floor(opts.atSeq) }),
...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }),
})
if (!result.ok) throw new SessionForkError(result.error, opts.sessionId)
this.projectList()
@@ -6,7 +6,7 @@ import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-atta
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
import { SessionEventStream } from '../transport.ts'
import type { SessionJournalChange } from '../transport.ts'
import type {
@@ -14,6 +14,7 @@ import type {
QueueAction,
SessionAddress,
SessionControlFrame,
SessionProjectionBaseline,
SessionQueuedItem,
SessionRequestId,
} from '../../types.ts'
@@ -36,6 +37,13 @@ import type { ProjectionsBaseline } from './projection-store.ts'
import { resolvedClientTimeZone } from '../time-zone.ts'
import { SessionQueueMirror } from './queue-mirror.ts'
function projectionsBaseline(value: SessionProjectionBaseline): ProjectionsBaseline {
return {
...value,
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
}
}
/** Messages requested per history page. */
export const PAGE_MESSAGES = 50
@@ -73,7 +81,7 @@ export interface SessionOptions {
*/
export class Session implements SessionFace {
// ---- Window and derived state (all private; the snapshot is the only read API) ----
private baseSeq = 0
private baseSeq = SessionLogOffset(0)
private hasMore = false
private openState: OpenState = 'cold'
private openError: RemoteFailure | null = null
@@ -83,7 +91,7 @@ export class Session implements SessionFace {
private openGeneration = 0
private loadingOlder = false
/** Shared low-water target of the running jump loop; null when no jump is paging. */
private jumpTargetSeq: number | null = null
private jumpTargetSeq: SessionSeq | null = null
/** The running jump loop's completion, shared by retargeting callers. */
private jumpPromise: Promise<void> | null = null
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
@@ -340,10 +348,12 @@ export class Session implements SessionFace {
* @param title - raw title text (the host normalizes acceptance).
* @returns the rename result (normalized accepted title + title event seq).
*/
async rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>> {
async rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>> {
const result = await this.remote.session.rename({ sessionId: this.sessionId, title })
if (result.ok) this.projections.apply('title', result.value.title, result.value.seq)
return result
if (!result.ok) return result
const seq = SessionSeq(result.value.seq)
this.projections.apply('title', result.value.title, seq)
return { ok: true, value: { title: result.value.title, seq } }
}
/**
@@ -391,11 +401,11 @@ export class Session implements SessionFace {
}
/** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
loadThrough(seq: number): Promise<void> {
loadThrough(seq: SessionSeq): Promise<void> {
if (this.openState !== 'open' || !this.hasMore || this.baseSeq <= seq) return Promise.resolve()
if (this.jumpPromise !== null) {
// Retarget the running loop to the lowest requested seq.
this.jumpTargetSeq = Math.min(this.jumpTargetSeq ?? seq, seq)
this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq))
return this.jumpPromise
}
// A plain single-page pull owns the busy flag; the jump does not queue
@@ -448,7 +458,7 @@ export class Session implements SessionFace {
this.openPromise = null
this.openState = 'cold'
this.openError = null
this.baseSeq = 0
this.baseSeq = SessionLogOffset(0)
this.notifier.markDirty()
await this.open()
}
@@ -620,7 +630,11 @@ export class Session implements SessionFace {
private acceptEventChange(change: SessionJournalChange): void {
switch (change.type) {
case 'replace':
this.installWindow(change.entries, change.hasMore, change.page.projections)
this.installWindow(
change.entries,
change.hasMore,
change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections),
)
return
case 'prepend':
this.prependWindow(change.entries, change.hasMore)
@@ -632,7 +646,7 @@ export class Session implements SessionFace {
/** Replace the complete contiguous window and apply page-owned projection metadata. */
private installWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean, projections?: ProjectionsBaseline): void {
this.baseSeq = entries[0]?.event.seq ?? 0
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0)
this.hasMore = hasMore
if (entries.some(entry => entry.event.type === 'turn/start')) this.firstPromptPendingTurn = false
if (projections !== undefined) this.projections.seed(projections)
@@ -643,7 +657,7 @@ export class Session implements SessionFace {
/** Prepend one stream-validated history page. */
private prependWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean): void {
this.baseSeq = entries[0]?.event.seq ?? this.baseSeq
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq)
this.hasMore = hasMore
this.eventSource.prepend(entries, hasMore)
}
@@ -11,6 +11,7 @@ import {
} from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-jobs'
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title'
@@ -188,9 +189,11 @@ export class SessionCommandController {
* @returns the new Session identity.
*/
async fork(request: SessionForkRequest): Promise<SessionForkValue> {
if (request.atSeq !== undefined
&& (!Number.isInteger(request.atSeq) || request.atSeq < 0)) {
throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative integer', {})
let atSeq: ReturnType<typeof SessionSeq> | undefined
try {
atSeq = request.atSeq === undefined ? undefined : SessionSeq(request.atSeq)
} catch {
throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative safe integer', {})
}
let observed: SessionObservation
try {
@@ -210,7 +213,6 @@ export class SessionCommandController {
}
using source = observed
const lastSeq = source.events.at(-1)?.seq ?? -1
const atSeq = request.atSeq
const anchoredBoundary = atSeq === undefined
? undefined
: source.events.find(event => event.type === 'turn/end' && event.seq >= atSeq)
@@ -227,8 +229,10 @@ export class SessionCommandController {
{ sessionId: request.sessionId },
)
}
let cut = boundary.seq + 1
while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') cut++
let cut = SessionLogOffset(boundary.seq + 1)
while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') {
cut = SessionLogOffset(cut + 1)
}
let workspace: Workspace | undefined
try {
workspace = await this.forkWorkspace(source.header)
@@ -246,10 +250,11 @@ export class SessionCommandController {
await this.ctx.agents.create({
sessionId: childId,
seed: source.events.slice(0, cut),
inheritedEventCount: cut,
meta: {
...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
parentSession: source.header.id,
seedLength: cut,
isSeeded: true,
...(composition.agentPreset === undefined
? {}
: { agentPreset: composition.agentPreset }),
+70 -28
View File
@@ -2,9 +2,19 @@
import type { Context } from '@deepseek-ai/cordis'
import { Deque } from '@deepseek-ai/dsh-deque'
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session'
import {
isAppendSurfaceEvent,
SessionLogOffset,
SessionSeq,
} from '@deepseek-ai/dsh-session'
import { isChunkRow, packChunkRuns, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type {
SessionEvent,
SessionHeader,
SessionId,
SessionLogOffset as SessionLogOffsetType,
SessionSeqCursor,
} from '@deepseek-ai/dsh-session'
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
import type {} from '@deepseek-ai/dsh-subagent'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
@@ -19,6 +29,7 @@ import type {
SessionPageRequest,
SessionProjectionBaseline,
SessionProjectionValues,
SessionWireHeader,
SessionWireEvent,
} from './types.ts'
@@ -51,26 +62,32 @@ export class SessionHistoryController {
*/
async page(request: SessionPageRequest, signal: AbortSignal): Promise<SessionPage> {
validatePageRequest(request)
const throughSeq: SessionSeqCursor = request.throughSeq === -1
? -1
: SessionSeq(request.throughSeq)
const beforeSeq = request.beforeSeq === undefined
? undefined
: SessionLogOffset(request.beforeSeq)
using source = await this.sourceFor(request.address, signal, false)
signal.throwIfAborted()
const sourceLog = source.events
const sourceCursor = sourceLog.at(-1)?.seq ?? -1
if (request.throughSeq > sourceCursor) {
const sourceCursor: SessionSeqCursor = sourceLog.at(-1)?.seq ?? -1
if (throughSeq > sourceCursor) {
throw new RemoteError(
'gateway/bad-request',
`session page through seq ${String(request.throughSeq)} is past cursor ${String(sourceCursor)}`,
`session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`,
{},
)
}
/* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
if (request.throughSeq >= 0 && sourceLog[request.throughSeq]?.seq !== request.throughSeq) {
throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(request.throughSeq)}`, {})
if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {})
}
const page = paginate(
sourceLog,
request.beforeSeq,
beforeSeq,
request.maxMessages ?? DEFAULT_MAX_MESSAGES,
request.throughSeq,
throughSeq,
)
const records = pageRecords(page.events)
return {
@@ -90,7 +107,7 @@ export class SessionHistoryController {
const { address } = request
const target = addressId(address)
const buffered = new Deque<SessionEvent>()
let snapshotCursor: number | undefined
let snapshotCursor: SessionSeqCursor | undefined
let wake: (() => void) | undefined
const notify = (): void => {
const resume = wake
@@ -115,7 +132,7 @@ export class SessionHistoryController {
// opening observation, replay everything beyond that snapshot cursor.
const suffix = session.snapshotEvents(snapshotCursor === undefined
? session.firstLiveSeq
: snapshotCursor + 1)
: SessionLogOffset(snapshotCursor + 1))
for (let index = suffix.length - 1; index >= 0; index -= 1) {
buffered.pushFront(suffix[index] as SessionEvent)
}
@@ -132,7 +149,7 @@ export class SessionHistoryController {
const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES)
yield {
type: 'snapshot',
header: source.header,
header: wireHeader(source.header, source.inheritedEventCount),
cursor,
records: pageRecords(page.events),
hasMore: page.hasMore,
@@ -149,18 +166,19 @@ export class SessionHistoryController {
throw error
}
}
let nextSeq = cursor + 1
let nextOffset = SessionLogOffset(cursor + 1)
while (!follower.closed && !signal.aborted) {
const item = buffered.popFront()
if (item === undefined) {
await new Promise<void>((resolve) => { wake = resolve })
continue
}
if (item.seq < nextSeq) continue
if (item.seq !== nextSeq) {
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(nextSeq)}`, {})
const expectedSeq = SessionSeq(nextOffset)
if (item.seq < expectedSeq) continue
if (item.seq !== expectedSeq) {
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(expectedSeq)}`, {})
}
nextSeq++
nextOffset = SessionLogOffset(nextOffset + 1)
yield entryFor(item)
}
} finally {
@@ -187,7 +205,12 @@ export class SessionHistoryController {
rejectNotFound(address)
}
try {
validateAddress(address, observation.header, observation.projections)
validateAddress(
address,
observation.header,
observation.inheritedEventCount,
observation.projections,
)
} catch (error: unknown) {
observation[Symbol.dispose]()
throw error
@@ -213,11 +236,15 @@ function projectionBlock(
}
function validatePageRequest(request: SessionPageRequest): void {
if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1) {
if (!Number.isSafeInteger(request.throughSeq)
|| request.throughSeq < -1
|| Object.is(request.throughSeq, -0)) {
throw new RemoteError('gateway/bad-request', 'throughSeq must be an integer greater than or equal to -1', {})
}
if (request.beforeSeq !== undefined
&& (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0)) {
&& (!Number.isSafeInteger(request.beforeSeq)
|| request.beforeSeq < 0
|| Object.is(request.beforeSeq, -0))) {
throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {})
}
if (request.maxMessages !== undefined
@@ -240,6 +267,7 @@ function addressId(address: SessionAddress): SessionId {
function validateAddress(
address: SessionAddress,
header: SessionHeader,
inheritedEventCount: SessionLogOffsetType,
projections: SessionObservation['projections'],
): void {
if (address.kind === 'session') {
@@ -263,7 +291,7 @@ function validateAddress(
reason: 'corrupt',
})
}
if (identity === undefined || identity.seq < (header.seedLength ?? 0)) {
if (identity === undefined || identity.seq < inheritedEventCount) {
throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', {
parentSessionId: address.parentSessionId,
childSessionId: address.childSessionId,
@@ -289,30 +317,44 @@ function rejectNotFound(address: SessionAddress): never {
function paginate(
events: readonly SessionEvent[],
beforeSeq: number | undefined,
beforeSeq: SessionLogOffsetType | undefined,
maxMessages: number,
throughSeq = events.at(-1)?.seq ?? -1,
throughSeq: SessionSeqCursor = events.at(-1)?.seq ?? -1,
): { readonly events: SessionEvent[]; readonly hasMore: boolean } {
const end = Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1)
const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1))
let count = 0
let cut = 0
let cut = SessionLogOffset(0)
for (let index = end - 1; index >= 0; index--) {
const event = events[index] as SessionEvent
if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event)) continue
count++
const sources = (event as { readonly sourceEventSeqs?: readonly number[] }).sourceEventSeqs
const sources = event.sourceEventSeqs
let groupStart = event.seq
if (sources !== undefined) {
for (const source of sources) groupStart = Math.min(groupStart, source)
for (const source of sources) {
if (source < groupStart) groupStart = source
}
}
if (count >= maxMessages) {
cut = groupStart
cut = SessionLogOffset(groupStart)
break
}
}
return { events: events.slice(cut, end), hasMore: cut > 0 }
}
/** Translate logical Session metadata to the unchanged v0 browser wire. */
function wireHeader(
header: SessionHeader,
inheritedEventCount: SessionLogOffsetType,
): SessionWireHeader {
const { isSeeded, ...wire } = header
return {
...wire,
...isSeeded ? { seedLength: inheritedEventCount } : {},
}
}
function entryFor(event: SessionEvent): SessionEventEntry {
return {
type: 'event',
+8 -3
View File
@@ -5,7 +5,8 @@ import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-jobs'
import { errorChain } from '@deepseek-ai/dsh-llm'
import { canOpenNativePath, openNativePath } from '@deepseek-ai/dsh-native-command'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import {
@@ -213,10 +214,14 @@ export class SessionController extends TypertRemoteService {
inspect(
sessionId: SessionId,
signal?: AbortSignal,
): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }> {
): Promise<SessionInspection> {
const attached = this.ctx.sessions.get(sessionId)
if (attached !== undefined) {
return Promise.resolve({ meta: attached.header, events: attached.snapshotEvents() })
return Promise.resolve({
meta: attached.header,
inheritedEventCount: attached.inheritedEventCount,
events: attached.snapshotEvents(),
})
}
return inspectApiSession(this.ctx, sessionId, signal)
}
+4 -1
View File
@@ -4,6 +4,7 @@ import { stat } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent-presets'
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
import { SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-projection'
import type {} from '@deepseek-ai/dsh-session-projection-cache'
@@ -329,7 +330,9 @@ export class ApiSessionList {
): SessionProjectionHints | undefined {
try {
const block = session === undefined
? this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header)
? header.isSeeded
? undefined
: this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header, SessionLogOffset(0))
: this.ctx.sessionProjections.cachedSnapshot(session)
return block !== undefined && Object.keys(block.values).length > 0
? {
+22 -3
View File
@@ -7,7 +7,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionHeader, SessionId, SurfaceOp } from '@deepseek-ai/dsh-session/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
@@ -397,6 +397,25 @@ export interface SessionEventEntry {
readonly event: SessionWireEvent
}
/** v0-compatible Session metadata carried on the browser wire. */
export interface SessionWireHeader {
readonly version: number
readonly id: SessionId
readonly createdAt: number
readonly cwd?: string
readonly parentSession?: SessionId
/** Exact inherited prefix length; absent for an unseeded Session. */
readonly seedLength?: number
readonly origin?: 'subagent'
readonly delegationDepth?: number
readonly agentPreset?: string
}
/** Browser wire form of one Session surface operation. */
export type SessionWireSurfaceOp =
| 'append'
| { readonly op: 'replace'; readonly start: number; readonly end: number }
/** Event-shaped wire representation of one packed chunk row. */
export type ChunkRowEvent = {
[Kind in ChunkRow['type']]: {
@@ -424,7 +443,7 @@ export interface SessionWireEvent {
readonly data: JsonValue
readonly ignorable?: true
readonly sourceEventSeqs?: number[]
readonly surfaceOp?: SurfaceOp
readonly surfaceOp?: SessionWireSurfaceOp
}
/** One message-aligned backwards-history request. */
@@ -452,7 +471,7 @@ export interface SessionPage {
export type SessionFollowFrame =
| {
readonly type: 'snapshot'
readonly header: SessionHeader
readonly header: SessionWireHeader
readonly cursor: number
readonly records: readonly SessionHistoryRecord[]
readonly hasMore: boolean
@@ -5,9 +5,10 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
@@ -47,10 +48,19 @@ function header(id: string, cwd: string | null = '/workspace'): SessionHeader {
version: 0,
id: SessionId(id),
createdAt: 1,
isSeeded: false,
...(cwd === null ? {} : { cwd }),
}
}
function unseededInspection(
meta: SessionHeader,
events: readonly SessionEvent[] = [],
): SessionInspection {
if (meta.isSeeded) throw new Error('seeded inspection fixtures require an explicit inherited cut')
return { meta, inheritedEventCount: SessionLogOffset(0), events }
}
function providePersistence(ctx: Context, persistence: Record<string, unknown>): () => void {
return ctx.provide('sessionPersistence', testSessionPersistence(ctx, persistence) as never)
}
@@ -97,7 +107,7 @@ describe('ApiSession identity failures', () => {
const listed = header('cwd-less-catalog', null)
const disposeListed = providePersistence(ctx, {
list: () => Promise.resolve([listed]),
inspect: () => Promise.resolve({ meta: listed, events: [] }),
inspect: () => Promise.resolve(unseededInspection(listed)),
})
await expect(inspectApiSession(ctx, listed.id)).rejects.toBeInstanceOf(ApiSessionNotFound)
disposeListed()
@@ -106,7 +116,7 @@ describe('ApiSession identity failures', () => {
const inspected = header('cwd-less-inspect', null)
providePersistence(ctx, {
list: () => Promise.resolve([catalog]),
inspect: () => Promise.resolve({ meta: inspected, events: [] }),
inspect: () => Promise.resolve(unseededInspection(inspected)),
})
await expect(inspectApiSession(ctx, catalog.id)).rejects.toBeInstanceOf(ApiSessionNotFound)
})
@@ -117,11 +127,11 @@ describe('ApiSession identity failures', () => {
await ctx.plugin(SessionStore)
installSessionReadTestServices(ctx)
const meta = header('signalled-inspection')
const inspect = vi.fn(() => Promise.resolve({ meta, events: [] }))
const inspect = vi.fn(() => Promise.resolve(unseededInspection(meta)))
providePersistence(ctx, { inspect })
const signal = new AbortController().signal
await expect(inspectApiSession(ctx, meta.id, signal)).resolves.toEqual({ meta, events: [] })
await expect(inspectApiSession(ctx, meta.id, signal)).resolves.toEqual(unseededInspection(meta))
expect(inspect).toHaveBeenCalledWith(meta.id, signal)
})
})
@@ -138,6 +148,7 @@ describe('ApiSession Agent lookup and recovery', () => {
const observed = {
source: 'prepared',
header: meta,
inheritedEventCount: SessionLogOffset(0),
events: [],
cursor: -1,
projections: { asOfSeq: -1, values: {} },
@@ -177,7 +188,7 @@ describe('ApiSession Agent lookup and recovery', () => {
const ordinaryMeta = header('ordinary-race')
providePersistence(ordinary.ctx, {
list: () => Promise.resolve([ordinaryMeta]),
inspect: () => Promise.resolve({ meta: ordinaryMeta, events: [] }),
inspect: () => Promise.resolve(unseededInspection(ordinaryMeta)),
})
const winner = agent(ordinary.ctx, ordinaryMeta)
vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => {
@@ -190,7 +201,7 @@ describe('ApiSession Agent lookup and recovery', () => {
const childMeta = header('child-race')
providePersistence(child.ctx, {
list: () => Promise.resolve([childMeta]),
inspect: () => Promise.resolve({ meta: childMeta, events: [] }),
inspect: () => Promise.resolve(unseededInspection(childMeta)),
})
vi.spyOn(child.ctx.agents, 'resume').mockImplementation(async () => {
child.ctx.sessions.create(childMeta.id, {
@@ -217,7 +228,7 @@ describe('ApiSession Agent lookup and recovery', () => {
const meta = header('failed')
providePersistence(failed.ctx, {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({ meta, events: [] }),
inspect: () => Promise.resolve(unseededInspection(meta)),
})
vi.spyOn(failed.ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable'))
await expect(failed.agents.resolveAgent(meta.id)).resolves.toMatchObject({
@@ -231,6 +242,7 @@ describe('ApiSession Agent lookup and recovery', () => {
const observed = {
source: 'prepared',
header: meta,
inheritedEventCount: SessionLogOffset(0),
events: [],
cursor: -1,
retain: vi.fn(),
@@ -363,27 +375,27 @@ describe('ApiSession create or adoption', () => {
const meta = { ...header('stored'), agentPreset: 'minimal' }
const events = [{
type: 'agent-preset/selected',
seq: 0,
seq: SessionSeq(0),
time: 1,
data: { agentPreset: 'minimal' },
}] as SessionEvent[]
providePersistence(ctx, {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({ meta, events }),
inspect: () => Promise.resolve(unseededInspection(meta, events)),
})
ctx.provide('agentPresets', {
resolve: (id?: string) => Promise.resolve({ id: id ?? 'minimal' }),
mount: () => Promise.resolve(),
} as never)
const resumedSession = ctx.sessions.prepare(meta.id, {
seed: structuredClone(events),
meta: structuredClone(meta),
inheritedEventCount: SessionLogOffset(0),
seedSource: 'persistence',
})
const resumed = {
id: meta.id,
session: {
id: meta.id,
header: meta,
snapshotEvents: () => events,
eventAt: (seq: number) => events[seq],
seq: events.length,
},
session: resumedSession,
status: 'idle',
ctx,
} as unknown as Agent
@@ -401,7 +413,7 @@ describe('ApiSession create or adoption', () => {
const childMeta = header('resume-child-race')
providePersistence(child.ctx, {
list: () => Promise.resolve([childMeta]),
inspect: () => Promise.resolve({ meta: childMeta, events: [] }),
inspect: () => Promise.resolve(unseededInspection(childMeta)),
})
child.ctx.provide('agentPresets', {
resolve: () => {
@@ -420,7 +432,7 @@ describe('ApiSession create or adoption', () => {
const stored = header('stored-cwd-conflict', '/stored')
providePersistence(conflict.ctx, {
list: () => Promise.resolve([stored]),
inspect: () => Promise.resolve({ meta: stored, events: [] }),
inspect: () => Promise.resolve(unseededInspection(stored)),
})
await expect(conflict.agents.ensureSession(stored.id, '/requested', true))
.rejects.toBeInstanceOf(ApiSessionCwdConflict)
Loaded 100 of 356 files, more files were not shown because too many files have changed in this diff. Show more