dsh-plugin-guide 0.3.3 → 0.3.4

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 (102) hide show
  1. package/README.es.md +5 -1
  2. package/README.hi.md +5 -1
  3. package/README.md +5 -1
  4. package/README.pt.md +5 -1
  5. package/README.zh.md +5 -1
  6. package/dist/dsh-plugin-dev.js +3 -3
  7. package/guide/plugin-dev-guide.md +2 -2
  8. package/guide/quick-reference.es.md +2 -2
  9. package/guide/quick-reference.hi.md +1 -1
  10. package/guide/quick-reference.md +2 -2
  11. package/guide/quick-reference.pt.md +2 -2
  12. package/guide/quick-reference.zh-CN.md +2 -2
  13. package/package.json +3 -3
  14. package/references/official-docs/AGENTS.md +2 -2
  15. package/references/official-docs/SNAPSHOT.md +3 -3
  16. package/references/official-docs/THIRD_PARTY_NOTICES.md +4 -6
  17. package/references/official-docs/docs/agent-lifecycle.i18n.yaml +2 -2
  18. package/references/official-docs/docs/agent-lifecycle.md +5 -3
  19. package/references/official-docs/docs/agent-lifecycle.zh.md +5 -3
  20. package/references/official-docs/docs/architecture.i18n.yaml +2 -2
  21. package/references/official-docs/docs/architecture.md +7 -3
  22. package/references/official-docs/docs/architecture.zh.md +7 -3
  23. package/references/official-docs/docs/capability-seams.i18n.yaml +2 -2
  24. package/references/official-docs/docs/capability-seams.md +5 -0
  25. package/references/official-docs/docs/capability-seams.zh.md +5 -0
  26. package/references/official-docs/docs/config-catalog.i18n.yaml +2 -2
  27. package/references/official-docs/docs/config-catalog.md +15 -21
  28. package/references/official-docs/docs/config-catalog.zh.md +15 -21
  29. package/references/official-docs/docs/cookbook/extension-cookbook.i18n.yaml +2 -2
  30. package/references/official-docs/docs/cookbook/extension-cookbook.md +12 -10
  31. package/references/official-docs/docs/cookbook/extension-cookbook.zh.md +12 -10
  32. package/references/official-docs/docs/deepseek-llm-api-wire-extensions.i18n.yaml +2 -2
  33. package/references/official-docs/docs/deepseek-llm-api-wire-extensions.md +16 -12
  34. package/references/official-docs/docs/deepseek-llm-api-wire-extensions.zh.md +16 -12
  35. package/references/official-docs/docs/event-producer-consumer.i18n.yaml +2 -2
  36. package/references/official-docs/docs/event-producer-consumer.md +28 -27
  37. package/references/official-docs/docs/event-producer-consumer.zh.md +32 -31
  38. package/references/official-docs/docs/module-graph.i18n.yaml +2 -2
  39. package/references/official-docs/docs/module-graph.md +36 -14
  40. package/references/official-docs/docs/module-graph.zh.md +36 -14
  41. package/references/official-docs/docs/persistence-catalog.i18n.yaml +2 -2
  42. package/references/official-docs/docs/persistence-catalog.md +49 -39
  43. package/references/official-docs/docs/persistence-catalog.zh.md +39 -29
  44. package/references/official-docs/docs/subsystems/attachment.i18n.yaml +2 -2
  45. package/references/official-docs/docs/subsystems/attachment.md +157 -4
  46. package/references/official-docs/docs/subsystems/attachment.zh.md +157 -4
  47. package/references/official-docs/docs/subsystems/commands.i18n.yaml +2 -2
  48. package/references/official-docs/docs/subsystems/commands.md +24 -15
  49. package/references/official-docs/docs/subsystems/commands.zh.md +24 -15
  50. package/references/official-docs/docs/subsystems/conversation.i18n.yaml +2 -2
  51. package/references/official-docs/docs/subsystems/conversation.md +4 -4
  52. package/references/official-docs/docs/subsystems/conversation.zh.md +4 -4
  53. package/references/official-docs/docs/subsystems/core.i18n.yaml +2 -2
  54. package/references/official-docs/docs/subsystems/core.md +62 -1
  55. package/references/official-docs/docs/subsystems/core.zh.md +62 -1
  56. package/references/official-docs/docs/subsystems/llm-streaming.i18n.yaml +2 -2
  57. package/references/official-docs/docs/subsystems/llm-streaming.md +23 -2
  58. package/references/official-docs/docs/subsystems/llm-streaming.zh.md +23 -2
  59. package/references/official-docs/docs/subsystems/persistence.i18n.yaml +2 -2
  60. package/references/official-docs/docs/subsystems/persistence.md +10 -10
  61. package/references/official-docs/docs/subsystems/persistence.zh.md +10 -10
  62. package/references/official-docs/docs/subsystems/session-projection.i18n.yaml +2 -2
  63. package/references/official-docs/docs/subsystems/session-projection.md +19 -0
  64. package/references/official-docs/docs/subsystems/session-projection.zh.md +19 -0
  65. package/references/official-docs/docs/subsystems/session-reference.i18n.yaml +2 -2
  66. package/references/official-docs/docs/subsystems/session-reference.md +25 -1
  67. package/references/official-docs/docs/subsystems/session-reference.zh.md +25 -1
  68. package/references/official-docs/docs/subsystems/session-telemetry.i18n.yaml +2 -2
  69. package/references/official-docs/docs/subsystems/session-telemetry.md +6 -5
  70. package/references/official-docs/docs/subsystems/session-telemetry.zh.md +6 -5
  71. package/references/official-docs/docs/subsystems/session.i18n.yaml +2 -2
  72. package/references/official-docs/docs/subsystems/session.md +52 -40
  73. package/references/official-docs/docs/subsystems/session.zh.md +52 -40
  74. package/references/official-docs/docs/subsystems/web-client.i18n.yaml +2 -2
  75. package/references/official-docs/docs/subsystems/web-client.md +1 -1
  76. package/references/official-docs/docs/subsystems/web-client.zh.md +1 -1
  77. package/references/official-docs/docs/subsystems/workspace.i18n.yaml +2 -2
  78. package/references/official-docs/docs/subsystems/workspace.md +8 -8
  79. package/references/official-docs/docs/subsystems/workspace.zh.md +8 -8
  80. package/references/official-docs/docs/testing.i18n.yaml +2 -2
  81. package/references/official-docs/docs/testing.md +2 -2
  82. package/references/official-docs/docs/testing.zh.md +2 -2
  83. package/references/official-docs/docs/user/guide/network-proxy.i18n.yaml +6 -0
  84. package/references/official-docs/docs/user/guide/network-proxy.md +85 -0
  85. package/references/official-docs/docs/user/guide/network-proxy.zh.md +85 -0
  86. package/references/official-docs/docs/user/guide/providers.i18n.yaml +2 -2
  87. package/references/official-docs/docs/user/guide/providers.md +70 -17
  88. package/references/official-docs/docs/user/guide/providers.zh.md +70 -17
  89. package/references/official-docs/docs/web-styling.i18n.yaml +2 -2
  90. package/references/official-docs/docs/web-styling.md +1 -0
  91. package/references/official-docs/docs/web-styling.zh.md +1 -0
  92. package/references/official-docs/website-docs.ts +8 -0
  93. package/templates/js/README.es.md +1 -1
  94. package/templates/js/README.hi.md +1 -1
  95. package/templates/js/README.md +1 -1
  96. package/templates/js/README.pt.md +1 -1
  97. package/templates/js/README.zh.md +1 -1
  98. package/templates/ts/README.es.md +1 -1
  99. package/templates/ts/README.hi.md +1 -1
  100. package/templates/ts/README.md +1 -1
  101. package/templates/ts/README.pt.md +1 -1
  102. package/templates/ts/README.zh.md +1 -1
@@ -21,8 +21,8 @@ interface UserMessage extends Message {
21
21
  /**
22
22
  * The merge-extensible, append-only source of truth for an agent interaction.
23
23
  * Message history is derived from this log. Every event is lossless JSON and
24
- * sequence numbers stay contiguous, including raw chunks, so persistence can
25
- * store the canonical log verbatim.
24
+ * sequence numbers stay contiguous. Assistant attempt events embed their exact
25
+ * compact raw streams so persistence stores one durable settlement per attempt.
26
26
  */
27
27
  interface SessionEventMap {
28
28
  /**
@@ -53,8 +53,6 @@ interface SessionEventMap {
53
53
  * project their `content` verbatim; `source` tells them apart.
54
54
  */
55
55
  'user/message': UserMessage
56
- /** Raw stream chunk — token-level replay fidelity. */
57
- 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
58
56
  /**
59
57
  * Assembled assistant message for one step (derived history uses this).
60
58
  * Carries the step's `usage` when the adapter reported token accounting, so
@@ -65,7 +63,21 @@ interface SessionEventMap {
65
63
  * marker distinguishes that prefix without re-deriving interruption from turn
66
64
  * boundaries. An aborted turn with no such event streamed no visible content.
67
65
  */
68
- 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
66
+ 'assistant/message': {
67
+ turn: number
68
+ step: number
69
+ message: AssistantMessage
70
+ /** Exact timed model stream, compacted without joining delta boundaries. */
71
+ stream: AssistantStreamRecord[]
72
+ usage?: TokenUsage
73
+ interrupted?: true
74
+ }
75
+ /**
76
+ * One model attempt that committed no surface message. The embedded stream
77
+ * preserves a failed, retried, cancelled, or stream-error attempt that
78
+ * reached settlement without fabricating model-visible history.
79
+ */
80
+ 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] }
69
81
  /**
70
82
  * The model requested one tool invocation: `name` with the raw `arguments`
71
83
  * JSON string exactly as the model produced it (unparsed). `callId` pairs the
@@ -109,12 +121,12 @@ interface SessionEventMap {
109
121
  * Marks the end of a constructor seed. Events before it have smaller seq
110
122
  * values and came from the seed (resume, fork, or replay); this lifecycle
111
123
  * produced none of them. This log-only event is the durable projection of
112
- * {@link Session.firstLiveSeq}. Its payload is empty — position and `time`
113
- * carry the meaning.
124
+ * {@link Session.firstLiveSeq}.
114
125
  *
115
- * Locate the LAST one in stored history. A seed already ending in one is not
116
- * re-marked, so reopening an untouched session does not grow its log per
117
- * pickup and the event need not be at the current `firstLiveSeq`.
126
+ * A fresh fork child owns one `{ inherited: true }` marker at its exact
127
+ * inherited-prefix cut, even when that prefix ends in an ancestor marker.
128
+ * The last tagged marker is the current Session's cut; untagged markers keep
129
+ * ordinary restore and replay lifecycle boundaries.
118
130
  *
119
131
  * `Session`'s constructor is the only legitimate writer. The invariant
120
132
  * companion deliberately constrains nothing here, so a plugin appending one
@@ -127,7 +139,7 @@ interface SessionEventMap {
127
139
  * writers — a concurrently live session holds its own boundary elsewhere,
128
140
  * so tolerating concurrent writers needs a signal beyond the log.
129
141
  */
130
- 'session/end-seed': Record<string, never>
142
+ 'session/end-seed': { inherited?: true }
131
143
  }
132
144
  ```
133
145
 
@@ -211,7 +223,7 @@ type OptionalSessionSeq = SessionSeq | null
211
223
  * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
212
224
  * they only exist on {@link SurfaceEventType} variants (`user/message`,
213
225
  * `assistant/message`, `tool/result`).
214
- * Non-surface events (boundary markers, chunks, usage, errors) never carry
226
+ * Non-surface events (boundary markers, attempts, errors) never carry
215
227
  * surface metadata — the compiler enforces this at `Session.append()`
216
228
  * call sites.
217
229
  */
@@ -236,12 +248,9 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
236
248
  ignorable?: true
237
249
  } & (K extends SurfaceEventType ? {
238
250
  /**
239
- * Seq numbers of earlier events that this event cites as sources
240
- * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
241
- * or the surface nodes shadowed by a compaction replace node). An
242
- * `assistant/message` may carry a present empty array for a known empty
243
- * provider stream; when the field is absent, the event does not record which
244
- * earlier events produced the message.
251
+ * Seq numbers of earlier events that this event cites as sources, such as
252
+ * the surface nodes shadowed by a compaction replacement. A v2
253
+ * `assistant/message` embeds its provider stream and cannot carry this field.
245
254
  */
246
255
  sourceEventSeqs?: SessionSeq[]
247
256
  /** How this event entered the surface; absent for non-surface events. */
@@ -252,7 +261,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
252
261
 
253
262
  `SessionEventType = keyof SessionEventMap`. Because `SessionEventMap` is merge-extensible, switches over `SessionEvent` must NOT use `assertNever` — a plugin-added variant is a valid unknown value; handle the known cases and fall through `default`.
254
263
 
255
- For `assistant/message`, a present `sourceEventSeqs: []` is a complete known-empty provider stream, while a legacy or foreign event with no field does not record which earlier events produced the message. The loop writes the field for every successful model call; every other surface event requires a non-empty list when the field is present.
264
+ V2 `assistant/message` embeds its provider stream and cannot carry `sourceEventSeqs`. User and tool surface events may cite a complete non-empty set of unique earlier events when their provenance or replacement operation requires it.
256
265
 
257
266
  ## Surface types
258
267
 
@@ -264,7 +273,8 @@ The three message-producing types (`SurfaceEventType` — `user/message`, `assis
264
273
  /**
265
274
  * The subset of {@link SessionEventType} values whose events produce LLM
266
275
  * messages and are eligible to appear on the ordered surface. Only these
267
- * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.
276
+ * event types may carry {@link SurfaceOp}; user and tool events may also cite
277
+ * earlier sources through {@link SessionEvent.sourceEventSeqs}.
268
278
  */
269
279
  type SurfaceEventType =
270
280
  | 'user/message'
@@ -302,21 +312,20 @@ type SurfaceOp =
302
312
  * Surface placement and cited source-event seqs for {@link Session.append}. Required on
303
313
  * message-producing events and forbidden on log-only events.
304
314
  */
305
- interface SurfaceIntent {
315
+ type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
306
316
  surfaceOp: SurfaceOp
307
- /**
308
- * Complete set of known source-event seqs. `assistant/message` may use a
309
- * present empty array for a known empty provider stream; when the field is
310
- * absent, the event does not record which earlier events produced the message.
311
- * Other surface events require a non-empty set when this field is present.
312
- */
317
+ } & (T extends 'assistant/message' ? {
318
+ /** V2 Assistant messages embed their provider stream instead of citing source events. */
319
+ sourceEventSeqs?: never
320
+ } : {
321
+ /** Complete non-empty set of known earlier source-event seqs. */
313
322
  sourceEventSeqs?: SessionSeq[]
314
- }
323
+ })
315
324
  ```
316
325
 
317
326
  Required for `SurfaceEventType` events — every message-producing event must declare how it joins the surface, the sole source of derived model history. A human-facing transcript is the other projection and reads the log's append-origin events instead, because the surface deliberately shadows the ranges a replacement summarizes (`isAppendSurfaceEvent` in [dsh-session](../../packages/core/session/README.md)). Non-surface types reject it at compile time.
318
327
 
319
- Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the field is absent, the event does not record which earlier events produced the message, and the provider may still have emitted chunks.
328
+ `assistant/message` cannot carry `sourceEventSeqs`; its `stream` owns exact provider evidence. Other surface events omit the field when they cite no earlier event and use a complete non-empty list when they do.
320
329
 
321
330
  ### `SessionSurface` — the live readonly surface projection
322
331
 
@@ -395,9 +404,10 @@ declare class Session {
395
404
  * The first seq appended IN THIS PROCESS: the length of the constructor
396
405
  * seed (0 without one). Events with smaller seq values entered through
397
406
  * construction — replay, fork, or resume — and were never published on the
398
- * `session/event` firehose (constructor seeds do not emit), so consumers
399
- * that replay the log as a publication substitute (telemetry adoption)
400
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
407
+ * `session/event` firehose (constructor seeds do not emit). This offset marks
408
+ * the constructor-input boundary for lifecycle ownership and persistence
409
+ * adoption; consumers that need complete canonical history still start at
410
+ * seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
401
411
  * fork-lineage cut: a resumed session's constructor seed is its full stored
402
412
  * log, while the inherited count keeps the original fork value — this field is the
403
413
  * in-process construction fact.
@@ -493,7 +503,8 @@ declare class Session {
493
503
  * declare how it joins the surface, the sole source of derived model
494
504
  * history) and
495
505
  * rejected by the compiler for non-surface types like `turn/start` or
496
- * `assistant/chunk`.
506
+ * `assistant/attempt`. Assistant messages embed their exact provider
507
+ * stream and cannot cite top-level source events.
497
508
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
498
509
  * `data` that entered the log, so reading `event.data` back sees the logged
499
510
  * value, never the caller's still-mutable input.
@@ -514,7 +525,7 @@ declare class Session {
514
525
  append<T extends SessionEventType>(
515
526
  type: T,
516
527
  data: SessionEventMap[T],
517
- ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
528
+ ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []
518
529
  ): SessionEvent<T>;
519
530
  /**
520
531
  * The {@link EpochHeader} in force after the log's last header event — the
@@ -565,11 +576,11 @@ declare class Session {
565
576
  `Session.deriveMessages()` projects the event log into the `Message[]` the model sees — cached (each surface node projected once, when first seen; a surface rewrite rebuilds) and frozen (a fresh array per call over shared, deep-frozen messages, so mutating logged history through a projection is unrepresentable). `deriveEventMessage(event)` is the per-node pure function the fold applies — public so external reconstructors and the dev invariant project a log prefix with exactly the same rules and cannot disagree with the cache. The projection rules:
566
577
 
567
578
  - `user/message` → a user message carrying exact `content`; an optional envelope remains log-only display metadata.
568
- - `assistant/message` → an assistant message with the provider and model that produced it plus optional adapter-private replay state. Raw `assistant/chunk` events are replay/UI data and are **skipped** in derivation (the assembled message is authoritative). An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to hold its usage, provider, and model, but a content-less assistant turn must not enter the provider transcript.
579
+ - `assistant/message` → an assistant message with the provider and model that produced it plus optional adapter-private replay state. Its embedded compact stream is replay, usage, and UI evidence rather than a second message. An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to hold its stream, usage, provider, and model, but a content-less assistant turn must not enter the provider transcript.
569
580
  - `tool/result` → a user message carrying a `tool-result` block.
570
581
  - `user/message` (injected context, i.e. non-`user` source) → a user-role message carrying its `content` verbatim at its chronological position; its typed source names the producer and carries any producer-specific data.
571
582
 
572
- Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message, so their usage chunk is the durable accounting record. Because this unreleased format intentionally has no compatibility promise, seed/load validation rejects request headers and assistant messages that omit provider/model instead of guessing a route for historical data.
583
+ Everything else (`turn/*`, `step/*`, `assistant/attempt`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting expands the embedded stream on each `assistant/message` or `assistant/attempt`, while the message's top-level `usage` remains the committed-message authority when present. A failed model-request attempt therefore retains its provider usage without fabricating an assistant message. Current logical validation rejects request headers and assistant messages that omit provider/model instead of guessing a route; supported historical representations are normalized and validated by their adjacent format edge before a current Session exists.
573
584
 
574
585
  ## Live-session fork API
575
586
 
@@ -626,9 +637,9 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
626
637
 
627
638
  ## The end-seed boundary: `session/end-seed`
628
639
 
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.
640
+ A fresh fork constructor requires its seed to equal the inherited prefix and appends `session/end-seed { inherited: true }` at the exact durable cut. A restore retains that tagged marker and appends an ordinary `session/end-seed {}` only when its complete stored seed does not already end in a marker. Both forms are log-only and produce no message; `Session`'s constructor is the only legitimate writer.
630
641
 
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`.
642
+ For fork lineage, locate the LAST marker whose payload carries `inherited: true`; v2 decoding requires it exactly when `SessionHeader.isSeeded` is true and derives `inheritedEventCount` from its seq. For lifecycle ownership, locate the last `session/end-seed` of either form. Reopening a seed that already ends in any marker does not append another ordinary marker.
632
643
 
633
644
  It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compaction/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compaction/*`.
634
645
 
@@ -644,7 +655,7 @@ The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-h
644
655
 
645
656
  ## Durability contract
646
657
 
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.
658
+ What a persistence backend relies on: the durable log persists every event losslessly, and every Assistant attempt is one `assistant/message` or `assistant/attempt` whose embedded compact stream preserves the original timed chunks. `seq` stays contiguous across these settlements and all interleaved events. A backend may choose its own storage framing for an event batch as long as a handle's `read()` returns the exact appended events; current JSONL v2 writes one row per event (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.
648
659
 
649
660
  The backends that consume this contract are on [persistence.md](persistence.md).
650
661
 
@@ -790,7 +801,8 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
790
801
  * Follow one Session log from its opening or resume cursor.
791
802
  * @param request - durable address and last committed sequence already held by the caller.
792
803
  * @param signal - cancellation owned by the Remote stream carrier.
793
- * @returns a complete opening snapshot followed by gap-free event frames.
804
+ * @returns a complete opening snapshot followed by gap-free durable event
805
+ * frames and optional cursorless assistant-stream frames.
794
806
  */
795
807
  @Remote({ mode: 'stream' }) follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>
796
808
 
@@ -21,8 +21,8 @@ interface UserMessage extends Message {
21
21
  /**
22
22
  * The merge-extensible, append-only source of truth for an agent interaction.
23
23
  * Message history is derived from this log. Every event is lossless JSON and
24
- * sequence numbers stay contiguous, including raw chunks, so persistence can
25
- * store the canonical log verbatim.
24
+ * sequence numbers stay contiguous. Assistant attempt events embed their exact
25
+ * compact raw streams so persistence stores one durable settlement per attempt.
26
26
  */
27
27
  interface SessionEventMap {
28
28
  /**
@@ -53,8 +53,6 @@ interface SessionEventMap {
53
53
  * project their `content` verbatim; `source` tells them apart.
54
54
  */
55
55
  'user/message': UserMessage
56
- /** Raw stream chunk — token-level replay fidelity. */
57
- 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
58
56
  /**
59
57
  * Assembled assistant message for one step (derived history uses this).
60
58
  * Carries the step's `usage` when the adapter reported token accounting, so
@@ -65,7 +63,21 @@ interface SessionEventMap {
65
63
  * marker distinguishes that prefix without re-deriving interruption from turn
66
64
  * boundaries. An aborted turn with no such event streamed no visible content.
67
65
  */
68
- 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
66
+ 'assistant/message': {
67
+ turn: number
68
+ step: number
69
+ message: AssistantMessage
70
+ /** Exact timed model stream, compacted without joining delta boundaries. */
71
+ stream: AssistantStreamRecord[]
72
+ usage?: TokenUsage
73
+ interrupted?: true
74
+ }
75
+ /**
76
+ * One model attempt that committed no surface message. The embedded stream
77
+ * preserves a failed, retried, cancelled, or stream-error attempt that
78
+ * reached settlement without fabricating model-visible history.
79
+ */
80
+ 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] }
69
81
  /**
70
82
  * The model requested one tool invocation: `name` with the raw `arguments`
71
83
  * JSON string exactly as the model produced it (unparsed). `callId` pairs the
@@ -109,12 +121,12 @@ interface SessionEventMap {
109
121
  * Marks the end of a constructor seed. Events before it have smaller seq
110
122
  * values and came from the seed (resume, fork, or replay); this lifecycle
111
123
  * produced none of them. This log-only event is the durable projection of
112
- * {@link Session.firstLiveSeq}. Its payload is empty — position and `time`
113
- * carry the meaning.
124
+ * {@link Session.firstLiveSeq}.
114
125
  *
115
- * Locate the LAST one in stored history. A seed already ending in one is not
116
- * re-marked, so reopening an untouched session does not grow its log per
117
- * pickup and the event need not be at the current `firstLiveSeq`.
126
+ * A fresh fork child owns one `{ inherited: true }` marker at its exact
127
+ * inherited-prefix cut, even when that prefix ends in an ancestor marker.
128
+ * The last tagged marker is the current Session's cut; untagged markers keep
129
+ * ordinary restore and replay lifecycle boundaries.
118
130
  *
119
131
  * `Session`'s constructor is the only legitimate writer. The invariant
120
132
  * companion deliberately constrains nothing here, so a plugin appending one
@@ -127,7 +139,7 @@ interface SessionEventMap {
127
139
  * writers — a concurrently live session holds its own boundary elsewhere,
128
140
  * so tolerating concurrent writers needs a signal beyond the log.
129
141
  */
130
- 'session/end-seed': Record<string, never>
142
+ 'session/end-seed': { inherited?: true }
131
143
  }
132
144
  ```
133
145
 
@@ -211,7 +223,7 @@ type OptionalSessionSeq = SessionSeq | null
211
223
  * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
212
224
  * they only exist on {@link SurfaceEventType} variants (`user/message`,
213
225
  * `assistant/message`, `tool/result`).
214
- * Non-surface events (boundary markers, chunks, usage, errors) never carry
226
+ * Non-surface events (boundary markers, attempts, errors) never carry
215
227
  * surface metadata — the compiler enforces this at `Session.append()`
216
228
  * call sites.
217
229
  */
@@ -236,12 +248,9 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
236
248
  ignorable?: true
237
249
  } & (K extends SurfaceEventType ? {
238
250
  /**
239
- * Seq numbers of earlier events that this event cites as sources
240
- * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
241
- * or the surface nodes shadowed by a compaction replace node). An
242
- * `assistant/message` may carry a present empty array for a known empty
243
- * provider stream; when the field is absent, the event does not record which
244
- * earlier events produced the message.
251
+ * Seq numbers of earlier events that this event cites as sources, such as
252
+ * the surface nodes shadowed by a compaction replacement. A v2
253
+ * `assistant/message` embeds its provider stream and cannot carry this field.
245
254
  */
246
255
  sourceEventSeqs?: SessionSeq[]
247
256
  /** How this event entered the surface; absent for non-surface events. */
@@ -252,7 +261,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
252
261
 
253
262
  `SessionEventType = keyof SessionEventMap`。由于 `SessionEventMap` 可通过合并扩展,对 `SessionEvent` 的 switch 语句禁止使用 `assertNever`:插件添加的变体是合法的未知值;处理已知 case 后在 `default` 中放行。
254
263
 
255
- 对于 `assistant/message`,存在的 `sourceEventSeqs: []` 表示提供方流已知且完整地为空;旧格式或外部事件缺少该字段时,没有记录这条消息由哪些早期事件产生。agent loop 会为每次成功的模型调用写入该字段;其他 surface 事件只要包含该字段,其列表就必须非空。
264
+ V2 `assistant/message` 嵌入 provider stream,不能携带 `sourceEventSeqs`。User tool surface event 可以在 provenance 或 replacement operation 需要时引用完整且非空的唯一较早 event 集合。
256
265
 
257
266
  <a id="surface-types"></a>
258
267
 
@@ -266,7 +275,8 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
266
275
  /**
267
276
  * The subset of {@link SessionEventType} values whose events produce LLM
268
277
  * messages and are eligible to appear on the ordered surface. Only these
269
- * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.
278
+ * event types may carry {@link SurfaceOp}; user and tool events may also cite
279
+ * earlier sources through {@link SessionEvent.sourceEventSeqs}.
270
280
  */
271
281
  type SurfaceEventType =
272
282
  | 'user/message'
@@ -304,21 +314,20 @@ type SurfaceOp =
304
314
  * Surface placement and cited source-event seqs for {@link Session.append}. Required on
305
315
  * message-producing events and forbidden on log-only events.
306
316
  */
307
- interface SurfaceIntent {
317
+ type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
308
318
  surfaceOp: SurfaceOp
309
- /**
310
- * Complete set of known source-event seqs. `assistant/message` may use a
311
- * present empty array for a known empty provider stream; when the field is
312
- * absent, the event does not record which earlier events produced the message.
313
- * Other surface events require a non-empty set when this field is present.
314
- */
319
+ } & (T extends 'assistant/message' ? {
320
+ /** V2 Assistant messages embed their provider stream instead of citing source events. */
321
+ sourceEventSeqs?: never
322
+ } : {
323
+ /** Complete non-empty set of known earlier source-event seqs. */
315
324
  sourceEventSeqs?: SessionSeq[]
316
- }
325
+ })
317
326
  ```
318
327
 
319
328
  对 `SurfaceEventType` 事件必填:每个产生消息的事件都必须声明它如何加入 surface(派生模型历史的唯一来源)。面向人类的 transcript(文本记录)是另一个投影,读取的是日志中追加来源的事件,因为 surface 会有意遮蔽替换所概括的范围(见 [dsh-session](../../packages/core/session/README.zh.md) 的 `isAppendSurfaceEvent`)。非 surface 类型在编译期拒绝此参数。
320
329
 
321
- 只有 `assistant/message` 可以携带存在但为空的 `sourceEventSeqs`;字段不存在时,该事件没有记录这条消息由哪些早期事件产生,但提供方仍可能发出过分片。
330
+ `assistant/message` 不能携带 `sourceEventSeqs`;它的 `stream` 拥有精确 provider 证据。其他 surface event 不引用较早 event 时省略该字段,需要引用时使用完整非空 list。
322
331
 
323
332
  ### `SessionSurface`:实时只读 surface 投影
324
333
 
@@ -397,9 +406,10 @@ declare class Session {
397
406
  * The first seq appended IN THIS PROCESS: the length of the constructor
398
407
  * seed (0 without one). Events with smaller seq values entered through
399
408
  * construction — replay, fork, or resume — and were never published on the
400
- * `session/event` firehose (constructor seeds do not emit), so consumers
401
- * that replay the log as a publication substitute (telemetry adoption)
402
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
409
+ * `session/event` firehose (constructor seeds do not emit). This offset marks
410
+ * the constructor-input boundary for lifecycle ownership and persistence
411
+ * adoption; consumers that need complete canonical history still start at
412
+ * seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
403
413
  * fork-lineage cut: a resumed session's constructor seed is its full stored
404
414
  * log, while the inherited count keeps the original fork value — this field is the
405
415
  * in-process construction fact.
@@ -495,7 +505,8 @@ declare class Session {
495
505
  * declare how it joins the surface, the sole source of derived model
496
506
  * history) and
497
507
  * rejected by the compiler for non-surface types like `turn/start` or
498
- * `assistant/chunk`.
508
+ * `assistant/attempt`. Assistant messages embed their exact provider
509
+ * stream and cannot cite top-level source events.
499
510
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
500
511
  * `data` that entered the log, so reading `event.data` back sees the logged
501
512
  * value, never the caller's still-mutable input.
@@ -516,7 +527,7 @@ declare class Session {
516
527
  append<T extends SessionEventType>(
517
528
  type: T,
518
529
  data: SessionEventMap[T],
519
- ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
530
+ ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []
520
531
  ): SessionEvent<T>;
521
532
  /**
522
533
  * The {@link EpochHeader} in force after the log's last header event — the
@@ -567,11 +578,11 @@ declare class Session {
567
578
  `Session.deriveMessages()` 将事件日志投影为模型看到的 `Message[]`。它是缓存的(每个 surface 节点在首次出现时投影一次;surface 重写触发重建)且冻结的(每次调用返回一个新数组,引用共享的深冻结消息,因此通过投影修改已记录的历史在类型上不可表达)。`deriveEventMessage(event)` 是折叠所应用的逐节点纯函数,公开暴露以便外部重建器和开发不变式检查能以完全相同的规则投影日志前缀,不会与缓存产生分歧。投影规则:
568
579
 
569
580
  - `user/message` → 一条携带确切 `content` 的 user 消息;可选 envelope 仅作为日志中的展示元数据保留。
570
- - `assistant/message` → 一条 assistant 消息,包含生成它的提供方和模型,以及可选的适配器私有回放状态。原始 `assistant/chunk` 事件属于回放/UI 数据,在派生时会被**跳过**(组装后的消息才是权威)。**内容为空的** `assistant/message` 也会跳过:因 max-tokens 而截断且无内容的步骤仍会记录一条 `assistant/message` 来保存用量、提供方和模型,但无内容的 assistant 轮次不得进入提供方 transcript(文本记录)。
581
+ - `assistant/message` → 一条 assistant 消息,包含生成它的提供方和模型,以及可选的适配器私有回放状态。其嵌入式紧凑 stream 是回放、usage 与 UI 证据,而不是第二条 message。**内容为空的** `assistant/message` 也会跳过:因 max-tokens 而截断且无内容的步骤仍会记录一条 `assistant/message` 来保存 stream、usage、提供方和模型,但无内容的 assistant 轮次不得进入提供方 transcript(文本记录)。
571
582
  - `tool/result` → 一条携带 `tool-result` 块的 user 消息。
572
583
  - `user/message`(注入上下文,即非 `user` 来源)→ 按时间顺序在相应位置生成一条 user-role 消息,并原样承载其 `content`;其类型化 source 标明生产方,并携带所有生产方专用数据。
573
584
 
574
- 其余所有事件(`turn/*`、`step/*`、插件所属的 `llm/retry`)均为结构信息,不会投影为消息。token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息,因此其用量分片是持久化的记账记录。由于这一尚未发布的格式有意不提供兼容性承诺,seed/load 校验会拒绝没有提供方/模型的请求头和 assistant 消息,而不会猜测历史数据应走的提供方路由。
585
+ 其余所有事件(`turn/*`、`step/*`、`assistant/attempt`、插件所属的 `llm/retry`)均为结构信息,不会投影为消息。token 记账会展开每个 `assistant/message` `assistant/attempt` 的嵌入式 stream,message 顶层 `usage` 存在时仍是已提交 message 的权威。失败的模型请求 attempt 因此可以保留提供方 usage,而无需虚构 assistant message。当前逻辑校验会拒绝没有提供方/模型的 request header 和 assistant 消息,而不会猜测路由;受支持的历史表示会在当前 Session 存在前,由其相邻格式迁移边归一化并校验。
575
586
 
576
587
  ## 活跃会话 fork API
577
588
 
@@ -630,9 +641,9 @@ interface TurnEndReasonMap {
630
641
 
631
642
  ## 种子结束边界:`session/end-seed`
632
643
 
633
- 用显式 seed 构造的 Session(restore、fork replay)会紧接该 constructor seed 之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且经由构造进入。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的 consumer 回答本 lifecycle 的写入从哪里开始,该事件则为只持有存储字节的 consumer 回答同一问题。它不定义 fork ownership;`isSeeded` `inheritedEventCount` 才定义。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
644
+ fork constructor 要求 seed 等于 inherited prefix,并在精确持久 cut 追加 `session/end-seed { inherited: true }`。restore 会保留该 tagged marker,并且只在完整 stored seed 尚未以 marker 结尾时追加普通 `session/end-seed {}`。两种形式都只进入 log 且不产生 message;`Session` constructor 是唯一合法 writer。
634
645
 
635
- 显式传入的空种子会在 seq 0 写入 `session/end-seed`,从而把从空日志恢复的会话与全新会话区分开来。种子本身已以 `session/end-seed` 结尾时不会重复标记,因此重新打开一个未被改动的会话不会每次拾起都增长日志。应定位存储历史中的最后一条 `session/end-seed`,而不是假定 `firstLiveSeq` 处一定有一条:在一次没有产生工作的拾起之后,该事件的 seq 会小于下一个生命周期的 `firstLiveSeq`。
646
+ 对于 fork lineage,定位 payload 携带 `inherited: true` 的最后一个 marker;v2 decoding 只在 `SessionHeader.isSeeded` true 时要求该 marker,并从其 seq 推导 `inheritedEventCount`。对于 lifecycle ownership,定位任一形式的最后一个 `session/end-seed`。重新打开已经以任一 marker 结尾的 seed 时,不会再追加普通 marker。
636
647
 
637
648
  它之所以必要,是因为种子历史与实时工作在字节层面完全相同,这会让任何拥有独立开/闭括号的插件失效:一个未配对的 `compaction/start`,无论写入方是在压缩中途崩溃、还是此刻正在压缩,读起来都一样。在 `session/end-seed` 之前的开启标记来自构造种子,并且属于一个已结束的生命周期,无论结束原因为何(崩溃、进程接替,或从仍在运行的父会话 fork 出来),因此其所有方可以视之为已死。这只覆盖*本*会话继承的括号:另一个并发存活的会话可能在同一段历史上持有开放括号,而它自己的边界在别处,因此容忍并发写入方还需要日志之外的存活信号。核心写入该边界但不从中读取任何内容——括号的词汇表仍归其所属插件,这也正是崩溃修复只关闭轮次/步骤/工具边界而从不处理 `compaction/*` 的原因。
638
649
 
@@ -648,7 +659,7 @@ interface TurnEndReasonMap {
648
659
 
649
660
  ## 持久性约定
650
661
 
651
- 持久化后端依赖的约定如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 handle `read()` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.zh.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.snapshotEvents()` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
662
+ 持久化后端依赖的约定如下:持久日志无损保存每个事件,每个 Assistant attempt 都是一个 `assistant/message` 或 `assistant/attempt`,其嵌入式紧凑 stream 会保留原始带时间 chunk。`seq` 在这些 settlement 与所有交错事件之间保持连续。后端可以为事件批次选择自己的存储 framing,只要句柄的 `read()` 返回与追加时完全一致的事件即可;当前 JSONL v2 每个事件写一行(见 [persistence.md](persistence.zh.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.snapshotEvents()` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
652
663
 
653
664
  消费此约定的后端见 [persistence.md](persistence.zh.md)。
654
665
 
@@ -794,7 +805,8 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
794
805
  * Follow one Session log from its opening or resume cursor.
795
806
  * @param request - durable address and last committed sequence already held by the caller.
796
807
  * @param signal - cancellation owned by the Remote stream carrier.
797
- * @returns a complete opening snapshot followed by gap-free event frames.
808
+ * @returns a complete opening snapshot followed by gap-free durable event
809
+ * frames and optional cursorless assistant-stream frames.
798
810
  */
799
811
  @Remote({ mode: 'stream' }) follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>
800
812
 
@@ -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/web-client.md
5
- web-client.md: 166ad50df661e37318c5ed2f271569c292cce39a
6
- web-client.zh.md: cdf91958e23c0ea5c99562e6ca347947fbeff292
5
+ web-client.md: 7e7f483131585dc66c45dbb147d40e848f40d6d1
6
+ web-client.zh.md: 603af505dbd1fc6e2f9fd3bf9542ce19cb63732f
@@ -55,7 +55,7 @@ This pairing is not a second source of business truth. Host controllers decide d
55
55
 
56
56
  `ui-session` installs the `session` scope adapter and publishes `useSessions`, `useSession`, `sessionId`, and `useProjection`. Domain adapters add further standard sources without putting React hooks on the model objects.
57
57
 
58
- `ui-conversation` binds once to each `SessionBinding.eventSource`. Its event registry correlates standard events and Client-only `chunkrow/*` history events into stable business Contexts, and its view registry materializes target snapshots. Packed runs stay single inputs and Matches through replay; Chat Assistant, Trajectory Assistant, and Turn Tail are the built-in Definitions that interpret them. `ui-chat` and `ui-trajectory` register separate Definitions and builders: they may interpret the same event family, but they do not import or share each other's final display model. The shell selects a registered view and passes its snapshot through standard hooks and Slots. [Conversation](conversation.md) defines Context identity, replay, Location data, target builders, and keyed renderers.
58
+ `ui-conversation` binds once to each `SessionBinding.eventSource`. Its event registry correlates durable Session events and Client-only `assistant/live-chunk` updates into stable business Contexts, and its view registry materializes target snapshots. Chat Assistant, Trajectory Assistant, and Turn Tail interpret both live chunks and the compact streams embedded in durable settlements, so reconnect and paged history reproduce the same Assistant state without durable token rows. `ui-chat` and `ui-trajectory` register separate Definitions and builders: they may interpret the same event family, but they do not import or share each other's final display model. The shell selects a registered view and passes its snapshot through standard hooks and Slots. [Conversation](conversation.md) defines Context identity, replay, Location data, target builders, and keyed renderers.
59
59
 
60
60
  `ui-slots` provides the typed registry and lifecycle ledger; `ui-renderer` is the only package that binds bare observables through `useSyncExternalStore`, owns React contexts, and renders the root tree. Feature components receive framework hooks, owner props, store actions, and explicit injection through their derived props. [Web Client Slots](slots.md) lists those inputs, extension APIs, and the current Slot hierarchy.
61
61
 
@@ -55,7 +55,7 @@ Connection 拥有 request correlation、`/api` carrier、trust check、精确 Fe
55
55
 
56
56
  `ui-session` 安装 `session` scope adapter,并提供 `useSessions`、`useSession`、`sessionId` 和 `useProjection`。领域 adapter 可以继续添加标准 source,但不会把 React hook 放进 model object。
57
57
 
58
- `ui-conversation` 对每个 `SessionBinding.eventSource` 只绑定一次。它的 event registry 把标准 event 与 Client-only `chunkrow/*` 历史 event 关联成稳定的业务 Context,view registry 则 materialize target snapshot。packed run 在 replay 全程保持为单个 input 与 Match;Chat Assistant、Trajectory Assistant Turn Tail 是解释它的三个内建 Definition。`ui-chat` 与 `ui-trajectory` 分别注册自己的 Definition 和 builder:它们可以解释同一 event family,但不会导入或共享彼此的最终 display model。Shell 选择一个已注册 view,再通过标准 hook 与 Slot 交付其 snapshot。[Conversation](conversation.zh.md)定义 Context identity、replay、Location data、target builder 与 keyed renderer。
58
+ `ui-conversation` 对每个 `SessionBinding.eventSource` 只绑定一次。它的 event registry 把持久 Session event 与 Client-only `assistant/live-chunk` update 关联成稳定的业务 Context,view registry 则 materialize target snapshot。Chat Assistant、Trajectory Assistant Turn Tail 同时解释 live chunk 和持久 settlement 中嵌入的紧凑 stream,因此重连与分页历史无需持久 token 行即可复现相同 Assistant 状态。`ui-chat` 与 `ui-trajectory` 分别注册自己的 Definition 和 builder:它们可以解释同一 event family,但不会导入或共享彼此的最终 display model。Shell 选择一个已注册 view,再通过标准 hook 与 Slot 交付其 snapshot。[Conversation](conversation.zh.md)定义 Context identity、replay、Location data、target builder 与 keyed renderer。
59
59
 
60
60
  `ui-slots` 提供类型化 registry 与 lifecycle ledger;`ui-renderer` 是唯一通过 `useSyncExternalStore` 绑定裸 observable、拥有 React context 并渲染 root tree 的包。功能 component 通过推导出的 props 接收 framework hook、owner prop、store action 与显式 injection。[Web Client Slots](slots.zh.md)列出这些输入、扩展 API 与当前 Slot 层级。
61
61
 
@@ -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/workspace.md
5
- workspace.md: 82bbe310d490653bae5d88f41d1fcbe76833b431
6
- workspace.zh.md: 9bf63b7a90f9136173fbec16b55a22821fa380e5
5
+ workspace.md: 3c53300a5be6678802da950d22854da76fa46bf1
6
+ workspace.zh.md: b651a21f7641619c28bad9d3fdf368c718ca2c29
@@ -40,7 +40,7 @@ interface Workspace {
40
40
  */
41
41
  readonly path: string
42
42
 
43
- /** Display title. Defaults to `basename(path)` at create; duplicates are allowed. */
43
+ /** Display title. Defaults to the final path segment, or a filesystem root's own spelling; duplicates are allowed. */
44
44
  readonly title: string
45
45
 
46
46
  /** ISO-8601 creation instant, stamped at create and never rewritten. */
@@ -117,7 +117,7 @@ Ownership truth is the record's ordered `sessionIds`, never derived from session
117
117
 
118
118
  ## The registry: `ctx.workspaceRegistry`
119
119
 
120
- `WorkspaceRegistry` ([signatures](#ctxworkspaceregistry--workspaceregistry)) owns registration and resolution. `create(path, title?)` canonicalizes the path, rejects a nonexistent path (the original `ENOENT`) or a non-directory, returns the existing entity unchanged when the canonical path is already owned, and otherwise creates a record with `title ?? basename(path)` prepended to the durable registry order (different canonical paths may share a display title). `get(id)` and the ordered `list()` are synchronous cache reads; `resolveByPath(path)` applies the same realpath canon without creating. `delete(id)` removes only the registration, order entry, and session account — the directory, user files, live sessions, and persisted logs are never touched, so those sessions become Ungrouped ([decision](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)); unknown ids return `false`. Create and delete persist a pending-mutation marker before their two writes (record + order) can diverge; startup resolves exactly the marked mutation — by deleting the marked table row, which completes an interrupted delete and rolls back an interrupted create (the registration is re-creatable, so rollback is the safe direction) — and an unmarked order/table mismatch fails loud as corruption.
120
+ `WorkspaceRegistry` ([signatures](#ctxworkspaceregistry--workspaceregistry)) owns registration and resolution. `create(path, title?)` requires a fully qualified path, canonicalizes it, rejects a nonexistent path (the original `ENOENT`) or a non-directory, returns the existing entity unchanged when the canonical path is already owned, and otherwise creates a record with `title ?? defaultWorkspaceTitle(path)` prepended to the durable registry order (different canonical paths may share a display title, and a path with no final segment uses its root spelling). `get(id)` and the ordered `list()` are synchronous cache reads; `resolveByPath(path)` applies the same fully qualified realpath canon without creating. `delete(id)` removes only the registration, order entry, and session account — the directory, user files, live sessions, and persisted logs are never touched, so those sessions become Ungrouped ([decision](../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)); unknown ids return `false`. Create and delete persist a pending-mutation marker before their two writes (record + order) can diverge; startup resolves exactly the marked mutation — by deleting the marked table row, which completes an interrupted delete and rolls back an interrupted create (the registration is re-creatable, so rollback is the safe direction) — and an unmarked order/table mismatch fails loud as corruption.
121
121
 
122
122
  Sessions get their cwd at create time from whoever creates them, not from this registry — the API gateway resolves a new session's cwd from the chosen workspace's `path` (falling back to an explicit or default cwd), creates the session so the cwd lands in its immutable [`SessionHeader`](persistence.md#sessionheader--metadata-beside-the-log), then calls `attachSession`, which re-validates that stored header cwd against the workspace path. On the first successful start, the registry bootstraps history from persisted headers alone (`id`, `cwd`, `createdAt` — never event bodies), grouping sessions with a valid canonical cwd into per-directory workspaces, newest first; the initialized marker is written last so an interrupted bootstrap resumes safely. The bootstrap is one-time: cwd-less legacy sessions stay Ungrouped, and sessions created afterwards join a workspace only through `attachSession`.
123
123
 
@@ -250,13 +250,13 @@ Durable workspace registry. Startup waits for `sessionPersistence`, builds one c
250
250
 
251
251
  ```ts cordis-catalog
252
252
  /**
253
- * Create or reuse a workspace for an existing directory. The path is
254
- * canonicalized through `fs.realpath`; a nonexistent path rejects with the
255
- * original error and a non-directory rejects. Repeated calls for the same
256
- * canonical path return the existing entity without changing its title.
253
+ * Create or reuse a workspace for an existing directory. The fully qualified
254
+ * path is canonicalized through `fs.realpath`; a relative, nonexistent, or
255
+ * non-directory path rejects. Repeated calls for the same canonical path
256
+ * return the existing entity without changing its title.
257
257
  * A newly created workspace is prepended to the durable registry order.
258
258
  * Different canonical paths may share a display title.
259
- * @param path - Existing directory to own, in any path spelling.
259
+ * @param path - Existing directory to own, in a fully qualified path spelling.
260
260
  * @param title - Display title used only when a new record is created.
261
261
  * @returns the existing or newly durable workspace.
262
262
  */
@@ -309,7 +309,7 @@ archiveSession(sessionId: SessionId): Promise<void>
309
309
  * Resolve by canonical directory path without creating or mutating a
310
310
  * workspace. A missing path rejects during `realpath`; an existing unowned
311
311
  * directory returns `undefined`.
312
- * @param path - Existing directory path in any spelling.
312
+ * @param path - Existing directory path in a fully qualified spelling.
313
313
  * @returns the workspace owning the canonical path, when one exists.
314
314
  */
315
315
  async resolveByPath(path: string): Promise<Workspace | undefined>