dsh-plugin-guide 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.es.md +1 -1
  2. package/README.hi.md +1 -1
  3. package/README.md +1 -1
  4. package/README.pt.md +1 -1
  5. package/README.zh.md +1 -1
  6. package/dist/dsh-plugin-dev.js +3 -3
  7. package/guide/plugin-dev-guide.md +1 -1
  8. package/guide/quick-reference.es.md +1 -1
  9. package/guide/quick-reference.md +1 -1
  10. package/guide/quick-reference.pt.md +1 -1
  11. package/guide/quick-reference.zh-CN.md +1 -1
  12. package/package.json +3 -3
  13. package/references/official-docs/AGENTS.md +1 -1
  14. package/references/official-docs/SNAPSHOT.md +3 -3
  15. package/references/official-docs/docs/architecture.i18n.yaml +2 -2
  16. package/references/official-docs/docs/architecture.md +2 -1
  17. package/references/official-docs/docs/architecture.zh.md +2 -1
  18. package/references/official-docs/docs/capability-seams.i18n.yaml +2 -2
  19. package/references/official-docs/docs/capability-seams.md +3 -1
  20. package/references/official-docs/docs/capability-seams.zh.md +3 -1
  21. package/references/official-docs/docs/config-catalog.i18n.yaml +2 -2
  22. package/references/official-docs/docs/config-catalog.md +96 -54
  23. package/references/official-docs/docs/config-catalog.zh.md +93 -51
  24. package/references/official-docs/docs/cookbook/adding-a-package.i18n.yaml +2 -2
  25. package/references/official-docs/docs/cookbook/adding-a-package.md +1 -1
  26. package/references/official-docs/docs/cookbook/adding-a-package.zh.md +1 -1
  27. package/references/official-docs/docs/event-producer-consumer.i18n.yaml +2 -2
  28. package/references/official-docs/docs/event-producer-consumer.md +17 -17
  29. package/references/official-docs/docs/event-producer-consumer.zh.md +17 -17
  30. package/references/official-docs/docs/module-graph.i18n.yaml +2 -2
  31. package/references/official-docs/docs/module-graph.md +193 -361
  32. package/references/official-docs/docs/module-graph.zh.md +193 -361
  33. package/references/official-docs/docs/persistence-catalog.i18n.yaml +2 -2
  34. package/references/official-docs/docs/persistence-catalog.md +41 -41
  35. package/references/official-docs/docs/persistence-catalog.zh.md +40 -40
  36. package/references/official-docs/docs/subsystems/agent-team.i18n.yaml +2 -2
  37. package/references/official-docs/docs/subsystems/agent-team.md +4 -3
  38. package/references/official-docs/docs/subsystems/agent-team.zh.md +4 -3
  39. package/references/official-docs/docs/subsystems/code-runtime.i18n.yaml +2 -2
  40. package/references/official-docs/docs/subsystems/code-runtime.md +7 -3
  41. package/references/official-docs/docs/subsystems/code-runtime.zh.md +7 -3
  42. package/references/official-docs/docs/subsystems/commands.i18n.yaml +2 -2
  43. package/references/official-docs/docs/subsystems/commands.md +1 -1
  44. package/references/official-docs/docs/subsystems/commands.zh.md +1 -1
  45. package/references/official-docs/docs/subsystems/compaction.i18n.yaml +2 -2
  46. package/references/official-docs/docs/subsystems/compaction.md +9 -9
  47. package/references/official-docs/docs/subsystems/compaction.zh.md +9 -9
  48. package/references/official-docs/docs/subsystems/core.i18n.yaml +2 -2
  49. package/references/official-docs/docs/subsystems/core.md +4 -3
  50. package/references/official-docs/docs/subsystems/core.zh.md +4 -3
  51. package/references/official-docs/docs/subsystems/feedback.i18n.yaml +2 -2
  52. package/references/official-docs/docs/subsystems/feedback.md +2 -2
  53. package/references/official-docs/docs/subsystems/feedback.zh.md +2 -2
  54. package/references/official-docs/docs/subsystems/persistence.i18n.yaml +2 -2
  55. package/references/official-docs/docs/subsystems/persistence.md +179 -187
  56. package/references/official-docs/docs/subsystems/persistence.zh.md +179 -187
  57. package/references/official-docs/docs/subsystems/schedule.i18n.yaml +2 -2
  58. package/references/official-docs/docs/subsystems/schedule.md +1 -1
  59. package/references/official-docs/docs/subsystems/schedule.zh.md +1 -1
  60. package/references/official-docs/docs/subsystems/session-projection.i18n.yaml +2 -2
  61. package/references/official-docs/docs/subsystems/session-projection.md +21 -17
  62. package/references/official-docs/docs/subsystems/session-projection.zh.md +21 -17
  63. package/references/official-docs/docs/subsystems/session-query.i18n.yaml +2 -2
  64. package/references/official-docs/docs/subsystems/session-query.md +19 -13
  65. package/references/official-docs/docs/subsystems/session-query.zh.md +19 -13
  66. package/references/official-docs/docs/subsystems/session-title.i18n.yaml +2 -2
  67. package/references/official-docs/docs/subsystems/session-title.md +5 -5
  68. package/references/official-docs/docs/subsystems/session-title.zh.md +5 -5
  69. package/references/official-docs/docs/subsystems/session.i18n.yaml +2 -2
  70. package/references/official-docs/docs/subsystems/session.md +94 -34
  71. package/references/official-docs/docs/subsystems/session.zh.md +94 -34
  72. package/references/official-docs/docs/subsystems/slots.i18n.yaml +2 -2
  73. package/references/official-docs/docs/subsystems/slots.md +1 -0
  74. package/references/official-docs/docs/subsystems/slots.zh.md +1 -0
  75. package/references/official-docs/docs/subsystems/storage.i18n.yaml +2 -2
  76. package/references/official-docs/docs/subsystems/storage.md +28 -5
  77. package/references/official-docs/docs/subsystems/storage.zh.md +28 -5
  78. package/references/official-docs/docs/subsystems/subagent.i18n.yaml +2 -2
  79. package/references/official-docs/docs/subsystems/subagent.md +32 -88
  80. package/references/official-docs/docs/subsystems/subagent.zh.md +31 -87
  81. package/references/official-docs/docs/subsystems/token-meter.i18n.yaml +2 -2
  82. package/references/official-docs/docs/subsystems/token-meter.md +2 -2
  83. package/references/official-docs/docs/subsystems/token-meter.zh.md +2 -2
  84. package/references/official-docs/docs/tool-catalog.i18n.yaml +2 -2
  85. package/references/official-docs/docs/tool-catalog.md +9 -63
  86. package/references/official-docs/docs/tool-catalog.zh.md +8 -62
  87. package/references/official-docs/docs/web-styling.i18n.yaml +2 -2
  88. package/references/official-docs/docs/web-styling.md +3 -0
  89. package/references/official-docs/docs/web-styling.zh.md +3 -0
  90. package/references/official-docs/packages/AGENTS.md +2 -2
  91. package/templates/js/README.es.md +1 -1
  92. package/templates/js/README.hi.md +1 -1
  93. package/templates/js/README.md +1 -1
  94. package/templates/js/README.pt.md +1 -1
  95. package/templates/js/README.zh.md +1 -1
  96. package/templates/ts/README.es.md +1 -1
  97. package/templates/ts/README.hi.md +1 -1
  98. package/templates/ts/README.md +1 -1
  99. package/templates/ts/README.pt.md +1 -1
  100. package/templates/ts/README.zh.md +1 -1
@@ -179,6 +179,28 @@ interface RequestContext {
179
179
 
180
180
  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.
181
181
 
182
+ ```ts type-equiv
183
+ /** Sequence number of one existing event in a Session log. */
184
+ type SessionSeq = BrandedNumber<'SessionSeq'>
185
+ ```
186
+
187
+ ```ts type-equiv
188
+ /** A Session log gap, prefix length, or read offset, which may equal the event count. */
189
+ type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
190
+ ```
191
+
192
+ ```ts type-equiv
193
+ /** Inclusive Session event watermark, or `-1` before any event exists. */
194
+ type SessionSeqCursor = SessionSeq | -1
195
+ ```
196
+
197
+ ```ts type-equiv
198
+ /** One existing Session event position, or explicit absence. */
199
+ type OptionalSessionSeq = SessionSeq | null
200
+ ```
201
+
202
+ `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.
203
+
182
204
  ```ts type-equiv
183
205
  /**
184
206
  * One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
197
219
  [K in SessionEventType]: {
198
220
  type: K
199
221
  /** Monotonic sequence number within the session. */
200
- seq: number
222
+ seq: SessionSeq
201
223
  /** Unix epoch milliseconds. */
202
224
  time: number
203
225
  data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
221
243
  * provider stream; when the field is absent, the event does not record which
222
244
  * earlier events produced the message.
223
245
  */
224
- sourceEventSeqs?: number[]
246
+ sourceEventSeqs?: SessionSeq[]
225
247
  /** How this event entered the surface; absent for non-surface events. */
226
248
  surfaceOp?: SurfaceOp
227
249
  } : object)
@@ -268,7 +290,7 @@ type SurfaceEventType =
268
290
  */
269
291
  type SurfaceOp =
270
292
  | 'append'
271
- | { op: 'replace'; start: number; end: number }
293
+ | { op: 'replace'; start: SessionSeq; end: SessionSeq }
272
294
  ```
273
295
 
274
296
  `'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 {
288
310
  * absent, the event does not record which earlier events produced the message.
289
311
  * Other surface events require a non-empty set when this field is present.
290
312
  */
291
- sourceEventSeqs?: number[]
313
+ sourceEventSeqs?: SessionSeq[]
292
314
  }
293
315
  ```
294
316
 
@@ -306,7 +328,7 @@ Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the f
306
328
  /** Readonly live projection of the message-producing session events. */
307
329
  interface SessionSurface {
308
330
  /** Current surface event sequences in model-visible order. */
309
- readonly nodes: readonly number[]
331
+ readonly nodes: readonly SessionSeq[]
310
332
  /** Monotonic count of committed positional replacements. */
311
333
  readonly replaceGeneration: number
312
334
  }
@@ -320,13 +342,13 @@ interface SessionSurface {
320
342
  /** One replacement operation observed while folding a session surface. */
321
343
  interface SurfaceFoldReplacement {
322
344
  /** Seq of the event that replaced the prior surface range. */
323
- seq: number
345
+ seq: SessionSeq
324
346
  /** Declared inclusive start seq of the replaced surface range. */
325
- start: number
347
+ start: SessionSeq
326
348
  /** Declared inclusive end seq of the replaced surface range. */
327
- end: number
349
+ end: SessionSeq
328
350
  /** Actual surface entries removed by the operation, in surface order. */
329
- shadowedSeqs: number[]
351
+ shadowedSeqs: SessionSeq[]
330
352
  }
331
353
  ```
332
354
 
@@ -334,7 +356,7 @@ interface SurfaceFoldReplacement {
334
356
  /** Complete result of replaying the surface operations in a session log. */
335
357
  interface SurfaceFoldResult {
336
358
  /** Current surface event sequences in model-visible order. */
337
- nodes: number[]
359
+ nodes: SessionSeq[]
338
360
  /** Replacement operations in event order. */
339
361
  replacements: SurfaceFoldReplacement[]
340
362
  }
@@ -358,13 +380,15 @@ declare class Session {
358
380
  get surface(): SessionSurface;
359
381
  /**
360
382
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
361
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
383
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
362
384
  * `Session` is created without a store-owned header, a minimal header is
363
385
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
364
386
  * `session.header` is always present. Kept out of the event log — it is a
365
387
  * storage concern, not replayable conversation state.
366
388
  */
367
389
  readonly header: SessionHeader;
390
+ /** Number of leading events inherited from this Session's fork parent. */
391
+ readonly inheritedEventCount: SessionLogOffset;
368
392
  /** The session identity, derived from its durable header's single copy. */
369
393
  get id(): SessionId;
370
394
  /**
@@ -373,9 +397,9 @@ declare class Session {
373
397
  * construction — replay, fork, or resume — and were never published on the
374
398
  * `session/event` firehose (constructor seeds do not emit), so consumers
375
399
  * that replay the log as a publication substitute (telemetry adoption)
376
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
377
- * boundary: a resumed session's constructor seed is its full stored log,
378
- * while its header keeps the original fork value — this field is the
400
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
401
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
402
+ * log, while the inherited count keeps the original fork value — this field is the
379
403
  * in-process construction fact.
380
404
  *
381
405
  * Not persisted itself: a seeded session projects it into the log as the
@@ -388,17 +412,23 @@ declare class Session {
388
412
  * When this lifecycle appends the marker, it occupies this seq before the
389
413
  * store attaches and therefore does not publish either. Otherwise this seq
390
414
  * holds an ordinary published write.
391
- */
392
- readonly firstLiveSeq: number;
415
+ */
416
+ readonly firstLiveSeq: SessionLogOffset;
393
417
  /**
394
418
  * Create a detached session by validating and snapshotting borrowed seed
395
419
  * events and storage metadata.
396
420
  * @param id - session identity.
397
421
  * @param seed - optional borrowed replay or fork events.
398
422
  * @param header - optional borrowed storage metadata.
423
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
399
424
  * @returns a detached session.
400
425
  */
401
- static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
426
+ static create(
427
+ id: SessionId,
428
+ seed?: readonly SessionEvent[],
429
+ header?: SessionHeader,
430
+ inheritedEventCount?: SessionLogOffset,
431
+ ): Session;
402
432
  /**
403
433
  * Restore a detached session by taking ownership of fresh persistence values.
404
434
  * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -406,18 +436,46 @@ declare class Session {
406
436
  * @param id - restored session identity.
407
437
  * @param seed - fresh detached events whose ownership is transferred.
408
438
  * @param header - fresh detached metadata whose ownership is transferred.
439
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
409
440
  * @returns a restored detached session.
410
441
  */
411
- static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
442
+ static fromRestore(
443
+ id: SessionId,
444
+ seed: readonly SessionEvent[],
445
+ header: SessionHeader,
446
+ inheritedEventCount: SessionLogOffset,
447
+ ): Session;
448
+ /**
449
+ * Return the immutable event stored at one exact sequence number.
450
+ * @param seq - event sequence number.
451
+ * @returns the accepted event, or undefined when the log does not contain it.
452
+ */
453
+ eventAt(seq: SessionSeq): SessionEvent | undefined;
454
+ /**
455
+ * Materialize an immutable snapshot of a half-open event sequence range.
456
+ * A full current snapshot is reused until the next append; every previously
457
+ * returned snapshot remains stable after later appends.
458
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
459
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
460
+ * @returns a frozen array of the selected deeply frozen events.
461
+ */
462
+ snapshotEvents(
463
+ fromSeq: SessionLogOffset = SessionLogOffset(0),
464
+ toSeqExclusive: SessionLogOffset = this.seq,
465
+ ): readonly SessionEvent[];
466
+ /**
467
+ * Return this Session's events after its fork-inherited prefix.
468
+ * @returns a fresh array containing child-owned events in log order.
469
+ */
470
+ ownEvents(): readonly SessionEvent[];
412
471
  /**
413
- * An immutable snapshot of the append-only event log. The snapshot is reused
414
- * until the next append; a previously returned array does not grow later.
415
- * Events and their nested data are deep-frozen at acceptance, so neither a
416
- * cast nor ordinary JavaScript can rewrite durable history.
472
+ * Whether one existing event position is outside the fork-inherited prefix.
473
+ * @param seq - event position in this Session.
474
+ * @returns true when the event belongs to this Session rather than its parent.
417
475
  */
418
- get events(): readonly SessionEvent[];
476
+ isOwnSeq(seq: SessionSeq): boolean;
419
477
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
420
- get seq(): number;
478
+ get seq(): SessionLogOffset;
421
479
  /**
422
480
  * Append one typed event to the log and synchronously notify observers via
423
481
  * the store-owned, module-private publication hooks. The hot path never blocks
@@ -462,7 +520,7 @@ declare class Session {
462
520
  * The {@link EpochHeader} in force after the log's last header event — the
463
521
  * header the NEXT request will be compared against — or undefined before
464
522
  * the first `request/header` snapshot. The live, incrementally-maintained
465
- * form of `foldRequestHeader(session.events)`: each header event is folded
523
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
466
524
  * once, when first seen, so a per-step read costs O(new events).
467
525
  * @returns the folded header, or undefined when no header event exists yet.
468
526
  */
@@ -517,7 +575,7 @@ Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and
517
575
 
518
576
  `ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API:
519
577
 
520
- - `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`).
578
+ - `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`.
521
579
 
522
580
  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.
523
581
 
@@ -549,8 +607,10 @@ interface TurnEndReasonMap {
549
607
  /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
550
608
  'max-tokens': { kind: 'max-tokens' }
551
609
  /**
552
- * A persistence backend closed a crash-orphaned turn on reload. The loop never
553
- * emits this marker, and the events recorded before the crash remain intact.
610
+ * A crash-orphaned turn was closed after the fact: agent-loop resume appends
611
+ * this closer for a stored log whose last turn never ended, and session-query
612
+ * synthesizes it on cold reads. The loop never emits this marker live, and
613
+ * the events recorded before the crash remain intact.
554
614
  */
555
615
  interrupted: { kind: 'interrupted' }
556
616
  }
@@ -566,7 +626,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
566
626
 
567
627
  ## The end-seed boundary: `session/end-seed`
568
628
 
569
- A seeded sessionresume, 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.
629
+ 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.
570
630
 
571
631
  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`.
572
632
 
@@ -584,7 +644,7 @@ The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-h
584
644
 
585
645
  ## Durability contract
586
646
 
587
- What a persistence backend relies on: the durable log persists every event losslessly, **including** `assistant/chunk` — `seq` must stay contiguous, so chunks cannot be filtered out of the canonical log. A backend may choose its own storage encoding for an event batch as long as `load` returns the exact appended events (the JSONL backend's default packed chunk rows are such an encoding — see [persistence.md](persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.events` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format.
647
+ What a persistence backend relies on: the durable log persists every event losslessly, **including** `assistant/chunk` — `seq` must stay contiguous, so chunks cannot be filtered out of the canonical log. A backend may choose its own storage encoding for an event batch as long as a handle's `read()` returns the exact appended events (the JSONL backend's default packed chunk rows are such an encoding — see [persistence.md](persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.snapshotEvents()` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format.
588
648
 
589
649
  The backends that consume this contract are on [persistence.md](persistence.md).
590
650
 
@@ -622,7 +682,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
622
682
  * @param signal - optional caller cancellation for persistence reads.
623
683
  * @returns the current attached state or persisted header and event prefix.
624
684
  */
625
- inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
685
+ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
626
686
 
627
687
  /**
628
688
  * Read all visible Session rows without resuming an Agent.
@@ -742,7 +802,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
742
802
  @Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
743
803
  ```
744
804
 
745
- Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
805
+ Types: [SessionId](core.md) · [SessionInspection](persistence.md) · [SessionSearchRequest](session-query.md)
746
806
 
747
807
  Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
748
808
 
@@ -752,7 +812,7 @@ Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/sess
752
812
 
753
813
  In-memory session store (`ctx.sessions`).
754
814
 
755
- Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
815
+ Persistence is intentionally not implemented here — the agent lifecycle attaches a session-log writer to each published session's write handle; a session published outside that lifecycle persists nothing.
756
816
 
757
817
  ```ts cordis-catalog
758
818
  /**
@@ -875,7 +935,7 @@ list(): Session[]
875
935
  * `SessionStore`'s id policy.
876
936
  * @returns The created live child session.
877
937
  */
878
- fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
938
+ fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
879
939
  ```
880
940
 
881
941
  Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
@@ -179,6 +179,28 @@ interface RequestContext {
179
179
 
180
180
  基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
181
181
 
182
+ ```ts type-equiv
183
+ /** Sequence number of one existing event in a Session log. */
184
+ type SessionSeq = BrandedNumber<'SessionSeq'>
185
+ ```
186
+
187
+ ```ts type-equiv
188
+ /** A Session log gap, prefix length, or read offset, which may equal the event count. */
189
+ type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
190
+ ```
191
+
192
+ ```ts type-equiv
193
+ /** Inclusive Session event watermark, or `-1` before any event exists. */
194
+ type SessionSeqCursor = SessionSeq | -1
195
+ ```
196
+
197
+ ```ts type-equiv
198
+ /** One existing Session event position, or explicit absence. */
199
+ type OptionalSessionSeq = SessionSeq | null
200
+ ```
201
+
202
+ `SessionSeq(value)` 与 `SessionLogOffset(value)` 只接纳非负安全整数,并拒绝负零。它们仅添加编译期品牌,不改变序列化后的数值;算术会返回普通 `number`,调用方必须按结果的预期角色通过对应构造器重新接纳。
203
+
182
204
  ```ts type-equiv
183
205
  /**
184
206
  * One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
197
219
  [K in SessionEventType]: {
198
220
  type: K
199
221
  /** Monotonic sequence number within the session. */
200
- seq: number
222
+ seq: SessionSeq
201
223
  /** Unix epoch milliseconds. */
202
224
  time: number
203
225
  data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
221
243
  * provider stream; when the field is absent, the event does not record which
222
244
  * earlier events produced the message.
223
245
  */
224
- sourceEventSeqs?: number[]
246
+ sourceEventSeqs?: SessionSeq[]
225
247
  /** How this event entered the surface; absent for non-surface events. */
226
248
  surfaceOp?: SurfaceOp
227
249
  } : object)
@@ -270,7 +292,7 @@ type SurfaceEventType =
270
292
  */
271
293
  type SurfaceOp =
272
294
  | 'append'
273
- | { op: 'replace'; start: number; end: number }
295
+ | { op: 'replace'; start: SessionSeq; end: SessionSeq }
274
296
  ```
275
297
 
276
298
  `'append'` 是常规的尾部追加路径。`replace` 会遮蔽从 `start` 到 `end`(含两端)的 surface 条目(两者都必须是有效的 surface seq;`start === end` 时仅替换单个条目),并在原位置插入新事件。
@@ -290,7 +312,7 @@ interface SurfaceIntent {
290
312
  * absent, the event does not record which earlier events produced the message.
291
313
  * Other surface events require a non-empty set when this field is present.
292
314
  */
293
- sourceEventSeqs?: number[]
315
+ sourceEventSeqs?: SessionSeq[]
294
316
  }
295
317
  ```
296
318
 
@@ -308,7 +330,7 @@ interface SurfaceIntent {
308
330
  /** Readonly live projection of the message-producing session events. */
309
331
  interface SessionSurface {
310
332
  /** Current surface event sequences in model-visible order. */
311
- readonly nodes: readonly number[]
333
+ readonly nodes: readonly SessionSeq[]
312
334
  /** Monotonic count of committed positional replacements. */
313
335
  readonly replaceGeneration: number
314
336
  }
@@ -322,13 +344,13 @@ interface SessionSurface {
322
344
  /** One replacement operation observed while folding a session surface. */
323
345
  interface SurfaceFoldReplacement {
324
346
  /** Seq of the event that replaced the prior surface range. */
325
- seq: number
347
+ seq: SessionSeq
326
348
  /** Declared inclusive start seq of the replaced surface range. */
327
- start: number
349
+ start: SessionSeq
328
350
  /** Declared inclusive end seq of the replaced surface range. */
329
- end: number
351
+ end: SessionSeq
330
352
  /** Actual surface entries removed by the operation, in surface order. */
331
- shadowedSeqs: number[]
353
+ shadowedSeqs: SessionSeq[]
332
354
  }
333
355
  ```
334
356
 
@@ -336,7 +358,7 @@ interface SurfaceFoldReplacement {
336
358
  /** Complete result of replaying the surface operations in a session log. */
337
359
  interface SurfaceFoldResult {
338
360
  /** Current surface event sequences in model-visible order. */
339
- nodes: number[]
361
+ nodes: SessionSeq[]
340
362
  /** Replacement operations in event order. */
341
363
  replacements: SurfaceFoldReplacement[]
342
364
  }
@@ -360,13 +382,15 @@ declare class Session {
360
382
  get surface(): SessionSurface;
361
383
  /**
362
384
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
363
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
385
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
364
386
  * `Session` is created without a store-owned header, a minimal header is
365
387
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
366
388
  * `session.header` is always present. Kept out of the event log — it is a
367
389
  * storage concern, not replayable conversation state.
368
390
  */
369
391
  readonly header: SessionHeader;
392
+ /** Number of leading events inherited from this Session's fork parent. */
393
+ readonly inheritedEventCount: SessionLogOffset;
370
394
  /** The session identity, derived from its durable header's single copy. */
371
395
  get id(): SessionId;
372
396
  /**
@@ -375,9 +399,9 @@ declare class Session {
375
399
  * construction — replay, fork, or resume — and were never published on the
376
400
  * `session/event` firehose (constructor seeds do not emit), so consumers
377
401
  * that replay the log as a publication substitute (telemetry adoption)
378
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
379
- * boundary: a resumed session's constructor seed is its full stored log,
380
- * while its header keeps the original fork value — this field is the
402
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
403
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
404
+ * log, while the inherited count keeps the original fork value — this field is the
381
405
  * in-process construction fact.
382
406
  *
383
407
  * Not persisted itself: a seeded session projects it into the log as the
@@ -390,17 +414,23 @@ declare class Session {
390
414
  * When this lifecycle appends the marker, it occupies this seq before the
391
415
  * store attaches and therefore does not publish either. Otherwise this seq
392
416
  * holds an ordinary published write.
393
- */
394
- readonly firstLiveSeq: number;
417
+ */
418
+ readonly firstLiveSeq: SessionLogOffset;
395
419
  /**
396
420
  * Create a detached session by validating and snapshotting borrowed seed
397
421
  * events and storage metadata.
398
422
  * @param id - session identity.
399
423
  * @param seed - optional borrowed replay or fork events.
400
424
  * @param header - optional borrowed storage metadata.
425
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
401
426
  * @returns a detached session.
402
427
  */
403
- static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
428
+ static create(
429
+ id: SessionId,
430
+ seed?: readonly SessionEvent[],
431
+ header?: SessionHeader,
432
+ inheritedEventCount?: SessionLogOffset,
433
+ ): Session;
404
434
  /**
405
435
  * Restore a detached session by taking ownership of fresh persistence values.
406
436
  * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -408,18 +438,46 @@ declare class Session {
408
438
  * @param id - restored session identity.
409
439
  * @param seed - fresh detached events whose ownership is transferred.
410
440
  * @param header - fresh detached metadata whose ownership is transferred.
441
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
411
442
  * @returns a restored detached session.
412
443
  */
413
- static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
444
+ static fromRestore(
445
+ id: SessionId,
446
+ seed: readonly SessionEvent[],
447
+ header: SessionHeader,
448
+ inheritedEventCount: SessionLogOffset,
449
+ ): Session;
450
+ /**
451
+ * Return the immutable event stored at one exact sequence number.
452
+ * @param seq - event sequence number.
453
+ * @returns the accepted event, or undefined when the log does not contain it.
454
+ */
455
+ eventAt(seq: SessionSeq): SessionEvent | undefined;
456
+ /**
457
+ * Materialize an immutable snapshot of a half-open event sequence range.
458
+ * A full current snapshot is reused until the next append; every previously
459
+ * returned snapshot remains stable after later appends.
460
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
461
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
462
+ * @returns a frozen array of the selected deeply frozen events.
463
+ */
464
+ snapshotEvents(
465
+ fromSeq: SessionLogOffset = SessionLogOffset(0),
466
+ toSeqExclusive: SessionLogOffset = this.seq,
467
+ ): readonly SessionEvent[];
468
+ /**
469
+ * Return this Session's events after its fork-inherited prefix.
470
+ * @returns a fresh array containing child-owned events in log order.
471
+ */
472
+ ownEvents(): readonly SessionEvent[];
414
473
  /**
415
- * An immutable snapshot of the append-only event log. The snapshot is reused
416
- * until the next append; a previously returned array does not grow later.
417
- * Events and their nested data are deep-frozen at acceptance, so neither a
418
- * cast nor ordinary JavaScript can rewrite durable history.
474
+ * Whether one existing event position is outside the fork-inherited prefix.
475
+ * @param seq - event position in this Session.
476
+ * @returns true when the event belongs to this Session rather than its parent.
419
477
  */
420
- get events(): readonly SessionEvent[];
478
+ isOwnSeq(seq: SessionSeq): boolean;
421
479
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
422
- get seq(): number;
480
+ get seq(): SessionLogOffset;
423
481
  /**
424
482
  * Append one typed event to the log and synchronously notify observers via
425
483
  * the store-owned, module-private publication hooks. The hot path never blocks
@@ -464,7 +522,7 @@ declare class Session {
464
522
  * The {@link EpochHeader} in force after the log's last header event — the
465
523
  * header the NEXT request will be compared against — or undefined before
466
524
  * the first `request/header` snapshot. The live, incrementally-maintained
467
- * form of `foldRequestHeader(session.events)`: each header event is folded
525
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
468
526
  * once, when first seen, so a per-step read costs O(new events).
469
527
  * @returns the folded header, or undefined when no header event exists yet.
470
528
  */
@@ -519,7 +577,7 @@ declare class Session {
519
577
 
520
578
  `ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork,`SessionStore` 暴露一个策略 API:
521
579
 
522
- - `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
580
+ - `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `SessionSeq` boundary(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的 seed event、`parentSession`、`isSeeded: true`、精确 `inheritedEventCount` 及继承的 `cwd`。
523
581
 
524
582
  显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。更广泛的执行关系健全性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork-in-process` 保留其已完成前缀截断逻辑,因为工具调用时的委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
525
583
 
@@ -553,8 +611,10 @@ interface TurnEndReasonMap {
553
611
  /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
554
612
  'max-tokens': { kind: 'max-tokens' }
555
613
  /**
556
- * A persistence backend closed a crash-orphaned turn on reload. The loop never
557
- * emits this marker, and the events recorded before the crash remain intact.
614
+ * A crash-orphaned turn was closed after the fact: agent-loop resume appends
615
+ * this closer for a stored log whose last turn never ended, and session-query
616
+ * synthesizes it on cold reads. The loop never emits this marker live, and
617
+ * the events recorded before the crash remain intact.
558
618
  */
559
619
  interrupted: { kind: 'interrupted' }
560
620
  }
@@ -570,7 +630,7 @@ interface TurnEndReasonMap {
570
630
 
571
631
  ## 种子结束边界:`session/end-seed`
572
632
 
573
- 带种子的会话(恢复、fork 或回放)紧接构造种子之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且来自种子。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的消费方回答本生命周期的写入从哪里开始,该事件则为只持有存储字节的消费方回答同一问题。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
633
+ 用显式 seed 构造的 Session(restore、fork replay)会紧接该 constructor seed 之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且经由构造进入。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的 consumer 回答本 lifecycle 的写入从哪里开始,该事件则为只持有存储字节的 consumer 回答同一问题。它不定义 fork ownership;`isSeeded` 与 `inheritedEventCount` 才定义。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
574
634
 
575
635
  显式传入的空种子会在 seq 0 写入 `session/end-seed`,从而把从空日志恢复的会话与全新会话区分开来。种子本身已以 `session/end-seed` 结尾时不会重复标记,因此重新打开一个未被改动的会话不会每次拾起都增长日志。应定位存储历史中的最后一条 `session/end-seed`,而不是假定 `firstLiveSeq` 处一定有一条:在一次没有产生工作的拾起之后,该事件的 seq 会小于下一个生命周期的 `firstLiveSeq`。
576
636
 
@@ -588,7 +648,7 @@ interface TurnEndReasonMap {
588
648
 
589
649
  ## 持久性约定
590
650
 
591
- 持久化后端依赖的约定如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.zh.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
651
+ 持久化后端依赖的约定如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 handle 的 `read()` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.zh.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.snapshotEvents()` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
592
652
 
593
653
  消费此约定的后端见 [persistence.md](persistence.zh.md)。
594
654
 
@@ -626,7 +686,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
626
686
  * @param signal - optional caller cancellation for persistence reads.
627
687
  * @returns the current attached state or persisted header and event prefix.
628
688
  */
629
- inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
689
+ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
630
690
 
631
691
  /**
632
692
  * Read all visible Session rows without resuming an Agent.
@@ -746,7 +806,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
746
806
  @Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
747
807
  ```
748
808
 
749
- Types: [SessionHeader](persistence.zh.md) · [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md)
809
+ Types: [SessionId](core.zh.md) · [SessionInspection](persistence.zh.md) · [SessionSearchRequest](session-query.zh.md)
750
810
 
751
811
  Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
752
812
 
@@ -756,7 +816,7 @@ Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/sess
756
816
 
757
817
  In-memory session store (`ctx.sessions`).
758
818
 
759
- Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
819
+ Persistence is intentionally not implemented here — the agent lifecycle attaches a session-log writer to each published session's write handle; a session published outside that lifecycle persists nothing.
760
820
 
761
821
  ```ts cordis-catalog
762
822
  /**
@@ -879,7 +939,7 @@ list(): Session[]
879
939
  * `SessionStore`'s id policy.
880
940
  * @returns The created live child session.
881
941
  */
882
- fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
942
+ fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
883
943
  ```
884
944
 
885
945
  Types: [CreateSessionOptions](persistence.zh.md) · [PrepareSessionOptions](persistence.zh.md) · [SessionId](core.zh.md)
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write docs/subsystems/slots.md
5
- slots.md: 8e115e30aed68e543eca2f1aac6e28ad9f57cf73
6
- slots.zh.md: 3894b69d8d020b4bb67ce325d389ab3f20cfcc9a
5
+ slots.md: a37374ff11f9d460a2566a7a30f8318e69cbfa0b
6
+ slots.zh.md: 09329b1f299f4912490f888efa3146ec0ce11e28
@@ -135,6 +135,7 @@ root
135
135
  │ │ │ ├─ conversation.chat.commandview
136
136
  │ │ │ ├─ conversation.chat.turnTail
137
137
  │ │ │ └─ tool.call.toolview
138
+ │ │ │ ├─ tool.call.images
138
139
  │ │ │ └─ tool.view.cordis
139
140
  │ │ ├─ conversation.message.images
140
141
  │ │ └─ conversation.trajectory.images
@@ -135,6 +135,7 @@ root
135
135
  │ │ │ ├─ conversation.chat.commandview
136
136
  │ │ │ ├─ conversation.chat.turnTail
137
137
  │ │ │ └─ tool.call.toolview
138
+ │ │ │ ├─ tool.call.images
138
139
  │ │ │ └─ tool.view.cordis
139
140
  │ │ ├─ conversation.message.images
140
141
  │ │ └─ conversation.trajectory.images
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write docs/subsystems/storage.md
5
- storage.md: 1e4141e6ef1c6f8e1c2593e21e788b626d6b1ed7
6
- storage.zh.md: f0433c600674741c3de0ce3e99430297839ce124
5
+ storage.md: 03b0fa1c674064994039d37c4fec906dc45794b3
6
+ storage.zh.md: ded8783fbfe7f0d7d8a846c7a66d780b48de1445