@actana/sdk 0.2.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/core-client.d.ts +616 -0
  4. package/dist/core-client.d.ts.map +1 -0
  5. package/dist/core-client.js +1036 -0
  6. package/dist/core-client.js.map +1 -0
  7. package/dist/core-link-cursor-storage.d.ts +32 -0
  8. package/dist/core-link-cursor-storage.d.ts.map +1 -0
  9. package/dist/core-link-cursor-storage.js +66 -0
  10. package/dist/core-link-cursor-storage.js.map +1 -0
  11. package/dist/core-link-frames.d.ts +1123 -0
  12. package/dist/core-link-frames.d.ts.map +1 -0
  13. package/dist/core-link-frames.js +349 -0
  14. package/dist/core-link-frames.js.map +1 -0
  15. package/dist/core-link-socket.d.ts +54 -0
  16. package/dist/core-link-socket.d.ts.map +1 -0
  17. package/dist/core-link-socket.js +74 -0
  18. package/dist/core-link-socket.js.map +1 -0
  19. package/dist/core-link-transport.d.ts +177 -0
  20. package/dist/core-link-transport.d.ts.map +1 -0
  21. package/dist/core-link-transport.js +432 -0
  22. package/dist/core-link-transport.js.map +1 -0
  23. package/dist/core-registration-blob.d.ts +52 -0
  24. package/dist/core-registration-blob.d.ts.map +1 -0
  25. package/dist/core-registration-blob.js +61 -0
  26. package/dist/core-registration-blob.js.map +1 -0
  27. package/dist/core-session.d.ts +321 -0
  28. package/dist/core-session.d.ts.map +1 -0
  29. package/dist/core-session.js +660 -0
  30. package/dist/core-session.js.map +1 -0
  31. package/dist/durable-core-client.d.ts +172 -0
  32. package/dist/durable-core-client.d.ts.map +1 -0
  33. package/dist/durable-core-client.js +264 -0
  34. package/dist/durable-core-client.js.map +1 -0
  35. package/dist/terminal-screen.d.ts +139 -0
  36. package/dist/terminal-screen.d.ts.map +1 -0
  37. package/dist/terminal-screen.js +807 -0
  38. package/dist/terminal-screen.js.map +1 -0
  39. package/package.json +51 -0
@@ -0,0 +1,1123 @@
1
+ export type CoreLinkPtySpawnHarness = "claude-code" | "codex" | "cursor-cli" | "opencode";
2
+ export type CoreLinkBaseSpawnOptions = {
3
+ taskId: string;
4
+ cwd: string;
5
+ command: string;
6
+ args?: string[];
7
+ cols?: number;
8
+ rows?: number;
9
+ mcEnv?: {
10
+ apiUrl?: string;
11
+ token?: string;
12
+ };
13
+ missionControlTheme?: "dark" | "light";
14
+ /**
15
+ * Discriminant for the VM Shell Session mode (issue 06). `never` here keeps
16
+ * agent/shell spawns out of that branch; {@link CoreLinkShellSessionSpawnOptions}
17
+ * sets it to `true`. Declared on the base so `opts.shellSession` is readable
18
+ * on the whole {@link CoreLinkPtySpawnOptions} union for a type-safe
19
+ * discriminated narrow (no `as` cast needed at the dispatch site).
20
+ */
21
+ shellSession?: never;
22
+ };
23
+ export type CoreLinkHarnessSpawnOptions = CoreLinkBaseSpawnOptions & {
24
+ agent: CoreLinkPtySpawnHarness;
25
+ dangerouslySkipPermissions?: boolean;
26
+ shell?: never;
27
+ initialInput?: string;
28
+ };
29
+ export type CoreLinkShellSpawnOptions = CoreLinkBaseSpawnOptions & {
30
+ shell: true;
31
+ agent?: never;
32
+ dangerouslySkipPermissions?: never;
33
+ home?: boolean;
34
+ };
35
+ /**
36
+ * A VM Shell Session spawn (issue 06) — a free-form interactive shell on the
37
+ * Core's machine, distinct from agent workspaces. `shellSession: true` is
38
+ * its own spawn mode: no `agent`, no `cwd`/project-root requirement (a VM shell
39
+ * has no project folder). The Core skips the project-root validation it
40
+ * applies to agent spawns and starts a login shell at its own home. Gated by
41
+ * core-link auth (mTLS + bearer), never auto-spawned — opened by an explicit
42
+ * Panel gesture. Streamed back over the same multiplexed core-link; replayable
43
+ * on reconnect like any other PTY.
44
+ */
45
+ export type CoreLinkShellSessionSpawnOptions = {
46
+ shellSession: true;
47
+ taskId: string;
48
+ /** Optional starting command; empty (or omitted) → interactive login shell. */
49
+ command?: string;
50
+ cols?: number;
51
+ rows?: number;
52
+ missionControlTheme?: "dark" | "light";
53
+ };
54
+ export type CoreLinkPtySpawnOptions = CoreLinkHarnessSpawnOptions | CoreLinkShellSpawnOptions | CoreLinkShellSessionSpawnOptions;
55
+ /**
56
+ * A window of a PTY's replay ring — the answer to a `replay` request. `from` is
57
+ * the seq of the first chunk in `data`, absent when `data` is empty; a caller
58
+ * that asked for a `sinceSeq` and is handed a larger `from` has a hole in front
59
+ * of the tail (see the `replayResult` frame).
60
+ */
61
+ export type CoreLinkPtyReplay = {
62
+ data: string;
63
+ nextSeq: number;
64
+ from?: number;
65
+ };
66
+ export type CoreLinkPortKillResult = {
67
+ port: number;
68
+ pids: number[];
69
+ killed: number[];
70
+ errors: string[];
71
+ };
72
+ export type CoreLinkLaunchProcessKillResult = {
73
+ ptyCount: number;
74
+ ports: CoreLinkPortKillResult[];
75
+ };
76
+ export type CoreLinkEvent = {
77
+ /** Sequential, monotonic, per-Core. Never 0. */
78
+ eventId: number;
79
+ /** Wall-clock ms when the event was appended. */
80
+ ts: number;
81
+ /**
82
+ * Stable kind string: `task:created`, `task:updated`, `session:finished`,
83
+ * `task:question`, `pty:spawn`, `pty:exit`, … Mirrors the server's AppEvent
84
+ * type names so the Panel can route by kind without a translation layer.
85
+ */
86
+ kind: string;
87
+ /** The PTY this event belongs to, if any (PTY spawn/exit). */
88
+ ptyId: string | null;
89
+ /** The Task this event belongs to, if any (task status, session finish). */
90
+ taskId: string | null;
91
+ /** JSON-serialized event-specific body. Shape is kind-specific. */
92
+ payload: string;
93
+ };
94
+ export type CoreLinkTaskStatus = string;
95
+ /**
96
+ * A task mutation — `create` (a new task under an existing project), `update`
97
+ * (patch an existing task row), or `delete` (remove one). The discriminant lets
98
+ * the Core dispatch to `createTask` / `updateTask` / `deleteTask` without a
99
+ * nullable-id sniff.
100
+ *
101
+ * On `create`, `projectId`, `title`, and `agent` are required — everything
102
+ * else defaults on the Core (status → `ready`, pinned/archived → false).
103
+ * `taskId` is optional; when omitted the Core generates one.
104
+ *
105
+ * On `update`, `taskId` is required and identifies the row; any of
106
+ * `status`/`title`/`pinned`/`archived` may be set. Fields omitted are left
107
+ * untouched (partial patch, mirroring the Panel server's PATCH shape).
108
+ *
109
+ * On `delete`, `taskId` is required and the row is removed outright — the
110
+ * Core's SQLite cascades the rows hanging off it, mirroring the Panel
111
+ * server's own DELETE. A missing row comes back as `task: null`, the same way
112
+ * a missing row on `update` does; it is not an error frame.
113
+ */
114
+ export type CoreLinkTaskMutation = {
115
+ op: "create";
116
+ taskId?: string;
117
+ projectId: string;
118
+ title: string;
119
+ agent: string;
120
+ status?: CoreLinkTaskStatus;
121
+ /** Optional session icon id at creation time; usually null and set later. */
122
+ icon?: string | null;
123
+ } | {
124
+ op: "update";
125
+ taskId: string;
126
+ status?: CoreLinkTaskStatus;
127
+ title?: string;
128
+ /**
129
+ * Whether the `title` on this patch is an operator's rename (issue 84).
130
+ * Omitted, a title reads as a rename and pins the row's
131
+ * manually-set-title flag — the shape every Panel-driven rename has
132
+ * always had. The Core's own title generator is the one caller that
133
+ * sends `false`, so a generated name never claims to be a rename and a
134
+ * rename is never overwritten by a generator that finished after it.
135
+ * Meaningless without `title`; ignored there.
136
+ */
137
+ titleManuallySet?: boolean;
138
+ /**
139
+ * The harness's own session id for this Task (issue 84). Captured by the
140
+ * Core when a hook reports one, and by the Panel when a resumed session
141
+ * hands back a fresh id — a Core-owned row's session id is Core state
142
+ * like every other column, and writing it to the Panel's database left
143
+ * the Core's row blank. `null` clears it.
144
+ */
145
+ claudeSessionId?: string | null;
146
+ pinned?: boolean;
147
+ archived?: boolean;
148
+ /**
149
+ * Session icon id (issue 09). `undefined` leaves the row untouched (partial
150
+ * patch); a string sets it; `null` clears it. Icon is Core-owned
151
+ * metadata (ADR 0005) — the Panel never stores it and every icon change
152
+ * routes through this frame.
153
+ */
154
+ icon?: string | null;
155
+ } | {
156
+ op: "delete";
157
+ taskId: string;
158
+ };
159
+ /**
160
+ * A project mutation — `create`, `rename`, or `archive`. The Core validates
161
+ * the VM path on `create` (absolute, resolvable, not a file) and rejects with
162
+ * an `error` frame if invalid — a Project's path is a VM path and only the
163
+ * Core can validate it (CONTEXT.md "Project").
164
+ *
165
+ * `archive` deletes the project row (SQLite cascades tasks under
166
+ * this project via ON DELETE CASCADE — that is the shared-DB shape). The word
167
+ * "archive" is used at the protocol layer to match the ticket's language and
168
+ * to leave room for a future soft-archive column without changing the frame
169
+ * shape; today it is destructive.
170
+ */
171
+ export type CoreLinkProjectMutation = {
172
+ op: "create";
173
+ projectId?: string;
174
+ name: string;
175
+ path: string;
176
+ icon?: string;
177
+ iconColor?: string;
178
+ pinned?: boolean;
179
+ /**
180
+ * The remembered session settings the Create Project dialog collected
181
+ * (issue 22). Omitted fields fall back to the column defaults, so a
182
+ * caller that only names the project still creates a valid row.
183
+ */
184
+ rememberHarnessSettings?: boolean;
185
+ savedHarness?: string | null;
186
+ savedSkipPermissions?: boolean;
187
+ savedBareSession?: boolean;
188
+ defaultGridView?: boolean;
189
+ } | {
190
+ op: "rename";
191
+ projectId: string;
192
+ name: string;
193
+ } | {
194
+ op: "archive";
195
+ projectId: string;
196
+ }
197
+ /**
198
+ * Pin / unpin a project (issue 10). Pin state is a Core fact stored on
199
+ * the project row; every Panel connected to the same Core sees the same
200
+ * value. Dedicated op (rather than piggy-backing on a generic
201
+ * `updateProject` patch) so the event kind can be `project:pinnedChanged`
202
+ * — a reconnecting Panel replays pin flips distinctly from other project
203
+ * edits.
204
+ */
205
+ | {
206
+ op: "pin";
207
+ projectId: string;
208
+ pinned: boolean;
209
+ }
210
+ /**
211
+ * Patch a project's remembered session settings (issue 22) — the "Remember
212
+ * settings for this project" checkbox and the grid-view default. These are
213
+ * Core facts on the project row, so every Panel connected to the same Core
214
+ * converges on them, exactly as pin state does.
215
+ *
216
+ * Follows the `pin` precedent rather than becoming a generic field patch: a
217
+ * dedicated op earns its own `project:settingsChanged` event kind, so a
218
+ * reconnecting Panel replays a settings change distinctly from a rename.
219
+ * Fields left `undefined` are untouched; `savedHarness: null` clears it.
220
+ */
221
+ | {
222
+ op: "settings";
223
+ projectId: string;
224
+ rememberHarnessSettings?: boolean;
225
+ savedHarness?: string | null;
226
+ savedSkipPermissions?: boolean;
227
+ savedBareSession?: boolean;
228
+ defaultGridView?: boolean;
229
+ }
230
+ /**
231
+ * Patch a project's icon and icon colour (issue 98). Both live on the Core's
232
+ * project row and already travel in {@link CoreLinkProjectSnapshot}, and
233
+ * `create` accepts them — but until this op nothing could change them
234
+ * afterwards, so the Edit-project dialog PATCHed them at a Panel row a
235
+ * Core-owned project does not have, and 404'd.
236
+ *
237
+ * Follows the `pin` / `settings` precedent rather than widening `rename` into
238
+ * a generic field patch: a dedicated op earns its own
239
+ * `project:appearanceChanged` event kind, so a reconnecting Panel replays an
240
+ * icon change distinctly from a rename. Fields left `undefined` are
241
+ * untouched; a blank string is not an erase (both columns are NOT NULL) and
242
+ * is ignored.
243
+ */
244
+ | {
245
+ op: "appearance";
246
+ projectId: string;
247
+ icon?: string;
248
+ iconColor?: string;
249
+ };
250
+ export type CoreLinkHookOp = {
251
+ op: "list";
252
+ taskId?: string;
253
+ } | {
254
+ op: "enable";
255
+ hookId: string;
256
+ taskId?: string;
257
+ } | {
258
+ op: "disable";
259
+ hookId: string;
260
+ taskId?: string;
261
+ };
262
+ /**
263
+ * Availability of one managed Harness on a single Core. `status` mirrors the
264
+ * Panel's `CliAvailability` shape so no translation is needed at the store
265
+ * boundary. `path` / `version` / `label` / `requiredVersion` / `packageUrl` /
266
+ * `updateCommands` are copied from the probe's `CliCheckResult` where known so
267
+ * the update-required and outdated flows keep working across every Core.
268
+ */
269
+ export type CoreLinkHarnessAvailability = {
270
+ status: "checking" | "available" | "missing" | "outdated";
271
+ path?: string;
272
+ reason?: string;
273
+ label?: string;
274
+ version?: string;
275
+ requiredVersion?: string;
276
+ packageUrl?: string;
277
+ updateCommands?: readonly string[];
278
+ };
279
+ /** Per-agent availability map. Keys are the `Harness` id strings. */
280
+ export type CoreLinkHarnessAvailabilityMap = Record<string, CoreLinkHarnessAvailability>;
281
+ /**
282
+ * The kind string appended to the event log when the availability map changes.
283
+ * Payload is the JSON-serialized full {@link CoreLinkHarnessAvailabilityMap}
284
+ * (self-contained; a Panel that misses N intermediate changes and replays only
285
+ * the tail lands on the latest state without stitching).
286
+ */
287
+ export declare const HARNESSES_AVAILABILITY_EVENT_KIND = "agents:availabilityChanged";
288
+ /**
289
+ * Answer to a `harnessInstall` request. `accepted` means the Core validated the
290
+ * Harness id and started the install — nothing about whether it succeeded.
291
+ * `accepted: false` is a refusal to start at all (an unknown id, or a Core with
292
+ * no install path wired) and carries the operator-facing reason.
293
+ */
294
+ export type CoreLinkHarnessInstallAck = {
295
+ accepted: boolean;
296
+ message?: string;
297
+ };
298
+ /**
299
+ * The kind appended to the event log when an install ends without the Harness
300
+ * becoming available. Payload is `{harness, message}` — the message is written
301
+ * for the operator, in the same register the `dirCreate` / `dirList` failures
302
+ * use. This is the only definitive failure signal: no failure is ever cached in
303
+ * the availability map, so a Panel that missed the event sees a plain `missing`
304
+ * Harness it can try again, never a permanently disabled one.
305
+ */
306
+ export declare const HARNESS_INSTALL_FAILED_EVENT_KIND = "harness:installFailed";
307
+ /** Payload of a {@link HARNESS_INSTALL_FAILED_EVENT_KIND} event. */
308
+ export type CoreLinkHarnessInstallFailedPayload = {
309
+ /** The Harness id the install was for. */
310
+ harness: string;
311
+ /** Operator-facing reason, e.g. "Installing Claude Code failed (exit 1)." */
312
+ message: string;
313
+ };
314
+ /** One directory inside a listing. `childCount` is 0 for a leaf. */
315
+ export type CoreLinkDirEntry = {
316
+ name: string;
317
+ /** Visible (non-hidden) subdirectory count — powers the drill-in affordance. */
318
+ childCount: number;
319
+ };
320
+ /**
321
+ * One directory's worth of listing, as the Core sees its own disk. `path`
322
+ * is the resolved absolute directory; `parent` is null at the filesystem root.
323
+ * `roots` are shortcut chips (home plus the standard folders that exist on
324
+ * that machine) so the picker opens somewhere useful on a VM whose layout the
325
+ * Panel knows nothing about.
326
+ */
327
+ export type CoreLinkDirListing = {
328
+ path: string;
329
+ parent: string | null;
330
+ home: string;
331
+ roots: Array<{
332
+ label: string;
333
+ path: string;
334
+ }>;
335
+ entries: CoreLinkDirEntry[];
336
+ /** True when the listing was capped and some folders are not shown. */
337
+ truncated: boolean;
338
+ };
339
+ /**
340
+ * The `code` on an `error` frame refusing a mutation because another connection
341
+ * holds that Session's lock (ADR 0024 D4/D6).
342
+ *
343
+ * A code rather than a message match, because the client has to tell this apart
344
+ * from the other way a mutation fails to land: "that Session is gone". Those two
345
+ * answers arrive on different frames entirely — a refusal is this `error`, while
346
+ * a Session that no longer exists is the ordinary `writeResult { ok: false }` /
347
+ * `killResult { ok: false }` / `tasksMutateResult { task: null }` the Core has
348
+ * always sent — so a client can distinguish them without parsing prose, and one
349
+ * of them is worth retrying after a claim while the other never is.
350
+ */
351
+ export declare const SESSION_LOCKED_ERROR_CODE = "session-locked";
352
+ /**
353
+ * Machine-readable `error` codes. Open by construction (a plain string union
354
+ * with one member today) so a later refusal can name itself without every
355
+ * client having to learn the whole set: an unrecognised code reads as a plain
356
+ * error, which is what a client that ignores the field already does.
357
+ */
358
+ export type CoreLinkErrorCode = typeof SESSION_LOCKED_ERROR_CODE;
359
+ /**
360
+ * Who lost a Session to a `forceTakeover`.
361
+ *
362
+ * A takeover always succeeds — that is D7, and a result field saying "yes it
363
+ * worked" would carry no information. What varies is whether anybody was
364
+ * actually evicted, and a client should not report having taken a Session off
365
+ * another client when the Session was sitting unlocked.
366
+ */
367
+ export type CoreLinkSessionTakenFrom = "nobody" | "another-connection" | "this-connection";
368
+ /**
369
+ * How a Session's lock looks **to the one connection this snapshot was sent
370
+ * to** (ADR 0024 D8).
371
+ *
372
+ * This field is *addressed*, not global: two connections listing the same
373
+ * Session are told different things about it, and that is the whole point. A
374
+ * snapshot that reported "locked: true" and left the recipient to work out
375
+ * whether the holder was itself would make every client carry the comparison,
376
+ * and every client would need an identity to compare against — which is exactly
377
+ * the identity D10 says the lock does not have and D3 keeps below the socket.
378
+ *
379
+ * So it says **whether you hold it**, never **who holds it**. `held-by-another`
380
+ * is the whole of what a watcher learns about a holder that is not itself: no
381
+ * client id, no connection id, no label, nothing that a second client could
382
+ * correlate a person or a program with.
383
+ *
384
+ * `writable` is the answer and `state` is why. The two are not redundant: which
385
+ * states are writable is a Core-side rule — an *unlocked* Session is writable by
386
+ * anybody, which is the D11 compatibility promise (see
387
+ * {@link SESSION_LOCKED_ERROR_CODE}) rather than an obvious property of the
388
+ * word "unlocked" — and publishing the answer keeps that rule in the one place
389
+ * that enforces it. A client rendering an editable terminal reads `writable`; a
390
+ * client choosing between a "Claim" and a "Take over" affordance reads `state`.
391
+ */
392
+ export type CoreLinkSessionLock = {
393
+ /**
394
+ * May the connection that was sent this snapshot mutate this Session right
395
+ * now? True when the Session is unlocked as well as when this connection
396
+ * holds it — the gate refuses only a Session **another** connection holds.
397
+ */
398
+ writable: boolean;
399
+ /** Which of the three states this Session is in, from that connection's side. */
400
+ state: CoreLinkSessionLockState;
401
+ };
402
+ /**
403
+ * The three states a Session's lock can be in for one connection.
404
+ *
405
+ * There is no fourth, and deliberately no name for the holder. `unlocked` is a
406
+ * real state (D5) and not an absence: a Session starts there, returns there on
407
+ * release, on the holder's connection dropping, and on a Core restart (D12).
408
+ */
409
+ export type CoreLinkSessionLockState = "unlocked" | "held-by-you" | "held-by-another";
410
+ /**
411
+ * The kind appended to the event log on every Session-lock change (ADR 0024 D8).
412
+ *
413
+ * A **dedicated kind**, following the precedent ADR 0022 set for
414
+ * `project:appearanceChanged` — and for the same reason both ADR 0017 and ADR
415
+ * 0022 rejected the alternative. Widening an existing mutation event to carry
416
+ * lock state would stop the frame documenting what changed: every client would
417
+ * have to refetch on every `task:updated` to find out whether this one was
418
+ * about the lock, and a reconnecting client replaying a tail could not tell a
419
+ * takeover it needs to react to from a title edit it does not.
420
+ *
421
+ * Payload is {@link CoreLinkSessionLockChangedPayload}. It rides the ordinary
422
+ * event log, so it replays by cursor exactly like every other event: a client
423
+ * that was away comes back, sends `subscribe { lastEventId }`, and reads the
424
+ * lock changes it missed in order, distinctly, alongside everything else.
425
+ */
426
+ export declare const SESSION_LOCK_CHANGED_EVENT_KIND = "session:lockChanged";
427
+ /**
428
+ * What happened to a Session's lock, in the vocabulary of the lock rather than
429
+ * of the frame that caused it.
430
+ *
431
+ * A `forceTakeover` against a Session **nobody** held is a `claimed`, not a
432
+ * `taken-over`: the wire says what happened to the lock, so no client reports an
433
+ * eviction that did not occur. The same distinction `CoreLinkSessionTakenFrom`
434
+ * draws for the requester, drawn once more for everybody watching.
435
+ *
436
+ * Open by construction — a reader that meets an unfamiliar transition falls back
437
+ * to `locked` on the payload, which is why that field is there.
438
+ */
439
+ export type CoreLinkSessionLockTransition = "claimed" | "released" | "taken-over";
440
+ /**
441
+ * Payload of a {@link SESSION_LOCK_CHANGED_EVENT_KIND} event.
442
+ *
443
+ * **It names no client, and it must not.** This row goes into a shared log that
444
+ * every connection replays: anything identifying in it would be broadcast to
445
+ * every watcher of the Core, which is the identity leak D10 (the lock is
446
+ * coordination, not security) and D3 (the holder is a connection, not a person)
447
+ * exist to prevent. A takeover therefore names neither winner nor loser, and
448
+ * needs to name neither: the winner learns it won on its own
449
+ * `forceTakeoverResult`, and every other connection reads `taken-over` as "the
450
+ * holder is now somebody who is not me" — which is the entire fact a loser needs
451
+ * and the only one an identity could have added.
452
+ *
453
+ * "Can *I* write to it now" is deliberately not answered here. It cannot be: one
454
+ * logged row is read by every connection, and the answer differs per connection.
455
+ * That question is the snapshot's ({@link CoreLinkSessionLock}), and this event
456
+ * is what tells a client the answer has moved.
457
+ */
458
+ export type CoreLinkSessionLockChangedPayload = {
459
+ /** The Session whose lock changed. Also on the event row's `taskId` column. */
460
+ taskId: string;
461
+ /** What happened to the lock. */
462
+ transition: CoreLinkSessionLockTransition;
463
+ /**
464
+ * Is the Session held by some connection now? Derivable from every transition
465
+ * this build defines, and carried anyway: a client can act on the state
466
+ * without knowing the whole transition vocabulary, which is what keeps a
467
+ * transition added later from silently reading as "no change" to a client that
468
+ * predates it.
469
+ */
470
+ locked: boolean;
471
+ };
472
+ export type CoreLinkRequestFrame = {
473
+ type: "spawn";
474
+ reqId: string;
475
+ opts: CoreLinkPtySpawnOptions;
476
+ } | {
477
+ type: "write";
478
+ reqId: string;
479
+ ptyId: string;
480
+ data: string;
481
+ } | {
482
+ type: "resize";
483
+ reqId: string;
484
+ ptyId: string;
485
+ cols: number;
486
+ rows: number;
487
+ } | {
488
+ type: "kill";
489
+ reqId: string;
490
+ ptyId: string;
491
+ } | {
492
+ type: "killLaunchProcesses";
493
+ reqId: string;
494
+ cwd: string;
495
+ commands: string[];
496
+ ports?: number[];
497
+ } | {
498
+ type: "findByTask";
499
+ reqId: string;
500
+ taskId: string;
501
+ } | {
502
+ type: "replay";
503
+ reqId: string;
504
+ ptyId: string;
505
+ sinceSeq?: number;
506
+ } | {
507
+ type: "subscribe";
508
+ reqId: string;
509
+ lastEventId: number;
510
+ } | {
511
+ type: "ptySubscribe";
512
+ reqId: string;
513
+ ptyId: string;
514
+ catchUp?: boolean;
515
+ } | {
516
+ type: "ptyUnsubscribe";
517
+ reqId: string;
518
+ ptyId: string;
519
+ } | {
520
+ type: "claim";
521
+ reqId: string;
522
+ taskId: string;
523
+ } | {
524
+ type: "release";
525
+ reqId: string;
526
+ taskId: string;
527
+ } | {
528
+ type: "forceTakeover";
529
+ reqId: string;
530
+ taskId: string;
531
+ } | {
532
+ type: "reclaim";
533
+ reqId: string;
534
+ clientId: string;
535
+ } | {
536
+ type: "tasksList";
537
+ reqId: string;
538
+ projectId?: string;
539
+ } | {
540
+ type: "archivedTasksList";
541
+ reqId: string;
542
+ projectId?: string;
543
+ } | {
544
+ type: "tasksMutate";
545
+ reqId: string;
546
+ mutation: CoreLinkTaskMutation;
547
+ }
548
+ /**
549
+ * A prompt the operator submitted, for a harness whose hooks do not report
550
+ * one (issue 84).
551
+ *
552
+ * Cursor's `beforeSubmitPrompt` never fires in cursor-agent, so the Panel
553
+ * reads the prompt off the terminal instead — and that is the only copy
554
+ * anyone has. Without a way to hand it to the owning Core, a Core-owned
555
+ * Cursor Session could never be named at all: the Core's title generator
556
+ * had nothing to name it from, and the Panel's names only Panel rows.
557
+ *
558
+ * It patches no column. The Core takes it as the trigger its own hook
559
+ * receiver would otherwise have supplied, and the same rules apply — a
560
+ * Session that already has a real title, or one the operator renamed, is
561
+ * left alone.
562
+ */
563
+ | {
564
+ type: "harnessPrompt";
565
+ reqId: string;
566
+ taskId: string;
567
+ prompt: string;
568
+ } | {
569
+ type: "projectsList";
570
+ reqId: string;
571
+ } | {
572
+ type: "projectsMutate";
573
+ reqId: string;
574
+ mutation: CoreLinkProjectMutation;
575
+ } | {
576
+ type: "sessionsList";
577
+ reqId: string;
578
+ projectId?: string;
579
+ } | {
580
+ type: "hooksOp";
581
+ reqId: string;
582
+ hook: CoreLinkHookOp;
583
+ } | {
584
+ type: "auth";
585
+ reqId: string;
586
+ bearer: string;
587
+ } | {
588
+ type: "agentsAvailabilityList";
589
+ reqId: string;
590
+ } | {
591
+ type: "harnessInstall";
592
+ reqId: string;
593
+ harness: string;
594
+ } | {
595
+ type: "dirList";
596
+ reqId: string;
597
+ path?: string | null;
598
+ } | {
599
+ type: "dirCreate";
600
+ reqId: string;
601
+ parent: string;
602
+ name: string;
603
+ };
604
+ /** Unsolicited stream frame — pushed by the Core whenever a PTY emits or exits. */
605
+ export type CoreLinkStreamFrame = {
606
+ type: "data";
607
+ ptyId: string;
608
+ data: string;
609
+ seq: number;
610
+ } | {
611
+ type: "exit";
612
+ ptyId: string;
613
+ exitCode: number;
614
+ signal?: number;
615
+ };
616
+ /**
617
+ * Unsolicited event frame — pushed by the Core for every domain event in
618
+ * the monotonic event log (task status, hook, session finish, PTY spawn/exit).
619
+ * Carries the sequential {@link CoreLinkEvent} envelope. During a replay the
620
+ * Core streams these back-to-back; live push uses the same frame shape.
621
+ */
622
+ export type CoreLinkEventFrame = {
623
+ type: "event";
624
+ event: CoreLinkEvent;
625
+ };
626
+ /**
627
+ * End-of-replay marker. Sent once after the `subscribe` tail has been fully
628
+ * streamed as `event` frames. `lastEventId` is the highest eventId the Panel
629
+ * has now seen — it persists it as its new cursor. After this frame the
630
+ * Core resumes live `event` push for any events appended after the cursor.
631
+ */
632
+ export type CoreLinkEventsReplayedFrame = {
633
+ type: "eventsReplayed";
634
+ lastEventId: number;
635
+ };
636
+ /**
637
+ * The `multiConnection` capability, announced on `ready` (ADR 0024 D11).
638
+ *
639
+ * `version` is the capability's own number, independent of
640
+ * {@link CORE_LINK_PROTOCOL_VERSION} — it moves when the multi-connection
641
+ * surface itself changes shape, and nothing about it marks a Core stale.
642
+ */
643
+ export type CoreLinkMultiConnectionCapability = {
644
+ version: 1;
645
+ };
646
+ /** Response frame — correlates to a request via `reqId`. */
647
+ export type CoreLinkResponseFrame = {
648
+ type: "ready";
649
+ version: string;
650
+ /**
651
+ * Present on a Core that serves many core-link connections at once
652
+ * (ADR 0024 D1). Absent means the single-connection Core: it evicts the
653
+ * previous socket on connect, holds no Session locks and has no
654
+ * per-connection subscriptions.
655
+ *
656
+ * Absence is a supported state, not a fault. Every frame that only makes
657
+ * sense against a multi-connection Core is withheld by the client when
658
+ * this is absent (see `MULTI_CONNECTION_ONLY_FRAME_TYPES` in the Panel's
659
+ * core-link client); everything else behaves exactly as it does today, so
660
+ * such a Core is connected and fully usable, never "needs update".
661
+ */
662
+ multiConnection?: CoreLinkMultiConnectionCapability;
663
+ } | {
664
+ type: "spawned";
665
+ reqId: string;
666
+ ptyId: string;
667
+ /**
668
+ * Will a hook report the START of a turn for this Session (issue 84)?
669
+ *
670
+ * The Panel's terminal-input fallback stands down on this and nothing
671
+ * else. It is deliberately narrower than "hooks were installed": Cursor
672
+ * takes our hooks file but never fires `beforeSubmitPrompt`, and Codex
673
+ * refuses to run newly-installed hooks until the operator reviews them
674
+ * with `/hooks` — both would report a turn's END and leave its start
675
+ * unannounced. Suppressing the fallback on either is a Session with no
676
+ * `running` signal at all, which is the bug this replaced.
677
+ *
678
+ * Absent from an older Core's answer, which reads as "no" — a redundant
679
+ * `running` write, never a silent card.
680
+ */
681
+ hooksReportTurnStart?: boolean;
682
+ } | {
683
+ type: "spawnError";
684
+ reqId: string;
685
+ message: string;
686
+ } | {
687
+ type: "writeResult";
688
+ reqId: string;
689
+ ok: boolean;
690
+ } | {
691
+ type: "resizeResult";
692
+ reqId: string;
693
+ ok: boolean;
694
+ } | {
695
+ type: "killResult";
696
+ reqId: string;
697
+ ok: boolean;
698
+ } | {
699
+ type: "killLaunchProcessesResult";
700
+ reqId: string;
701
+ result: CoreLinkLaunchProcessKillResult;
702
+ } | {
703
+ type: "findByTaskResult";
704
+ reqId: string;
705
+ ptyId: string | null;
706
+ } | {
707
+ type: "replayResult";
708
+ reqId: string;
709
+ data: string;
710
+ nextSeq: number;
711
+ from?: number;
712
+ } | {
713
+ type: "subscribeAck";
714
+ reqId: string;
715
+ fromEventId: number;
716
+ } | {
717
+ type: "ptySubscribeAck";
718
+ reqId: string;
719
+ ptyId: string;
720
+ subscribed: true;
721
+ holding: boolean;
722
+ } | {
723
+ type: "ptyUnsubscribeAck";
724
+ reqId: string;
725
+ ptyId: string;
726
+ subscribed: false;
727
+ } | {
728
+ type: "claimResult";
729
+ reqId: string;
730
+ taskId: string;
731
+ granted: boolean;
732
+ } | {
733
+ type: "releaseResult";
734
+ reqId: string;
735
+ taskId: string;
736
+ released: boolean;
737
+ } | {
738
+ type: "forceTakeoverResult";
739
+ reqId: string;
740
+ taskId: string;
741
+ takenFrom: CoreLinkSessionTakenFrom;
742
+ } | {
743
+ type: "reclaimResult";
744
+ reqId: string;
745
+ clientId: string;
746
+ replaced: boolean;
747
+ taskIds: string[];
748
+ } | {
749
+ type: "tasksListResult";
750
+ reqId: string;
751
+ tasks: CoreLinkTaskSnapshot[];
752
+ archivedCount: number;
753
+ } | {
754
+ type: "archivedTasksListResult";
755
+ reqId: string;
756
+ tasks: CoreLinkTaskSnapshot[];
757
+ } | {
758
+ type: "tasksMutateResult";
759
+ reqId: string;
760
+ task: CoreLinkTaskSnapshot | null;
761
+ }
762
+ /**
763
+ * Was the prompt taken up? `false` means this Core has no title generator
764
+ * wired — not that the Session was left unnamed for a reason, which is a
765
+ * normal outcome the Core does not report on.
766
+ */
767
+ | {
768
+ type: "harnessPromptResult";
769
+ reqId: string;
770
+ accepted: boolean;
771
+ } | {
772
+ type: "projectsListResult";
773
+ reqId: string;
774
+ projects: CoreLinkProjectSnapshot[];
775
+ } | {
776
+ type: "projectsMutateResult";
777
+ reqId: string;
778
+ project: CoreLinkProjectSnapshot | null;
779
+ } | {
780
+ type: "sessionsListResult";
781
+ reqId: string;
782
+ sessions: CoreLinkSessionSnapshot[];
783
+ } | {
784
+ type: "hooksOpResult";
785
+ reqId: string;
786
+ hooks: CoreLinkHookEntry[];
787
+ } | {
788
+ type: "authOk";
789
+ reqId: string;
790
+ coreId: string;
791
+ exp: number;
792
+ } | {
793
+ type: "authError";
794
+ reqId?: string;
795
+ reason: "expired" | "bad-signature" | "malformed";
796
+ } | {
797
+ type: "agentsAvailabilityListResult";
798
+ reqId: string;
799
+ availability: CoreLinkHarnessAvailabilityMap;
800
+ } | ({
801
+ type: "harnessInstallAck";
802
+ reqId: string;
803
+ } & CoreLinkHarnessInstallAck) | {
804
+ type: "dirListResult";
805
+ reqId: string;
806
+ listing: CoreLinkDirListing;
807
+ } | {
808
+ type: "dirCreateResult";
809
+ reqId: string;
810
+ path: string;
811
+ }
812
+ /**
813
+ * `code` is optional and additive (issue 144): it exists so a client can act
814
+ * on *why* a request failed without matching on prose. Absent on every error
815
+ * that shipped before it, which is why nothing may require it — a client
816
+ * reads the code when it is there and falls back to the message when it is
817
+ * not. See {@link SESSION_LOCKED_ERROR_CODE}, its first and so far only value.
818
+ */
819
+ | {
820
+ type: "error";
821
+ reqId?: string;
822
+ message: string;
823
+ code?: CoreLinkErrorCode;
824
+ };
825
+ /**
826
+ * A flattened project snapshot carried over the core-link (issue 07). The
827
+ * Core is the source of truth for projects; the Panel holds none. The shape
828
+ * mirrors the server's project row so the Panel can render per-Core navigation
829
+ * without a separate HTTP round-trip per project.
830
+ *
831
+ * `path` is a VM path — only the Core can validate it (CONTEXT.md
832
+ * "Project": "A Project's path is a VM path. Only the Core can validate
833
+ * it."). The Panel renders it as-is and never assumes it exists locally.
834
+ */
835
+ export type CoreLinkProjectSnapshot = {
836
+ projectId: string;
837
+ name: string;
838
+ /** Absolute path on the Core's machine (a VM path, not a Panel path). */
839
+ path: string;
840
+ /** 2-letter monogram shown in the Panel (mirrors the projects table). */
841
+ icon: string;
842
+ /** Hex color for the icon background. */
843
+ iconColor: string;
844
+ pinned: boolean;
845
+ /**
846
+ * Remembered session settings (issue 22). Core facts on the project row —
847
+ * the Panel holds no copy, so a second Panel on the same Core reads the
848
+ * same values, the way pin state already behaves.
849
+ *
850
+ * `savedSkipPermissions` is carried for symmetry with the column that
851
+ * already exists; nothing on the launch path reads it. Auto-mode is the
852
+ * unconditional default for every Harness that has such a flag, so wiring
853
+ * this back into a session launch would reintroduce the removed control.
854
+ */
855
+ rememberHarnessSettings: boolean;
856
+ savedHarness: string | null;
857
+ savedSkipPermissions: boolean;
858
+ savedBareSession: boolean;
859
+ defaultGridView: boolean;
860
+ updatedAt: number;
861
+ };
862
+ /**
863
+ * A flattened task snapshot carried over the core-link. The Core is the
864
+ * source of truth for tasks; the Panel holds none. The shape mirrors the
865
+ * server's task row so the Panel can render a fleet view without a separate
866
+ * HTTP round-trip per task.
867
+ */
868
+ export type CoreLinkTaskSnapshot = {
869
+ taskId: string;
870
+ projectId: string;
871
+ title: string;
872
+ /**
873
+ * True once an operator has renamed this Session (issue 84). The Core's
874
+ * title generator refuses to write over a row carrying it, and the flag
875
+ * lives on the row rather than in Panel memory so the protection survives a
876
+ * Panel reload and a generator that finishes after the rename. The Panel
877
+ * renders from this field instead of synthesizing `false`, which made every
878
+ * Core-owned Session look un-renamed.
879
+ */
880
+ titleManuallySet: boolean;
881
+ /**
882
+ * The harness's own session id for this Task, or `null` before one has been
883
+ * observed (issue 84). The Core's hook pipeline reads it to tell a hook from
884
+ * this Session apart from one belonging to a session that has since been
885
+ * replaced.
886
+ */
887
+ claudeSessionId: string | null;
888
+ agent: string;
889
+ status: string;
890
+ pinned: boolean;
891
+ archived: boolean;
892
+ /**
893
+ * Session icon id — a stable string drawn from the {@link SESSION_ICON_OPTIONS}
894
+ * list (issue 09). `null` means the row has no user- or generator-assigned
895
+ * icon and the Panel should fall back to `DEFAULT_SESSION_ICON`. Icon is
896
+ * Core-owned metadata (ADR 0005); the Panel never persists it.
897
+ */
898
+ icon: string | null;
899
+ updatedAt: number;
900
+ /**
901
+ * This Session's lock, as it looks to the connection this snapshot was sent
902
+ * to (issue 145, ADR 0024 D8). See {@link CoreLinkSessionLock} — it says
903
+ * whether *you* may write, never who else holds it.
904
+ *
905
+ * Optional, and absent means "this Core does not publish lock state", which
906
+ * is the single-connection Core that shipped before the capability. Absence
907
+ * therefore yields exactly today's behaviour — no read-only rendering,
908
+ * because there is no lock table to be a Reader of — which is what lets this
909
+ * ride the `multiConnection` capability instead of moving
910
+ * {@link CORE_LINK_PROTOCOL_VERSION}, as #142, #143 and #144 did before it.
911
+ * A Core that announces `multiConnection` always sets it.
912
+ *
913
+ * Stamped by the server as the snapshot goes out, not read from a row: the
914
+ * lock table is in memory (D12) and the answer depends on who is asking, so
915
+ * no query port can produce it and no two recipients need get the same value.
916
+ */
917
+ lock?: CoreLinkSessionLock;
918
+ };
919
+ /** A session snapshot — a task's live or replayable conversation. */
920
+ export type CoreLinkSessionSnapshot = {
921
+ taskId: string;
922
+ ptyId: string | null;
923
+ status: string;
924
+ updatedAt: number;
925
+ /**
926
+ * This Session's lock, as it looks to the connection this snapshot was sent
927
+ * to (issue 145, ADR 0024 D8) — the same field, on the same terms, as the one
928
+ * on {@link CoreLinkTaskSnapshot}, because a client reading either of them is
929
+ * asking the same question about the same Session.
930
+ *
931
+ * This is the one `actana session ls` reads: it lists Sessions, and "can I
932
+ * write to this" is what it has to show about each. Optional for the reason
933
+ * given above — absent is the Core that does not publish lock state.
934
+ */
935
+ lock?: CoreLinkSessionLock;
936
+ };
937
+ /** A hook entry returned by `hooksOp`. */
938
+ export type CoreLinkHookEntry = {
939
+ hookId: string;
940
+ taskId: string | null;
941
+ enabled: boolean;
942
+ };
943
+ export type CoreLinkServerFrame = CoreLinkStreamFrame | CoreLinkEventFrame | CoreLinkEventsReplayedFrame | CoreLinkResponseFrame;
944
+ /**
945
+ * Protocol version advertised in the `ready` frame. Bumped on breaking changes.
946
+ * Issue 02 adds the event-cursor replay (`subscribe` / `event` /
947
+ * `eventsReplayed`) and the task/session/hook op frames → 0.2.0. Issue 04 adds
948
+ * the mTLS bearer `auth` / `authOk` / `authError` frames → 0.3.0. Issue 06 adds
949
+ * the `shellSession: true` VM-shell spawn mode (no `agent`, no project-root) →
950
+ * 0.4.0. Issue 07 adds the `projectsList` / `projectsListResult` frames for
951
+ * per-Core navigation (additive — same 0.4.0). Issue 04 lands the write path
952
+ * (`projectsMutate` + real `tasksMutate` handlers + real `sessionsList`) and
953
+ * discriminant-typed mutation shapes → 0.5.0. `tasksMutate`'s payload shape
954
+ * changed from a flat `{taskId, projectId?, status?, ...}` to a discriminated
955
+ * `{op: "create"|"update", ...}` union. `parseCoreLinkRequestFrame` only
956
+ * validates the outer `type`, so a stale-shape mutation payload lands at the
957
+ * mutation store's runtime `op` check and comes back as an actionable `error`
958
+ * frame ("unknown task mutation op: undefined"). No shipped Panel yet routes
959
+ * writes through this frame (the loopback API still owns local writes; remote
960
+ * writes were stubbed as `task: null`), so no live caller regresses. Issue 09
961
+ * adds the `icon` field to {@link CoreLinkTaskSnapshot} and the create/update
962
+ * variants of {@link CoreLinkTaskMutation}, plus a dedicated
963
+ * `task:iconChanged` event kind so replays surface icon-only edits distinctly
964
+ * from other task updates → 0.6.0. Issue 11 adds the `agentsAvailabilityList`
965
+ * request/response + the `agents:availabilityChanged` event kind → 0.7.0.
966
+ * Additive on both sides — a Core that has not been upgraded ignores the
967
+ * new request frame (the outer `parseCoreLinkRequestFrame` rejects unknown
968
+ * types), and a Panel hydrating from an older Core sees an empty
969
+ * availability map and falls back to the same "checking…" affordance a fresh
970
+ * boot shows. Issue 10 adds a dedicated `pin` op to
971
+ * {@link CoreLinkProjectMutation} plus the `project:pinnedChanged` /
972
+ * `task:pinnedChanged` event kinds (task pin-only updates now surface
973
+ * distinctly, mirroring the icon-only path from issue 09) → 0.8.0. Web-panel
974
+ * issue 06 adds the `dirList` / `dirCreate` request frames and their results,
975
+ * so the browser's folder picker browses the Core's disk instead of a
976
+ * machine-local dialog that no longer exists → 0.9.0. Additive: a Core that
977
+ * has not been upgraded rejects the unknown request type, which the Panel
978
+ * surfaces as the same actionable error any other failed listing produces.
979
+ * Issue 22 adds a `settings` op to {@link CoreLinkProjectMutation}, the same
980
+ * remembered-settings fields on its `create` variant, those fields on
981
+ * {@link CoreLinkProjectSnapshot}, and the `project:settingsChanged` event
982
+ * kind → 0.10.0. Every column they land in already exists in the shared
983
+ * schema bootstrap, so no migration rides along. Unlike the additive bumps
984
+ * above, there is no partial-compatibility story to describe here and none is
985
+ * wanted: the minor moved, so a Core still speaking 0.9.0 is incompatible by
986
+ * the major.minor rule below and renders as "needs update" (ADR 0005). It
987
+ * never reaches the mutation store's runtime `op` check.
988
+ * Issue 63 adds a `delete` op to {@link CoreLinkTaskMutation} and the
989
+ * `task:deleted` event kind the Core appends for it → 0.11.0. Deleting a
990
+ * Core-owned Session had no operation to carry, so every delete call site fell
991
+ * through to the Panel's own endpoint and 404'd. Same rule as above: the minor
992
+ * moved, so a Core on 0.10.0 is "needs update" rather than a Core that accepts
993
+ * the frame and silently drops the op.
994
+ * Issue 62 adds the `archivedTasksList` / `archivedTasksListResult` frames and
995
+ * an unconditional `archivedCount` on {@link CoreLinkServerFrame}'s
996
+ * `tasksListResult` → 0.12.0 (ADR 0019). Archived rows had no way across the
997
+ * link at all, so the Panel's Archived view was permanently empty for a Core
998
+ * and restore could not be invoked. The count is required rather than
999
+ * optional: a Core on 0.11.0 would answer without it and the Archived tab
1000
+ * would silently never appear, which is exactly the bug — so the minor moves
1001
+ * and such a Core renders as "needs update".
1002
+ * Issue 84 adds `titleManuallySet` and `claudeSessionId` to
1003
+ * {@link CoreLinkTaskSnapshot} and to the `update` variant of
1004
+ * {@link CoreLinkTaskMutation}, plus `hooksReportTurnStart` on the `spawned`
1005
+ * response, and the `harnessPrompt` frame that carries a terminal-captured
1006
+ * prompt to the Core that can act on it → 0.13.0 (ADR 0020). The snapshot
1007
+ * fields are the load-bearing part: a Core that grew them without this bump
1008
+ * would hand a Panel that predates it a snapshot whose rename protection is
1009
+ * silently absent, and a Panel that predates the Core would keep synthesizing
1010
+ * `false` — both render a Session the generator is free to rename out from
1011
+ * under its operator. Same rule as above: the minor moved, so either side on
1012
+ * 0.12.0 is "needs update" rather than a partial snapshot nobody notices. No
1013
+ * migration rides along — the columns (`title_manually_set`,
1014
+ * `claude_session_id`) have been in the shared schema bootstrap since the
1015
+ * fork; only the wire and the Core's readers/writers of them are new.
1016
+ * Issue 83 adds the `harnessInstall` request frame, its `harnessInstallAck`
1017
+ * response, and the `harness:installFailed` event kind → 0.14.0 (ADR 0021).
1018
+ * Availability crossed the link Core→Panel only, so a missing CLI was a wall:
1019
+ * the picker greyed the row out and told the operator to go run a command on
1020
+ * another machine. This is the trigger that was absent — the Core still owns
1021
+ * probing and installing, and still publishes availability as the one source of
1022
+ * truth. Same rule as the bumps above: the minor moved, so a Core on 0.13.0
1023
+ * renders as "needs update" rather than one that silently drops the frame.
1024
+ * Issue 98 adds an `appearance` op to {@link CoreLinkProjectMutation} and the
1025
+ * `project:appearanceChanged` event kind the Core appends for it → 0.15.0
1026
+ * (ADR 0022). A project's icon and icon colour are Core facts that `create`
1027
+ * could set and nothing could change afterwards, so the Edit-project dialog
1028
+ * PATCHed them at a Panel row a Core-owned project does not have, and 404'd with
1029
+ * the rename already applied. Same rule as above: the minor moved, so a Core on
1030
+ * 0.14.0 is "needs update" rather than one that takes the frame and drops the
1031
+ * op. The columns (`icon`, `icon_color`) have been in the shared schema
1032
+ * bootstrap since the fork, so no migration rides along on the Core's side.
1033
+ *
1034
+ * Issue 142 adds the `ptySubscribe` / `ptyUnsubscribe` frames and their acks —
1035
+ * and deliberately **does not move this version** (ADR 0024 D11). Every bump
1036
+ * above marks every Core in every fleet needs-update, and multi-connection is a
1037
+ * capability a one-Panel fleet never exercises. It is announced on `ready`
1038
+ * instead, by #143 below, which also gates the Panel's sends on it, so these
1039
+ * frames are never put on the wire to a Core that cannot answer them.
1040
+ *
1041
+ * Issue 143 adds the optional `multiConnection` capability to the `ready`
1042
+ * frame and deliberately does NOT move this version (ADR 0024 D11). It is the
1043
+ * first entry in this log that does not: the field is additive and its absence
1044
+ * is the single-connection Core that shipped before it, which is exactly
1045
+ * today's behaviour rather than a lesser one. Moving the minor would mark every
1046
+ * Core in every fleet "needs update" to buy a capability a one-Panel fleet
1047
+ * never exercises. A capability that changed what an existing frame means
1048
+ * would not qualify, and the minor would move as it does above.
1049
+ *
1050
+ * Issue 144 adds the Session lock's `claim` / `release` / `forceTakeover`
1051
+ * frames, their results, and the optional `code` on `error` — and does NOT move
1052
+ * this version either (ADR 0024 D11), for the same reason #142 and #143 did not.
1053
+ * The three frames are gated by the `multiConnection` capability, so they are
1054
+ * never put on the wire to a Core that has no lock table to address. The gating
1055
+ * of `write` / `kill` / `tasksMutate` is additive in the sense the rule
1056
+ * requires: it can only refuse a Session that some connection has explicitly
1057
+ * claimed, and a client that never claims — every client that predates this —
1058
+ * meets an unlocked Session and is served exactly as it is today. `code` is
1059
+ * optional and absent from every error frame that shipped before it, so a client
1060
+ * that has never heard of it reads the same `message` it always did.
1061
+ *
1062
+ * Issue 145 publishes that lock state — the optional `lock` on
1063
+ * {@link CoreLinkTaskSnapshot} and {@link CoreLinkSessionSnapshot}, and the
1064
+ * {@link SESSION_LOCK_CHANGED_EVENT_KIND} event — and does NOT move this version
1065
+ * either (ADR 0024 D11), for the fourth time and the same reason. Note that
1066
+ * issue 84 *did* bump for snapshot fields, and the difference is the rule
1067
+ * itself: a Panel that predated `titleManuallySet` synthesized `false` and
1068
+ * rendered a Session its Core's generator could rename out from under the
1069
+ * operator — the absence yielded something *lesser* than today. A Panel that
1070
+ * predates `lock` ignores an unknown field and renders exactly what it renders
1071
+ * today, because read-only rendering is new surface (#147) rather than a
1072
+ * protection quietly going missing; a new Panel against a Core that omits the
1073
+ * field reads "no lock table here", which is that Core, truthfully. The event
1074
+ * kind is additive in the way every kind before it was: a client routes on kinds
1075
+ * it knows and ignores the rest.
1076
+ *
1077
+ * Issue 146 adds the `reclaim` frame and its `reclaimResult` — and does NOT
1078
+ * move this version either (ADR 0024 D11), for the fifth time and the same
1079
+ * reason. The frame is gated by the `multiConnection` capability, as #142 and
1080
+ * #144 are, so it is never put on the wire to a Core with no lock table to
1081
+ * transfer out of — and a Core that predates the capability evicts every client
1082
+ * but one on connect, which is the reclaim the frame would have asked it for.
1083
+ * The client that presents no id — every client that shipped before this — is
1084
+ * served exactly as it is today: its lost socket is reaped by the heartbeat 45s
1085
+ * later, still the backstop for a client that never comes back. What the frame
1086
+ * buys is those 45 seconds for the client that does come back, so its absence
1087
+ * is slower rather than lesser, which is the rule. `clientId` is not identity
1088
+ * and carries no authority a connection did not already have (D10), so there is
1089
+ * nothing here for an older client to be missing.
1090
+ */
1091
+ export declare const CORE_LINK_PROTOCOL_VERSION = "0.15.0";
1092
+ /**
1093
+ * Does a Core advertising `reported` speak this build's core-link?
1094
+ *
1095
+ * The rule is major.minor equality, and it is deliberately blunt: ADR 0005 says
1096
+ * the Panel carries no feature detection, so there is no "mostly compatible"
1097
+ * state to describe. A Core is either speaking this vocabulary or it is a chore
1098
+ * — one command on that machine — and the Panel says so rather than degrading.
1099
+ * Patch is ignored because the version above only moves on wire changes;
1100
+ * reserving the patch segment for fixes that touch no frame keeps a
1101
+ * behaviour-only release from grounding a fleet.
1102
+ *
1103
+ * A missing or unparseable version is incompatible: every Core that speaks
1104
+ * a version the Panel could accept says so in its `ready` frame, so silence is
1105
+ * evidence of something older than the frame that carries it.
1106
+ */
1107
+ export declare function coreLinkProtocolCompatible(reported: string | null | undefined): boolean;
1108
+ /**
1109
+ * Read the `multiConnection` capability off a raw `ready` frame, or null.
1110
+ *
1111
+ * Null for every shape that is not exactly `{ version: 1 }` — absent, a
1112
+ * non-object, or a version this build does not know. An unrecognised future
1113
+ * version reads as absent on purpose: the safe fallback is the single-connection
1114
+ * behaviour that predates the capability, never a guess at what a higher version
1115
+ * means. Unlike {@link coreLinkProtocolCompatible}, nothing here can mark a Core
1116
+ * incompatible — a null answer withholds frames, it does not fail a Core.
1117
+ */
1118
+ export declare function readMultiConnectionCapability(raw: unknown): CoreLinkMultiConnectionCapability | null;
1119
+ /** Parse and validate a raw WS message into a known request frame, or null. */
1120
+ export declare function parseCoreLinkRequestFrame(raw: string): CoreLinkRequestFrame | null;
1121
+ /** Serialize a server frame for sending over the WebSocket. */
1122
+ export declare function serializeCoreLinkFrame(frame: CoreLinkServerFrame): string;
1123
+ //# sourceMappingURL=core-link-frames.d.ts.map