@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,237 @@
1
+ import { ok, refuse } from '../core/result.js';
2
+ import { isCauseEvent, isCauseKind } from './model.js';
3
+ /**
4
+ * The state a machine holds before anything has been recorded.
5
+ *
6
+ * A machine is constructed at the moment a process is asked for, and the first record is the
7
+ * `spawning` one. Until then `from` has to be something, and `spawning` is the honest answer: the
8
+ * caller has already decided to start a session.
9
+ */
10
+ const INITIAL_STATE = 'spawning';
11
+ export class SessionStateMachine {
12
+ /** Only until the first record. After that `where` is derived like everything else. */
13
+ #initialWhere;
14
+ #clock;
15
+ #ticker;
16
+ #correlationId;
17
+ #entries = new Map();
18
+ #listeners = new Set();
19
+ #rejectionListeners = new Set();
20
+ /** The last recorded transition. The only mutable state, assigned in exactly one place. */
21
+ #current = null;
22
+ #seq = 0;
23
+ #rejectedCount = 0;
24
+ constructor(options) {
25
+ this.#initialWhere = options.where;
26
+ this.#clock = options.clock;
27
+ this.#ticker = options.ticker;
28
+ this.#correlationId = options.correlationId ?? null;
29
+ }
30
+ /** Derived from the last transition. There is no field to set. */
31
+ get state() {
32
+ return this.#current?.to ?? INITIAL_STATE;
33
+ }
34
+ /** Derived from the open-entry set. There is no field to set. */
35
+ get activity() {
36
+ return foregroundActivity(this.#entries);
37
+ }
38
+ /** Null until the agent reports itself; carried forward after. There is no field to set. */
39
+ get sessionId() {
40
+ return this.#current?.sessionId ?? null;
41
+ }
42
+ get correlationId() {
43
+ return this.#correlationId;
44
+ }
45
+ /** Derived from the last transition. There is no field to set. */
46
+ get where() {
47
+ return this.#current?.where ?? this.#initialWhere;
48
+ }
49
+ get transitionCount() {
50
+ return this.#seq;
51
+ }
52
+ /** How many transitions were refused for an unnameable cause. Never silently zero. */
53
+ get rejectedCount() {
54
+ return this.#rejectedCount;
55
+ }
56
+ /** Every entry that has not exited, with the number a human wants: how long it has been open. */
57
+ openEntries() {
58
+ const now = this.#ticker();
59
+ return [...this.#entries.values()].map((entry) => ({
60
+ ...entry,
61
+ ageMs: Math.max(0, now - Date.parse(entry.openedAt)),
62
+ }));
63
+ }
64
+ onTransition(listener) {
65
+ this.#listeners.add(listener);
66
+ return () => this.#listeners.delete(listener);
67
+ }
68
+ onRejected(listener) {
69
+ this.#rejectionListeners.add(listener);
70
+ return () => this.#rejectionListeners.delete(listener);
71
+ }
72
+ /**
73
+ * Record a transition. The only way a session's state changes.
74
+ *
75
+ * The only thing this branches on is whether the cause can be named. Nothing about which
76
+ * session this is, how it was created, or whether anyone is listening is reachable from here —
77
+ * see this file's header. `#commit` below is straight-line by construction.
78
+ */
79
+ record(request) {
80
+ const named = nameable(request.cause);
81
+ if (named !== null)
82
+ return this.#reject(named, request);
83
+ return ok(this.#commit(request));
84
+ }
85
+ /** What this host can say about this session. Raw material for a controller — not a roster. */
86
+ snapshot() {
87
+ return {
88
+ sessionId: this.sessionId,
89
+ correlationId: this.#correlationId,
90
+ state: this.state,
91
+ activity: this.activity,
92
+ where: this.where,
93
+ openEntries: this.openEntries(),
94
+ lastTransitionAt: this.#current?.at ?? null,
95
+ transitionCount: this.#seq,
96
+ };
97
+ }
98
+ // -------------------------------------------------------------------------
99
+ /**
100
+ * The single mutation point. Straight-line: every call that reaches here records.
101
+ *
102
+ * Do not add a guard to this method. A condition here is a session whose state changed without
103
+ * saying so, which is the defect this whole module exists to prevent.
104
+ */
105
+ #commit(request) {
106
+ const carried = carryForward(request, this.#current, this.#initialWhere, this.#clock);
107
+ const entryId = applyEntryOp(this.#entries, carried.entry, carried.at, request.cause);
108
+ this.#seq += 1;
109
+ const transition = {
110
+ sessionId: carried.sessionId,
111
+ seq: this.#seq,
112
+ at: carried.at,
113
+ from: carried.from,
114
+ to: request.to,
115
+ activity: foregroundActivity(this.#entries),
116
+ entryId,
117
+ cause: request.cause,
118
+ where: carried.where,
119
+ correlationId: this.#correlationId,
120
+ };
121
+ this.#current = transition;
122
+ for (const listener of this.#listeners)
123
+ listener(transition);
124
+ return transition;
125
+ }
126
+ #reject(detail, attempted) {
127
+ this.#rejectedCount += 1;
128
+ const refusal = { reason: 'transition-cause-unnamed', detail };
129
+ const rejected = { refusal, at: this.#clock(), attempted };
130
+ for (const listener of this.#rejectionListeners)
131
+ listener(rejected);
132
+ return refuse('transition-cause-unnamed', detail);
133
+ }
134
+ }
135
+ /**
136
+ * The four values a transition inherits from the one before it, resolved in one place.
137
+ *
138
+ * They are lifted out of `#commit` deliberately. Each is a defaulting expression — "use what was
139
+ * given, else what the last transition carried" — and a defaulting expression is still a branch in
140
+ * the shape of one. Keeping them here leaves the commit path with no conditional of any kind, so
141
+ * "the record cannot be skipped" is a claim a reader can check by looking rather than by trusting.
142
+ */
143
+ function carryForward(request, current, initialWhere, clock) {
144
+ return {
145
+ at: request.at ?? clock(),
146
+ from: current?.to ?? INITIAL_STATE,
147
+ sessionId: request.sessionId ?? current?.sessionId ?? null,
148
+ where: request.where ?? current?.where ?? initialWhere,
149
+ entry: request.entry ?? null,
150
+ };
151
+ }
152
+ /**
153
+ * Null when the cause is nameable, otherwise why it is not.
154
+ *
155
+ * The compiler already closes `CauseEvent`, so this only fires for a value that never met the
156
+ * compiler — a cause rebuilt from a decoded frame, or a caller in plain JavaScript.
157
+ */
158
+ function nameable(cause) {
159
+ if (cause === undefined || cause === null)
160
+ return 'a transition carried no cause at all';
161
+ if (!isCauseKind(cause.kind))
162
+ return `cause.kind "${String(cause.kind)}" is not a declared cause kind`;
163
+ if (typeof cause.event !== 'string' || cause.event.length === 0)
164
+ return 'cause.event is empty';
165
+ if (!isCauseEvent(cause.event))
166
+ return `cause.event "${String(cause.event)}" is not a declared cause event`;
167
+ return null;
168
+ }
169
+ /**
170
+ * What holds the session right now: the most recently opened foreground entry.
171
+ *
172
+ * Background entries are excluded by construction — that is the whole point of the lane. Abandoned
173
+ * entries are excluded too: they are still recorded and still ageing, but a session is not blocked
174
+ * on work that has been marked as never coming back.
175
+ *
176
+ * Most-recent rather than oldest, because `activity` answers "what is it doing now". "What has it
177
+ * been stuck on longest" is a different question, and `openEntries()` with its ages answers it
178
+ * without this field having to mean two things.
179
+ */
180
+ function foregroundActivity(entries) {
181
+ let held = null;
182
+ for (const entry of entries.values()) {
183
+ if (entry.lane !== 'foreground')
184
+ continue;
185
+ if (entry.abandonedAt !== null)
186
+ continue;
187
+ held = entry;
188
+ }
189
+ return held?.activity ?? null;
190
+ }
191
+ /** Applies one entry operation and returns the entry it touched, or null. Pure over the map. */
192
+ function applyEntryOp(entries, op, at, cause) {
193
+ if (op === null)
194
+ return null;
195
+ if (op.op === 'open') {
196
+ // Re-opening the same entry keeps the original openedAt. Two sources can report the same
197
+ // condition (the compaction hook and the status message both say "compacting"), and taking the
198
+ // later timestamp would silently reset the age — which is the one number an unpaired entry
199
+ // exists to carry. The transition still records either way; only the clock is protected.
200
+ const existing = entries.get(op.entryId);
201
+ const sameThing = existing !== undefined && existing.activity.kind === op.activity.kind;
202
+ entries.set(op.entryId, {
203
+ entryId: op.entryId,
204
+ activity: op.activity,
205
+ lane: 'foreground',
206
+ openedAt: sameThing ? existing.openedAt : at,
207
+ backgroundedAt: null,
208
+ abandonedAt: null,
209
+ abandonReason: null,
210
+ cause,
211
+ agentId: op.agentId ?? null,
212
+ });
213
+ return op.entryId;
214
+ }
215
+ if (op.op === 'close') {
216
+ entries.delete(op.entryId);
217
+ return op.entryId;
218
+ }
219
+ if (op.op === 'background') {
220
+ const open = entries.get(op.entryId);
221
+ // A background report for an entry this host never saw open is not an error and not a state
222
+ // change: the transition still records, naming the id, and the entry set is simply unchanged.
223
+ if (open === undefined)
224
+ return op.entryId;
225
+ entries.set(op.entryId, { ...open, lane: 'background', backgroundedAt: at });
226
+ return op.entryId;
227
+ }
228
+ // abandon-open: mark, never erase. An entry that never exited is the most useful thing this
229
+ // model can report, and a cleanup that deleted it would destroy exactly that signal.
230
+ for (const [entryId, entry] of entries) {
231
+ if (entry.abandonedAt !== null)
232
+ continue;
233
+ entries.set(entryId, { ...entry, abandonedAt: at, abandonReason: op.reason });
234
+ }
235
+ return null;
236
+ }
237
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1,233 @@
1
+ /**
2
+ * The declared state model — what a session is at a moment, and what caused it to become that.
3
+ *
4
+ * This is the one model. There is no second status word anywhere in this package: a layer that
5
+ * finds itself wanting one has found a state missing from here, not a column of its own. A single
6
+ * word such as `running` that covers responding, tool-calling, waiting on a permission decision,
7
+ * compacting, and finished-and-waiting-for-input answers nobody's question, and every reader
8
+ * downstream of it ends up inferring.
9
+ *
10
+ * Two fields, never one composed string, and structured all the way down.
11
+ * `state` is the coarse lifecycle; `activity` is what a working session is blocked on right now. A
12
+ * composed string cannot be queried or aggregated without parsing it back apart, and "show me every
13
+ * session waiting on a permission decision" is the question this model is built to answer. The same
14
+ * argument applies one level in, which is why `activity` is `{kind, name}` rather than `tool:Bash`:
15
+ * `formatActivity` exists for display, and the structure is the truth.
16
+ *
17
+ * The vocabulary is the SDK's wherever the SDK has a word. `requesting` and `compacting` are
18
+ * `SDKStatus` verbatim; `permission`, `tool`, `subagent` are the SDK's nouns. Where the SDK is
19
+ * silent this package is the author, and the table below says which is which — the same separation
20
+ * `core/vocab.ts` draws between SDK_NOUNS and HOST_NOUNS, for the same reason.
21
+ *
22
+ * Nothing here imports the SDK or a runtime, so the model ships on `periscope/protocol` and a
23
+ * controller can read a transition without acquiring a package that can spawn a process.
24
+ */
25
+ import type { RefusalReason } from '../core/refusal.js';
26
+ export declare const SESSION_STATES: readonly ["spawning", "ready", "working", "idle", "errored", "interrupted", "ended"];
27
+ export type SessionState = (typeof SESSION_STATES)[number];
28
+ /**
29
+ * Where each activity kind's name comes from. Two are the SDK's own words; four extend it.
30
+ *
31
+ * `requesting` and `compacting` are `SDKStatus` verbatim — two values and a null. The SDK has no
32
+ * word for the other four situations, so this package names them, and this table is where that is
33
+ * recorded rather than left for a reader to guess.
34
+ */
35
+ export declare const ACTIVITY_KINDS: readonly ["requesting", "compacting", "tool", "subagent", "permission", "elicitation"];
36
+ export type ActivityKind = (typeof ACTIVITY_KINDS)[number];
37
+ /** Which activity kinds this package named itself, kept visible the way HOST_NOUNS is. */
38
+ export declare const HOST_ACTIVITY_KINDS: readonly ActivityKind[];
39
+ /** SDK: `SDKStatus`'s two non-null values, adopted verbatim. */
40
+ export declare const SDK_ACTIVITY_KINDS: readonly ActivityKind[];
41
+ export interface SessionActivity {
42
+ readonly kind: ActivityKind;
43
+ /** The tool, subagent type or server this is about. Null where the kind carries no name. */
44
+ readonly name: string | null;
45
+ }
46
+ /** The display form — `tool:Bash`, `requesting`. For logs and humans; never parsed back. */
47
+ export declare function formatActivity(activity: SessionActivity | null): string;
48
+ export declare function sameActivity(left: SessionActivity | null, right: SessionActivity | null): boolean;
49
+ /**
50
+ * Where a transition came from. A transition that cannot name its cause is not recorded.
51
+ *
52
+ * `refusal` is the sixth and it is load-bearing: a permission denial is a hook event, but a
53
+ * permission-path outage has no hook at all, and with five kinds the difference would have to ride
54
+ * `detail` — free text a reader greps instead of a fact code branches on. Conflating an outage with
55
+ * a deliberate "no" is the most expensive confusion a controller can make, so the two differ in
56
+ * `kind` and in `event`, twice over, neither of them prose.
57
+ */
58
+ export declare const CAUSE_KINDS: readonly ["hook", "sdk-message", "control", "timeout", "process", "refusal"];
59
+ export type CauseKind = (typeof CAUSE_KINDS)[number];
60
+ /**
61
+ * The SDK's hook events, mirrored as values so they can be validated at runtime.
62
+ *
63
+ * Kept in step by two checks, because a list copied out of a type rots silently: `coverage.ts`
64
+ * declares its table `satisfies Record<HookEvent, …>` so a new SDK event breaks the build, and
65
+ * `pins/hook-coverage.test.ts` parses the union straight out of the shipped `sdk.d.ts` and fails
66
+ * when this list disagrees with it.
67
+ */
68
+ export declare const HOOK_EVENTS: readonly ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "Notification", "UserPromptSubmit", "UserPromptExpansion", "SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "PermissionRequest", "PermissionDenied", "Setup", "TeammateIdle", "TaskCreated", "TaskCompleted", "Elicitation", "ElicitationResult", "ConfigChange", "WorktreeCreate", "WorktreeRemove", "InstructionsLoaded", "CwdChanged", "FileChanged", "DirectoryAdded", "MessageDisplay"];
69
+ export type HookEventName = (typeof HOOK_EVENTS)[number];
70
+ /**
71
+ * The SDK message discriminators this model names as causes — `type` for the plain kinds,
72
+ * `type/subtype` for the `system` family. Only the ones that can move a session appear here;
73
+ * `coverage.ts` accounts for all 39 members of the union, wired or declined.
74
+ */
75
+ export declare const MESSAGE_EVENTS: readonly ["system/init", "system/status", "system/session_state_changed", "system/compact_boundary", "system/task_started", "system/task_updated", "system/task_notification", "system/permission_denied", "system/elicitation_complete", "system/worker_shutting_down", "assistant", "result"];
76
+ export type MessageEventName = (typeof MESSAGE_EVENTS)[number];
77
+ /**
78
+ * Calls the host makes on itself or is asked to make. Not SDK events — decisions.
79
+ *
80
+ * `kind` and `event` are validated independently and never as a pair. `machine.ts`'s `nameable()`
81
+ * checks that the kind is a declared kind and that the event is a declared event, in two separate
82
+ * guards — so `{kind:'hook', event:'permission_denied'}` passes while being incoherent. Pairing a
83
+ * cause with the right kind is the author's job, not the machine's.
84
+ *
85
+ * `permission_denied` sits here rather than in HOOK_EVENTS because a gate's deny is a call the host
86
+ * makes on itself: it is decided by this package, not reported to it. The SDK's `PermissionDenied`
87
+ * hook is a different thing that belongs to the SDK's own prompt path and does not fire for a
88
+ * hook-authored deny — measured — so naming it here would put an event in the trace that never
89
+ * happened, and would make a reader grepping traces conclude the hook fires.
90
+ */
91
+ export declare const CONTROL_EVENTS: readonly ["create_requested", "prompt_submitted", "interrupt_requested", "stop_requested", "permission_denied"];
92
+ export type ControlEventName = (typeof CONTROL_EVENTS)[number];
93
+ /**
94
+ * The agent process's own lifecycle, as causes.
95
+ *
96
+ * These subsume `SESSION_END_CAUSES` deliberately. `sessions/session.ts` states four end causes
97
+ * local to the handle, and its own comment says the layer owning the declared state model maps them
98
+ * onto its causes. This is that layer, and this is that mapping — one cause vocabulary, not two.
99
+ * `state/reconciliation.test.ts` asserts the containment so it cannot drift.
100
+ */
101
+ export declare const PROCESS_EVENTS: readonly ["process_started", "stop_requested", "process_ended", "process_failed", "start_timed_out"];
102
+ export type ProcessEventName = (typeof PROCESS_EVENTS)[number];
103
+ export declare const TIMEOUT_EVENTS: readonly ["start_timed_out", "hook_timed_out"];
104
+ export type TimeoutEventName = (typeof TIMEOUT_EVENTS)[number];
105
+ /**
106
+ * The gate's expiry, named once.
107
+ *
108
+ * One concept with two spellings — `hook_timed_out` on the transition, and `hook-timed-out` as
109
+ * the prefix of the deny reason handed to the model — looks exactly like a declared refusal reason
110
+ * (its sibling branch really is one) while resolving to nothing, so a reader who looks it up finds
111
+ * no such name. `satisfies` ties it to the closed vocabulary above, so the two cannot drift apart
112
+ * without breaking the build.
113
+ */
114
+ export declare const HOOK_TIMEOUT_EVENT = "hook_timed_out";
115
+ /**
116
+ * Every name a cause may carry. Closed, so a transition that cannot name its cause does not
117
+ * compile — the strongest available form of "cause is not nullable".
118
+ */
119
+ export type CauseEvent = HookEventName | MessageEventName | ControlEventName | ProcessEventName | TimeoutEventName | RefusalReason;
120
+ /** The runtime half. A frame decoded from the wire was never seen by the compiler. */
121
+ export declare function isCauseEvent(value: string): value is CauseEvent;
122
+ export declare function isCauseKind(value: string): value is CauseKind;
123
+ export interface TransitionCause {
124
+ readonly kind: CauseKind;
125
+ /** The literal hook, message or control event that fired. Never free text. */
126
+ readonly event: CauseEvent;
127
+ /** For a human reading a log. Never branched on — that is what `kind` and `event` are for. */
128
+ readonly detail: string;
129
+ }
130
+ /**
131
+ * Which session, which worktree, which branch — so the trace answers "where" without a transcript.
132
+ *
133
+ * A null `worktree` or `branch` is a named outcome: `unknownReason` says which of the ordinary
134
+ * situations produced it (not a repository, detached HEAD, unreadable HEAD). An empty string would
135
+ * make "not in a repository" and "in a repository whose HEAD could not be read" the same value.
136
+ */
137
+ export interface TransitionWhere {
138
+ readonly cwd: string;
139
+ readonly worktree: string | null;
140
+ readonly branch: string | null;
141
+ readonly unknownReason: string | null;
142
+ }
143
+ /**
144
+ * One recorded transition. The shape follows `control/link-state.ts`'s `LinkTransition` on purpose
145
+ * — `{from, to, cause, at, detail}` with a required cause is this package's established shape for a
146
+ * state machine, and two shapes for one idea is how vocabularies drift apart.
147
+ */
148
+ export interface SessionTransition {
149
+ /**
150
+ * The agent's own session id — null until it has reported itself, and never null again after.
151
+ *
152
+ * The host does not mint one. An agent emits nothing until a turn is queued, so there is a
153
+ * real window with a live process and no id, and inventing a name for it would be exactly the
154
+ * "coin a word by observation" failure this package is written against. A controller that needs
155
+ * to correlate its request with the session it became supplies `correlationId`.
156
+ */
157
+ readonly sessionId: string | null;
158
+ /** Dense from 1, per session-machine. A gap is detectable by arithmetic alone. */
159
+ readonly seq: number;
160
+ readonly at: string;
161
+ readonly from: SessionState;
162
+ readonly to: SessionState;
163
+ /** What the session is blocked on after this transition. Null when nothing holds it. */
164
+ readonly activity: SessionActivity | null;
165
+ /** The open entry this transition opened, closed or backgrounded. Null when it touched none. */
166
+ readonly entryId: string | null;
167
+ readonly cause: TransitionCause;
168
+ readonly where: TransitionWhere;
169
+ /**
170
+ * Opaque, controller-supplied, and never interpreted here.
171
+ *
172
+ * It exists so a controller can tie a session back to whatever it means on its side. The host
173
+ * does not parse it, branch on it, or derive anything from it — the moment it did, this package
174
+ * would know something about one product's conventions and stop being general.
175
+ */
176
+ readonly correlationId: string | null;
177
+ }
178
+ /**
179
+ * Foreground work holds the session; background work does not.
180
+ *
181
+ * A backgrounded task produces an entry that never exits while the turn moves on — without this
182
+ * split, a session that starts a long build reads as blocked on it forever. The move happens at the
183
+ * moment the SDK reports the task backgrounded, with that as the cause; never on a timer and never
184
+ * by inference. A backgrounded entry is not a finished one: it stays open, it keeps ageing, and it
185
+ * still gets an exit.
186
+ */
187
+ export type EntryLane = 'foreground' | 'background';
188
+ export interface OpenEntry {
189
+ /** The SDK's own id for the thing — `tool_use_id`, `agent_id`, `task_id`. Never minted here. */
190
+ readonly entryId: string;
191
+ readonly activity: SessionActivity;
192
+ readonly lane: EntryLane;
193
+ readonly openedAt: string;
194
+ /** When it stopped holding the session. Null while it still does. */
195
+ readonly backgroundedAt: string | null;
196
+ /**
197
+ * When cleanup marked it abandoned. Cleanup may mark; it may never erase.
198
+ *
199
+ * A session sitting in one tool call for forty minutes is the most useful thing this model can
200
+ * report, and a reconciler that quietly closes the entry destroys exactly that signal. So an
201
+ * unpaired entry is surfaced with its age and a reason, and the fact that it happened survives.
202
+ */
203
+ readonly abandonedAt: string | null;
204
+ readonly abandonReason: string | null;
205
+ /** Why it opened. Carried so an entry read weeks later still says what started it. */
206
+ readonly cause: TransitionCause;
207
+ /** Set when the entry belongs to a subagent rather than the main thread. */
208
+ readonly agentId: string | null;
209
+ }
210
+ /** An open entry with the number a human actually wants. */
211
+ export interface AgedEntry extends OpenEntry {
212
+ readonly ageMs: number;
213
+ }
214
+ /**
215
+ * One session, as this host sees it.
216
+ *
217
+ * This is not a roster and must not grow into one. A roster spans every session everywhere, and
218
+ * sessions live on different hosts — an agent runs where its host runs, so no single host can
219
+ * produce one. This enumerates the sessions this host holds, which is the raw material a controller
220
+ * aggregates into a roster on its side. If this grows a filter, a search or a notion of what a
221
+ * session means, the boundary has been crossed.
222
+ */
223
+ export interface SessionSnapshot {
224
+ readonly sessionId: string | null;
225
+ readonly correlationId: string | null;
226
+ readonly state: SessionState;
227
+ readonly activity: SessionActivity | null;
228
+ readonly where: TransitionWhere;
229
+ readonly openEntries: readonly AgedEntry[];
230
+ readonly lastTransitionAt: string | null;
231
+ readonly transitionCount: number;
232
+ }
233
+ //# sourceMappingURL=model.d.ts.map