@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,73 @@
1
+ /**
2
+ * The durable transition log — the only place a gate decision is stored durably.
3
+ *
4
+ * Read this before adding any cleanup, prune or tidy-up. `state/store.ts` retains transitions in
5
+ * a bounded in-memory ring that actively drops its oldest once a session outruns the window, and
6
+ * its own header says durability belongs here. Every recorded transition also reaches the link —
7
+ * `forwardSession` in `control/stream.ts` subscribes to the machine and forwards whichever lane
8
+ * produced it — but the link is a stream, not a record: once a frame has been sent, nothing in
9
+ * this package can read it back. So every local refusal this host decides — a path-jail denial, a
10
+ * boundary command, a credential-path read, an unconvertible tool descriptor — is held in-process
11
+ * only by a ring that will eventually overwrite it. This file is what makes such a decision
12
+ * readable afterwards. A tidy-up here does not lose a log line; it loses the only stored evidence
13
+ * that the package's headline feature ever ran.
14
+ *
15
+ * Append-only, and an abandoned entry is marked rather than erased. "This session sat in
16
+ * `tool:Bash` for forty minutes" is the most useful sentence this system can produce, and a
17
+ * reconciler that quietly closed the entry would destroy exactly that. So `markAbandoned` writes a
18
+ * new record carrying the reason; it never rewrites or removes the one that is already stored.
19
+ *
20
+ * `cause` survives the round trip or the read refuses. A transition read back without the event
21
+ * that produced it would leave a reader with a state change and no way to know what caused it,
22
+ * which is exactly the inference the declared model exists to eliminate. So the encoder writes all
23
+ * three parts of a cause and the decoder refuses an entry that lost any of them, rather than
24
+ * substituting a plausible default.
25
+ */
26
+ import type { OpenEntry, SessionTransition } from '../state/model.js';
27
+ import type { Result } from '../core/result.js';
28
+ import type { TranscriptEntry } from './entry.js';
29
+ /**
30
+ * The entry `type` a stored transition carries.
31
+ *
32
+ * Namespaced so it cannot collide with a transcript entry. These records may share a store with
33
+ * mirrored transcript lines, whose `type` values are the CLI's. A prefix nothing in that vocabulary
34
+ * uses keeps the two readable side by side and keeps a reader of either from mistaking one for the
35
+ * other.
36
+ */
37
+ export declare const TRANSITION_ENTRY_TYPE = "periscope.transition";
38
+ /** The entry `type` an abandonment mark carries. A record in its own right, never an edit. */
39
+ export declare const ABANDONED_ENTRY_TYPE = "periscope.entry_abandoned";
40
+ /** Encode one transition as a storable entry. Lossless for everything a reader branches on. */
41
+ export declare function encodeTransition(transition: SessionTransition): TranscriptEntry;
42
+ /**
43
+ * Read a stored transition back.
44
+ *
45
+ * A lost or unrecognised cause is a refusal. Both halves are checked independently — the same way
46
+ * the machine validates them — because a cause that survived as a shape but not as a declared value
47
+ * is a record a reader would branch on wrongly.
48
+ */
49
+ export declare function decodeTransition(entry: TranscriptEntry): Result<SessionTransition>;
50
+ /**
51
+ * A record that an open entry was abandoned.
52
+ *
53
+ * This is an append, not an edit, and that is the whole design. The entry it refers to stays
54
+ * exactly as it was written, with the time it opened and the cause that opened it. A reader
55
+ * assembling the two sees "opened at X, still open at Y, marked abandoned because Z" — which is the
56
+ * signal. Rewriting the original would leave "closed", which is the signal's opposite and is
57
+ * indistinguishable from an ordinary completion.
58
+ */
59
+ export declare function markAbandoned(entry: OpenEntry, at: string, reason: string): TranscriptEntry;
60
+ /** Every stored transition in an entry list, in stored order. Non-transition entries are ignored. */
61
+ export declare function transitionsIn(entries: readonly TranscriptEntry[]): Result<SessionTransition[]>;
62
+ /** Every abandonment mark in an entry list, by the entry id it refers to. */
63
+ export declare function abandonmentsIn(entries: readonly TranscriptEntry[]): Map<string, TranscriptEntry>;
64
+ /**
65
+ * A stable uuid for one transition, so a re-append is deduplicated rather than doubled.
66
+ *
67
+ * Derived, not minted. The adapter contract treats `uuid` as an idempotency key, and a mirror
68
+ * retries. A random id would make every retry a new row, so the same transition written twice must
69
+ * carry the same id — and a session's `seq` is dense from 1 per machine, which makes the pair
70
+ * unique without inventing anything.
71
+ */
72
+ export declare function transitionUuid(transition: SessionTransition): string;
73
+ //# sourceMappingURL=transition-log.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { isCauseEvent, isCauseKind } from '../state/model.js';
2
+ import { ok, refuse } from '../core/result.js';
3
+ /**
4
+ * The entry `type` a stored transition carries.
5
+ *
6
+ * Namespaced so it cannot collide with a transcript entry. These records may share a store with
7
+ * mirrored transcript lines, whose `type` values are the CLI's. A prefix nothing in that vocabulary
8
+ * uses keeps the two readable side by side and keeps a reader of either from mistaking one for the
9
+ * other.
10
+ */
11
+ export const TRANSITION_ENTRY_TYPE = 'periscope.transition';
12
+ /** The entry `type` an abandonment mark carries. A record in its own right, never an edit. */
13
+ export const ABANDONED_ENTRY_TYPE = 'periscope.entry_abandoned';
14
+ /** Encode one transition as a storable entry. Lossless for everything a reader branches on. */
15
+ export function encodeTransition(transition) {
16
+ return {
17
+ type: TRANSITION_ENTRY_TYPE,
18
+ uuid: transitionUuid(transition),
19
+ timestamp: transition.at,
20
+ sessionId: transition.sessionId,
21
+ seq: transition.seq,
22
+ from: transition.from,
23
+ to: transition.to,
24
+ activity: transition.activity,
25
+ entryId: transition.entryId,
26
+ cause: { kind: transition.cause.kind, event: transition.cause.event, detail: transition.cause.detail },
27
+ where: transition.where,
28
+ correlationId: transition.correlationId,
29
+ };
30
+ }
31
+ /**
32
+ * Read a stored transition back.
33
+ *
34
+ * A lost or unrecognised cause is a refusal. Both halves are checked independently — the same way
35
+ * the machine validates them — because a cause that survived as a shape but not as a declared value
36
+ * is a record a reader would branch on wrongly.
37
+ */
38
+ export function decodeTransition(entry) {
39
+ if (entry.type !== TRANSITION_ENTRY_TYPE) {
40
+ return refuse('transcript-entry-malformed', `not a transition entry: type ${entry.type}`);
41
+ }
42
+ const cause = entry['cause'];
43
+ if (cause === null || typeof cause !== 'object' || Array.isArray(cause)) {
44
+ return refuse('transcript-entry-malformed', 'the transition carries no cause');
45
+ }
46
+ const { kind, event, detail } = cause;
47
+ if (typeof kind !== 'string' || !isCauseKind(kind)) {
48
+ return refuse('transcript-entry-malformed', `the cause kind is not a declared kind: ${String(kind)}`);
49
+ }
50
+ if (typeof event !== 'string' || !isCauseEvent(event)) {
51
+ return refuse('transcript-entry-malformed', `the cause event is not a declared event: ${String(event)}`);
52
+ }
53
+ const seq = entry['seq'];
54
+ if (typeof seq !== 'number' || !Number.isFinite(seq)) {
55
+ return refuse('transcript-entry-malformed', 'the transition carries no seq');
56
+ }
57
+ const where = entry['where'];
58
+ if (where === null || typeof where !== 'object') {
59
+ return refuse('transcript-entry-malformed', 'the transition carries no where');
60
+ }
61
+ return ok({
62
+ sessionId: entry['sessionId'] ?? null,
63
+ seq,
64
+ at: typeof entry.timestamp === 'string' ? entry.timestamp : '',
65
+ from: entry['from'],
66
+ to: entry['to'],
67
+ activity: entry['activity'] ?? null,
68
+ entryId: entry['entryId'] ?? null,
69
+ cause: { kind, event, detail: typeof detail === 'string' ? detail : '' },
70
+ where: where,
71
+ correlationId: entry['correlationId'] ?? null,
72
+ });
73
+ }
74
+ /**
75
+ * A record that an open entry was abandoned.
76
+ *
77
+ * This is an append, not an edit, and that is the whole design. The entry it refers to stays
78
+ * exactly as it was written, with the time it opened and the cause that opened it. A reader
79
+ * assembling the two sees "opened at X, still open at Y, marked abandoned because Z" — which is the
80
+ * signal. Rewriting the original would leave "closed", which is the signal's opposite and is
81
+ * indistinguishable from an ordinary completion.
82
+ */
83
+ export function markAbandoned(entry, at, reason) {
84
+ return {
85
+ type: ABANDONED_ENTRY_TYPE,
86
+ uuid: `abandoned:${entry.entryId}:${at}`,
87
+ timestamp: at,
88
+ entryId: entry.entryId,
89
+ activity: entry.activity,
90
+ lane: entry.lane,
91
+ openedAt: entry.openedAt,
92
+ agentId: entry.agentId,
93
+ reason,
94
+ cause: { kind: entry.cause.kind, event: entry.cause.event, detail: entry.cause.detail },
95
+ };
96
+ }
97
+ /** Every stored transition in an entry list, in stored order. Non-transition entries are ignored. */
98
+ export function transitionsIn(entries) {
99
+ const found = [];
100
+ for (const entry of entries) {
101
+ if (entry.type !== TRANSITION_ENTRY_TYPE)
102
+ continue;
103
+ const decoded = decodeTransition(entry);
104
+ if (!decoded.ok)
105
+ return refuse(decoded.refusal.reason, decoded.refusal.detail);
106
+ found.push(decoded.value);
107
+ }
108
+ return ok(found);
109
+ }
110
+ /** Every abandonment mark in an entry list, by the entry id it refers to. */
111
+ export function abandonmentsIn(entries) {
112
+ const marks = new Map();
113
+ for (const entry of entries) {
114
+ if (entry.type !== ABANDONED_ENTRY_TYPE)
115
+ continue;
116
+ const entryId = entry['entryId'];
117
+ if (typeof entryId === 'string')
118
+ marks.set(entryId, entry);
119
+ }
120
+ return marks;
121
+ }
122
+ /**
123
+ * A stable uuid for one transition, so a re-append is deduplicated rather than doubled.
124
+ *
125
+ * Derived, not minted. The adapter contract treats `uuid` as an idempotency key, and a mirror
126
+ * retries. A random id would make every retry a new row, so the same transition written twice must
127
+ * carry the same id — and a session's `seq` is dense from 1 per machine, which makes the pair
128
+ * unique without inventing anything.
129
+ */
130
+ export function transitionUuid(transition) {
131
+ return `transition:${transition.sessionId ?? 'unidentified'}:${transition.correlationId ?? '-'}:${transition.seq}`;
132
+ }
133
+ //# sourceMappingURL=transition-log.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The wire contract, on its own subpath — `@naswerks/periscope/protocol`.
3
+ *
4
+ * This is the only import path into the package that structurally cannot reach `src/host/`, so a
5
+ * controller written against it cannot transitively acquire `node:fs`, `node:child_process` or
6
+ * `node:os`, and does not pull the Agent SDK. That turns the package's blast-radius claim into
7
+ * something a consumer can verify at the package boundary instead of taking on trust from the
8
+ * package's own test suite. Pinned by src/pins/protocol-closure.test.ts.
9
+ *
10
+ * The condition that keeps it true: `exports` in package.json has exactly these two entries and no
11
+ * catch-all `"./*"`. A third subpath re-opens the boundary and is a decision, not a refactor.
12
+ */
13
+ export type { AgentMessageUpdate, BulkDelivered, BulkFailed, BulkRequest, ControlFrame, ControlPayload, ControlPayloadKind, Frame, HostConfiguration, HostConfigure, HostConfigureEntry, HostConfigureResult, JsonObject, JsonValue, LinkAck, LinkBye, LinkHello, LinkPing, LinkPong, LinkWelcome, ProtocolRange, ReadRefusal, SessionCancel, SessionConfigure, SessionCursor, SessionDelta, SessionFrame, SessionList, SessionListEntry, SessionListResult, SessionNew, SessionNewEnv, SessionNewGate, SessionNewPlugin, SessionNewRequest, SessionPayload, SessionPayloadKind, SessionPrompt, SessionUpdate, SessionUpdateBody, StateTransitionUpdate, TranscriptFailed, TranscriptList, TranscriptListEntry, TranscriptListResult, TranscriptTail, TranscriptTailResult, WireRefusal, WireRefusalUpdate, RepositoryEntry, RepositoryList, RepositoryListResult, RepositoryRead, RepositoryReadResult, WorkspaceList, WorkspaceListEntry, WorkspaceListResult, WorkspaceRelease, WorkspaceReleaseBulk, WorkspaceReleaseBulkResult, WorkspaceReleaseEntry, WorkspaceReleaseEntryResult, WorkspaceReleaseFlags, WorkspaceReleaseResult, } from './control/frames.js';
14
+ export type { ActivityKind, AgedEntry, CauseEvent, CauseKind, EntryLane, HookEventName, OpenEntry, SessionActivity, SessionSnapshot, SessionState, SessionTransition, TransitionCause, TransitionWhere, } from './state/model.js';
15
+ export { ACTIVITY_KINDS, CAUSE_KINDS, HOOK_EVENTS, HOST_ACTIVITY_KINDS, SDK_ACTIVITY_KINDS, SESSION_STATES, formatActivity, isCauseEvent, isCauseKind, sameActivity, } from './state/model.js';
16
+ export { DROPPABLE_KINDS, MAX_CONFIGURATION_VALUE_LENGTH, MAX_BULK_RELEASES, MAX_CONFIGURE_ENTRIES, MAX_FRAME_BYTES, MAX_REPOSITORY_ENTRIES, MAX_REPOSITORY_READ_BYTES, PROTOCOL_VERSION, PROTOCOL_VERSION_MIN, TRANSCRIPT_PAGE_SIZE, TRANSCRIPT_WHAT_PREFIX, WORKSPACE_PAGE_SIZE, agentMessageDelta, agentMessageUpdate, bulkDelivered, frameId, hostConfigure, hostConfigureResult, isControlFrame, isDroppable, isSessionFrame, readAgentMessage, readRefusal, readStateTransition, readWireRefusal, repositoryList, repositoryListResult, repositoryRead, repositoryReadResult, sessionList, sessionListResult, sessionNew, sessionNewRequest, stateTransitionUpdate, transcriptFailed, transcriptList, transcriptListResult, transcriptTail, transcriptTailResult, unsetHostConfiguration, wireRefusalUpdate, workspaceList, workspaceListResult, workspaceRelease, workspaceReleaseBulk, workspaceReleaseBulkResult, workspaceReleaseEntryResult, workspaceReleaseResult, } from './control/frames.js';
17
+ export { decode, encode } from './control/codec.js';
18
+ export type { InboundCheck } from './control/seq.js';
19
+ export { SeqTracker } from './control/seq.js';
20
+ export type { Refusal, RefusalReason } from './core/refusal.js';
21
+ export { REFUSAL_REASONS, isRefusalReason } from './core/refusal.js';
22
+ export type { Result } from './core/result.js';
23
+ export type { Decider, Decision, DecisionReading, DecisionRequest } from './gate/decision.js';
24
+ export { describeRaw, readDecision } from './gate/decision.js';
25
+ export type { EscalationOptions, EscalationResponse, EscalationTransport } from './gate/escalate.js';
26
+ export type { Authorization, ControllerCredential } from './control/credential.js';
27
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1,12 @@
1
+ export { ACTIVITY_KINDS, CAUSE_KINDS, HOOK_EVENTS, HOST_ACTIVITY_KINDS, SDK_ACTIVITY_KINDS, SESSION_STATES, formatActivity, isCauseEvent, isCauseKind, sameActivity, } from './state/model.js';
2
+ export { DROPPABLE_KINDS, MAX_CONFIGURATION_VALUE_LENGTH, MAX_BULK_RELEASES, MAX_CONFIGURE_ENTRIES, MAX_FRAME_BYTES, MAX_REPOSITORY_ENTRIES, MAX_REPOSITORY_READ_BYTES, PROTOCOL_VERSION, PROTOCOL_VERSION_MIN, TRANSCRIPT_PAGE_SIZE, TRANSCRIPT_WHAT_PREFIX, WORKSPACE_PAGE_SIZE, agentMessageDelta, agentMessageUpdate, bulkDelivered, frameId, hostConfigure, hostConfigureResult, isControlFrame, isDroppable, isSessionFrame, readAgentMessage, readRefusal, readStateTransition, readWireRefusal, repositoryList, repositoryListResult, repositoryRead, repositoryReadResult, sessionList, sessionListResult, sessionNew, sessionNewRequest, stateTransitionUpdate, transcriptFailed, transcriptList, transcriptListResult, transcriptTail, transcriptTailResult, unsetHostConfiguration, wireRefusalUpdate, workspaceList, workspaceListResult, workspaceRelease, workspaceReleaseBulk, workspaceReleaseBulkResult, workspaceReleaseEntryResult, workspaceReleaseResult, } from './control/frames.js';
3
+ // `control/stream-routing.ts` is not exported here, deliberately. It is keyed off the SDK's own
4
+ // message union, so it reaches `host/` and would drag the privileged module into this subpath's
5
+ // closure — the one thing this file exists to prevent. What a wire consumer needs is already here:
6
+ // `DROPPABLE_KINDS` and `isDroppable` say which payload kinds may be lost. The routing table is the
7
+ // host's emission policy, and it ships from the main barrel.
8
+ export { decode, encode } from './control/codec.js';
9
+ export { SeqTracker } from './control/seq.js';
10
+ export { REFUSAL_REASONS, isRefusalReason } from './core/refusal.js';
11
+ export { describeRaw, readDecision } from './gate/decision.js';
12
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,8 @@
1
+ /** The agent session lifecycle and the registry of live sessions. */
2
+ export type { EnvSource, SpawnEnvPolicy } from './spawn-env.js';
3
+ export { DECLARED_EXACT_KEYS, DECLARED_PREFIXES, DECLARED_SUFFIXES, STRIPPED_HOST_SESSION_KEYS, composeSpawnEnv, isDeclaredSpawnEnvKey, redactProxyCredential, } from './spawn-env.js';
4
+ export type { HostedSessionFacts, SessionDegrade, SessionDegradeListener, SessionEndCause, SessionEndListener, SessionEnded, SessionLifecycle, SessionListener, Unsubscribe, } from './session.js';
5
+ export { SESSION_END_CAUSES, HostedSession } from './session.js';
6
+ export type { SessionRegistryOptions, SessionRequest } from './registry.js';
7
+ export { SessionRegistry } from './registry.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { DECLARED_EXACT_KEYS, DECLARED_PREFIXES, DECLARED_SUFFIXES, STRIPPED_HOST_SESSION_KEYS, composeSpawnEnv, isDeclaredSpawnEnvKey, redactProxyCredential, } from './spawn-env.js';
2
+ export { SESSION_END_CAUSES, HostedSession } from './session.js';
3
+ export { SessionRegistry } from './registry.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The live sessions this host is running, and the only thing that starts or ends one.
3
+ *
4
+ * Instance state on a class, never module globals, and that is the whole reason this is a class
5
+ * at all. A module-level map would make the package a singleton for its process: two embedders in
6
+ * one host — a test beside a running host, two controllers, one library used twice — would share a
7
+ * session table and end each other's work. Every piece of per-session state here is a private
8
+ * field, and a test runs two registries side by side to prove they cannot see each other.
9
+ *
10
+ * The ownership model in one line: this object owns lifetime, everything else borrows a handle.
11
+ * `create` mints, `get` borrows, `stop`/`stopAll` end. A session removes itself from here the
12
+ * moment it ends, whichever way it ended, so "in the registry" and "alive" are the same fact rather
13
+ * than two that can drift.
14
+ */
15
+ import type { AgentSystemPrompt, HookRegistrations, McpServerConfig, SdkPluginConfig, SessionStore, SessionStoreFlush, SettingSource, SpawnAgentProcess, ThinkingConfig, EffortLevel, PermissionMode } from '../host/agent-process.js';
16
+ import type { WorkspaceTrust } from '../host/workspace-trust.js';
17
+ import type { Clock } from '../core/time.js';
18
+ import type { Result } from '../core/result.js';
19
+ import type { SpawnEnvPolicy } from './spawn-env.js';
20
+ import { HostedSession } from './session.js';
21
+ /** What a caller asks for when starting a session. */
22
+ export interface SessionRequest {
23
+ /** Absolute. Refused otherwise — a relative cwd means "relative to something you cannot see". */
24
+ readonly cwd: string;
25
+ /**
26
+ * Which on-disk settings tiers the agent may load. Defaults to none.
27
+ *
28
+ * Reading no settings files is the default because it is the only setting under which the host
29
+ * can state what an agent's permissions are. Loading the project tier also brings the trust
30
+ * problem back: its rules are silently void in a workspace nobody accepted. A caller that wants
31
+ * project instructions loaded must ask, and gets told what it costs.
32
+ */
33
+ readonly settingSources?: readonly SettingSource[];
34
+ readonly plugins?: readonly SdkPluginConfig[];
35
+ /** Hook callbacks for this session. Carried to the process untouched — see host/hooks.ts. */
36
+ readonly hooks?: HookRegistrations;
37
+ /** Extra allow/deny/literal declarations on top of the general spawn environment. */
38
+ readonly env?: SpawnEnvPolicy;
39
+ /** A session id to continue. The controller decides when; the host only carries it out. */
40
+ readonly resume?: string;
41
+ /** With `resume`, continue into a new id instead of extending the old session. */
42
+ readonly fork?: boolean;
43
+ /**
44
+ * Stream this session's turns as they compose. Defaults to on.
45
+ *
46
+ * On because the incremental lane is droppable by construction — a delta never enters the replay
47
+ * ring or the durable store, so what it costs under pressure is a repaint. Off, a turn can only
48
+ * be rendered after it is over, which is a different product.
49
+ */
50
+ readonly includePartialMessages?: boolean;
51
+ /**
52
+ * How much reasoning this session emits. Defaults to the SDK's own default (deltas fire, prose
53
+ * is empty), so summarized prose is something a caller asks for.
54
+ *
55
+ * Why the default is not "on when someone is watching": that rule cannot be written in this
56
+ * package. Whether a run is watched is a judgement about what the session is for, and this host
57
+ * has no fact that distinguishes one — inventing one would be a name earned by observation, which
58
+ * is the failure the declared model exists to end. The caller knows; the host offers the knob.
59
+ * The asymmetry in the defaults carries the intent instead: the cheap half is on, and the half
60
+ * that costs tokens on the wire and puts reasoning text into transcripts and mirrors is opt-in.
61
+ */
62
+ readonly thinking?: ThinkingConfig;
63
+ /** Forward a subagent's whole conversation rather than only its tool calls. Defaults to off. */
64
+ readonly forwardSubagentText?: boolean;
65
+ /**
66
+ * In-process MCP servers this session may call, by server name. Build them with `mcp/`.
67
+ *
68
+ * Every tool these add is decided by the same gate as `Bash` — `PreToolUse` is registered with
69
+ * no matcher, so coverage does not depend on the host recognising the tool. What the host's own
70
+ * gate does with them is a different question: see `gate/local.ts`, which matches on tool name and
71
+ * therefore has no opinion about an `mcp__…` tool unless the embedder names it in `ToolFamilies`.
72
+ */
73
+ readonly mcpServers?: Readonly<Record<string, McpServerConfig>>;
74
+ /**
75
+ * Ignore every MCP server this host did not declare. Defaults to true.
76
+ *
77
+ * On by default, and this is the one place a workspace's contents could otherwise decide what
78
+ * tools exist. Without it the agent also loads project `.mcp.json`, user settings, plugin MCP and
79
+ * on-disk agent frontmatter — so a repository the host checked out into a workspace can register
80
+ * its own servers, and a `WorkspaceProvider` that clones untrusted code becomes a tool-registration
81
+ * channel. The host provisions those directories programmatically, which is exactly the case where
82
+ * nobody reviewed what is in them.
83
+ *
84
+ * Turning it off is legitimate and costs something named. A consumer whose own repository
85
+ * declares the servers it wants needs this off, and forcing them to fork the package would be
86
+ * worse. But the cost is not only a wider tool surface: an in-process server cannot fail to
87
+ * connect, while the stdio and HTTP servers this re-admits can — and this package deliberately
88
+ * does not expose the SDK's `mcpServerStatus()` / `reconnectMcpServer()`, because reaching them
89
+ * means handing out the query object whose narrowing is a pinned security property. So with strict
90
+ * off, a failed server is neither detectable nor recoverable through this package. Strict on is
91
+ * what makes that absence harmless.
92
+ */
93
+ readonly strictMcpConfig?: boolean;
94
+ /**
95
+ * Mirror this session's transcript to a store. Defaults to none.
96
+ *
97
+ * The store receives every message the agent saw, so supplying one is an egress decision and it
98
+ * is deliberately the embedder's — this type is the local API, not the wire. Nothing arriving over
99
+ * the link can reach this field: a store is an object with methods and has no JSON form, so a
100
+ * controller cannot name a destination for a transcript even in principle. Pinned by
101
+ * pins/persistence-egress.test.ts.
102
+ *
103
+ * Local disk remains the primary record whatever is set here. The subprocess writes locally
104
+ * first and the mirror runs after that write succeeds, so a store can legitimately lag — and when
105
+ * a batch is dropped it stays behind, silently. See host/session-store.ts on reading that report.
106
+ */
107
+ readonly sessionStore?: SessionStore;
108
+ /** How eagerly the mirror flushes. Defaults to the SDK's batching. Ignored with no store. */
109
+ readonly sessionStoreFlush?: SessionStoreFlush;
110
+ /** Create the process yourself — a VM, a container, a remote machine. See agent-process.ts. */
111
+ readonly spawn?: SpawnAgentProcess;
112
+ /**
113
+ * Which model runs this session. Defaults to the CLI's own.
114
+ *
115
+ * Absent, not narrowed — this and `systemPrompt` were simply not composable before, so they fill
116
+ * a gap rather than widen the permission narrowing. See `AGENT_SELECTION_OPTION_KEYS`.
117
+ *
118
+ * Whether it took effect is provable: the agent reports `model` on its own `system/init`, so a
119
+ * caller reads it back off `HostedSession`'s facts rather than trusting the request.
120
+ */
121
+ readonly model?: string;
122
+ /**
123
+ * What the agent is told at the start. Defaults to the CLI's own preset.
124
+ *
125
+ * This one is not provable the same way. The init message carries no system prompt, so nothing
126
+ * the agent says back confirms it. Asking for it is a decision made blind, by construction.
127
+ */
128
+ readonly systemPrompt?: AgentSystemPrompt;
129
+ /** The effort level. Defaults to the SDK's own. */
130
+ readonly effort?: EffortLevel;
131
+ /** The permission mode. Defaults to the SDK's own. See `CLI_PARITY_OPTION_KEYS`. */
132
+ readonly permissionMode?: PermissionMode;
133
+ }
134
+ export interface SessionRegistryOptions {
135
+ /** The environment sessions are filtered from. The composition root passes `process.env`. */
136
+ readonly baseEnv: Readonly<Record<string, string | undefined>>;
137
+ /** Absolute path to the user's home, for the trust read. */
138
+ readonly homeDir: string;
139
+ readonly clock?: Clock;
140
+ /** How long the agent has to report itself before the start is called failed. */
141
+ readonly startTimeoutMs?: number;
142
+ }
143
+ export declare class SessionRegistry {
144
+ #private;
145
+ constructor(options: SessionRegistryOptions);
146
+ /** Every live session. A copy — a caller iterating this cannot be surprised by one ending. */
147
+ list(): HostedSession[];
148
+ get liveCount(): number;
149
+ /** Sessions that have started but not yet reported themselves. Separate because they have no id. */
150
+ get provisioningCount(): number;
151
+ get(sessionId: string): Result<HostedSession>;
152
+ /**
153
+ * Start a session. Returns as soon as the process exists — synchronously, and without an id.
154
+ *
155
+ * Why there is no id yet, and why this is not an oversight. The agent reports itself only once
156
+ * a turn has been queued; before that it emits nothing at all, for as long as you care to wait
157
+ * (observed: 45 seconds of silence with no turn, 2.5 seconds with one). So a `create` that
158
+ * waited for the id would deadlock against the `prompt` that causes it.
159
+ *
160
+ * The shape a caller wants is therefore: create, prompt, then `whenLive()` if it needs the id, the
161
+ * version receipt or the tool list. Until then the session is in `provisioning` — held by this
162
+ * registry, counted by `provisioningCount`, stopped by `stopAll`, but not yet reachable by id
163
+ * because there is no id to reach it by.
164
+ */
165
+ create(request: SessionRequest): Result<HostedSession>;
166
+ /**
167
+ * Start a session, send its first turn, and wait until it has reported itself.
168
+ *
169
+ * The ordinary shape, offered as one call because getting it wrong deadlocks: the turn must be
170
+ * queued before the wait, since it is what makes the agent initialize.
171
+ */
172
+ open(request: SessionRequest & {
173
+ readonly prompt: string;
174
+ }): Promise<Result<HostedSession>>;
175
+ /** End one session. Refuses an id this registry does not hold rather than succeeding quietly. */
176
+ stop(sessionId: string, detail?: string): Result<void>;
177
+ /**
178
+ * End everything this registry holds, including sessions still provisioning.
179
+ *
180
+ * Provisioning sessions are included deliberately: they hold a real process even though they have
181
+ * no id yet, and a shutdown that only walked the keyed map would leave those running.
182
+ */
183
+ stopAll(detail?: string): void;
184
+ }
185
+ export type { WorkspaceTrust };
186
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,190 @@
1
+ import { startAgentProcess } from '../host/agent-process.js';
2
+ import { isUntrustedWorkspaceWarning, readWorkspaceTrust, trustConfigPath } from '../host/workspace-trust.js';
3
+ import { systemClock } from '../core/time.js';
4
+ import { ok, refuse } from '../core/result.js';
5
+ import { requireAbsolute } from '../core/paths.js';
6
+ import { composeSpawnEnv } from './spawn-env.js';
7
+ import { HostedSession } from './session.js';
8
+ /**
9
+ * 60 seconds. The process is a large native binary starting cold, and a loaded host has been
10
+ * observed holding a trivial invocation of it past ten. Too short turns a slow machine into a
11
+ * spurious outage; there is no upper bound that is too generous for a case that otherwise hangs.
12
+ */
13
+ const DEFAULT_START_TIMEOUT_MS = 60_000;
14
+ export class SessionRegistry {
15
+ #live = new Map();
16
+ #provisioning = new Set();
17
+ #baseEnv;
18
+ #homeDir;
19
+ #clock;
20
+ #startTimeoutMs;
21
+ #startProcess;
22
+ constructor(options) {
23
+ this.#baseEnv = options.baseEnv;
24
+ this.#homeDir = options.homeDir;
25
+ this.#clock = options.clock ?? systemClock;
26
+ this.#startTimeoutMs = options.startTimeoutMs ?? DEFAULT_START_TIMEOUT_MS;
27
+ this.#startProcess = options.startProcess ?? startAgentProcess;
28
+ }
29
+ /** Every live session. A copy — a caller iterating this cannot be surprised by one ending. */
30
+ list() {
31
+ return [...this.#live.values()];
32
+ }
33
+ get liveCount() {
34
+ return this.#live.size;
35
+ }
36
+ /** Sessions that have started but not yet reported themselves. Separate because they have no id. */
37
+ get provisioningCount() {
38
+ return this.#provisioning.size;
39
+ }
40
+ get(sessionId) {
41
+ const session = this.#live.get(sessionId);
42
+ if (session === undefined) {
43
+ return refuse('session-unknown', `no live session ${sessionId} in this registry`);
44
+ }
45
+ return ok(session);
46
+ }
47
+ /**
48
+ * Start a session. Returns as soon as the process exists — synchronously, and without an id.
49
+ *
50
+ * Why there is no id yet, and why this is not an oversight. The agent reports itself only once
51
+ * a turn has been queued; before that it emits nothing at all, for as long as you care to wait
52
+ * (observed: 45 seconds of silence with no turn, 2.5 seconds with one). So a `create` that
53
+ * waited for the id would deadlock against the `prompt` that causes it.
54
+ *
55
+ * The shape a caller wants is therefore: create, prompt, then `whenLive()` if it needs the id, the
56
+ * version receipt or the tool list. Until then the session is in `provisioning` — held by this
57
+ * registry, counted by `provisioningCount`, stopped by `stopAll`, but not yet reachable by id
58
+ * because there is no id to reach it by.
59
+ */
60
+ create(request) {
61
+ const cwd = requireAbsolute(request.cwd);
62
+ if (!cwd.ok)
63
+ return refuse(cwd.refusal.reason, cwd.refusal.detail);
64
+ const settingSources = request.settingSources ?? [];
65
+ const trust = readWorkspaceTrust(trustConfigPath(this.#homeDir), cwd.value);
66
+ const session = new HostedSession(this.#startProcess({
67
+ cwd: cwd.value,
68
+ env: composeSpawnEnv(this.#baseEnv, request.env ?? {}),
69
+ settingSources,
70
+ plugins: request.plugins ?? null,
71
+ hooks: request.hooks ?? null,
72
+ resume: request.resume ?? null,
73
+ fork: request.fork ?? false,
74
+ // The two defaults that carry the streaming policy are these two lines. See SessionRequest
75
+ // for why the rule is stated as an asymmetry rather than as attendedness.
76
+ includePartialMessages: request.includePartialMessages ?? true,
77
+ thinking: request.thinking ?? null,
78
+ forwardSubagentText: request.forwardSubagentText ?? false,
79
+ mcpServers: request.mcpServers ?? null,
80
+ // The default is strict, and it is stated rather than left to the SDK. Omitting the key
81
+ // lets a provisioned workspace's own files register servers — see SessionRequest.
82
+ strictMcpConfig: request.strictMcpConfig ?? true,
83
+ // No default destination, and there must never be one: a host that mirrored somewhere by
84
+ // default would be sending transcripts off-box without anyone choosing to.
85
+ sessionStore: request.sessionStore ?? null,
86
+ sessionStoreFlush: request.sessionStoreFlush ?? null,
87
+ spawn: request.spawn ?? null,
88
+ // No default for either: naming a model here would make this package's own choice look like
89
+ // the CLI's, and the CLI's default is the one an operator can actually see and change.
90
+ model: request.model ?? null,
91
+ systemPrompt: request.systemPrompt ?? null,
92
+ effort: request.effort ?? null,
93
+ permissionMode: request.permissionMode ?? null,
94
+ // The untrusted-workspace condition is reported here and nowhere else in the SDK, so the
95
+ // callback is wired on every start rather than only when it is expected.
96
+ onStderr: (data) => {
97
+ if (isUntrustedWorkspaceWarning(data)) {
98
+ session.degrade('workspace_untrusted', data.trim());
99
+ }
100
+ },
101
+ }), this.#clock, cwd.value, trust, (live) => this.#adopt(live), (finished) => this.#release(finished));
102
+ this.#provisioning.add(session);
103
+ // Stated before the first message, because a caller that asked for settings files in a
104
+ // workspace nobody trusted has already lost its rules by the time anything runs — and the only
105
+ // other notice is a line on stderr.
106
+ if (settingSources.length > 0 && trust !== 'trusted') {
107
+ session.degrade('workspace_untrusted', `settings sources ${settingSources.join(', ')} were requested but this workspace is ` +
108
+ `${trust}: permission rules from settings files will be ignored`);
109
+ }
110
+ return ok(session);
111
+ }
112
+ /**
113
+ * Start a session, send its first turn, and wait until it has reported itself.
114
+ *
115
+ * The ordinary shape, offered as one call because getting it wrong deadlocks: the turn must be
116
+ * queued before the wait, since it is what makes the agent initialize.
117
+ */
118
+ async open(request) {
119
+ const created = this.create(request);
120
+ if (!created.ok)
121
+ return created;
122
+ const sent = created.value.prompt(request.prompt);
123
+ if (!sent.ok) {
124
+ created.value.stop('the first turn could not be queued');
125
+ return refuse(sent.refusal.reason, sent.refusal.detail);
126
+ }
127
+ const live = await created.value.whenLive(this.#startTimeoutMs);
128
+ if (!live.ok)
129
+ return refuse(live.refusal.reason, live.refusal.detail);
130
+ return ok(created.value);
131
+ }
132
+ /** End one session. Refuses an id this registry does not hold rather than succeeding quietly. */
133
+ stop(sessionId, detail = 'stop requested') {
134
+ const session = this.#live.get(sessionId);
135
+ if (session === undefined) {
136
+ return refuse('session-unknown', `no live session ${sessionId} in this registry`);
137
+ }
138
+ session.stop(detail);
139
+ return ok(undefined);
140
+ }
141
+ /**
142
+ * End everything this registry holds, including sessions still provisioning.
143
+ *
144
+ * Provisioning sessions are included deliberately: they hold a real process even though they have
145
+ * no id yet, and a shutdown that only walked the keyed map would leave those running.
146
+ */
147
+ stopAll(detail = 'host shutting down') {
148
+ for (const session of [...this.#live.values(), ...this.#provisioning]) {
149
+ session.stop(detail);
150
+ }
151
+ }
152
+ /**
153
+ * The agent has named itself, so the session becomes reachable by that id and stops provisioning.
154
+ *
155
+ * An id already held is not overwritten, and the reason is an observed SDK property: a resume
156
+ * without `fork` keeps the same session id. So resuming a session this registry is already
157
+ * running produces two live handles claiming one key. Overwriting would make the older one
158
+ * untracked-but-alive — invisible to `list`, unreachable by `get`, missed by `stopAll`, still
159
+ * holding a process — and then its eventual end would evict the newer session's entry, so a live
160
+ * session would become unreachable because a different one finished. Both losses are silent, and
161
+ * both falsify this file's own rule that "in the registry" and "alive" are the same fact.
162
+ *
163
+ * The newcomer is refused rather than the incumbent evicted: the incumbent is the one already
164
+ * being observed, and a degrade names the collision on the session that is about to be dropped —
165
+ * whose caller is the one that can do something about it.
166
+ */
167
+ #adopt(session) {
168
+ const id = session.facts?.id;
169
+ if (id === undefined)
170
+ return;
171
+ this.#provisioning.delete(session);
172
+ const incumbent = this.#live.get(id);
173
+ if (incumbent !== undefined && incumbent !== session) {
174
+ session.degrade('session_id_collision', `the agent reported session id ${id}, which this registry already holds — a resume without ` +
175
+ `fork keeps the original id. This session stays unregistered; stop the one that holds the ` +
176
+ `id, or resume with fork so the agent mints a new one`);
177
+ session.stop(`session id ${id} is already held by a live session in this registry`);
178
+ return;
179
+ }
180
+ this.#live.set(id, session);
181
+ }
182
+ /** Only the holder of an id may release it — a colliding session must not evict the incumbent. */
183
+ #release(session) {
184
+ this.#provisioning.delete(session);
185
+ const id = session.facts?.id;
186
+ if (id !== undefined && this.#live.get(id) === session)
187
+ this.#live.delete(id);
188
+ }
189
+ }
190
+ //# sourceMappingURL=registry.js.map