@naswerks/periscope 1.0.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 (230) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -0
  4. package/SECURITY.md +315 -0
  5. package/dist/bin/command.d.ts +97 -0
  6. package/dist/bin/command.js +141 -0
  7. package/dist/bin/config.d.ts +7 -0
  8. package/dist/bin/config.js +90 -0
  9. package/dist/bin/login.d.ts +48 -0
  10. package/dist/bin/login.js +82 -0
  11. package/dist/bin/main.d.ts +33 -0
  12. package/dist/bin/main.js +173 -0
  13. package/dist/bin/pair.d.ts +28 -0
  14. package/dist/bin/pair.js +198 -0
  15. package/dist/bin/periscope.d.ts +3 -0
  16. package/dist/bin/periscope.js +4 -0
  17. package/dist/bin/posture.d.ts +64 -0
  18. package/dist/bin/posture.js +157 -0
  19. package/dist/bin/reconfigure.d.ts +64 -0
  20. package/dist/bin/reconfigure.js +195 -0
  21. package/dist/bin/serve.d.ts +103 -0
  22. package/dist/bin/serve.js +441 -0
  23. package/dist/bin/status.d.ts +11 -0
  24. package/dist/bin/status.js +49 -0
  25. package/dist/bin/workspaces.d.ts +109 -0
  26. package/dist/bin/workspaces.js +215 -0
  27. package/dist/control/backoff.d.ts +25 -0
  28. package/dist/control/backoff.js +20 -0
  29. package/dist/control/codec.d.ts +15 -0
  30. package/dist/control/codec.js +501 -0
  31. package/dist/control/credential.d.ts +23 -0
  32. package/dist/control/credential.js +8 -0
  33. package/dist/control/frames.d.ts +1180 -0
  34. package/dist/control/frames.js +441 -0
  35. package/dist/control/link-state.d.ts +32 -0
  36. package/dist/control/link-state.js +39 -0
  37. package/dist/control/link.d.ts +109 -0
  38. package/dist/control/link.js +736 -0
  39. package/dist/control/queue.d.ts +110 -0
  40. package/dist/control/queue.js +174 -0
  41. package/dist/control/seq.d.ts +49 -0
  42. package/dist/control/seq.js +52 -0
  43. package/dist/control/stream-routing.d.ts +200 -0
  44. package/dist/control/stream-routing.js +167 -0
  45. package/dist/control/stream.d.ts +43 -0
  46. package/dist/control/stream.js +64 -0
  47. package/dist/core/async-queue.d.ts +26 -0
  48. package/dist/core/async-queue.js +63 -0
  49. package/dist/core/failure.d.ts +22 -0
  50. package/dist/core/failure.js +53 -0
  51. package/dist/core/index.d.ts +18 -0
  52. package/dist/core/index.js +7 -0
  53. package/dist/core/keyed-turns.d.ts +28 -0
  54. package/dist/core/keyed-turns.js +44 -0
  55. package/dist/core/paths.d.ts +25 -0
  56. package/dist/core/paths.js +77 -0
  57. package/dist/core/refusal.d.ts +43 -0
  58. package/dist/core/refusal.js +399 -0
  59. package/dist/core/result.d.ts +22 -0
  60. package/dist/core/result.js +16 -0
  61. package/dist/core/time.d.ts +19 -0
  62. package/dist/core/time.js +25 -0
  63. package/dist/core/vocab.d.ts +23 -0
  64. package/dist/core/vocab.js +37 -0
  65. package/dist/core/workspace-id.d.ts +12 -0
  66. package/dist/core/workspace-id.js +12 -0
  67. package/dist/gate/command.d.ts +136 -0
  68. package/dist/gate/command.js +551 -0
  69. package/dist/gate/decision.d.ts +111 -0
  70. package/dist/gate/decision.js +97 -0
  71. package/dist/gate/escalate.d.ts +82 -0
  72. package/dist/gate/escalate.js +92 -0
  73. package/dist/gate/gate.d.ts +173 -0
  74. package/dist/gate/gate.js +417 -0
  75. package/dist/gate/index.d.ts +39 -0
  76. package/dist/gate/index.js +31 -0
  77. package/dist/gate/jail.d.ts +92 -0
  78. package/dist/gate/jail.js +131 -0
  79. package/dist/gate/local.d.ts +69 -0
  80. package/dist/gate/local.js +51 -0
  81. package/dist/gate/outcome.d.ts +72 -0
  82. package/dist/gate/outcome.js +91 -0
  83. package/dist/gate/shell.d.ts +36 -0
  84. package/dist/gate/shell.js +226 -0
  85. package/dist/host/agent-process.d.ts +452 -0
  86. package/dist/host/agent-process.js +382 -0
  87. package/dist/host/bulk-post.d.ts +44 -0
  88. package/dist/host/bulk-post.js +114 -0
  89. package/dist/host/claude-transcripts.d.ts +84 -0
  90. package/dist/host/claude-transcripts.js +339 -0
  91. package/dist/host/config-file.d.ts +60 -0
  92. package/dist/host/config-file.js +216 -0
  93. package/dist/host/git-facts.d.ts +7 -0
  94. package/dist/host/git-facts.js +106 -0
  95. package/dist/host/hooks.d.ts +56 -0
  96. package/dist/host/hooks.js +75 -0
  97. package/dist/host/host.d.ts +356 -0
  98. package/dist/host/host.js +1294 -0
  99. package/dist/host/index.d.ts +35 -0
  100. package/dist/host/index.js +22 -0
  101. package/dist/host/link-state-file.d.ts +18 -0
  102. package/dist/host/link-state-file.js +66 -0
  103. package/dist/host/loopback.d.ts +38 -0
  104. package/dist/host/loopback.js +122 -0
  105. package/dist/host/machine.d.ts +9 -0
  106. package/dist/host/machine.js +19 -0
  107. package/dist/host/mcp-server.d.ts +11 -0
  108. package/dist/host/mcp-server.js +48 -0
  109. package/dist/host/package-facts.d.ts +3 -0
  110. package/dist/host/package-facts.js +26 -0
  111. package/dist/host/paired-credential-store.d.ts +32 -0
  112. package/dist/host/paired-credential-store.js +112 -0
  113. package/dist/host/paths.d.ts +80 -0
  114. package/dist/host/paths.js +165 -0
  115. package/dist/host/repository-read.d.ts +33 -0
  116. package/dist/host/repository-read.js +201 -0
  117. package/dist/host/session-store.d.ts +53 -0
  118. package/dist/host/session-store.js +100 -0
  119. package/dist/host/sign-in.d.ts +90 -0
  120. package/dist/host/sign-in.js +239 -0
  121. package/dist/host/telemetry.d.ts +42 -0
  122. package/dist/host/telemetry.js +74 -0
  123. package/dist/host/token-cache.d.ts +62 -0
  124. package/dist/host/token-cache.js +185 -0
  125. package/dist/host/transcript-fs.d.ts +4 -0
  126. package/dist/host/transcript-fs.js +104 -0
  127. package/dist/host/wire-request.d.ts +80 -0
  128. package/dist/host/wire-request.js +196 -0
  129. package/dist/host/workspace-fs.d.ts +14 -0
  130. package/dist/host/workspace-fs.js +84 -0
  131. package/dist/host/workspace-trust.d.ts +23 -0
  132. package/dist/host/workspace-trust.js +80 -0
  133. package/dist/identity/authorize.d.ts +59 -0
  134. package/dist/identity/authorize.js +72 -0
  135. package/dist/identity/config.d.ts +98 -0
  136. package/dist/identity/config.js +130 -0
  137. package/dist/identity/credential.d.ts +91 -0
  138. package/dist/identity/credential.js +76 -0
  139. package/dist/identity/device-code.d.ts +115 -0
  140. package/dist/identity/device-code.js +134 -0
  141. package/dist/identity/index.d.ts +33 -0
  142. package/dist/identity/index.js +10 -0
  143. package/dist/identity/mode.d.ts +106 -0
  144. package/dist/identity/mode.js +69 -0
  145. package/dist/identity/paired-credential.d.ts +52 -0
  146. package/dist/identity/paired-credential.js +43 -0
  147. package/dist/identity/pkce.d.ts +50 -0
  148. package/dist/identity/pkce.js +94 -0
  149. package/dist/identity/store.d.ts +43 -0
  150. package/dist/identity/store.js +43 -0
  151. package/dist/identity/token.d.ts +58 -0
  152. package/dist/identity/token.js +149 -0
  153. package/dist/index.d.ts +45 -0
  154. package/dist/index.js +53 -0
  155. package/dist/mcp/descriptor.d.ts +59 -0
  156. package/dist/mcp/descriptor.js +14 -0
  157. package/dist/mcp/index.d.ts +14 -0
  158. package/dist/mcp/index.js +4 -0
  159. package/dist/mcp/schema.d.ts +37 -0
  160. package/dist/mcp/schema.js +175 -0
  161. package/dist/mcp/server.d.ts +107 -0
  162. package/dist/mcp/server.js +66 -0
  163. package/dist/persistence/entry.d.ts +74 -0
  164. package/dist/persistence/entry.js +105 -0
  165. package/dist/persistence/index.d.ts +22 -0
  166. package/dist/persistence/index.js +8 -0
  167. package/dist/persistence/key.d.ts +46 -0
  168. package/dist/persistence/key.js +33 -0
  169. package/dist/persistence/mirror.d.ts +71 -0
  170. package/dist/persistence/mirror.js +57 -0
  171. package/dist/persistence/receipt.d.ts +111 -0
  172. package/dist/persistence/receipt.js +85 -0
  173. package/dist/persistence/retention.d.ts +68 -0
  174. package/dist/persistence/retention.js +68 -0
  175. package/dist/persistence/store.d.ts +106 -0
  176. package/dist/persistence/store.js +86 -0
  177. package/dist/persistence/transition-log.d.ts +73 -0
  178. package/dist/persistence/transition-log.js +133 -0
  179. package/dist/protocol.d.ts +27 -0
  180. package/dist/protocol.js +12 -0
  181. package/dist/sessions/index.d.ts +8 -0
  182. package/dist/sessions/index.js +4 -0
  183. package/dist/sessions/registry.d.ts +186 -0
  184. package/dist/sessions/registry.js +190 -0
  185. package/dist/sessions/session.d.ts +178 -0
  186. package/dist/sessions/session.js +288 -0
  187. package/dist/sessions/spawn-env.d.ts +88 -0
  188. package/dist/sessions/spawn-env.js +277 -0
  189. package/dist/state/coverage.d.ts +308 -0
  190. package/dist/state/coverage.js +315 -0
  191. package/dist/state/index.d.ts +12 -0
  192. package/dist/state/index.js +7 -0
  193. package/dist/state/machine.d.ts +112 -0
  194. package/dist/state/machine.js +237 -0
  195. package/dist/state/model.d.ts +233 -0
  196. package/dist/state/model.js +214 -0
  197. package/dist/state/observer.d.ts +55 -0
  198. package/dist/state/observer.js +413 -0
  199. package/dist/state/reporter.d.ts +45 -0
  200. package/dist/state/reporter.js +35 -0
  201. package/dist/state/store.d.ts +56 -0
  202. package/dist/state/store.js +120 -0
  203. package/dist/telemetry/index.d.ts +11 -0
  204. package/dist/telemetry/index.js +2 -0
  205. package/dist/telemetry/usage.d.ts +78 -0
  206. package/dist/telemetry/usage.js +69 -0
  207. package/dist/workspace/git-worktree.d.ts +150 -0
  208. package/dist/workspace/git-worktree.js +417 -0
  209. package/dist/workspace/index.d.ts +9 -0
  210. package/dist/workspace/index.js +4 -0
  211. package/dist/workspace/plain-dir.d.ts +34 -0
  212. package/dist/workspace/plain-dir.js +90 -0
  213. package/dist/workspace/provider.d.ts +152 -0
  214. package/dist/workspace/provider.js +2 -0
  215. package/dist/workspace/worktree-porcelain.d.ts +29 -0
  216. package/dist/workspace/worktree-porcelain.js +100 -0
  217. package/docs/architecture.md +277 -0
  218. package/docs/configuration.md +187 -0
  219. package/docs/gate.md +219 -0
  220. package/docs/identity.md +107 -0
  221. package/docs/protocol.md +501 -0
  222. package/docs/state-machine.md +160 -0
  223. package/examples/README.md +141 -0
  224. package/examples/minimal-controller/controller.ts +38 -0
  225. package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
  226. package/examples/parallel-run-proof/run.ts +850 -0
  227. package/examples/test-controller/controller.ts +655 -0
  228. package/examples/test-controller/serve.ts +29 -0
  229. package/examples/tsconfig.json +30 -0
  230. package/package.json +98 -0
@@ -0,0 +1,1180 @@
1
+ /**
2
+ * The wire contract.
3
+ *
4
+ * Naming follows the Agent Client Protocol (agentclientprotocol.com, Zed Industries, Apache-2.0):
5
+ * its method names, camelCase keys and snake_case discriminators. Names and conventions only;
6
+ * every type here is written from scratch and derived from none of its artifacts.
7
+ *
8
+ * Two conventions this file holds to, both of which bite later if broken:
9
+ * - Absent is `null`, never `undefined`. JSON has no `undefined`, so an optional property makes
10
+ * "field omitted" and "field present and unset" indistinguishable across a round trip.
11
+ * - Every discriminator begins with a noun from core/vocab.ts.
12
+ */
13
+ import type { Refusal } from '../core/refusal.js';
14
+ import type { SessionTransition } from '../state/model.js';
15
+ /**
16
+ * Bumped when a change is not backward-compatible. Exchanged in the hello handshake.
17
+ *
18
+ * The rule: a new payload kind, a new member on an existing kind, a member whose nullability
19
+ * changed, or a new hello member is a bump, even when the change is additive in practice. The
20
+ * handshake's job is to let a peer refuse a version it does not know rather than to guess whether
21
+ * a difference happened to be survivable: every declared member is required on the wire, so a
22
+ * peer one version behind refuses the whole frame, and because refusals are not acknowledged that
23
+ * refusal would be a permanent silent wedge on the session lane. The handshake refuses by version
24
+ * instead, by name, once. A new value in the open `capabilities` list is not a bump; a new
25
+ * `RefusalReason` is one, because a strict encoder on the other side must learn it first.
26
+ *
27
+ * Every bump re-approves `contracts/wire-vectors/` (`npm run contracts:update`) and regenerates
28
+ * any consumer's readers.
29
+ */
30
+ export declare const PROTOCOL_VERSION = 9;
31
+ /**
32
+ * The oldest protocol version this build still speaks. A hello advertises the window
33
+ * `[PROTOCOL_VERSION_MIN, PROTOCOL_VERSION]` beside `protocolVersion`; the controller answers with
34
+ * its choice inside the overlap and the host accepts any version in its own window. The window
35
+ * opens at the first negotiated version; from the next bump on it is one minor wide, the version
36
+ * before the current one staying supported for one release. A hello with no range does not
37
+ * decode, so a version older than the first negotiated one cannot be inside the window.
38
+ */
39
+ export declare const PROTOCOL_VERSION_MIN = 9;
40
+ /** The versions a peer speaks, inclusive at both ends. */
41
+ export interface ProtocolRange {
42
+ readonly min: number;
43
+ readonly max: number;
44
+ }
45
+ /**
46
+ * A frame larger than this is refused by the codec.
47
+ *
48
+ * This is the mechanical half of "commands only, never payloads": bulk bytes cannot ride the link
49
+ * even by accident, because a frame carrying them will not encode. The refusal names the bulk lane
50
+ * so the failure teaches the fix rather than just reporting a size.
51
+ */
52
+ export declare const MAX_FRAME_BYTES: number;
53
+ /**
54
+ * What a session update carries. ACP: `session/update`, whose payload is likewise a union.
55
+ *
56
+ * Three members. A later kind of update adds a member here rather than a new payload kind, which
57
+ * keeps `session_update` meaning "a settled fact about this session" (the non-droppable lane)
58
+ * instead of splitting one idea across several discriminators.
59
+ *
60
+ * `WireRefusalUpdate` is the third, and it is a body member rather than a new payload kind for
61
+ * exactly that reason. A host-side refusal that the controller must act on is a settled fact about
62
+ * the session, so it belongs on the non-droppable lane beside a transition. Adding a kind would
63
+ * also have meant a dispatcher change on both ends for a frame that carries no new idea.
64
+ */
65
+ export type SessionUpdateBody = StateTransitionUpdate | AgentMessageUpdate | WireRefusalUpdate;
66
+ /**
67
+ * A declared state transition. The reason this lane is not droppable: losing one is not a repaint
68
+ * that can be redone, it is a lie about what the session did.
69
+ */
70
+ export interface StateTransitionUpdate {
71
+ readonly update: 'state_transition';
72
+ readonly transition: SessionTransition;
73
+ }
74
+ /**
75
+ * One message the agent emitted, carried verbatim.
76
+ *
77
+ * Verbatim is the contract, not a shortcut. The SDK's own message shapes are this package's
78
+ * vocabulary, so forwarding them unchanged means there is exactly one place SDK models become this
79
+ * package's models: `state/observer.ts`, which says so in its first line. Normalizing here would be
80
+ * a second translation site, sitting where drift is hardest to see, and it would not remove the
81
+ * need to track the SDK anyway. A consumer reads `message.type` and gets the SDK's answer.
82
+ *
83
+ * Which messages ride here and which ride the delta lane is declared in `control/stream-routing.ts`,
84
+ * one row per discriminator. It is not a judgement made at the call site.
85
+ */
86
+ export interface AgentMessageUpdate {
87
+ readonly update: 'agent_message';
88
+ /** The SDK message as it arrived. Opaque to this layer; `message.type` is the discriminator. */
89
+ readonly message: JsonObject;
90
+ }
91
+ /**
92
+ * A settled fact about a session. ACP: `session/update`.
93
+ *
94
+ * `body` is structurally open and read through a typed pair, which is a deliberate choice rather
95
+ * than a missing narrowing. Declaring `body: SessionUpdateBody` would reject a newer peer's richer
96
+ * body at the type level while the codec is built to carry unknown keys across intact — the two
97
+ * would disagree about the same field. So the contract lives in `stateTransitionUpdate` and
98
+ * `readStateTransition`: a producer cannot build a malformed one, a consumer cannot read one
99
+ * without checking, and the wire stays tolerant.
100
+ */
101
+ export interface SessionUpdate {
102
+ readonly kind: 'session_update';
103
+ readonly body: JsonObject;
104
+ }
105
+ /** Build the update carrying a transition. The only supported way to put one on the wire. */
106
+ export declare function stateTransitionUpdate(transition: SessionTransition): SessionUpdate;
107
+ /**
108
+ * Read a transition back out, or null when the body is some other kind of update.
109
+ *
110
+ * Null rather than a throw: an older host receiving a body it does not model must keep going, and
111
+ * a consumer that gets null knows to leave the frame alone rather than to treat it as corrupt.
112
+ */
113
+ export declare function readStateTransition(body: JsonObject): SessionTransition | null;
114
+ /** Build the update carrying an agent message. The only supported way to put one on the wire. */
115
+ export declare function agentMessageUpdate(message: JsonObject): SessionUpdate;
116
+ /** Read a forwarded message back out, or null when the body is some other kind of update. */
117
+ export declare function readAgentMessage(body: JsonObject): JsonObject | null;
118
+ /**
119
+ * A refusal the host decided about the controller's own traffic, sent back so the controller can act.
120
+ *
121
+ * The one this exists for is `seq-gap`, and it closes a hole that can only be seen from here.
122
+ * The receiver's `SeqTracker` advances only on an exact `last + 1`, so a frame arriving with a hole
123
+ * in front of it is dropped and `last` does not move; every later frame on that session is then
124
+ * also a gap. There is no NACK, no retransmit request and no resync frame, so without this the only
125
+ * signal is a host-local event: the sender, the single party able to fix it, is told nothing and
126
+ * the lane simply goes quiet forever. A quiet lane and a healthy idle lane look identical.
127
+ *
128
+ * `expected` is not diagnostic; it is the instruction. It is the exact seq the receiver will
129
+ * accept next, so a sender that re-sends from there heals the lane with no further protocol. That is
130
+ * why this carries two numbers instead of a message: a refusal a peer can act on beats one it can
131
+ * only log.
132
+ *
133
+ * It is not a new payload kind, deliberately. See `SessionUpdateBody`.
134
+ */
135
+ export interface WireRefusalUpdate {
136
+ readonly update: 'wire_refusal';
137
+ readonly refusal: WireRefusal;
138
+ /** The seq the receiver will accept next. Re-send from here. */
139
+ readonly expected: number;
140
+ /** What actually arrived, so the size of the hole is legible without arithmetic. */
141
+ readonly received: number;
142
+ }
143
+ /** Build the update carrying a refusal about the peer's own traffic. The only supported way. */
144
+ export declare function wireRefusalUpdate(refusal: WireRefusal, expected: number, received: number): SessionUpdate;
145
+ /**
146
+ * Read a wire refusal back out, or null when the body is some other kind of update.
147
+ *
148
+ * Null rather than a throw, for the same reason `readStateTransition` returns null: a peer that does
149
+ * not model this member must keep going rather than treat the frame as corrupt. A v1 controller
150
+ * reaching this body gets null from all three readers and leaves the frame alone — which is a
151
+ * degraded outcome, not a broken one, and is the honest cost the version bump exists to announce.
152
+ */
153
+ export declare function readWireRefusal(body: JsonObject): WireRefusalUpdate | null;
154
+ /**
155
+ * An incremental fragment: streamed tokens, thinking prose, progress tickers.
156
+ *
157
+ * The one droppable kind. A delta is superseded by the update that settles it, so losing one under
158
+ * pressure costs a repaint and never a fact. Which messages ride here is declared per discriminator
159
+ * in `control/stream-routing.ts`.
160
+ *
161
+ * The notify contract for consumers, which fails silently when broken: a consumer folding these
162
+ * into view state must return a new top-level state reference for every fold that changes
163
+ * anything, and the same reference for a true no-op. UI frameworks commonly bind rendered state
164
+ * through a reference-equality check, so a fold that mutates its state in place and returns the
165
+ * object it was given produces no notification: mid-turn painting stops dead and only resumes when
166
+ * something else happens to replace the reference, typically the turn's own result. There is no
167
+ * error, no dropped frame and nothing null; the frames arrive correctly and the view simply stops
168
+ * moving, which reads as a hung session.
169
+ *
170
+ * This is stated here rather than left to the consumer because it is inherited by whoever renders
171
+ * these frames; the symptom is a whole turn appearing in one lump.
172
+ */
173
+ export interface SessionDelta {
174
+ readonly kind: 'session_delta';
175
+ readonly body: JsonObject;
176
+ }
177
+ /**
178
+ * Build a delta carrying one message verbatim.
179
+ *
180
+ * The body is the same shape a `session_update` carries, deliberately: the lanes differ in what
181
+ * losing a frame costs, not in what a frame says, so `readAgentMessage` reads either. A consumer
182
+ * that had to parse two shapes for one idea would have been handed the drift this package's
183
+ * vocabulary rules exist to prevent.
184
+ */
185
+ export declare function agentMessageDelta(message: JsonObject): SessionDelta;
186
+ /**
187
+ * Start a session. ACP: `session/new`.
188
+ *
189
+ * This payload carries more than `cwd` so that a controller can ask for a session to be resumed,
190
+ * forked, given tools, or told which model to run over the wire, rather than having to bypass the
191
+ * link and call the in-process composer.
192
+ *
193
+ * The test for what belongs here, so the next addition is decided rather than argued: is this a
194
+ * decision about the session (resume this one, fork it, register these tools, use that model), or a
195
+ * capability handle (a store, a spawn function, a hook)? Decisions travel. Handles stay with the
196
+ * code that composes this host; see `SessionNewRequest`'s note on the five that stay refused.
197
+ *
198
+ * Every added field is `T | null` and null means "decide as you did before". A controller that
199
+ * sends only `cwd` gets the previous behaviour, which is what makes a widening safe to land under a
200
+ * host already running.
201
+ */
202
+ export interface SessionNew {
203
+ readonly kind: 'session_new';
204
+ /**
205
+ * Where the session runs. Absolute when stated; null means "no ask".
206
+ *
207
+ * Advisory when the host has a workspace provider: the provider decides, and the session's
208
+ * first `spawning` transition carries `where.cwd`, the directory it actually got. A controller
209
+ * learns where its session is from that transition rather than assuming this value was honoured.
210
+ * Deliberately not echoed a second time anywhere: two names for one fact is how a vocabulary
211
+ * drifts apart, and this file has no `session_started` kind for exactly that reason.
212
+ *
213
+ * Null is "the provider decides", and nothing else. Under a provider it changes nothing;
214
+ * the ask was already ignored. With no provider there is nowhere honest to fall back to (the
215
+ * controller's own cwd is the weakest isolation this host knows, and recreating it as a default
216
+ * would smuggle a policy through an absence), so the host refuses `path-input-missing` by name.
217
+ */
218
+ readonly cwd: string | null;
219
+ /**
220
+ * The key the workspace provider provisions AT, when it should not be the session key.
221
+ *
222
+ * Null is "use the session key", and nothing else: the 1:1 topology exactly as it was, so
223
+ * every existing caller is unchanged. A non-null key is what makes a shared workspace
224
+ * expressible: two sessions naming the same key resolve to the same directory (and, under the git
225
+ * provider, the same branch), which is the one-worktree-per-unit-of-work shape a session key can
226
+ * never spell because it is unique per session by construction.
227
+ *
228
+ * It becomes a git branch component and a directory segment, so the host validates it against
229
+ * the same union-of-two-rules screen the provider applies to a session key (`rejectUnusableId`),
230
+ * before any workspace is claimed; a key that passes a loose guard and dies inside git on every
231
+ * open is the defect class this prevents. With no workspace provider a non-null key is refused by
232
+ * name: there is nothing to provision at any key, and running in the controller's cwd instead
233
+ * would be the silent-wrong-topology failure this field exists to end (the `path-input-missing`
234
+ * posture's mirror image).
235
+ */
236
+ readonly workspaceKey: string | null;
237
+ /**
238
+ * The controller's own meaning handle for this session, carried onto every transition.
239
+ *
240
+ * Never interpreted by the host. It exists so a controller can tie a session back to whatever
241
+ * it means on its side, and the moment the host parsed it, this package would know something about
242
+ * one product and stop being general. See the three-ids warning on `SessionFrame`.
243
+ */
244
+ readonly correlationId: string | null;
245
+ /** Everything else the session is being asked for. Null means every default. */
246
+ readonly request: SessionNewRequest | null;
247
+ /** Per-session gate timings. Null means the host's own defaults. */
248
+ readonly gate: SessionNewGate | null;
249
+ }
250
+ /**
251
+ * The JSON-expressible subset of what a session can be asked for.
252
+ *
253
+ * Five keys of the in-process API are refused here and stay refused: the transcript mirror and
254
+ * its flush mode, the custom process spawner, the stderr callback and the hook registrations. That
255
+ * is a boundary, not an omission, and it is structural rather than enforced: every one of them is an
256
+ * object with methods or a function, so none has a JSON representation and none can cross a wire
257
+ * even in principle. The mirror is the sharpest: it receives every message the agent saw, so a
258
+ * controller able to name one could name a destination for a transcript. Nothing here can.
259
+ *
260
+ * They are described here and spelled out in `host/wire-request.ts`, which is deliberate rather
261
+ * than coy. `pins/persistence-egress.test.ts` scans this file's text for the mirror's key names and
262
+ * fails on a hit, so that a wire field for one cannot be added without the pin firing. Writing the
263
+ * names here, even inside a comment forbidding them, would trip that scan, and a pin loosened to
264
+ * tell prose from code is a pin with a new way to be wrong. The list lives one file over, at the
265
+ * only place that converts a wire request into a local one, and the pin keeps its teeth.
266
+ */
267
+ export interface SessionNewRequest {
268
+ /** A session id to continue. Null starts a new one. */
269
+ readonly resume: string | null;
270
+ /** With `resume`, continue into a NEW session id instead of extending the old one. */
271
+ readonly fork: boolean | null;
272
+ /**
273
+ * Which on-disk settings tiers the agent may load. `[]` means none, which is the host's default.
274
+ *
275
+ * Asking for any tier can be refused before a process exists. A host that grants what its gate
276
+ * approves plus loaded operator settings is two authorities over one question with no stated
277
+ * precedence, so the composer refuses the pair by name (`permission-grant-shadows-settings`). The
278
+ * refusal arrives at start-up, naming the tiers, never as a session that runs with rules silently
279
+ * in conflict.
280
+ *
281
+ * Carried as plain strings: the narrow union lives with the SDK, on the far side of a boundary
282
+ * this file is not allowed to reach, so the host narrows and refuses an unknown value by name.
283
+ */
284
+ readonly settingSources: readonly string[] | null;
285
+ readonly plugins: readonly SessionNewPlugin[] | null;
286
+ /**
287
+ * MCP servers this session may call, by server name.
288
+ *
289
+ * Every tool these add is decided by the same gate as `Bash`: the permission hook registers
290
+ * `PreToolUse` with no matcher, so a tool nobody predicted reaches the same handler. That is why
291
+ * a controller may register tools at all: coverage is by construction, not by enumeration.
292
+ *
293
+ * Carried opaquely because the config union is the SDK's. The host narrows it, and a name that
294
+ * collides with a server the host itself registered is refused rather than silently resolved.
295
+ */
296
+ readonly mcpServers: JsonObject | null;
297
+ /**
298
+ * Ignore every MCP server the host did not declare. Null leaves the host's default, which is true.
299
+ *
300
+ * True is the default and turning it off is a real decision. With it off the agent also loads
301
+ * project `.mcp.json`, user settings, plugin MCP and on-disk agent frontmatter — so a repository
302
+ * a provider checked out becomes a tool-registration channel, in exactly the case where nobody
303
+ * reviewed what is in it.
304
+ */
305
+ readonly strictMcpConfig: boolean | null;
306
+ /** Stream turns as they compose. Null leaves the host's default, which is ON. */
307
+ readonly includePartialMessages: boolean | null;
308
+ /** How much reasoning this session emits. Null leaves the SDK's own default. Opaque here. */
309
+ readonly thinking: JsonObject | null;
310
+ /**
311
+ * The effort level (`low` · `medium` · `high` · `xhigh` · `max`). Null leaves the SDK's own default.
312
+ * A plain string here; the host narrows it and refuses an unknown level by name.
313
+ */
314
+ readonly effort: string | null;
315
+ /**
316
+ * The permission mode (`default` · `acceptEdits` · `bypassPermissions` · `plan` · `dontAsk` · `auto`).
317
+ * Null leaves the SDK's default. This lane is open by design (CLI parity): a session exposes what
318
+ * `claude` exposes. The gate's authority never rides the mode; it rides the `PreToolUse` hook,
319
+ * which the SDK fires under every mode, so the boundary set (push · remote surgery · branch
320
+ * delete · `gh pr merge`) is still held for a decision under bypass. What stays closed:
321
+ * `settings`, `managedSettings`, `allowedTools`, `disallowedTools`, `canUseTool`, `permissions`,
322
+ * `toolAliases`, `permissionPromptToolName`. A mode is a posture the operator chooses in the open;
323
+ * those are rule files and pre-answers nobody can see.
324
+ */
325
+ readonly permissionMode: string | null;
326
+ /** Forward a subagent's whole conversation rather than only its tool calls. Null means OFF. */
327
+ readonly forwardSubagentText: boolean | null;
328
+ /**
329
+ * Extra allow / deny / literal declarations on top of the host's spawn environment.
330
+ *
331
+ * What survives an off-box widening is a short, named list, and it is not "whatever the host
332
+ * refuses". Two facts in `sessions/spawn-env.ts` are true and are worth having: an extra allowed
333
+ * key cannot override a denied one, and the host-session strip runs last, after the literals. What
334
+ * does not follow from them is that a variable the host declines to carry into a child cannot be
335
+ * re-admitted from off-box. It can.
336
+ *
337
+ * The host's posture is an allow-list: everything undeclared is absent, and `extraAllowedKeys`
338
+ * re-admits any of those by name. Further, `extraEnv` is applied after the filter with no allow
339
+ * or deny check at all. That is deliberate, because setting a value is a different act from
340
+ * inheriting one and was designed for an embedder with an author. Carrying it here makes the
341
+ * author a controller instead. So `NODE_OPTIONS`, `PATH`, `ANTHROPIC_BASE_URL` and even
342
+ * `NODE_TLS_REJECT_UNAUTHORIZED` (which the host refuses to inherit by explicit pattern) are all
343
+ * settable through this field.
344
+ *
345
+ * Irrevocable, and exhaustive: the 2 `DENIED_PATTERNS` (only against inheritance, not against
346
+ * `extraEnv`) and the 15 `HOST_SESSION_MARKERS`, which are stripped last and unconditionally. The
347
+ * marker set is the one real guarantee: a spawned agent is never told it is a continuation of the
348
+ * host process, whatever any of these three arrays says.
349
+ *
350
+ * This is disclosure, not an argument against the widening, which is the point of carrying `env`
351
+ * at all. Narrowing it later is a versioned break, so an embedder that wants a floor beneath a
352
+ * controller's declarations needs one built for it; none exists in this package.
353
+ */
354
+ readonly env: SessionNewEnv | null;
355
+ /**
356
+ * Which model runs. Null leaves the CLI's default.
357
+ *
358
+ * This and `systemPrompt` select what the process emits; neither can answer a permission, so
359
+ * carrying them does not widen a security narrowing. The eight `SHADOWING_LANES` that can answer
360
+ * a permission before the gate does are a deliberate narrowing and they stay closed. Two different
361
+ * facts; see `AGENT_SELECTION_OPTION_KEYS` in host/agent-process.ts for the checked version of
362
+ * this sentence.
363
+ */
364
+ readonly model: string | null;
365
+ /**
366
+ * What the agent is told at the start. Null leaves the CLI's own preset.
367
+ *
368
+ * Not provable from the host's report. The SDK's init message carries `model`, `tools`,
369
+ * `mcp_servers`, `skills`, `plugins` and more, and no system prompt, so unlike every other field
370
+ * here, a controller cannot confirm from the host's own report that this took effect. It ships
371
+ * unproven-by-report, stated rather than discovered. Carried opaquely because the SDK accepts a
372
+ * string, a list of strings, or a preset object.
373
+ */
374
+ readonly systemPrompt: JsonValue | null;
375
+ }
376
+ /**
377
+ * Build a `session_new`. The only supported way to put one on the wire.
378
+ *
379
+ * It exists for the reason `stateTransitionUpdate` does, and for one more. The wire's rule is that
380
+ * absent is `null` and never an omitted property, but a hand-written literal makes that a
381
+ * discipline every author has to remember, and the moment one is forgotten the frame carries
382
+ * `undefined`, which `JSON.stringify` deletes. The field then arrives absent, and "absent" and
383
+ * "explicitly unset" are the two states this file spends its opening paragraph refusing to
384
+ * conflate. Passing `asked` as a partial and filling every remaining key with `null` here makes
385
+ * the rule a property of construction instead of a rule in a comment.
386
+ *
387
+ * The argument is a partial of the wire type; what comes out is fully populated. So a caller writes
388
+ * only what it means, and no caller can produce a frame that says nothing where it meant nothing.
389
+ */
390
+ export declare function sessionNew(cwd: string | null, asked?: Partial<Omit<SessionNew, 'kind' | 'cwd'>>): SessionNew;
391
+ /**
392
+ * Build a `session_new.request`. Same argument as `sessionNew`: every unstated key becomes `null`.
393
+ *
394
+ * The partial is the convenience and the full object is the contract. Nothing about the wire is
395
+ * relaxed here: a controller in another language builds the whole object, and this is the shortcut
396
+ * for the one that happens to be written in this one.
397
+ */
398
+ export declare function sessionNewRequest(asked?: Partial<SessionNewRequest>): SessionNewRequest;
399
+ /**
400
+ * Every key a `session_new.request` carries, as data, so a check can enumerate them at run time.
401
+ *
402
+ * It exists so a pin cannot quietly stop covering something. The egress pin walks a populated
403
+ * request looking for a field that could name a transcript destination, and a walk is only as
404
+ * complete as the object it is handed: a hand-built fixture silently stops being full the moment a
405
+ * later change adds a key, and the pin then passes while covering less, with nothing to say so.
406
+ *
407
+ * `satisfies Record<keyof SessionNewRequest, true>` makes the compiler the enforcer: adding a field
408
+ * to the type without adding it here does not build. So the pin's fixture is checked against a list
409
+ * that cannot fall behind the type it describes.
410
+ */
411
+ export declare const SESSION_NEW_REQUEST_KEYS: {
412
+ readonly resume: true;
413
+ readonly fork: true;
414
+ readonly settingSources: true;
415
+ readonly plugins: true;
416
+ readonly mcpServers: true;
417
+ readonly strictMcpConfig: true;
418
+ readonly includePartialMessages: true;
419
+ readonly thinking: true;
420
+ readonly forwardSubagentText: true;
421
+ readonly env: true;
422
+ readonly model: true;
423
+ readonly systemPrompt: true;
424
+ readonly effort: true;
425
+ readonly permissionMode: true;
426
+ };
427
+ /** The same, for the payload itself. Same argument, one level up. */
428
+ export declare const SESSION_NEW_KEYS: {
429
+ readonly kind: true;
430
+ readonly cwd: true;
431
+ readonly workspaceKey: true;
432
+ readonly correlationId: true;
433
+ readonly request: true;
434
+ readonly gate: true;
435
+ };
436
+ /** A plugin the session should load. Mirrors the SDK's shape; the host narrows it. */
437
+ export interface SessionNewPlugin {
438
+ readonly type: string;
439
+ readonly path: string;
440
+ readonly skipMcpDiscovery: boolean | null;
441
+ }
442
+ /** Extra environment declarations for the spawn. See `SessionNewRequest.env`. */
443
+ export interface SessionNewEnv {
444
+ readonly extraAllowedKeys: readonly string[] | null;
445
+ readonly extraDeniedKeys: readonly string[] | null;
446
+ readonly extraEnv: Readonly<Record<string, string>> | null;
447
+ }
448
+ /**
449
+ * Per-session gate timings. Three timeouts, and deliberately nothing else.
450
+ *
451
+ * `grantOnAllow` is not here, and its absence is a decision rather than an oversight. It is a
452
+ * permission posture, not a timing: turned off it yields a gate that can refuse a call and cannot
453
+ * approve one, so a controller able to set it could disable its own session's tools from off-box
454
+ * while every check still reported healthy. Timings decide how long the host waits; this decides
455
+ * whether an answer means anything. Different questions, and only the first travels.
456
+ *
457
+ * The two-deadline invariant is enforced: the host's own deadline must expire before the
458
+ * matcher's, and a pair that inverts it is refused at composition. It is reachable from the wire,
459
+ * which is why the refusal happens before any process exists rather than at the first tool call.
460
+ */
461
+ export interface SessionNewGate {
462
+ readonly decisionTimeoutMs: number | null;
463
+ readonly holdAfterMs: number | null;
464
+ readonly matcherTimeoutSeconds: number | null;
465
+ }
466
+ /** Send a turn into a session. ACP: `session/prompt`. */
467
+ export interface SessionPrompt {
468
+ readonly kind: 'session_prompt';
469
+ readonly text: string;
470
+ }
471
+ /** Interrupt the current turn. ACP: `session/cancel`. */
472
+ export interface SessionCancel {
473
+ readonly kind: 'session_cancel';
474
+ }
475
+ /**
476
+ * Change a running session's model, permission mode or thinking: the SDK's streaming-input setters
477
+ * (`setModel` · `setPermissionMode` · `setMaxThinkingTokens`), reached over the wire. Each member
478
+ * is "not asked" when null; the host applies the asked ones in order and reports a failure by name.
479
+ */
480
+ export interface SessionConfigure {
481
+ readonly kind: 'session_configure';
482
+ /** The model id to switch to. Null = not asked. */
483
+ readonly model: string | null;
484
+ /** The permission mode to switch to — the same vocabulary as `SessionNewRequest.permissionMode`. */
485
+ readonly permissionMode: string | null;
486
+ /** `{type:'adaptive'}` · `{type:'disabled'}` · `{type:'enabled', budgetTokens}` — the SDK's own shapes. */
487
+ readonly thinking: JsonObject | null;
488
+ }
489
+ /**
490
+ * Ask for bulk content. The link carries this locator, never the content.
491
+ *
492
+ * The host answers with an outbound HTTP POST to `postUrl` and reports the outcome back over the
493
+ * link. `bulk` is this package's noun, not ACP's; ACP has no equivalent concept, so nothing is
494
+ * being renamed.
495
+ */
496
+ export interface BulkRequest {
497
+ readonly kind: 'bulk_request';
498
+ readonly deliveryId: string;
499
+ readonly what: string;
500
+ readonly fromOffset: number;
501
+ readonly postUrl: string;
502
+ }
503
+ /**
504
+ * The bytes were POSTed. A receipt, so it is not droppable.
505
+ *
506
+ * `sizeBytes` / `mtimeMs`: the delivered file's stat at the moment it was streamed, so a
507
+ * caller pulling a transcript can detect the CLI rewriting the file under it. The CLI rewrites
508
+ * transcripts on compaction, which makes a byte-offset resume across a rewrite invalid. Both are
509
+ * `T | null` (the wire's optionality rule): null means the deliverer did not read a stat, which is
510
+ * exactly what an older host sends, so a v3 receipt decodes as a v4 receipt with the two unknowns
511
+ * stated rather than invented.
512
+ */
513
+ export interface BulkDelivered {
514
+ readonly kind: 'bulk_delivered';
515
+ readonly deliveryId: string;
516
+ readonly byteCount: number;
517
+ /** The file's total size when the delivery was read. Null when the deliverer did not stat it. */
518
+ readonly sizeBytes: number | null;
519
+ /** The file's mtime (integer epoch ms) when the delivery was read. Null when unknown. */
520
+ readonly mtimeMs: number | null;
521
+ }
522
+ /** Build a `bulk_delivered`. Fills the stat pair with null so an absent value is stated, not deleted. */
523
+ export declare function bulkDelivered(deliveryId: string, byteCount: number, stat?: {
524
+ sizeBytes?: number;
525
+ mtimeMs?: number;
526
+ }): BulkDelivered;
527
+ /** The delivery did not happen, and this says which named way it failed. */
528
+ export interface BulkFailed {
529
+ readonly kind: 'bulk_failed';
530
+ readonly deliveryId: string;
531
+ readonly refusal: WireRefusal;
532
+ }
533
+ /** Ask which sessions this host is running. */
534
+ export interface SessionList {
535
+ readonly kind: 'session_list';
536
+ readonly requestId: string;
537
+ }
538
+ /**
539
+ * One running session, as this host knows it.
540
+ *
541
+ * `sessionKey` is the controller's own routing handle (see the three-ids note on `SessionFrame`),
542
+ * the one id the asker can act on. `sessionId` is the agent's own id, null until the agent has
543
+ * named itself; `state` is the session lifecycle word as the host holds it, carried as a plain
544
+ * string so a newer host's added state survives the crossing.
545
+ */
546
+ export interface SessionListEntry {
547
+ readonly sessionKey: string;
548
+ readonly sessionId: string | null;
549
+ readonly state: string;
550
+ readonly cwd: string | null;
551
+ readonly startedAt: string | null;
552
+ }
553
+ /** The answer: every session behind a handle, plus the registry's own two counts. */
554
+ export interface SessionListResult {
555
+ readonly kind: 'session_list_result';
556
+ readonly requestId: string;
557
+ readonly sessions: readonly SessionListEntry[];
558
+ readonly liveCount: number;
559
+ /** Sessions started but not yet self-named. Counted, not listable: they have no agent id yet. */
560
+ readonly provisioningCount: number;
561
+ }
562
+ /** Ask which Claude sessions exist on this machine, from `fromIndex`, newest first. */
563
+ export interface TranscriptList {
564
+ readonly kind: 'transcript_list';
565
+ readonly requestId: string;
566
+ readonly fromIndex: number;
567
+ }
568
+ /**
569
+ * One transcript on disk: an opaque project slug, the session id, the stat pair, and the working directory
570
+ * the CLI recorded on it — the cwd a resume of this transcript must run in (transcripts live per cwd).
571
+ * Null when the file's head carries none.
572
+ */
573
+ export interface TranscriptListEntry {
574
+ readonly projectSlug: string;
575
+ readonly sessionId: string;
576
+ readonly sizeBytes: number;
577
+ readonly mtimeMs: number;
578
+ readonly cwd: string | null;
579
+ }
580
+ /**
581
+ * One page of the enumeration. Paged because the answer must fit `MAX_FRAME_BYTES`: a machine's
582
+ * corpus reaches hundreds of sessions, and one frame carrying all of them would be refused by the
583
+ * very codec that carries it. `nextIndex` null means this page ends the listing; re-ask from it
584
+ * otherwise. An empty first page is a fresh machine, not an error.
585
+ */
586
+ export interface TranscriptListResult {
587
+ readonly kind: 'transcript_list_result';
588
+ readonly requestId: string;
589
+ readonly entries: readonly TranscriptListEntry[];
590
+ readonly totalCount: number;
591
+ readonly nextIndex: number | null;
592
+ }
593
+ /**
594
+ * Probe one transcript's tail: has a matching user entry landed at or past `fromOffset`?
595
+ *
596
+ * `needle` null means "any user-text entry". The answer carries the file's current size as the
597
+ * offset to resume from, plus the stat pair for rewrite detection.
598
+ */
599
+ export interface TranscriptTail {
600
+ readonly kind: 'transcript_tail';
601
+ readonly requestId: string;
602
+ readonly projectSlug: string;
603
+ readonly sessionId: string;
604
+ readonly fromOffset: number;
605
+ readonly needle: string | null;
606
+ }
607
+ /**
608
+ * The probe's answer. `absent` is a value, not a failure: a transcript that does not exist is a
609
+ * real negative the asker may trust, and it must not wear a refusal's clothes; a refusal here
610
+ * names a malformed request, never a missing file. When `absent` is true the stat pair is null.
611
+ */
612
+ export interface TranscriptTailResult {
613
+ readonly kind: 'transcript_tail_result';
614
+ readonly requestId: string;
615
+ readonly found: boolean;
616
+ readonly absent: boolean;
617
+ /** The file's current size — resume the next probe from here. 0 when absent. */
618
+ readonly newOffset: number;
619
+ readonly sizeBytes: number | null;
620
+ readonly mtimeMs: number | null;
621
+ }
622
+ /**
623
+ * A discovery request that could not be answered, and which named way it failed. The `bulk_failed`
624
+ * model: a request that dies in the host must say so on the wire, because to the asker a swallowed
625
+ * failure and a host that hung are the same silence.
626
+ */
627
+ export interface TranscriptFailed {
628
+ readonly kind: 'transcript_failed';
629
+ readonly requestId: string;
630
+ readonly refusal: WireRefusal;
631
+ }
632
+ /**
633
+ * Ask this host to remove a workspace's directory from disk. Host-scoped, like the discovery
634
+ * asks: the routing key is a channel the controller chose, and no session needs to exist behind it.
635
+ *
636
+ * This is the one production path to `release(remove: true)`, and it is on-demand by design. A
637
+ * session ending does not remove its directory; `ReleaseOptions.remove` defaults false because a
638
+ * session that ended badly is one whose directory somebody wants to look at. Removal is something a
639
+ * caller asks for (a UI control, a PR-close hook), never something that happens because a session
640
+ * ended, so it arrives as its own command rather than as a flag remembered from `session_new`;
641
+ * a flag would spend the decision at session start, where no button and no webhook can ever act.
642
+ *
643
+ * The branch is deleted only when the ask says so (`deleteBranch`), and an unmerged branch only
644
+ * when the ask also says `force`; that is the provider's own rule. Removing a directory is
645
+ * reversible (the commits are still on the branch); deleting a branch is not, so it is opt-in per
646
+ * ask and never a standing setting.
647
+ *
648
+ * A worktree is addressed by its key OR by its path — exactly one. A key is what the controller
649
+ * asked for at `session_new`; a path is what the host observed, and the one name a controller holds
650
+ * for a worktree whose session asked for no key. The host resolves a path to the key of the
651
+ * directory directly under its workspace root, and refuses any other path by name.
652
+ */
653
+ export interface WorkspaceRelease {
654
+ readonly kind: 'workspace_release';
655
+ readonly requestId: string;
656
+ /** The key the workspace was provisioned at, or null when `path` names it. */
657
+ readonly workspaceKey: string | null;
658
+ /** The worktree's absolute path, or null when `workspaceKey` names it. */
659
+ readonly path: string | null;
660
+ /** Delete the branch the worktree is on, after the directory. */
661
+ readonly deleteBranch: boolean;
662
+ /** Delete the branch even when it is not merged into the repository's default branch. */
663
+ readonly force: boolean;
664
+ }
665
+ /**
666
+ * The reap's answer, and every exit is this one kind.
667
+ *
668
+ * `refusal: null` means the workspace was released and its directory removed; a named refusal
669
+ * rides the same kind otherwise: an unusable key, a workspace still backing a live session, a
670
+ * provider failure, a host with no provider at all. One kind for every exit is deliberate rather
671
+ * than the bulk lane's success/failure pair: an ask whose answer dies inside the host is a silent
672
+ * failure on this lane, and a single result kind makes "every exit is a wire answer" a property of
673
+ * the shape instead of a discipline across two builders. It also keeps the answer one small frame,
674
+ * nowhere near the size cap: keys are bounded at `MAX_WORKSPACE_ID_LENGTH` and every refusal echoes
675
+ * at most `keyPreview`, so no key, however degenerate, can inflate this answer toward the cap.
676
+ */
677
+ /**
678
+ * What one release did, entry by entry. The two flags are the receipt: a refusal with
679
+ * `directoryRemoved: true` is a partial (the directory went, the branch stayed), stated rather
680
+ * than hidden. `refusal: null` with both flags false is the idempotent answer — already absent.
681
+ */
682
+ export interface WorkspaceReleaseEntryResult {
683
+ /** The key the ask resolved to; null when it named nothing this host could resolve. */
684
+ readonly workspaceKey: string | null;
685
+ /** The worktree's path as the provider knows it; null when unknown. */
686
+ readonly path: string | null;
687
+ /** A directory existed and is gone. */
688
+ readonly directoryRemoved: boolean;
689
+ readonly branchDeleted: boolean;
690
+ /** Null = released, or already absent. Otherwise the named reason, `branch-not-merged` included. */
691
+ readonly refusal: WireRefusal | null;
692
+ }
693
+ export interface WorkspaceReleaseResult extends WorkspaceReleaseEntryResult {
694
+ readonly kind: 'workspace_release_result';
695
+ readonly requestId: string;
696
+ }
697
+ /** One release in a `workspace_release_bulk`: the single ask's members without the envelope. */
698
+ export interface WorkspaceReleaseEntry {
699
+ readonly workspaceKey: string | null;
700
+ readonly path: string | null;
701
+ readonly deleteBranch: boolean;
702
+ readonly force: boolean;
703
+ }
704
+ /**
705
+ * The most releases one `workspace_release_bulk` may carry: one inventory page, so a controller
706
+ * can clean what it was just shown in one ask, and an answer of that size stays well under the
707
+ * frame cap.
708
+ */
709
+ export declare const MAX_BULK_RELEASES = 25;
710
+ /**
711
+ * Release several worktrees in one ask. Each entry is judged and released on its own; one refusal
712
+ * never aborts the rest, and the answer carries one result per entry in the ask's order. A key
713
+ * named twice in one ask is refused on its second entry.
714
+ */
715
+ export interface WorkspaceReleaseBulk {
716
+ readonly kind: 'workspace_release_bulk';
717
+ readonly requestId: string;
718
+ readonly releases: readonly WorkspaceReleaseEntry[];
719
+ }
720
+ export interface WorkspaceReleaseBulkResult {
721
+ readonly kind: 'workspace_release_bulk_result';
722
+ readonly requestId: string;
723
+ readonly results: readonly WorkspaceReleaseEntryResult[];
724
+ }
725
+ /** The two per-ask flags, both defaulting to the safe side. */
726
+ export interface WorkspaceReleaseFlags {
727
+ readonly deleteBranch?: boolean;
728
+ readonly force?: boolean;
729
+ }
730
+ /** Build a `workspace_release`. A string target is a key; `{ path }` addresses by path. */
731
+ export declare function workspaceRelease(requestId: string, target: string | {
732
+ readonly path: string;
733
+ }, flags?: WorkspaceReleaseFlags): WorkspaceRelease;
734
+ /** Build a `workspace_release_result`. Every omitted member takes the released-or-absent value. */
735
+ export declare function workspaceReleaseResult(requestId: string, outcome?: Partial<WorkspaceReleaseEntryResult>): WorkspaceReleaseResult;
736
+ /** Build one entry result with the released-or-absent defaults. */
737
+ export declare function workspaceReleaseEntryResult(outcome?: Partial<WorkspaceReleaseEntryResult>): WorkspaceReleaseEntryResult;
738
+ /** Build a `workspace_release_bulk`. */
739
+ export declare function workspaceReleaseBulk(requestId: string, releases: readonly WorkspaceReleaseEntry[]): WorkspaceReleaseBulk;
740
+ /** Build a `workspace_release_bulk_result`. */
741
+ export declare function workspaceReleaseBulkResult(requestId: string, results: readonly WorkspaceReleaseEntryResult[]): WorkspaceReleaseBulkResult;
742
+ /**
743
+ * The most entries one `host_configure` may carry: the closed config key set is smaller than this,
744
+ * so a legal ask never meets the bound and an ask past it is malformed rather than large.
745
+ */
746
+ export declare const MAX_CONFIGURE_ENTRIES = 8;
747
+ /**
748
+ * One setting in a `host_configure` ask. `value: null` removes the key from the host's config
749
+ * file; the wire has no `undefined`, and a list of entries carries exactly what was asked, so no
750
+ * sentinel is needed for "not mentioned".
751
+ */
752
+ export interface HostConfigureEntry {
753
+ readonly key: string;
754
+ readonly value: string | null;
755
+ }
756
+ /**
757
+ * Ask this host to change its own configuration. Host-scoped, like the discovery asks and the
758
+ * reap: the routing key is a channel the controller chose.
759
+ *
760
+ * The host validates every entry before it touches disk (the keys it takes over the wire, the
761
+ * shape of each value, the posture the whole set produces), writes its config file through the
762
+ * same module `periscope config` uses, and rebuilds what can be rebuilt live. A change to the
763
+ * workspace roots is refused while any session is live or opening: a session releases through
764
+ * the provider that provisioned it, and swapping roots under one turns that release into a guess.
765
+ * The environment still wins per key; a written value the environment shadows is reported as
766
+ * overridden rather than silently inert.
767
+ */
768
+ export interface HostConfigure {
769
+ readonly kind: 'host_configure';
770
+ readonly requestId: string;
771
+ readonly entries: readonly HostConfigureEntry[];
772
+ }
773
+ /**
774
+ * The configure's answer, and every exit is this one kind.
775
+ *
776
+ * `configuration` is the EFFECTIVE view after the act, refused or not: on a refusal it is the
777
+ * unchanged view, so a controller always learns what the host runs with. `overriddenByEnvironment`
778
+ * names the wire-settable keys the host's environment sets, whose file values are unreachable.
779
+ */
780
+ export interface HostConfigureResult {
781
+ readonly kind: 'host_configure_result';
782
+ readonly requestId: string;
783
+ readonly configuration: HostConfiguration;
784
+ readonly overriddenByEnvironment: readonly string[];
785
+ /** The keys written but not in effect until the next start: the two control-plane URLs. */
786
+ readonly pendingRestart: readonly string[];
787
+ /** Null = every entry applied. Otherwise the named reason nothing was written. */
788
+ readonly refusal: WireRefusal | null;
789
+ }
790
+ /**
791
+ * How many worktrees one `workspace_list_result` carries. A path is bounded at 1000 characters, a
792
+ * branch at 400 and a key at 200, so a full page stays under the frame cap with the same margin
793
+ * the transcript page keeps.
794
+ */
795
+ export declare const WORKSPACE_PAGE_SIZE = 25;
796
+ /**
797
+ * How many entries one `transcript_list_result` carries.
798
+ *
799
+ * Bounded so the frame stays well under `MAX_FRAME_BYTES` (64 KiB): an entry is a slug (measured
800
+ * up to about 80 characters on one real 139-slug corpus), a 36-character session id and two
801
+ * integers, comfortably under 300 bytes of JSON each, so 100 entries is under half the cap even
802
+ * with slugs twice as long as any measured one.
803
+ */
804
+ export declare const TRANSCRIPT_PAGE_SIZE = 100;
805
+ /**
806
+ * The bulk-lane locator namespace for a transcript read: `claude-transcript:{projectSlug}/{sessionId}`.
807
+ * A `bulk_request.what` that a host resolves to the agent CLI's own transcript files starts with
808
+ * this prefix; the slug is the CLI's flattened project directory name and is opaque.
809
+ */
810
+ export declare const TRANSCRIPT_WHAT_PREFIX = "claude-transcript:";
811
+ /**
812
+ * One worktree under the host's workspace root, as git reports it. `key` is the directory's
813
+ * last segment, which is the workspace key the host provisioned it at, so a controller can address
814
+ * it by key; `path` is what the host's transitions carry as `where.worktree`, so a controller can
815
+ * address it by path as well. `merged` says whether the branch's tip is already in the
816
+ * repository's default branch, null when no default branch could be named.
817
+ */
818
+ export interface WorkspaceListEntry {
819
+ readonly key: string;
820
+ readonly path: string;
821
+ readonly branch: string | null;
822
+ readonly head: string | null;
823
+ readonly detached: boolean;
824
+ readonly locked: boolean;
825
+ readonly prunable: boolean;
826
+ readonly merged: boolean | null;
827
+ /**
828
+ * How many commits the branch holds past the default branch (`rev-list --count <default>..<branch>`).
829
+ * Zero with `merged: true` is "nothing here yet", not "merged work"; null when detached or when no
830
+ * default branch could be named.
831
+ */
832
+ readonly aheadCount: number | null;
833
+ readonly lastCommitAt: string | null;
834
+ }
835
+ /**
836
+ * Ask this host what worktrees exist under its workspace root, from disk. Host-scoped and paged
837
+ * like `transcript_list`. Only what the host provisioned is listed: the repository itself and any
838
+ * checkout outside the workspace root never appear, so no cleanup a controller composes from this
839
+ * answer can name them.
840
+ */
841
+ export interface WorkspaceList {
842
+ readonly kind: 'workspace_list';
843
+ readonly requestId: string;
844
+ readonly fromIndex: number;
845
+ }
846
+ /** The inventory's answer; every exit is this one kind. `refusal` non-null = the host could not look. */
847
+ export interface WorkspaceListResult {
848
+ readonly kind: 'workspace_list_result';
849
+ readonly requestId: string;
850
+ readonly entries: readonly WorkspaceListEntry[];
851
+ readonly totalCount: number;
852
+ readonly nextIndex: number | null;
853
+ readonly defaultBranch: string | null;
854
+ readonly refusal: WireRefusal | null;
855
+ }
856
+ /** Build a `workspace_list`. The only supported way to put one on the wire. */
857
+ export declare function workspaceList(requestId: string, fromIndex?: number): WorkspaceList;
858
+ /** Build a `workspace_list_result`. Fills `refusal` with null, the answered page. */
859
+ export declare function workspaceListResult(requestId: string, entries: readonly WorkspaceListEntry[], page: {
860
+ readonly totalCount: number;
861
+ readonly nextIndex?: number;
862
+ readonly defaultBranch?: string | null;
863
+ }, refusal?: WireRefusal): WorkspaceListResult;
864
+ /** Build a `host_configure`. The only supported way to put one on the wire. */
865
+ export declare function hostConfigure(requestId: string, entries: readonly HostConfigureEntry[]): HostConfigure;
866
+ /** Build a `host_configure_result`. Fills `refusal` with null, the applied answer. */
867
+ export declare function hostConfigureResult(requestId: string, configuration: HostConfiguration, overriddenByEnvironment: readonly string[], refusal?: WireRefusal, pendingRestart?: readonly string[]): HostConfigureResult;
868
+ /** Build a `session_list`. The only supported way to put one on the wire. */
869
+ export declare function sessionList(requestId: string): SessionList;
870
+ /** Build a `session_list_result`. */
871
+ export declare function sessionListResult(requestId: string, sessions: readonly SessionListEntry[], counts: {
872
+ liveCount: number;
873
+ provisioningCount: number;
874
+ }): SessionListResult;
875
+ /** Build a `transcript_list`. `fromIndex` defaults to the start. */
876
+ export declare function transcriptList(requestId: string, fromIndex?: number): TranscriptList;
877
+ /** Build a `transcript_list_result`. Fills `nextIndex` with null so absence is stated, not deleted. */
878
+ export declare function transcriptListResult(requestId: string, entries: readonly TranscriptListEntry[], totals: {
879
+ totalCount: number;
880
+ nextIndex?: number;
881
+ }): TranscriptListResult;
882
+ /** Build a `transcript_tail`. Fills `needle` with null, meaning "any user-text entry". */
883
+ export declare function transcriptTail(requestId: string, projectSlug: string, sessionId: string, fromOffset: number, needle?: string): TranscriptTail;
884
+ /** Build a `transcript_tail_result`. Fills the stat pair with null so absence is stated. */
885
+ export declare function transcriptTailResult(requestId: string, answer: {
886
+ found: boolean;
887
+ absent: boolean;
888
+ newOffset: number;
889
+ sizeBytes?: number;
890
+ mtimeMs?: number;
891
+ }): TranscriptTailResult;
892
+ /** Build a `transcript_failed`. */
893
+ export declare function transcriptFailed(requestId: string, refusal: WireRefusal): TranscriptFailed;
894
+ /**
895
+ * How many entries one `repository_list_result` carries. A directory with more than this many
896
+ * children is listed to the cap and marked `truncated`; names are bounded by the filesystem, so a
897
+ * full page stays well under the frame cap.
898
+ */
899
+ export declare const MAX_REPOSITORY_ENTRIES = 500;
900
+ /**
901
+ * The most text one `repository_read_result` carries. The frame cap is 64 KiB and the envelope plus
902
+ * JSON escaping must fit inside it, so the text is bounded below the cap with margin; a longer file
903
+ * is answered to this many bytes and marked `truncated`.
904
+ */
905
+ export declare const MAX_REPOSITORY_READ_BYTES: number;
906
+ /**
907
+ * Ask this host to list one directory of its repository, by a path relative to the repository root
908
+ * (`''` is the root itself). Host-scoped like `transcript_list`. The host answers names only and
909
+ * never leaves the root: a path that resolves outside it is refused by name, so a controller can
910
+ * show a checkout's shape and pick a file to read without the host ever serving another directory.
911
+ */
912
+ export interface RepositoryList {
913
+ readonly kind: 'repository_list';
914
+ readonly requestId: string;
915
+ readonly path: string;
916
+ }
917
+ /** One child of a listed directory: its name, whether it is a directory, and its stat pair. */
918
+ export interface RepositoryEntry {
919
+ readonly name: string;
920
+ readonly directory: boolean;
921
+ readonly sizeBytes: number;
922
+ readonly mtimeMs: number;
923
+ }
924
+ /** The listing's answer; every exit is this one kind. `refusal` non-null = nothing was listed. */
925
+ export interface RepositoryListResult {
926
+ readonly kind: 'repository_list_result';
927
+ readonly requestId: string;
928
+ /** Sorted by name, at most `MAX_REPOSITORY_ENTRIES`. */
929
+ readonly entries: readonly RepositoryEntry[];
930
+ /** True when the directory held more than the cap. */
931
+ readonly truncated: boolean;
932
+ readonly refusal: WireRefusal | null;
933
+ }
934
+ /**
935
+ * Ask this host for the text of one file of its repository, by a path relative to the repository
936
+ * root, at most `maxBytes` of it (1 to `MAX_REPOSITORY_READ_BYTES`). Text files only: a file with a
937
+ * NUL byte in its head is refused rather than served, because the answer is a string.
938
+ */
939
+ export interface RepositoryRead {
940
+ readonly kind: 'repository_read';
941
+ readonly requestId: string;
942
+ readonly path: string;
943
+ readonly maxBytes: number;
944
+ }
945
+ /** The read's answer; every exit is this one kind. `text` is null exactly when `refusal` is not. */
946
+ export interface RepositoryReadResult {
947
+ readonly kind: 'repository_read_result';
948
+ readonly requestId: string;
949
+ /** UTF-8 text, the first `maxBytes` of the file at most. */
950
+ readonly text: string | null;
951
+ /** The file's whole size, so a caller can see how much `truncated` left behind. */
952
+ readonly sizeBytes: number;
953
+ readonly truncated: boolean;
954
+ readonly refusal: WireRefusal | null;
955
+ }
956
+ /** Build a `repository_list`. The only supported way to put one on the wire. */
957
+ export declare function repositoryList(requestId: string, path?: string): RepositoryList;
958
+ /** Build a `repository_list_result`. Fills `refusal` with null, the listed answer. */
959
+ export declare function repositoryListResult(requestId: string, entries: readonly RepositoryEntry[], truncated?: boolean, refusal?: WireRefusal): RepositoryListResult;
960
+ /** Build a `repository_read`. Fills `maxBytes` with the cap. */
961
+ export declare function repositoryRead(requestId: string, path: string, maxBytes?: number): RepositoryRead;
962
+ /** Build a `repository_read_result`. Fills `refusal` with null, the read answer. */
963
+ export declare function repositoryReadResult(requestId: string, read: {
964
+ readonly text: string | null;
965
+ readonly sizeBytes: number;
966
+ readonly truncated: boolean;
967
+ }, refusal?: WireRefusal): RepositoryReadResult;
968
+ /**
969
+ * A refusal as it arrives from the wire: `reason` is a plain string, not the closed enum.
970
+ *
971
+ * Strict out, tolerant in, and the asymmetry is deliberate. Nothing this host writes can
972
+ * carry an undeclared reason: `encode` validates it on the way out, so the guarantee is enforced at
973
+ * the one place a frame becomes bytes rather than resting on a type a cast defeats. But a frame
974
+ * arriving with a reason this build has never seen is a newer peer, not a corrupt frame, and
975
+ * rejecting it would destroy a delivery receipt (the outcome, the delivery id, the detail) over a
976
+ * word. `codec.ts`'s own header already promises "unknown fields survive… an older host relaying a
977
+ * frame does not quietly destroy information it did not understand"; a closed enum value is that
978
+ * same information one level in.
979
+ *
980
+ * Read it with `readRefusal`. Same shape and same argument as `SessionUpdate.body` above: a
981
+ * producer cannot build a malformed one, a consumer cannot read one without checking, and the wire
982
+ * stays tolerant.
983
+ */
984
+ export interface WireRefusal {
985
+ /** A `RefusalReason` when this build knows it; any string when the peer is newer. */
986
+ readonly reason: string;
987
+ readonly detail: string;
988
+ }
989
+ /**
990
+ * A refusal read off the wire: recognised and narrowed, or unrecognised with the raw value kept.
991
+ *
992
+ * The raw string travels. An unrecognised reason is never mapped onto a known one and never
993
+ * dropped: a consumer that discards what it did not understand makes a newer peer's failure
994
+ * invisible on the only side that could have reported it. This is the gate's unknown-decision rule
995
+ * in the transport: an unrecognised value is a named outcome, not a silent conversion.
996
+ */
997
+ export type ReadRefusal = {
998
+ readonly recognised: true;
999
+ readonly refusal: Refusal;
1000
+ } | {
1001
+ readonly recognised: false;
1002
+ readonly raw: string;
1003
+ readonly detail: string;
1004
+ };
1005
+ export declare function readRefusal(wire: WireRefusal): ReadRefusal;
1006
+ export type SessionPayload = SessionUpdate | SessionDelta | SessionNew | SessionPrompt | SessionCancel | SessionConfigure | BulkRequest | BulkDelivered | BulkFailed | SessionList | SessionListResult | TranscriptList | TranscriptListResult | TranscriptTail | TranscriptTailResult | TranscriptFailed | WorkspaceRelease | WorkspaceReleaseResult | WorkspaceReleaseBulk | WorkspaceReleaseBulkResult | HostConfigure | HostConfigureResult | WorkspaceList | WorkspaceListResult | RepositoryList | RepositoryListResult | RepositoryRead | RepositoryReadResult;
1007
+ export type SessionPayloadKind = SessionPayload['kind'];
1008
+ /**
1009
+ * What may be dropped when the offline queue is full, stated as data so the decision lives in one
1010
+ * place instead of inside a queue method. Everything absent from this list is a transition or a
1011
+ * receipt, and losing one of those is a lie about what happened.
1012
+ */
1013
+ export declare const DROPPABLE_KINDS: readonly SessionPayloadKind[];
1014
+ export declare function isDroppable(kind: SessionPayloadKind): boolean;
1015
+ /**
1016
+ * The longest value `HostConfiguration` carries. Paths and URLs are host-controlled text on a
1017
+ * frame the controller must accept before anything else, so each is bounded here rather than
1018
+ * trusted; a controller stores them under the same cap.
1019
+ */
1020
+ export declare const MAX_CONFIGURATION_VALUE_LENGTH = 1000;
1021
+ /**
1022
+ * How this host is configured, as values: the read half of `periscope config`.
1023
+ *
1024
+ * The `workspace:*` capability markers say which mode a host is in; these say what it is pointed
1025
+ * at. Every member is `T | null` (absent is null, never undefined), and null means the setting is
1026
+ * not set on the host, not that the host declined to say. `controllerUrl` is what the host dialled
1027
+ * to deliver this frame, reported so a controller can show it beside the rest; nothing here is an
1028
+ * ask, so nothing here can be refused.
1029
+ */
1030
+ export interface HostConfiguration {
1031
+ readonly repositoryRoot: string | null;
1032
+ readonly workspaceRoot: string | null;
1033
+ readonly branchScheme: string | null;
1034
+ /** Where the agent keeps its transcripts: derived from `agentHome`. Reported, never set on its own. */
1035
+ readonly transcriptsRoot: string | null;
1036
+ /**
1037
+ * The controller link URL the host's configuration names. Settable over the link; the value
1038
+ * in effect is the one dialled at start, so a change rides `pendingRestart` until the next start.
1039
+ */
1040
+ readonly controllerUrl: string | null;
1041
+ /** The decision endpoint the host's configuration names. Settable over the link, same rule. */
1042
+ readonly decisionUrl: string | null;
1043
+ /** Where the agent CLI keeps its state. Settable over the link; the transcripts root derives from it. */
1044
+ readonly agentHome: string | null;
1045
+ }
1046
+ /** A `HostConfiguration` with nothing set: what a host composed without one reports. */
1047
+ export declare function unsetHostConfiguration(): HostConfiguration;
1048
+ /** The host's opening frame. Capability negotiation at hello, borrowed from ACP. */
1049
+ export interface LinkHello {
1050
+ readonly kind: 'link_hello';
1051
+ readonly protocolVersion: number;
1052
+ readonly hostId: string;
1053
+ readonly capabilities: readonly string[];
1054
+ /** What the host already holds per session, so the controller knows what can be replayed. */
1055
+ readonly cursors: readonly SessionCursor[];
1056
+ /** The values this host runs with. Reported, never negotiated. */
1057
+ readonly configuration: HostConfiguration;
1058
+ /**
1059
+ * The keys whose configured value is not the one in effect: written to the file, dialled
1060
+ * only at the next start. Empty on a host that runs what its file says.
1061
+ */
1062
+ readonly pendingRestart: readonly string[];
1063
+ /**
1064
+ * The versions this host speaks. `protocolVersion` stays the newest of them; the controller
1065
+ * answers with its choice inside the overlap of the two windows.
1066
+ */
1067
+ readonly protocolRange: ProtocolRange;
1068
+ }
1069
+ /**
1070
+ * The controller's answer. `protocolVersion` is the version the controller chose inside the overlap
1071
+ * of the two windows; the host accepts any version in its own window and refuses the rest, naming
1072
+ * both windows.
1073
+ */
1074
+ export interface LinkWelcome {
1075
+ readonly kind: 'link_welcome';
1076
+ readonly protocolVersion: number;
1077
+ readonly capabilities: readonly string[];
1078
+ /** The last `seq` the controller received per session — the host replays past these. */
1079
+ readonly cursors: readonly SessionCursor[];
1080
+ }
1081
+ export interface SessionCursor {
1082
+ readonly sessionId: string;
1083
+ readonly seq: number;
1084
+ }
1085
+ /**
1086
+ * What the controller has durably received, per session.
1087
+ *
1088
+ * Without this the host cannot know when a frame is safe to forget, so it would either discard on
1089
+ * write — losing anything in flight when a socket dies — or retain forever. The ack is what makes
1090
+ * the retention window finite AND replay complete.
1091
+ */
1092
+ export interface LinkAck {
1093
+ readonly kind: 'link_ack';
1094
+ readonly cursors: readonly SessionCursor[];
1095
+ }
1096
+ /** Heartbeat, both directions. A half-open socket answers neither. */
1097
+ export interface LinkPing {
1098
+ readonly kind: 'link_ping';
1099
+ readonly nonce: string;
1100
+ }
1101
+ export interface LinkPong {
1102
+ readonly kind: 'link_pong';
1103
+ readonly nonce: string;
1104
+ }
1105
+ /** A deliberate close, carrying why. A silent disconnect is the thing this exists to distinguish. */
1106
+ export interface LinkBye {
1107
+ readonly kind: 'link_bye';
1108
+ readonly cause: string;
1109
+ }
1110
+ export type ControlPayload = LinkHello | LinkWelcome | LinkAck | LinkPing | LinkPong | LinkBye;
1111
+ export type ControlPayloadKind = ControlPayload['kind'];
1112
+ /**
1113
+ * A sequenced, replayable frame belonging to one session.
1114
+ *
1115
+ * `seq` is monotonic per session per direction, from 1, dense (+1 per frame). Dense is what makes
1116
+ * a gap detectable by arithmetic alone: the receiver's expected next is always `last + 1`.
1117
+ *
1118
+ * A seq is minted only when its frame is first written to the wire. Anything refused or dropped
1119
+ * before that moment never had one, so designed loss (an oversized payload, a discard or a
1120
+ * displacement under pressure) cannot put a hole in the numbering. That is what lets "dense" be
1121
+ * a construction rather than a hope.
1122
+ *
1123
+ * ---------------------------------------------------------------------------------------------
1124
+ * There are three session ids in this package and they are not interchangeable. Conflating any
1125
+ * pair produces a trace that lies, so all three are named here, once:
1126
+ *
1127
+ * `SessionFrame.sessionId` The routing and ordering key. The controller chooses it, and
1128
+ * it must, because `session_new` addresses a session that does
1129
+ * not exist yet. It is what `seq` is dense per, and it is
1130
+ * stable for the session's whole life. The host maps and orders
1131
+ * by it; interpreting it is this field's job.
1132
+ *
1133
+ * `SessionTransition.sessionId` The agent's own id, and a fact rather than a key. The agent
1134
+ * mints it at `system/init`; it is `null` before that and never
1135
+ * null again. The host carries it and never invents one.
1136
+ *
1137
+ * `SessionTransition.correlationId` The controller's meaning handle, and opaque. Never
1138
+ * interpreted here; see its own comment in state/model.ts.
1139
+ *
1140
+ * A controller will very likely pass the same string as the routing key and the correlation id.
1141
+ * That is fine and expected. The host must still never assume it, derive one from the other, or
1142
+ * fall back from one to the other: they differ in contract even when they coincide in value. One
1143
+ * is interpreted by construction, the other is forbidden to be.
1144
+ *
1145
+ * The consequence that makes this load-bearing rather than tidy: a session's first transitions are
1146
+ * recorded before the agent has named itself (`UserPromptSubmit` fires ahead of `system/init`), and
1147
+ * a session that dies in start-up never names itself at all. Keying frames by the agent's id would
1148
+ * make exactly those frames unsendable, and they are the ones that explain the failure.
1149
+ * ---------------------------------------------------------------------------------------------
1150
+ */
1151
+ export interface SessionFrame {
1152
+ readonly frame: 'session';
1153
+ readonly sessionId: string;
1154
+ readonly seq: number;
1155
+ readonly at: string;
1156
+ readonly payload: SessionPayload;
1157
+ }
1158
+ /** An unsequenced frame about the link itself. */
1159
+ export interface ControlFrame {
1160
+ readonly frame: 'control';
1161
+ readonly at: string;
1162
+ readonly payload: ControlPayload;
1163
+ }
1164
+ export type Frame = SessionFrame | ControlFrame;
1165
+ export declare function isSessionFrame(frame: Frame): frame is SessionFrame;
1166
+ export declare function isControlFrame(frame: Frame): frame is ControlFrame;
1167
+ /**
1168
+ * A frame's identity, and the reason `seq` is shaped this way: this string is exactly what an SSE
1169
+ * `Last-Event-ID` header carries. Moving to one stream per session later needs no change to the
1170
+ * frame; the cursor set decomposes into per-stream ids one for one.
1171
+ */
1172
+ export declare function frameId(frame: SessionFrame): string;
1173
+ /** JSON, as a type. Bodies are parsed but not interpreted here. */
1174
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
1175
+ [key: string]: JsonValue;
1176
+ };
1177
+ export type JsonObject = {
1178
+ [key: string]: JsonValue;
1179
+ };
1180
+ //# sourceMappingURL=frames.d.ts.map