dsh-plugin-dev-kb 1.0.8 → 1.1.0

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 (188) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +6 -6
  3. package/README.md +6 -6
  4. package/kb/INDEX.md +21 -5
  5. package/kb/README.md +11 -10
  6. package/kb/extra/AGENTS.md +4 -4
  7. package/kb/extra/cookbook/adding-a-remote-api.md +197 -0
  8. package/kb/extra/cookbook/adding-a-remote-api.zh.md +197 -0
  9. package/kb/extra/cookbook/adding-a-vendored-package.md +2 -2
  10. package/kb/extra/cookbook/adding-a-vendored-package.zh.md +2 -2
  11. package/kb/extra/deepseek-llm-api-wire-extensions.md +163 -0
  12. package/kb/extra/deepseek-llm-api-wire-extensions.zh.md +163 -0
  13. package/kb/extra/development.md +8 -14
  14. package/kb/extra/development.zh.md +8 -14
  15. package/kb/extra/event-producer-consumer.md +55 -48
  16. package/kb/extra/event-producer-consumer.zh.md +58 -51
  17. package/kb/extra/glossary.md +1 -1
  18. package/kb/extra/glossary.zh.md +1 -1
  19. package/kb/extra/graph-atlas.md +0 -2
  20. package/kb/extra/graph-atlas.zh.md +0 -2
  21. package/kb/extra/i18n/README.md +4 -4
  22. package/kb/extra/i18n/README.zh.md +4 -4
  23. package/kb/extra/i18n/style-samples.md +2 -2
  24. package/kb/extra/module-graph.md +646 -926
  25. package/kb/extra/module-graph.zh.md +648 -928
  26. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.md +2 -2
  27. package/kb/extra/postmortem/0001-acp-default-export-drops-inject.zh.md +2 -2
  28. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.md +2 -2
  29. package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +2 -2
  30. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.md +2 -2
  31. package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.zh.md +2 -2
  32. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +1 -1
  33. package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +1 -1
  34. package/kb/extra/rescope.md +2 -2
  35. package/kb/extra/rescope.zh.md +2 -2
  36. package/kb/extra/subsystems/agent-team.md +28 -4
  37. package/kb/extra/subsystems/agent-team.zh.md +28 -4
  38. package/kb/extra/subsystems/attachment.md +168 -7
  39. package/kb/extra/subsystems/attachment.zh.md +168 -7
  40. package/kb/extra/subsystems/extensions.md +18 -0
  41. package/kb/extra/subsystems/extensions.zh.md +18 -0
  42. package/kb/extra/subsystems/feedback.md +4 -4
  43. package/kb/extra/subsystems/feedback.zh.md +4 -4
  44. package/kb/extra/subsystems/todo.md +32 -0
  45. package/kb/extra/subsystems/todo.zh.md +32 -0
  46. package/kb/extra/subsystems/webhook.md +70 -0
  47. package/kb/extra/subsystems/webhook.zh.md +70 -0
  48. package/kb/extra/testing.md +15 -10
  49. package/kb/extra/testing.zh.md +13 -8
  50. package/kb/extra/web-styling.md +4 -0
  51. package/kb/extra/web-styling.zh.md +4 -0
  52. package/kb/meta/search-index.json +309 -177
  53. package/kb/meta/site-pages.txt +183 -167
  54. package/kb/meta/source.json +5 -5
  55. package/kb/meta/topics.md +14 -6
  56. package/kb/site/develop/basic/publish.md +2 -2
  57. package/kb/site/develop/basic/tool.md +1 -1
  58. package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +5 -4
  59. package/kb/site/develop/framework/events.md +1 -1
  60. package/kb/site/develop/practice/dynamic-cordis.md +17 -0
  61. package/kb/site/develop/practice/llm-adapter.md +4 -3
  62. package/kb/site/en/develop/basic/publish.md +2 -2
  63. package/kb/site/en/develop/basic/tool.md +1 -1
  64. package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +5 -4
  65. package/kb/site/en/develop/framework/events.md +1 -1
  66. package/kb/site/en/develop/practice/dynamic-cordis.md +17 -0
  67. package/kb/site/en/develop/practice/llm-adapter.md +4 -3
  68. package/kb/site/en/guide/github-review.md +104 -0
  69. package/kb/site/en/guide/mcp-memory.md +103 -0
  70. package/kb/site/en/guide/network-proxy.md +87 -0
  71. package/kb/site/en/guide/providers.md +70 -17
  72. package/kb/site/en/guide/python-sdk.md +80 -34
  73. package/kb/site/en/guide/schedule.md +23 -0
  74. package/kb/site/en/reference/agent-lifecycle.md +6 -4
  75. package/kb/{extra → site/en/reference}/api-gateway.md +12 -10
  76. package/kb/site/en/reference/capability-seams.md +128 -73
  77. package/kb/site/en/reference/config-catalog.md +481 -360
  78. package/kb/site/en/reference/cookbook/adding-a-package.md +3 -4
  79. package/kb/site/en/reference/cookbook/adding-a-settings-card.md +12 -10
  80. package/kb/site/en/reference/cookbook/adding-a-tool.md +11 -4
  81. package/kb/site/en/reference/cookbook/adding-an-llm-adapter.md +1 -1
  82. package/kb/site/en/reference/cookbook/extension-cookbook.md +20 -17
  83. package/kb/site/en/reference/cordis-api/inherited.md +1 -1
  84. package/kb/site/en/reference/cordis-primer.md +2 -1
  85. package/kb/site/en/reference/index.md +30 -11
  86. package/kb/site/en/reference/persistence-catalog.md +148 -80
  87. package/kb/site/en/reference/subsystems/approval.md +10 -10
  88. package/kb/site/en/reference/subsystems/client-modules.md +58 -16
  89. package/kb/site/en/reference/subsystems/code-runtime.md +10 -6
  90. package/kb/site/en/reference/subsystems/commands.md +25 -16
  91. package/kb/site/en/reference/subsystems/compaction.md +11 -11
  92. package/kb/site/en/reference/{cookbook/adding-a-conversation-node.md → subsystems/conversation.md} +50 -24
  93. package/kb/site/en/reference/subsystems/core.md +156 -17
  94. package/kb/site/en/reference/subsystems/credentials.md +44 -3
  95. package/kb/site/en/reference/subsystems/filesystem.md +12 -2
  96. package/kb/site/en/reference/subsystems/goal.md +1 -1
  97. package/kb/site/en/reference/subsystems/index.md +7 -2
  98. package/kb/site/en/reference/subsystems/jobs.md +1 -1
  99. package/kb/site/en/reference/subsystems/llm-streaming.md +154 -12
  100. package/kb/site/en/reference/subsystems/permission-presets.md +6 -6
  101. package/kb/site/en/reference/subsystems/persistence.md +185 -175
  102. package/kb/site/en/reference/subsystems/plan.md +2 -2
  103. package/kb/site/en/reference/subsystems/sandbox.md +2 -0
  104. package/kb/site/en/reference/subsystems/schedule.md +9 -3
  105. package/kb/site/en/reference/subsystems/session-projection.md +115 -48
  106. package/kb/site/en/reference/subsystems/session-query.md +28 -14
  107. package/kb/site/en/reference/subsystems/session-reference.md +53 -8
  108. package/kb/site/en/reference/subsystems/session-telemetry.md +8 -8
  109. package/kb/site/en/reference/subsystems/session-title.md +6 -6
  110. package/kb/site/en/reference/subsystems/session.md +401 -99
  111. package/kb/site/en/reference/subsystems/settings.md +101 -6
  112. package/kb/site/en/reference/subsystems/skills.md +23 -0
  113. package/kb/site/en/reference/subsystems/slots.md +178 -0
  114. package/kb/site/en/reference/subsystems/spill.md +2 -2
  115. package/kb/site/en/reference/subsystems/storage.md +34 -3
  116. package/kb/site/en/reference/subsystems/subagent.md +122 -109
  117. package/kb/site/en/reference/subsystems/system-prompt.md +17 -4
  118. package/kb/site/en/reference/subsystems/token-meter.md +27 -12
  119. package/kb/site/en/reference/subsystems/tools.md +39 -39
  120. package/kb/site/en/reference/subsystems/typert.md +62 -55
  121. package/kb/site/en/reference/subsystems/user-questions.md +33 -33
  122. package/kb/site/en/reference/subsystems/web-client.md +98 -0
  123. package/kb/site/en/reference/subsystems/web-server.md +11 -5
  124. package/kb/site/en/reference/subsystems/web.md +7 -1
  125. package/kb/site/en/reference/subsystems/workspace.md +102 -9
  126. package/kb/site/en/reference/tool-catalog.md +86 -82
  127. package/kb/site/en/reference/tool-execution-pipeline.md +1 -1
  128. package/kb/site/guide/github-review.md +104 -0
  129. package/kb/site/guide/mcp-memory.md +103 -0
  130. package/kb/site/guide/network-proxy.md +87 -0
  131. package/kb/site/guide/providers.md +70 -17
  132. package/kb/site/guide/python-sdk.md +87 -41
  133. package/kb/site/guide/schedule.md +23 -0
  134. package/kb/site/reference/agent-lifecycle.md +6 -4
  135. package/kb/{extra/api-gateway.zh.md → site/reference/api-gateway.md} +12 -10
  136. package/kb/site/reference/capability-seams.md +128 -73
  137. package/kb/site/reference/config-catalog.md +481 -360
  138. package/kb/site/reference/cookbook/adding-a-package.md +3 -4
  139. package/kb/site/reference/cookbook/adding-a-settings-card.md +12 -10
  140. package/kb/site/reference/cookbook/adding-a-tool.md +11 -4
  141. package/kb/site/reference/cookbook/adding-an-llm-adapter.md +1 -1
  142. package/kb/site/reference/cookbook/extension-cookbook.md +20 -17
  143. package/kb/site/reference/cordis-api/inherited.md +1 -1
  144. package/kb/site/reference/cordis-primer.md +2 -1
  145. package/kb/site/reference/index.md +30 -11
  146. package/kb/site/reference/persistence-catalog.md +148 -80
  147. package/kb/site/reference/subsystems/approval.md +10 -10
  148. package/kb/site/reference/subsystems/client-modules.md +58 -16
  149. package/kb/site/reference/subsystems/code-runtime.md +10 -6
  150. package/kb/site/reference/subsystems/commands.md +25 -16
  151. package/kb/site/reference/subsystems/compaction.md +11 -11
  152. package/kb/site/reference/{cookbook/adding-a-conversation-node.md → subsystems/conversation.md} +50 -24
  153. package/kb/site/reference/subsystems/core.md +156 -17
  154. package/kb/site/reference/subsystems/credentials.md +44 -3
  155. package/kb/site/reference/subsystems/filesystem.md +12 -2
  156. package/kb/site/reference/subsystems/goal.md +1 -1
  157. package/kb/site/reference/subsystems/index.md +7 -2
  158. package/kb/site/reference/subsystems/jobs.md +1 -1
  159. package/kb/site/reference/subsystems/llm-streaming.md +154 -12
  160. package/kb/site/reference/subsystems/permission-presets.md +5 -5
  161. package/kb/site/reference/subsystems/persistence.md +184 -174
  162. package/kb/site/reference/subsystems/plan.md +2 -2
  163. package/kb/site/reference/subsystems/schedule.md +9 -3
  164. package/kb/site/reference/subsystems/session-projection.md +115 -48
  165. package/kb/site/reference/subsystems/session-query.md +28 -14
  166. package/kb/site/reference/subsystems/session-reference.md +53 -8
  167. package/kb/site/reference/subsystems/session-telemetry.md +8 -8
  168. package/kb/site/reference/subsystems/session-title.md +6 -6
  169. package/kb/site/reference/subsystems/session.md +401 -99
  170. package/kb/site/reference/subsystems/settings.md +101 -6
  171. package/kb/site/reference/subsystems/skills.md +23 -0
  172. package/kb/site/reference/subsystems/slots.md +178 -0
  173. package/kb/site/reference/subsystems/spill.md +2 -2
  174. package/kb/site/reference/subsystems/storage.md +34 -3
  175. package/kb/site/reference/subsystems/subagent.md +122 -109
  176. package/kb/site/reference/subsystems/system-prompt.md +17 -4
  177. package/kb/site/reference/subsystems/token-meter.md +27 -12
  178. package/kb/site/reference/subsystems/tools.md +39 -39
  179. package/kb/site/reference/subsystems/typert.md +62 -55
  180. package/kb/site/reference/subsystems/user-questions.md +33 -33
  181. package/kb/site/reference/subsystems/web-client.md +98 -0
  182. package/kb/site/reference/subsystems/web-server.md +11 -5
  183. package/kb/site/reference/subsystems/web.md +7 -1
  184. package/kb/site/reference/subsystems/workspace.md +102 -9
  185. package/kb/site/reference/tool-catalog.md +85 -81
  186. package/kb/site/reference/tool-execution-pipeline.md +1 -1
  187. package/package.json +2 -2
  188. package/skills/dsh-plugin-dev-kb.md +8 -6
@@ -24,8 +24,8 @@ interface UserMessage extends Message {
24
24
  /**
25
25
  * The merge-extensible, append-only source of truth for an agent interaction.
26
26
  * Message history is derived from this log. Every event is lossless JSON and
27
- * sequence numbers stay contiguous, including raw chunks, so persistence can
28
- * store the canonical log verbatim.
27
+ * sequence numbers stay contiguous. Assistant attempt events embed their exact
28
+ * compact raw streams so persistence stores one durable settlement per attempt.
29
29
  */
30
30
  interface SessionEventMap {
31
31
  /**
@@ -56,8 +56,6 @@ interface SessionEventMap {
56
56
  * project their `content` verbatim; `source` tells them apart.
57
57
  */
58
58
  'user/message': UserMessage
59
- /** Raw stream chunk — token-level replay fidelity. */
60
- 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
61
59
  /**
62
60
  * Assembled assistant message for one step (derived history uses this).
63
61
  * Carries the step's `usage` when the adapter reported token accounting, so
@@ -68,13 +66,27 @@ interface SessionEventMap {
68
66
  * marker distinguishes that prefix without re-deriving interruption from turn
69
67
  * boundaries. An aborted turn with no such event streamed no visible content.
70
68
  */
71
- 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true }
69
+ 'assistant/message': {
70
+ turn: number
71
+ step: number
72
+ message: AssistantMessage
73
+ /** Exact timed model stream, compacted without joining delta boundaries. */
74
+ stream: AssistantStreamRecord[]
75
+ usage?: TokenUsage
76
+ interrupted?: true
77
+ }
78
+ /**
79
+ * One model attempt that committed no surface message. The embedded stream
80
+ * preserves a failed, retried, cancelled, or stream-error attempt that
81
+ * reached settlement without fabricating model-visible history.
82
+ */
83
+ 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] }
72
84
  /**
73
85
  * The model requested one tool invocation: `name` with the raw `arguments`
74
86
  * JSON string exactly as the model produced it (unparsed). `callId` pairs the
75
87
  * call with its `tool/result`.
76
88
  */
77
- 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }
89
+ 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string }
78
90
  /**
79
91
  * A completed tool call's model-facing result, optional internal failure
80
92
  * identity, and optional tool-private `meta` presentation payload. `meta` is
@@ -93,13 +105,16 @@ interface SessionEventMap {
93
105
  error?: { name: string; code: string }
94
106
  meta?: JsonValue
95
107
  }
96
- /** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
97
- 'todo/write': { todos: TodoItem[] }
98
108
  /**
99
109
  * Full header for the next request, appended inside its step before dispatch.
100
110
  * It is log-only; the latest snapshot reconstructs the request header.
101
111
  */
102
- 'request/header': { header: EpochHeader; reason: RequestHeaderReason }
112
+ 'request/header': {
113
+ header: EpochHeader
114
+ reason: RequestHeaderReason
115
+ /** A changed header also begins a distinct model-message series. */
116
+ startsSeries?: true
117
+ }
103
118
  /**
104
119
  * Route metadata for the next request, logged only when the route or capacity
105
120
  * changes. It does not participate in request reconstruction or header equality.
@@ -109,12 +124,12 @@ interface SessionEventMap {
109
124
  * Marks the end of a constructor seed. Events before it have smaller seq
110
125
  * values and came from the seed (resume, fork, or replay); this lifecycle
111
126
  * 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.
127
+ * {@link Session.firstLiveSeq}.
114
128
  *
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`.
129
+ * A fresh fork child owns one `{ inherited: true }` marker at its exact
130
+ * inherited-prefix cut, even when that prefix ends in an ancestor marker.
131
+ * The last tagged marker is the current Session's cut; untagged markers keep
132
+ * ordinary restore and replay lifecycle boundaries.
118
133
  *
119
134
  * `Session`'s constructor is the only legitimate writer. The invariant
120
135
  * companion deliberately constrains nothing here, so a plugin appending one
@@ -127,40 +142,17 @@ interface SessionEventMap {
127
142
  * writers — a concurrently live session holds its own boundary elsewhere,
128
143
  * so tolerating concurrent writers needs a signal beyond the log.
129
144
  */
130
- 'session/end-seed': Record<string, never>
145
+ 'session/end-seed': { inherited?: true }
131
146
  }
132
147
  ```
133
148
 
134
149
  `UserMessage` is the identified, frozen user-role value shared by ordinary prompts, injected context, steering, and live inbox events. Event wrappers add only event-local position or outcome facts; the loop adds only driver-owned routing state while an item remains pending.
135
150
 
136
- ### `TodoItem` — one todo-list entry
137
-
138
- The unit of the `todo/write` event's whole-list snapshot. Deliberately minimal — a `content` line and a three-state `status` (no id, priority, or `activeForm`): the list is replaced wholesale on every write, so entries need no stable identity. See the [todo_write Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md).
139
-
140
- ```ts type-equiv
141
- /**
142
- * One entry in an agent's todo list — the unit of the `todo/write`
143
- * {@link SessionEventMap} event's whole-list snapshot.
144
- *
145
- * Deliberately minimal: a human-readable `content` line and a three-state
146
- * `status`. No id, priority, or `activeForm` — the list is replaced wholesale
147
- * on every write (last-write-wins), so entries need no stable identity. The
148
- * three statuses describe the complete portable lifecycle needed by model and
149
- * UI consumers.
150
- */
151
- interface TodoItem {
152
- /** What this task is — a short imperative line shown in the UI. */
153
- content: string
154
- /** Lifecycle state. `in_progress` marks a task being worked now; parallel work may mark several. */
155
- status: 'pending' | 'in_progress' | 'completed'
156
- }
157
- ```
158
-
159
151
  <a id="the-request-header-event-requestheader"></a>
160
152
 
161
153
  ### The request header event: `request/header`
162
154
 
163
- The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
155
+ The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a changed request appends a snapshot with reason `'change'`; and an unchanged envelope beginning an explicitly declared message series or following a surface replacement appends a snapshot with reason `'series'`. A changed snapshot carries `startsSeries: true` when that request also begins a series. Ordinary append-only later Turns, further Steps, and retries in the same model-message series inherit the latest snapshot. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
164
156
 
165
157
  ```ts type-equiv
166
158
  /**
@@ -202,6 +194,28 @@ interface RequestContext {
202
194
 
203
195
  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.
204
196
 
197
+ ```ts type-equiv
198
+ /** Sequence number of one existing event in a Session log. */
199
+ type SessionSeq = BrandedNumber<'SessionSeq'>
200
+ ```
201
+
202
+ ```ts type-equiv
203
+ /** A Session log gap, prefix length, or read offset, which may equal the event count. */
204
+ type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
205
+ ```
206
+
207
+ ```ts type-equiv
208
+ /** Inclusive Session event watermark, or `-1` before any event exists. */
209
+ type SessionSeqCursor = SessionSeq | -1
210
+ ```
211
+
212
+ ```ts type-equiv
213
+ /** One existing Session event position, or explicit absence. */
214
+ type OptionalSessionSeq = SessionSeq | null
215
+ ```
216
+
217
+ `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.
218
+
205
219
  ```ts type-equiv
206
220
  /**
207
221
  * One immutable entry in the session log.
@@ -212,7 +226,7 @@ A proper discriminated union over `type` (not independent `type`/`data` unions),
212
226
  * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
213
227
  * they only exist on {@link SurfaceEventType} variants (`user/message`,
214
228
  * `assistant/message`, `tool/result`).
215
- * Non-surface events (boundary markers, chunks, usage, errors) never carry
229
+ * Non-surface events (boundary markers, attempts, errors) never carry
216
230
  * surface metadata — the compiler enforces this at `Session.append()`
217
231
  * call sites.
218
232
  */
@@ -220,7 +234,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
220
234
  [K in SessionEventType]: {
221
235
  type: K
222
236
  /** Monotonic sequence number within the session. */
223
- seq: number
237
+ seq: SessionSeq
224
238
  /** Unix epoch milliseconds. */
225
239
  time: number
226
240
  data: SessionEventMap[K]
@@ -237,14 +251,11 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
237
251
  ignorable?: true
238
252
  } & (K extends SurfaceEventType ? {
239
253
  /**
240
- * Seq numbers of earlier events that this event cites as sources
241
- * (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
242
- * or the surface nodes shadowed by a compaction replace node). An
243
- * `assistant/message` may carry a present empty array for a known empty
244
- * provider stream; when the field is absent, the event does not record which
245
- * earlier events produced the message.
254
+ * Seq numbers of earlier events that this event cites as sources, such as
255
+ * the surface nodes shadowed by a compaction replacement. A v2
256
+ * `assistant/message` embeds its provider stream and cannot carry this field.
246
257
  */
247
- sourceEventSeqs?: number[]
258
+ sourceEventSeqs?: SessionSeq[]
248
259
  /** How this event entered the surface; absent for non-surface events. */
249
260
  surfaceOp?: SurfaceOp
250
261
  } : object)
@@ -253,7 +264,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
253
264
 
254
265
  `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`.
255
266
 
256
- 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.
267
+ 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.
257
268
 
258
269
  ## Surface types
259
270
 
@@ -265,7 +276,8 @@ The three message-producing types (`SurfaceEventType` — `user/message`, `assis
265
276
  /**
266
277
  * The subset of {@link SessionEventType} values whose events produce LLM
267
278
  * messages and are eligible to appear on the ordered surface. Only these
268
- * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}.
279
+ * event types may carry {@link SurfaceOp}; user and tool events may also cite
280
+ * earlier sources through {@link SessionEvent.sourceEventSeqs}.
269
281
  */
270
282
  type SurfaceEventType =
271
283
  | 'user/message'
@@ -291,7 +303,7 @@ type SurfaceEventType =
291
303
  */
292
304
  type SurfaceOp =
293
305
  | 'append'
294
- | { op: 'replace'; start: number; end: number }
306
+ | { op: 'replace'; start: SessionSeq; end: SessionSeq }
295
307
  ```
296
308
 
297
309
  `'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.
@@ -303,21 +315,20 @@ type SurfaceOp =
303
315
  * Surface placement and cited source-event seqs for {@link Session.append}. Required on
304
316
  * message-producing events and forbidden on log-only events.
305
317
  */
306
- interface SurfaceIntent {
318
+ type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
307
319
  surfaceOp: SurfaceOp
308
- /**
309
- * Complete set of known source-event seqs. `assistant/message` may use a
310
- * present empty array for a known empty provider stream; when the field is
311
- * absent, the event does not record which earlier events produced the message.
312
- * Other surface events require a non-empty set when this field is present.
313
- */
314
- sourceEventSeqs?: number[]
315
- }
320
+ } & (T extends 'assistant/message' ? {
321
+ /** V2 Assistant messages embed their provider stream instead of citing source events. */
322
+ sourceEventSeqs?: never
323
+ } : {
324
+ /** Complete non-empty set of known earlier source-event seqs. */
325
+ sourceEventSeqs?: SessionSeq[]
326
+ })
316
327
  ```
317
328
 
318
329
  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](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/README.md)). Non-surface types reject it at compile time.
319
330
 
320
- 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.
331
+ `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.
321
332
 
322
333
  ### `SessionSurface` — the live readonly surface projection
323
334
 
@@ -329,7 +340,7 @@ Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the f
329
340
  /** Readonly live projection of the message-producing session events. */
330
341
  interface SessionSurface {
331
342
  /** Current surface event sequences in model-visible order. */
332
- readonly nodes: readonly number[]
343
+ readonly nodes: readonly SessionSeq[]
333
344
  /** Monotonic count of committed positional replacements. */
334
345
  readonly replaceGeneration: number
335
346
  }
@@ -343,13 +354,13 @@ interface SessionSurface {
343
354
  /** One replacement operation observed while folding a session surface. */
344
355
  interface SurfaceFoldReplacement {
345
356
  /** Seq of the event that replaced the prior surface range. */
346
- seq: number
357
+ seq: SessionSeq
347
358
  /** Declared inclusive start seq of the replaced surface range. */
348
- start: number
359
+ start: SessionSeq
349
360
  /** Declared inclusive end seq of the replaced surface range. */
350
- end: number
361
+ end: SessionSeq
351
362
  /** Actual surface entries removed by the operation, in surface order. */
352
- shadowedSeqs: number[]
363
+ shadowedSeqs: SessionSeq[]
353
364
  }
354
365
  ```
355
366
 
@@ -357,7 +368,7 @@ interface SurfaceFoldReplacement {
357
368
  /** Complete result of replaying the surface operations in a session log. */
358
369
  interface SurfaceFoldResult {
359
370
  /** Current surface event sequences in model-visible order. */
360
- nodes: number[]
371
+ nodes: SessionSeq[]
361
372
  /** Replacement operations in event order. */
362
373
  replacements: SurfaceFoldReplacement[]
363
374
  }
@@ -381,24 +392,27 @@ declare class Session {
381
392
  get surface(): SessionSurface;
382
393
  /**
383
394
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
384
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
395
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
385
396
  * `Session` is created without a store-owned header, a minimal header is
386
397
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
387
398
  * `session.header` is always present. Kept out of the event log — it is a
388
399
  * storage concern, not replayable conversation state.
389
400
  */
390
401
  readonly header: SessionHeader;
402
+ /** Number of leading events inherited from this Session's fork parent. */
403
+ readonly inheritedEventCount: SessionLogOffset;
391
404
  /** The session identity, derived from its durable header's single copy. */
392
405
  get id(): SessionId;
393
406
  /**
394
407
  * The first seq appended IN THIS PROCESS: the length of the constructor
395
408
  * seed (0 without one). Events with smaller seq values entered through
396
409
  * construction — replay, fork, or resume — and were never published on the
397
- * `session/event` firehose (constructor seeds do not emit), so consumers
398
- * that replay the log as a publication substitute (telemetry adoption)
399
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
400
- * boundary: a resumed session's constructor seed is its full stored log,
401
- * while its header keeps the original fork value this field is the
410
+ * `session/event` firehose (constructor seeds do not emit). This offset marks
411
+ * the constructor-input boundary for lifecycle ownership and persistence
412
+ * adoption; consumers that need complete canonical history still start at
413
+ * seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
414
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
415
+ * log, while the inherited count keeps the original fork value — this field is the
402
416
  * in-process construction fact.
403
417
  *
404
418
  * Not persisted itself: a seeded session projects it into the log as the
@@ -411,17 +425,23 @@ declare class Session {
411
425
  * When this lifecycle appends the marker, it occupies this seq before the
412
426
  * store attaches and therefore does not publish either. Otherwise this seq
413
427
  * holds an ordinary published write.
414
- */
415
- readonly firstLiveSeq: number;
428
+ */
429
+ readonly firstLiveSeq: SessionLogOffset;
416
430
  /**
417
431
  * Create a detached session by validating and snapshotting borrowed seed
418
432
  * events and storage metadata.
419
433
  * @param id - session identity.
420
434
  * @param seed - optional borrowed replay or fork events.
421
435
  * @param header - optional borrowed storage metadata.
436
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
422
437
  * @returns a detached session.
423
438
  */
424
- static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
439
+ static create(
440
+ id: SessionId,
441
+ seed?: readonly SessionEvent[],
442
+ header?: SessionHeader,
443
+ inheritedEventCount?: SessionLogOffset,
444
+ ): Session;
425
445
  /**
426
446
  * Restore a detached session by taking ownership of fresh persistence values.
427
447
  * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -429,18 +449,46 @@ declare class Session {
429
449
  * @param id - restored session identity.
430
450
  * @param seed - fresh detached events whose ownership is transferred.
431
451
  * @param header - fresh detached metadata whose ownership is transferred.
452
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
432
453
  * @returns a restored detached session.
433
454
  */
434
- static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
455
+ static fromRestore(
456
+ id: SessionId,
457
+ seed: readonly SessionEvent[],
458
+ header: SessionHeader,
459
+ inheritedEventCount: SessionLogOffset,
460
+ ): Session;
461
+ /**
462
+ * Return the immutable event stored at one exact sequence number.
463
+ * @param seq - event sequence number.
464
+ * @returns the accepted event, or undefined when the log does not contain it.
465
+ */
466
+ eventAt(seq: SessionSeq): SessionEvent | undefined;
467
+ /**
468
+ * Materialize an immutable snapshot of a half-open event sequence range.
469
+ * A full current snapshot is reused until the next append; every previously
470
+ * returned snapshot remains stable after later appends.
471
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
472
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
473
+ * @returns a frozen array of the selected deeply frozen events.
474
+ */
475
+ snapshotEvents(
476
+ fromSeq: SessionLogOffset = SessionLogOffset(0),
477
+ toSeqExclusive: SessionLogOffset = this.seq,
478
+ ): readonly SessionEvent[];
435
479
  /**
436
- * An immutable snapshot of the append-only event log. The snapshot is reused
437
- * until the next append; a previously returned array does not grow later.
438
- * Events and their nested data are deep-frozen at acceptance, so neither a
439
- * cast nor ordinary JavaScript can rewrite durable history.
480
+ * Return this Session's events after its fork-inherited prefix.
481
+ * @returns a fresh array containing child-owned events in log order.
440
482
  */
441
- get events(): readonly SessionEvent[];
483
+ ownEvents(): readonly SessionEvent[];
484
+ /**
485
+ * Whether one existing event position is outside the fork-inherited prefix.
486
+ * @param seq - event position in this Session.
487
+ * @returns true when the event belongs to this Session rather than its parent.
488
+ */
489
+ isOwnSeq(seq: SessionSeq): boolean;
442
490
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
443
- get seq(): number;
491
+ get seq(): SessionLogOffset;
444
492
  /**
445
493
  * Append one typed event to the log and synchronously notify observers via
446
494
  * the store-owned, module-private publication hooks. The hot path never blocks
@@ -458,7 +506,8 @@ declare class Session {
458
506
  * declare how it joins the surface, the sole source of derived model
459
507
  * history) and
460
508
  * rejected by the compiler for non-surface types like `turn/start` or
461
- * `assistant/chunk`.
509
+ * `assistant/attempt`. Assistant messages embed their exact provider
510
+ * stream and cannot cite top-level source events.
462
511
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
463
512
  * `data` that entered the log, so reading `event.data` back sees the logged
464
513
  * value, never the caller's still-mutable input.
@@ -468,7 +517,7 @@ declare class Session {
468
517
  * Map/Set/Date/class instance), or when the candidate violates the
469
518
  * canonical surface contract (marker shape and eligibility, unique
470
519
  * earlier source-event references, positional replacement validity, and complete
471
- * shadowed-node coverage). One recursive pass reads, validates, and
520
+ * shadowed-node coverage). One iterative pass reads, validates, and
472
521
  * copies each nested value once, so a stateful getter cannot supply one value
473
522
  * to validation and another to storage. The event log is the durable source
474
523
  * of truth, so a bad event fails at the append site rather than later during
@@ -479,13 +528,13 @@ declare class Session {
479
528
  append<T extends SessionEventType>(
480
529
  type: T,
481
530
  data: SessionEventMap[T],
482
- ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
531
+ ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []
483
532
  ): SessionEvent<T>;
484
533
  /**
485
534
  * The {@link EpochHeader} in force after the log's last header event — the
486
535
  * header the NEXT request will be compared against — or undefined before
487
536
  * the first `request/header` snapshot. The live, incrementally-maintained
488
- * form of `foldRequestHeader(session.events)`: each header event is folded
537
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
489
538
  * once, when first seen, so a per-step read costs O(new events).
490
539
  * @returns the folded header, or undefined when no header event exists yet.
491
540
  */
@@ -530,17 +579,17 @@ declare class Session {
530
579
  `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:
531
580
 
532
581
  - `user/message` → a user message carrying exact `content`; an optional envelope remains log-only display metadata.
533
- - `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.
582
+ - `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.
534
583
  - `tool/result` → a user message carrying a `tool-result` block.
535
584
  - `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.
536
585
 
537
- 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.
586
+ 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.
538
587
 
539
588
  ## Live-session fork API
540
589
 
541
590
  `ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API:
542
591
 
543
- - `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`).
592
+ - `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`.
544
593
 
545
594
  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.
546
595
 
@@ -572,8 +621,10 @@ interface TurnEndReasonMap {
572
621
  /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
573
622
  'max-tokens': { kind: 'max-tokens' }
574
623
  /**
575
- * A persistence backend closed a crash-orphaned turn on reload. The loop never
576
- * emits this marker, and the events recorded before the crash remain intact.
624
+ * A crash-orphaned turn was closed after the fact: agent-loop resume appends
625
+ * this closer for a stored log whose last turn never ended, and session-query
626
+ * synthesizes it on cold reads. The loop never emits this marker live, and
627
+ * the events recorded before the crash remain intact.
577
628
  */
578
629
  interrupted: { kind: 'interrupted' }
579
630
  }
@@ -589,9 +640,9 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
589
640
 
590
641
  ## The end-seed boundary: `session/end-seed`
591
642
 
592
- A seeded session — resume, fork, or replay — appends this log-only event immediately after its constructor seed, as its first live write. Events before it have smaller seq values and came from the seed. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
643
+ 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.
593
644
 
594
- 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`.
645
+ 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.
595
646
 
596
647
  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/*`.
597
648
 
@@ -599,18 +650,24 @@ Consumers that order Sessions by human activity exclude this boundary: picking a
599
650
 
600
651
  ## Plugin-contributed log-only events
601
652
 
602
- A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history). Their owner decides whether they belong to an open execution turn or may stand between turns, and enforces any relation in its own invariant companion. The generated [persistence log event catalog](../persistence-catalog.md) enumerates every core and plugin-contributed event with its payload, surface badge, and declaration site; the compaction seam's `compaction/*` semantics are discussed on [compaction.md](./compaction.md).
653
+ A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history). Their owner decides whether they belong to an open execution turn or may stand between turns, and enforces any relation in its own invariant companion. The generated [persistence log event catalog](../persistence-catalog.md) enumerates every core and plugin-contributed event; the compaction seam's `compaction/*` semantics are discussed on [compaction.md](./compaction.md).
603
654
 
604
- When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation Node cookbook](../cookbook/adding-a-conversation-node.md).
655
+ When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation subsystem](./conversation.md).
605
656
 
606
657
  The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn (see [the hook-bridges Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
607
658
 
608
659
  ## Durability contract
609
660
 
610
- 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.
661
+ 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.
611
662
 
612
663
  The backends that consume this contract are on [persistence.md](./persistence.md).
613
664
 
665
+ ## Remote catalog and workspace opening
666
+
667
+ `ModelCatalog` is the Host-generation model directory returned by `session/modelCatalog`: it carries the deployment default, routable provider ids, successful provider groups, and isolated provider failures. It is not derived from one Session and remains separate from Session projections.
668
+
669
+ `SessionOpenWorkspacePathRequest` carries an absolute or workspace-resolved `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. A Session-aware Client resolves relative paths against its current Session cwd when known; the controller hands the path to the opener unchanged and reports invalid requests, cancellation, and opener failures through the Session Remote error vocabulary.
670
+
614
671
  <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
615
672
 
616
673
  <a id="cordis-surface"></a>
@@ -619,13 +676,158 @@ The backends that consume this contract are on [persistence.md](./persistence.md
619
676
 
620
677
  Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
621
678
 
679
+ <a id="ctxsessioncontroller--sessioncontroller"></a>
680
+
681
+ ### `ctx.sessionController` — `SessionController`
682
+
683
+ Host service backing the generated `ctx.remote.session` namespace.
684
+
685
+ ```ts cordis-catalog
686
+ /**
687
+ * Resolve or resume one ordinary Session for another Host API domain.
688
+ * @param sessionId - Session identity whose Agent owns the operation.
689
+ * @returns the live Agent or the stable Session-domain failure.
690
+ */
691
+ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
692
+
693
+ /**
694
+ * Inspect one attached or persisted Session without activating its Agent.
695
+ * @param sessionId - durable Session identity.
696
+ * @param signal - optional caller cancellation for persistence reads.
697
+ * @returns the current attached state or persisted header and event prefix.
698
+ */
699
+ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
700
+
701
+ /**
702
+ * Read all visible Session rows without resuming an Agent.
703
+ * @param _request - reserved empty list request.
704
+ * @param signal - cancellation for persistence reads.
705
+ * @returns visible Session summaries ordered by activity.
706
+ */
707
+ @Remote('list') async list(_request: SessionListRequest, signal: AbortSignal): Promise<SessionListValue>
708
+
709
+ /**
710
+ * Search visible Session content without resuming an Agent.
711
+ * @param request - literal message-content query.
712
+ * @param signal - cancellation for list and search reads.
713
+ * @returns authorized bounded Session search results.
714
+ */
715
+ @Remote('search') search(request: SessionSearchRequest, signal: AbortSignal): Promise<SessionSearchValue>
716
+
717
+ /**
718
+ * Create or idempotently adopt one ordinary Session.
719
+ * @param request - requested identity, location, and Agent preset.
720
+ * @returns the Session identity and resolved preset when configured.
721
+ */
722
+ @Remote('create') create(request: SessionCreateRequest): Promise<SessionCreateValue>
723
+
724
+ /**
725
+ * Select one Session-local model after explicitly resuming the Session.
726
+ * @param request - Session identity and requested model selection.
727
+ * @returns the normalized selection installed for the Session.
728
+ */
729
+ @Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>
730
+
731
+ /**
732
+ * Describe every currently routable model for Host-generation selectors.
733
+ * @returns provider-grouped models, the deployment default, and isolated provider failures.
734
+ */
735
+ @Remote('modelCatalog') modelCatalog(): Promise<ModelCatalog>
736
+
737
+ /**
738
+ * Report whether this deployment can hand a Session workspace path to a native desktop.
739
+ * @returns true when the matching open operation is available.
740
+ */
741
+ @Remote canOpenWorkspacePath(): boolean
742
+
743
+ /**
744
+ * Open one path prepared by a Session-aware caller on the Host desktop.
745
+ * @param request - path after best-effort Session workspace resolution.
746
+ * @param signal - caller lifetime; abort terminates the native command.
747
+ * @returns confirmation after the native opener accepts the path.
748
+ * @throws RemoteError when the request is invalid, cancelled, or the opener fails.
749
+ */
750
+ @Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise<SessionOpenWorkspacePathValue>
751
+
752
+ /**
753
+ * Rename one Session after explicitly resuming it.
754
+ * @param request - Session identity and proposed title.
755
+ * @returns the accepted title and durable event sequence.
756
+ */
757
+ @Remote('rename') rename(request: SessionRenameRequest): Promise<SessionRenameValue>
758
+
759
+ /**
760
+ * Fork one cold-readable completed-turn prefix into a new Session.
761
+ * @param request - source Session and optional event anchor.
762
+ * @returns the new Session identity.
763
+ */
764
+ @Remote('fork') fork(request: SessionForkRequest): Promise<SessionForkValue>
765
+
766
+ /**
767
+ * Admit one prompt after explicitly resuming its Session.
768
+ * @param request - Session identity, prompt content, source metadata, and delivery mode.
769
+ * @param signal - caller cancellation before prompt admission begins.
770
+ * @returns acknowledgement that the Agent accepted the prompt.
771
+ */
772
+ @Remote('prompt') prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue>
773
+
774
+ /**
775
+ * Read one image proven reachable from the addressed Session log.
776
+ * @param request - Session and attachment identities used for authorization.
777
+ * @returns the durable attachment reference and base64-encoded bytes.
778
+ */
779
+ @Remote('attachment') attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>
780
+
781
+ /**
782
+ * Mutate one still-pending queue occurrence on a live Agent.
783
+ * @param request - Session, queue item, and requested mutation.
784
+ * @returns acknowledgement that the queue mutation was applied.
785
+ */
786
+ @Remote('updateQueue') updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue
787
+
788
+ /**
789
+ * Cancel one active Agent turn without dropping its pending inbox.
790
+ * @param request - Session whose active Agent turn is cancelled.
791
+ * @returns acknowledgement that cancellation was requested.
792
+ */
793
+ @Remote('cancel') cancel(request: SessionCancelRequest): SessionCancelValue
794
+
795
+ /**
796
+ * Read one cold-safe, message-aligned Session history page.
797
+ * @param request - durable address, backward cursor, and page budget.
798
+ * @param signal - cancellation for persistence reads.
799
+ * @returns one chronological page.
800
+ */
801
+ @Remote('page') page(request: SessionPageRequest, signal: AbortSignal): Promise<SessionPage>
802
+
803
+ /**
804
+ * Follow one Session log from its opening or resume cursor.
805
+ * @param request - durable address and last committed sequence already held by the caller.
806
+ * @param signal - cancellation owned by the Remote stream carrier.
807
+ * @returns a complete opening snapshot followed by gap-free durable event
808
+ * frames and optional cursorless assistant-stream frames.
809
+ */
810
+ @Remote({ mode: 'stream' }) follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>
811
+
812
+ /**
813
+ * Stream a complete live-control baseline followed by replacement frames.
814
+ * @param signal - cancellation owned by the Remote stream carrier.
815
+ * @returns one complete baseline followed by live replacement frames.
816
+ */
817
+ @Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
818
+ ```
819
+
820
+ Types: [SessionId](./core.md) · [SessionInspection](./persistence.md) · [SessionSearchRequest](./session-query.md)
821
+
822
+ Source: [`packages/api/session-controller/src/index.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/index.ts)
823
+
622
824
  <a id="ctxsessions--sessionstore"></a>
623
825
 
624
826
  ### `ctx.sessions` — `SessionStore`
625
827
 
626
828
  In-memory session store (`ctx.sessions`).
627
829
 
628
- Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
830
+ 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.
629
831
 
630
832
  ```ts cordis-catalog
631
833
  /**
@@ -748,13 +950,113 @@ list(): Session[]
748
950
  * `SessionStore`'s id policy.
749
951
  * @returns The created live child session.
750
952
  */
751
- fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
953
+ fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
752
954
  ```
753
955
 
754
956
  Types: [CreateSessionOptions](./persistence.md) · [PrepareSessionOptions](./persistence.md) · [SessionId](./core.md)
755
957
 
756
958
  Source: [`packages/core/session/src/index.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/index.ts)
757
959
 
960
+ <a id="api-session-events"></a>
961
+
962
+ ### `api-session/*` events
963
+
964
+ <a id="api-sessionactivity--emit"></a>
965
+
966
+ #### `api-session/activity` — emit
967
+
968
+ One user-authored durable message advanced Session list activity.
969
+
970
+ ```ts cordis-catalog
971
+ /**
972
+ * One user-authored durable message advanced Session list activity.
973
+ * @mode emit
974
+ * @param sessionId - addressed Session identity.
975
+ * @param updatedAt - durable message time used for list ordering.
976
+ */
977
+ 'api-session/activity'(sessionId: SessionId, updatedAt: number): void
978
+ ```
979
+
980
+ Types: [SessionId](./core.md)
981
+
982
+ Source: [`packages/api/session-controller/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/types.ts)
983
+
984
+ <a id="api-sessionadded--emit"></a>
985
+
986
+ #### `api-session/added` — emit
987
+
988
+ A Session became visible to Session list consumers.
989
+
990
+ ```ts cordis-catalog
991
+ /**
992
+ * A Session became visible to Session list consumers.
993
+ * @mode emit
994
+ * @param summary - initial list row for the Session.
995
+ */
996
+ 'api-session/added'(summary: SessionSummary): void
997
+ ```
998
+
999
+ Source: [`packages/api/session-controller/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/types.ts)
1000
+
1001
+ <a id="api-sessionerror--emit"></a>
1002
+
1003
+ #### `api-session/error` — emit
1004
+
1005
+ One Agent failed outside a durable turn position.
1006
+
1007
+ ```ts cordis-catalog
1008
+ /**
1009
+ * One Agent failed outside a durable turn position.
1010
+ * @mode emit
1011
+ * @param sessionId - Agent and Session identity.
1012
+ * @param message - user-safe failure chain.
1013
+ */
1014
+ 'api-session/error'(sessionId: SessionId, message: string): void
1015
+ ```
1016
+
1017
+ Types: [SessionId](./core.md)
1018
+
1019
+ Source: [`packages/api/session-controller/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/types.ts)
1020
+
1021
+ <a id="api-sessionremoved--emit"></a>
1022
+
1023
+ #### `api-session/removed` — emit
1024
+
1025
+ A Session left the live Host registry.
1026
+
1027
+ ```ts cordis-catalog
1028
+ /**
1029
+ * A Session left the live Host registry.
1030
+ * @mode emit
1031
+ * @param sessionId - removed Session identity.
1032
+ */
1033
+ 'api-session/removed'(sessionId: SessionId): void
1034
+ ```
1035
+
1036
+ Types: [SessionId](./core.md)
1037
+
1038
+ Source: [`packages/api/session-controller/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/types.ts)
1039
+
1040
+ <a id="api-sessionstatus--emit"></a>
1041
+
1042
+ #### `api-session/status` — emit
1043
+
1044
+ One Agent changed running state.
1045
+
1046
+ ```ts cordis-catalog
1047
+ /**
1048
+ * One Agent changed running state.
1049
+ * @mode emit
1050
+ * @param sessionId - Agent and Session identity.
1051
+ * @param running - whether the Agent is running.
1052
+ */
1053
+ 'api-session/status'(sessionId: SessionId, running: boolean): void
1054
+ ```
1055
+
1056
+ Types: [SessionId](./core.md)
1057
+
1058
+ Source: [`packages/api/session-controller/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/src/types.ts)
1059
+
758
1060
  <a id="session-events"></a>
759
1061
 
760
1062
  ### `session/*` events