@platless/axom-cli 0.3.13 → 0.5.9

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 (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. package/dist/workstation/tools/terminal.js +0 -214
@@ -0,0 +1,330 @@
1
+ /**
2
+ * ## Subagent relations bypass the window (fix round 1, spec §5.6)
3
+ *
4
+ * The server holds back its applied watermark — so its acks — at the first child frame whose subagent relation (`subagent_spawned`,
5
+ * the server's `TimelineEvent.SubagentSpawned`) it has not received yet (`noteApplied` capped at `nativePendingBefore - 1`,
6
+ * `ExternalAgentEventApplier.kt`). Every later frame then counts against the window; if the relation comes 256 or more frames after
7
+ * that first orphan child frame, a window that held it back would stall the session for good (the server waits 30 s, poisons, the
8
+ * reopen repeats it). So:
9
+ *
10
+ * - **producing**: while a child frame whose relation is not recorded yet is unacknowledged ({@link orphanOutstanding}), the record
11
+ * loop may run past the window, so the relation can be folded at all. Whatever the branch, this life never holds more than
12
+ * {@link RELATION_WAIT_EVENTS} / {@link RELATION_WAIT_BYTES} (1024 frames / 32 MiB) unacknowledged — pinned entries included — plus
13
+ * the frames of the one record being folded: that is the daemon's memory bound per session;
14
+ * - **sending**: once a relation that resolves such a child (a *resolving* relation) is recorded, it and every entry before it stop
15
+ * counting against either window ({@link pinnedThrough}): the sender takes them past the window, in seq order (sending the
16
+ * relation first would make the server drop the lower seqs as duplicates); the socket gate still applies. They must keep not
17
+ * counting after they are sent: the server applied them but wasted its acks while capped (each ack attempt resets its 32-event
18
+ * count), so its next ack needs up to 31 more applied frames — a window still full of them would deadlock there. They leave the
19
+ * outbox with that ack. A relation that resolves nothing recorded is not what the server waits for and keeps to the window
20
+ * (sending past it could overflow a parked server's 512-frame hold-off).
21
+ *
22
+ * ## Residual (documented, not fixed)
23
+ *
24
+ * While the producing window is full — a server parked on an approval with ≥ 256 root frames held, a connection that is down, an
25
+ * applier that never acknowledges — the record loop folds nothing: a steer or config RPC waits for the processor's reply and can
26
+ * time out on the server; an `agent_interrupt` note is folded (and the `session/cancel` written) only once the server unparks.
27
+ */
28
+ /** Unacknowledged events per session (spec §5.6, `W_EVT`). `ACK_EVERY_EVENTS(32) < W_EVT ≤ HOLD_OFF_CAPACITY(512)`. */
29
+ export const SESSION_WINDOW_EVENTS = 256;
30
+ /** Unacknowledged bytes per session (spec §5.6): a few huge frames cannot hog the window. */
31
+ export const SESSION_WINDOW_BYTES = 8 * 1024 * 1024;
32
+ /** How far production may run past the window while a recorded child frame still waits for its relation (bounds memory). */
33
+ export const RELATION_WAIT_EVENTS = 4 * SESSION_WINDOW_EVENTS;
34
+ export const RELATION_WAIT_BYTES = 4 * SESSION_WINDOW_BYTES;
35
+ /** A frame carrying a subagent relation (the server's `SubagentSpawned`): it bypasses the session window. */
36
+ export function isRelationFrame(frame) {
37
+ return frame.t === 'agent_event' && frame.eventName === 'subagent_spawned';
38
+ }
39
+ /**
40
+ * `src.i` of an outbox entry no record produced: a frame this daemon numbered itself (a degraded session's failed `turn_done`,
41
+ * `src.o` = the last record folded before it) or one imported from a legacy journal (`src.o` = 0). A replay never reproduces it
42
+ * (T21 skips it when comparing). Far above any per-record frame index.
43
+ */
44
+ export const DAEMON_FRAME_INDEX = 1 << 24;
45
+ /**
46
+ * Connection epoch of an entry that may have reached the server on an earlier connection (or in an earlier daemon life): it is not
47
+ * sent again until the server reopens the ref and says how far it got ({@link SessionOutbox.resendAll} after an `ack` of the open's
48
+ * `resumeFromSeq`) — resending a frame the server already applied could, for a `turn_done`, end a newer turn (analysis B §3.1).
49
+ */
50
+ export const SENT_EARLIER = -1;
51
+ const frameBytes = (frame) => Buffer.byteLength(JSON.stringify(frame));
52
+ export function isOutboxTail(value) {
53
+ if (typeof value !== 'object' || value === null)
54
+ return false;
55
+ const { seq, src } = value;
56
+ return Number.isSafeInteger(seq) && typeof src === 'object' && src !== null &&
57
+ Number.isSafeInteger(src.o) && Number.isSafeInteger(src.i);
58
+ }
59
+ export class SessionOutbox {
60
+ store;
61
+ items = [];
62
+ committed;
63
+ highestSeq = 0;
64
+ ownEvents = 0;
65
+ ownBytes = 0;
66
+ chain = Promise.resolve();
67
+ roomWaiters = new Set();
68
+ listeners = new Set();
69
+ stopped = false;
70
+ /** Children whose relation frame this outbox recorded. */
71
+ related = new Set();
72
+ /** Seq of the latest resolving relation: entries at or below it count against neither window (file comment). */
73
+ pinned = 0;
74
+ isRelated;
75
+ /**
76
+ * `isRelated`: whether the session already knows a child's relation from elsewhere (the processor's relation graph — a relation
77
+ * recorded by an earlier life, or replayed by a load). Without it only relations this outbox recorded count.
78
+ */
79
+ constructor(store, init = {}) {
80
+ this.store = store;
81
+ this.isRelated = init.isRelated ?? (() => false);
82
+ this.committed = isOutboxTail(init.committedTail) ? { seq: init.committedTail.seq, src: { ...init.committedTail.src } } : null;
83
+ this.highestSeq = this.committed?.seq ?? 0;
84
+ for (const record of init.leftovers ?? []) {
85
+ // Read back past the watermark only; a record at or below it is acknowledged (truncation is by whole segments).
86
+ if (this.committed && record.seq <= this.committed.seq)
87
+ continue;
88
+ this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes: frameBytes(record.frame), leftover: true,
89
+ ...this.classify(record.seq, record.frame), sentOn: SENT_EARLIER });
90
+ this.highestSeq = Math.max(this.highestSeq, record.seq);
91
+ }
92
+ }
93
+ /**
94
+ * Resolving relation / orphan frame, in record order (a relation relates its child for every later frame). An orphan is a frame
95
+ * the server retains until a relation it has not received: a child frame (`acpSessionId`) whose child is unrelated, and — Task
96
+ * 22 — a `subagent_state_update` (any origin) naming an unrelated child (`payload.subagentSessionId`): the server's
97
+ * `receiveNativeEventLocked` holds it, and with it its acks, until that child's `subagent_spawned` exactly as it holds a child
98
+ * frame. The state update's own child is the key: a root-origin one would otherwise not be an orphan at all, its relation would
99
+ * keep to the window, and the session would stall where Task 20 fixed it for child frames.
100
+ */
101
+ classify(seq, frame) {
102
+ const unrelated = (child) => child !== undefined && !this.related.has(child) && !this.isRelated(child) ? child : undefined;
103
+ const origin = frame.t === 'agent_event' && typeof frame.acpSessionId === 'string' ? frame.acpSessionId : undefined;
104
+ const stateChild = frame.t === 'agent_event' && frame.eventName === 'subagent_state_update'
105
+ ? frame.payload?.subagentSessionId
106
+ : undefined;
107
+ const orphanOf = unrelated(typeof stateChild === 'string' ? stateChild : undefined) ?? unrelated(origin);
108
+ let resolving = false;
109
+ if (isRelationFrame(frame)) {
110
+ const child = frame.payload?.subagentSessionId;
111
+ if (typeof child === 'string') {
112
+ resolving = this.items.some((item) => item.orphanOf === child);
113
+ this.related.add(child);
114
+ if (resolving)
115
+ this.pinned = Math.max(this.pinned, seq);
116
+ }
117
+ }
118
+ return { resolving, ...(orphanOf !== undefined ? { orphanOf } : {}) };
119
+ }
120
+ /** Entries at or below this seq count against neither window (the latest resolving relation; 0: none). */
121
+ get pinnedThrough() { return this.pinned; }
122
+ /** An unacknowledged child frame whose relation is still not recorded: the server is (likely) holding its acks for it. */
123
+ orphanOutstanding() {
124
+ return this.items.some((item) => item.orphanOf !== undefined && !this.related.has(item.orphanOf) && !this.isRelated(item.orphanOf));
125
+ }
126
+ /** Highest seq this outbox ever held (entries, or the acknowledged watermark): a new life numbers after it (§5.5). */
127
+ get lastSeq() { return this.highestSeq; }
128
+ /** The acknowledged watermark (`committedTail`), `null` before the first truncating ack. */
129
+ get committedTail() { return this.committed; }
130
+ /** Unacknowledged entries, in seq order. */
131
+ get pending() { return this.items; }
132
+ /** Unacknowledged entries appended by this life (the producing window), past {@link pinnedThrough}. */
133
+ get backlog() {
134
+ if (this.pinned === 0)
135
+ return { events: this.ownEvents, bytes: this.ownBytes };
136
+ let events = 0;
137
+ let bytes = 0;
138
+ for (const item of this.items)
139
+ if (!item.leftover && item.seq > this.pinned) {
140
+ events += 1;
141
+ bytes += item.bytes;
142
+ }
143
+ return { events, bytes };
144
+ }
145
+ /**
146
+ * Entries sent on connection `epoch` and not acknowledged (the sending window). Entries at or below {@link pinnedThrough} are
147
+ * sent past the window either way (the sender bypasses it for them); once sent they count only when `pinnedCountsWhenSent` —
148
+ * a server that acknowledges as soon as the relation releases its cap (`AGENT_WIRE_RELATION_ACK_VERSION`, Task 22). An older
149
+ * server wastes its acks while capped, so its first ack after the relation can need up to 31 more applied frames: counting the
150
+ * pinned ones there would deadlock right after the relation (fix round 1 of Task 20).
151
+ */
152
+ inflight(epoch, pinnedCountsWhenSent = false) {
153
+ let events = 0;
154
+ let bytes = 0;
155
+ for (const item of this.items) {
156
+ if (item.sentOn === epoch && (pinnedCountsWhenSent || item.seq > this.pinned)) {
157
+ events += 1;
158
+ bytes += item.bytes;
159
+ }
160
+ }
161
+ return { events, bytes };
162
+ }
163
+ /** Called whenever there may be something new to send (an append, an ack opening the window, a resend). */
164
+ onChange(listener) {
165
+ this.listeners.add(listener);
166
+ return () => { this.listeners.delete(listener); };
167
+ }
168
+ changed() { for (const listener of [...this.listeners])
169
+ listener(); }
170
+ run(op) {
171
+ const result = this.chain.then(op);
172
+ this.chain = result.then(() => undefined, () => undefined);
173
+ return result;
174
+ }
175
+ /** Waits for every stream operation queued so far. */
176
+ flush() { return this.chain; }
177
+ /**
178
+ * Record frames before they are sent. Resolves once they are durable; rejects when the stream refused them (the caller degrades
179
+ * the session — a frame that could not be recorded is not sent either).
180
+ */
181
+ append(frames) {
182
+ if (frames.length === 0)
183
+ return this.chain;
184
+ return this.run(async () => {
185
+ const entries = frames.map((f) => ({ seq: f.seq, src: { o: f.src.o, i: f.src.i }, frame: { ...f.frame } }));
186
+ const { first } = await this.store.append(entries);
187
+ entries.forEach((entry, index) => {
188
+ const bytes = frameBytes(entry.frame);
189
+ this.items.push({ o: first + index, seq: entry.seq, src: entry.src, frame: entry.frame, bytes, leftover: false, ...this.classify(entry.seq, entry.frame), sentOn: null });
190
+ this.highestSeq = Math.max(this.highestSeq, entry.seq);
191
+ this.ownEvents += 1;
192
+ this.ownBytes += bytes;
193
+ });
194
+ this.wakeRoom(); // a resolving relation unpins the producing window
195
+ this.changed();
196
+ });
197
+ }
198
+ /**
199
+ * The server applied everything through `throughSeq` (`agent_event_ack`, or an open's `resumeFromSeq`): forget those entries and
200
+ * truncate the stream through the last of them, persisting `{seq, src}` of that entry as the watermark. Nothing held at or below
201
+ * it: nothing changes. A failed commit is reported (`onError`) — the entries are acknowledged either way; the stream keeps them
202
+ * until a later commit passes them.
203
+ */
204
+ ack(throughSeq, onError) {
205
+ return this.run(async () => {
206
+ let k = -1;
207
+ for (let i = 0; i < this.items.length && this.items[i].seq <= throughSeq; i++)
208
+ k = i;
209
+ if (k < 0)
210
+ return;
211
+ const last = this.items[k];
212
+ const removed = this.items.splice(0, k + 1);
213
+ for (const item of removed)
214
+ if (!item.leftover) {
215
+ this.ownEvents -= 1;
216
+ this.ownBytes -= item.bytes;
217
+ }
218
+ this.committed = { seq: last.seq, src: { ...last.src } };
219
+ this.wakeRoom();
220
+ this.changed();
221
+ try {
222
+ await this.store.commit({ outbox: { through: last.o, tail: { seq: last.seq, src: { ...last.src } } } });
223
+ }
224
+ catch (error) {
225
+ onError?.(error);
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * A processor checkpoint at raw `offset`, and raw truncation through it (the keeper refuses anything past it) unless `truncate` is
231
+ * false (a marker checkpoint, Task 21).
232
+ */
233
+ commitCheckpoint(offset, blob, truncate = true) {
234
+ return this.run(async () => {
235
+ await this.store.commit({ checkpoint: { offset, blob }, ...(truncate ? { rawThrough: offset } : {}) });
236
+ });
237
+ }
238
+ /**
239
+ * The session re-attached to keeper-acp after its connection was lost (Task 21): the stream is the truth again. An append whose
240
+ * reply was lost may have landed (it is in `leftovers` now) or not (it is gone, and the record loop folds its record again). The
241
+ * entries are replaced by what the stream holds past its watermark; an entry this object already had keeps whether (and on which
242
+ * connection) it was sent and whether an earlier life left it; one it never saw was never sent. Call it with no stream operation
243
+ * in flight ({@link flush} first). Listeners and room waiters are kept.
244
+ */
245
+ rebase(init) {
246
+ const previous = new Map(this.items.map((item) => [item.seq, item]));
247
+ this.items.length = 0;
248
+ this.related.clear();
249
+ this.pinned = 0;
250
+ this.ownEvents = 0;
251
+ this.ownBytes = 0;
252
+ if (isOutboxTail(init.committedTail) && (this.committed === null || init.committedTail.seq >= this.committed.seq)) {
253
+ this.committed = { seq: init.committedTail.seq, src: { ...init.committedTail.src } };
254
+ }
255
+ this.highestSeq = Math.max(this.highestSeq, this.committed?.seq ?? 0);
256
+ for (const record of init.leftovers) {
257
+ if (this.committed && record.seq <= this.committed.seq)
258
+ continue;
259
+ const before = previous.get(record.seq);
260
+ const leftover = before?.leftover ?? false;
261
+ const bytes = frameBytes(record.frame);
262
+ this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes, leftover,
263
+ ...this.classify(record.seq, record.frame), sentOn: before ? before.sentOn : null });
264
+ if (!leftover) {
265
+ this.ownEvents += 1;
266
+ this.ownBytes += bytes;
267
+ }
268
+ this.highestSeq = Math.max(this.highestSeq, record.seq);
269
+ }
270
+ this.wakeRoom();
271
+ this.changed();
272
+ }
273
+ /** Mark every unacknowledged entry unsent: the sender resends them in order (a reopen's replay, after the ack of its watermark). */
274
+ resendAll() {
275
+ for (const item of this.items)
276
+ item.sentOn = null;
277
+ this.changed();
278
+ }
279
+ /**
280
+ * A new connection: everything recorded so far — sent on an earlier connection or produced while none was bound — is held until
281
+ * the server reopens the ref and says how far it got ({@link SENT_EARLIER}); what is recorded from now on is sent at once. (The
282
+ * journal's semantics: frames produced while unbound were replayed by the reopen, never sent on their own.)
283
+ */
284
+ connectionChanged() {
285
+ for (const item of this.items)
286
+ item.sentOn = SENT_EARLIER;
287
+ }
288
+ /** The first entry never sent (the sender's next candidate), in seq order. */
289
+ nextUnsent() {
290
+ return this.items.find((item) => item.sentOn === null);
291
+ }
292
+ /** Resolves when this life's unacknowledged backlog is below the window (immediately once {@link stopWaiting} ran). */
293
+ room() {
294
+ if (this.hasRoom())
295
+ return undefined;
296
+ return new Promise((resolve) => { this.roomWaiters.add(resolve); });
297
+ }
298
+ hasRoom() {
299
+ if (this.stopped)
300
+ return true;
301
+ // A hard bound on everything this life holds unacknowledged, pinned or not: chained resolving relations each reset `backlog`,
302
+ // but pinned entries leave memory only with an ack.
303
+ if (this.ownEvents >= RELATION_WAIT_EVENTS || this.ownBytes >= RELATION_WAIT_BYTES)
304
+ return false;
305
+ const { events, bytes } = this.backlog;
306
+ if (events < SESSION_WINDOW_EVENTS && bytes < SESSION_WINDOW_BYTES)
307
+ return true;
308
+ // Past the window only while a relation is awaited: the server cannot acknowledge before it, so it must be folded.
309
+ return this.orphanOutstanding();
310
+ }
311
+ wakeRoom() {
312
+ if (!this.hasRoom())
313
+ return;
314
+ for (const resolve of [...this.roomWaiters])
315
+ resolve();
316
+ this.roomWaiters.clear();
317
+ }
318
+ /** The session is closing or degraded: nobody waits for room any more. */
319
+ stopWaiting() {
320
+ this.stopped = true;
321
+ this.wakeRoom();
322
+ }
323
+ }
324
+ /** An outbox store over a transport session (`acp.outbox.append` / `acp.commit`). */
325
+ export function transportOutboxStore(transport, sessionKey) {
326
+ return {
327
+ append: (entries) => transport.appendOutbox(sessionKey, entries),
328
+ commit: (params) => transport.commit(sessionKey, params),
329
+ };
330
+ }
@@ -20,9 +20,10 @@ function validateCondition(id, when, until, whenKey, untilKey) {
20
20
  export function validateOverride(entry) {
21
21
  validateCondition(entry.id, entry.when_registry_version, entry.until_registry_version, 'when_registry_version', 'until_registry_version');
22
22
  const until = entry.until_registry_version;
23
- // A pin at or past its own bound would be replaced by the registry the moment it applied.
24
- if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lt(entry.distribution.version, until))) {
25
- throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must sort below ${until}`);
23
+ // A catch-up pin may equal its bound: use it until the registry publishes that release.
24
+ // A pin above the bound would instead let the registry replace it with an older release.
25
+ if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lte(entry.distribution.version, until))) {
26
+ throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must not sort above ${until}`);
26
27
  }
27
28
  }
28
29
  export function validatePackagePatch(recipe) {
@@ -61,3 +62,19 @@ export const packagePatches = runtime.package_patches.map((file) => {
61
62
  validatePackagePatch(recipe);
62
63
  return recipe;
63
64
  });
65
+ /** The declared native wrappers, for the catalog↔patch consistency guard in `catalog.test.ts`. */
66
+ export const nativeExtensions = runtime.native_extensions ?? [];
67
+ /** The bundled overrides, for the same catalog↔patch consistency guard. */
68
+ export const runtimeOverrides = runtime.overrides;
69
+ /** Native wrappers are declared in the patch inventory, never selected by agent-specific launch branches. */
70
+ export function nativeExtensionFor(agent, version) {
71
+ const extension = runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version));
72
+ if (!extension)
73
+ return undefined;
74
+ if (extension.entry.startsWith('/') || extension.entry.split('/').includes('..'))
75
+ throw new Error('Invalid native extension path');
76
+ return new URL(extension.entry, patchAssetRoot);
77
+ }
78
+ export function nativeAccountLimitsVersionFor(agent, version) {
79
+ return runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version))?.accountLimitsVersion ?? 2;
80
+ }
@@ -78,7 +78,6 @@
78
78
  * complete it, so `authenticate` remembers every id it handed out and retracts what is left when
79
79
  * the login settles.
80
80
  */
81
- import { randomUUID } from 'node:crypto';
82
81
  import { methods } from '@agentclientprotocol/sdk';
83
82
  import { validateWireEvent } from './validate.js';
84
83
  /** A value guaranteed not to equal any real `optionId` in `input.options` for a given call — used to purge a pending entry without accidentally supplying a matchable answer. Regenerated per call rather than a fixed literal, for the same anti-collision reasoning as `AcpSession`'s own abandonment sentinel (review finding, task-5 item 6). */
@@ -123,8 +122,9 @@ function toolCallPayload(toolCall) {
123
122
  * ACP `RequestPermissionRequest` (`types.gen.d.ts:108`) on Axom's wire: `{toolCall, options}` plus
124
123
  * `requestId`.
125
124
  *
126
- * `requestId` is a host-generated resolver token. The raw JSON-RPC ID stays local to its
127
- * connection; exact `sessionId` travels in the wire event's `acpSessionId` origin field.
125
+ * `requestId` is a host-derived resolver token — {@link permissionRequestId}, the same on the live
126
+ * path and in the ACP processor. The raw JSON-RPC ID stays local to its connection; exact `sessionId`
127
+ * travels in the wire event's `acpSessionId` origin field.
128
128
  *
129
129
  * **`toolCall` is nested, not flattened.** The flattened version lifted the tool's
130
130
  * `name`/`title`/`kind`/`arguments` to the top level, which put `name` (the tool's programmatic name,
@@ -135,7 +135,7 @@ function toolCallPayload(toolCall) {
135
135
  * `toolCall.title`, and the renderer computes it from the same field now
136
136
  * (`WorkTimelineInteractions.foldPermission`) instead of storing a second, drifting copy.
137
137
  */
138
- function buildPermissionRequestedEvent(input) {
138
+ export function buildPermissionRequestedEvent(input) {
139
139
  return {
140
140
  eventName: 'permission_request',
141
141
  ...(input.sessionId !== undefined ? { acpSessionId: input.sessionId } : {}),
@@ -164,7 +164,7 @@ function buildPermissionRequestedEvent(input) {
164
164
  * `sink.answer` — the agent gave up on this specific request, so nothing should still expect an
165
165
  * answer for it. Session-level abandonment (the session closing, or the connection dying) is instead
166
166
  * `AcpSession`'s own job (`registerPendingApproval` resolving immediately when already closed, and
167
- * `runReadLoop` draining on an aborted connection) — see `acpSession.ts`.
167
+ * `runReadLoop` draining on an aborted connection) — see the old `acpSession.ts` (deleted in Task 29).
168
168
  *
169
169
  * The eventual answer is whatever string reaches `sink`'s registered resolver: normally one of
170
170
  * `input.options`' own `optionId`s (relayed from the user through `AcpSession.answer`), but both
@@ -320,7 +320,7 @@ function isFoldedCompanionKey(key, properties) {
320
320
  * each return `null` from `elicitationRequestFromPayload`, because `MODE_FORM` requires
321
321
  * `fields.isNotEmpty()`. Everything else it now decodes (see {@link handleFormElicitation}).
322
322
  */
323
- function elicitationFieldKeys(requestedSchema) {
323
+ export function elicitationFieldKeys(requestedSchema) {
324
324
  const properties = requestedSchema?.properties;
325
325
  if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
326
326
  return [];
@@ -336,7 +336,7 @@ function elicitationFieldKeys(requestedSchema) {
336
336
  * request is dropped server-side, which is why this check survived Task 9's removal of the
337
337
  * per-property profile predicate (see the ruling-R24 block below).
338
338
  */
339
- function isBlank(value) {
339
+ export function isBlank(value) {
340
340
  return value.trim().length === 0;
341
341
  }
342
342
  /**
@@ -406,7 +406,7 @@ function isBlank(value) {
406
406
  * event always carries exactly what the agent asked; {@link handleFormElicitation} is what decides,
407
407
  * from that same schema, whether to wait for an answer or decline right after raising the card.
408
408
  */
409
- function buildElicitationRequestedEvent(request, requestId) {
409
+ export function buildElicitationRequestedEvent(request, requestId) {
410
410
  return {
411
411
  eventName: 'elicitation_request',
412
412
  payload: {
@@ -521,6 +521,35 @@ async function handleFormElicitation(request, ctx, sink) {
521
521
  export function handleElicitationComplete(notification, deps) {
522
522
  deps.dismiss(notification.elicitationId);
523
523
  }
524
+ /**
525
+ * The card id of one `session/request_permission`: `perm:<hostGeneration>:<JSON agent id>` (spec
526
+ * 2026-10-11 §5.4).
527
+ *
528
+ * ## Why derived, not random
529
+ *
530
+ * It used to be `randomUUID()` per invocation. A card id the server is already showing must still
531
+ * resolve after the daemon restarts underneath a live adapter, and a random id cannot: the re-presented
532
+ * request would get a new one, `settleApproval` ignores unknown ids, and the user's answer to the
533
+ * visible card would be lost (analysis A §4). Derived from the request itself, the same request gets
534
+ * the same id in every daemon generation, and the ACP processor — which may not draw randomness at
535
+ * all (`processor/guard.test.ts`) — computes it with this very function.
536
+ *
537
+ * ## Why it is still unique
538
+ *
539
+ * - `hostGeneration` identifies one adapter process: the keeper's `spawnId` on the processor path,
540
+ * `AgentRuntimeDiagnostics.generation` (fresh per `connectAgent`) on today's live path — the same
541
+ * value published as `callbackContext.hostGeneration`. A reconnect is a new process, so a new
542
+ * generation: the agent's ids restart at 0 there without colliding with an earlier card.
543
+ * - Within one process the agent never reuses a pending JSON-RPC id. `JSON.stringify` keeps the
544
+ * number `1` and the string `"1"` apart.
545
+ *
546
+ * Kept short enough for `AcpSession.registerPendingApproval`'s 256-byte rule for every id a real
547
+ * adapter sends; a pathological id longer than ~200 bytes is refused there, as an overlong
548
+ * `rpcRequestId` already was.
549
+ */
550
+ export function permissionRequestId(hostGeneration, agentRequestId) {
551
+ return `perm:${hostGeneration}:${JSON.stringify(agentRequestId)}`;
552
+ }
524
553
  /**
525
554
  * Register this module's three handlers onto `app`. Must run inside `connectAgent`'s `configure`
526
555
  * hook — see the file doc comment for why a post-connect `.onRequest`/`.onNotification` would compile
@@ -534,15 +563,18 @@ export function handleElicitationComplete(notification, deps) {
534
563
  * the call site instead. Its own `present` may still be omitted (see {@link PresentElicitationUrl}) —
535
564
  * that is `acpAgents.ts`'s session connection saying, on purpose, that a `url` elicitation raised
536
565
  * outside a login has nowhere to go, and it declines rather than accepting into nothing.
566
+ *
567
+ * `connection.generation` is the adapter process's generation — `connectAgent` hands it to its
568
+ * `configure` hook — and scopes the card ids {@link permissionRequestId} derives.
537
569
  */
538
- export function registerPermissionHandlers(app, sink, elicitationDeps) {
570
+ export function registerPermissionHandlers(app, sink, elicitationDeps, connection) {
539
571
  app
540
572
  .onRequest(methods.client.session.requestPermission, async (ctx) => {
541
573
  if (typeof ctx.params.sessionId !== 'string' || !ctx.params.sessionId.trim()) {
542
574
  console.error(JSON.stringify({ component: 'acp', error: 'invalid_permission_origin', rpcRequestId: String(ctx.requestId) }));
543
575
  return { outcome: { outcome: 'cancelled' } };
544
576
  }
545
- const decision = await handleRequestPermission({ requestId: randomUUID(), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
577
+ const decision = await handleRequestPermission({ requestId: permissionRequestId(connection.generation, ctx.requestId), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
546
578
  return {
547
579
  outcome: decision.outcome === 'answered' ? { outcome: 'selected', optionId: decision.optionId } : { outcome: 'cancelled' },
548
580
  };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Processor checkpoints (spec 2026-10-11 §5.5): the state after record `offset`, as JSON.
3
+ *
4
+ * ## Two versions, two questions
5
+ *
6
+ * - `schema` answers "can this daemon **read** it?". An unknown schema throws
7
+ * {@link CheckpointUnreadableError}; the caller then replays from the session's start (outbox and
8
+ * watermark cover the frames already sent), or, if the log was truncated, marks the session
9
+ * degraded and switches it at the next open (§5.5.6). Readability of older schemas is a test-guarded
10
+ * promise: never change the meaning of an existing field without bumping `schema` and adding a
11
+ * reader for the old one.
12
+ * - `processorVersion` answers "would **this** code have produced the same frames?". Bump it whenever
13
+ * `step` can emit different output for the same records. A readable checkpoint from another
14
+ * processor version is still restored — that is the whole point of cutting a head checkpoint at
15
+ * upgrade — but the caller must not count replay-vs-outbox differences as divergence then.
16
+ *
17
+ * Checkpoints are cut only on record boundaries (a synchronous `step` leaves no half-processed record)
18
+ * and carry no secrets (`state.ts`).
19
+ */
20
+ import { PROCESSOR_STATE_SCHEMA } from './state.js';
21
+ /**
22
+ * Bump when `step`'s output for the same records changes. 2: Task 17a (turns, cards, `seq`; prompt
23
+ * methods refused by `rpc.request`). 3: Task 17b (`session/update` mapped, the open session bound
24
+ * from its response, config/steer/levels; events validated before numbering). 4: commands carry
25
+ * their `spawnId`; `processor.history_end` carries `droppedCount`; a prompt closes open history.
26
+ * 5: Task 17c (the callback snapshot in the runtime diagnostics and its revision bumps; Qoder SDK
27
+ * cards carry a `callbackContext`; the Qoder adapter boundaries; side-channel notes). 6: a process call's
28
+ * result waits for its child's first status; ended/delivered calls keep no dead data.
29
+ * 7: Task 20 — the `agent_install_outcome` note becomes a `step` event (it was an unknown note).
30
+ * 8: Task 21 — a level update after a load's response is folded live, not as history; the `agent_output_gap` note is a `step`.
31
+ */
32
+ export const PROCESSOR_VERSION = 8;
33
+ export const CHECKPOINT_SCHEMA = 1;
34
+ export class CheckpointUnreadableError extends Error {
35
+ }
36
+ function clone(value) {
37
+ return JSON.parse(JSON.stringify(value));
38
+ }
39
+ /** A detached copy of `state`, so later steps cannot reach into a checkpoint already handed out. */
40
+ export function checkpointOf(state) {
41
+ return { schema: CHECKPOINT_SCHEMA, processorVersion: PROCESSOR_VERSION, offset: state.offset, state: clone(state) };
42
+ }
43
+ export function encodeCheckpoint(checkpoint) {
44
+ return JSON.stringify(checkpoint);
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+ /** Parse and check a checkpoint blob (string or already-parsed JSON). */
50
+ export function decodeCheckpoint(blob) {
51
+ let value = blob;
52
+ if (typeof blob === 'string') {
53
+ try {
54
+ value = JSON.parse(blob);
55
+ }
56
+ catch {
57
+ throw new CheckpointUnreadableError('checkpoint is not JSON');
58
+ }
59
+ }
60
+ if (!isRecord(value))
61
+ throw new CheckpointUnreadableError('checkpoint is not an object');
62
+ if (value.schema !== CHECKPOINT_SCHEMA)
63
+ throw new CheckpointUnreadableError(`unknown checkpoint schema ${JSON.stringify(value.schema)}`);
64
+ const { processorVersion, offset, state } = value;
65
+ if (typeof processorVersion !== 'number' || !Number.isInteger(processorVersion))
66
+ throw new CheckpointUnreadableError('checkpoint has no processorVersion');
67
+ if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < -1)
68
+ throw new CheckpointUnreadableError('checkpoint offset is invalid');
69
+ if (!isRecord(state) || state.schema !== PROCESSOR_STATE_SCHEMA)
70
+ throw new CheckpointUnreadableError('checkpoint state schema is unknown');
71
+ if (state.offset !== offset)
72
+ throw new CheckpointUnreadableError('checkpoint offset disagrees with its state');
73
+ for (const key of ['process', 'initialize', 'outbound', 'inbound', 'diagnostics', 'permissions', 'elicitations',
74
+ 'permissionOrigins', 'permissionParents', 'unresolvedPermissionOrigins', 'open', 'mapperScopes', 'message', 'steers',
75
+ 'asyncTasks', 'runtime', 'callbacks', 'boundaries']) {
76
+ if (!isRecord(state[key]))
77
+ throw new CheckpointUnreadableError(`checkpoint state has no ${key}`);
78
+ }
79
+ if (typeof state.seq !== 'number' || !Array.isArray(state.turns) || !Array.isArray(state.settledPermissions)) {
80
+ throw new CheckpointUnreadableError('checkpoint state has no session layer (seq/turns)');
81
+ }
82
+ if (!Array.isArray(state.steered) || !('configOptions' in state) || !('delegation' in state) || !('history' in state)
83
+ || !('activity' in state)) {
84
+ throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
85
+ }
86
+ return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone(state) };
87
+ }
@@ -0,0 +1,35 @@
1
+ /** The line to write for `command`: `fill`ed from `resolve`, or `undefined` if its blocks are gone. */
2
+ export function materializeLine(command, resolve) {
3
+ if (command.fill === undefined)
4
+ return command.line;
5
+ const blocks = resolve?.(command.fill.blocksRef);
6
+ if (blocks === undefined)
7
+ return undefined;
8
+ const message = JSON.parse(command.line);
9
+ // Assigning an existing key keeps its position: the line keeps the SDK's key order.
10
+ message.params.prompt = blocks;
11
+ return JSON.stringify(message);
12
+ }
13
+ export function executeDownCommands(sink, commands, resolve) {
14
+ const written = [];
15
+ const dropped = [];
16
+ const refused = [];
17
+ const unresolved = [];
18
+ for (const command of commands) {
19
+ if (sink.hasDown(command.cause)) {
20
+ dropped.push(command);
21
+ continue;
22
+ }
23
+ const line = materializeLine(command, resolve);
24
+ if (line === undefined) {
25
+ unresolved.push(command);
26
+ continue;
27
+ }
28
+ const { fill: _fill, ...rest } = command;
29
+ if (sink.writeDown({ ...rest, line }) === 'refused')
30
+ refused.push(command);
31
+ else
32
+ written.push(command);
33
+ }
34
+ return { written, dropped, refused, unresolved };
35
+ }