@platless/axom-cli 0.1.0 → 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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The large-id echo contract (spec 2026-10-11 §5.4, analysis A §3 "Risks"): outbound request ids are
3
+ * `o * 16 + i`, which on a long-lived session's log reach far past what any adapter sees today, so
4
+ * every adapter must echo a numeric id near 2^50 **exactly** — same number, still a number.
5
+ *
6
+ * {@link probeLargeIdEcho} is the reusable hook: it speaks raw ndjson to any launch spec, sends `initialize` and two `session/new`
7
+ * requests (Task 23's ruling: nothing else — no prompt, so no model usage; an agent that wants credentials answers `session/new`
8
+ * with an error, which echoes the id just the same), and reports what came back. `largeIdEcho.test.ts` runs it against
9
+ * `acp/test/fakeAgent.ts` and — with `AXOM_REAL_ADAPTER_SMOKE=1` — against the real claude and codex adapters
10
+ * ({@link REAL_ADAPTER_ECHO_TARGETS_T23}).
11
+ */
12
+ import { AXOM_AGENT_CATALOG } from '../../registry/catalog.js';
13
+ import { initializeRequestParams } from '../../initializeResponse.js';
14
+ import { outboundRequestId } from '../jsonrpc.js';
15
+ import { startRawAgent } from './rawAgent.js';
16
+ /** Ids around 2^50, including one built the way the processor builds them. */
17
+ export const LARGE_ECHO_IDS = [2 ** 50 - 1, 2 ** 50 + 15, outboundRequestId({ o: 2 ** 46 + 12_345, i: 7 })];
18
+ /** Every catalog adapter that must echo large ids: Task 23 runs claude and codex ({@link REAL_ADAPTER_SMOKE_T23}); the rest wait for
19
+ * Task 30's real-machine acceptance. */
20
+ export const REAL_ADAPTER_ECHO_TARGETS_T23 = AXOM_AGENT_CATALOG.map((entry) => entry.registryAgent);
21
+ /** The adapters Task 23's smoke installs and probes (the ruling: claude and codex, `initialize`/`session/new` only). */
22
+ export const REAL_ADAPTER_SMOKE_T23 = ['claude-acp', 'codex-acp'];
23
+ /** Pull the top-level `"id":…` token out of a response line without going through `JSON.parse`. */
24
+ function idToken(line) {
25
+ const match = /"id"\s*:\s*("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|null)/.exec(line);
26
+ return match ? match[1] : '<none>';
27
+ }
28
+ export async function probeLargeIdEcho(launch, cwd, timeoutMs = 60_000) {
29
+ const agent = startRawAgent(launch);
30
+ const observations = [];
31
+ try {
32
+ const [initId, ...rest] = LARGE_ECHO_IDS;
33
+ const probes = [
34
+ ['initialize', initId, initializeRequestParams()],
35
+ ...rest.map((id) => ['session/new', id, { cwd, mcpServers: [] }]),
36
+ ];
37
+ for (const [method, id, params] of probes) {
38
+ agent.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
39
+ // Match on the number *or* any other spelling of it, so a mangled echo is reported, not timed out.
40
+ const line = await agent.nextLine((m) => !('method' in m) && (m.id === id || String(m.id) === String(id) || Number(m.id) === id), timeoutMs);
41
+ const parsed = JSON.parse(line);
42
+ observations.push({ method, sent: id, echoedText: idToken(line), echoed: parsed.id,
43
+ ...(parsed.error ? { error: `${String(parsed.error.code)} ${String(parsed.error.message)}`.slice(0, 200) } : {}) });
44
+ }
45
+ }
46
+ finally {
47
+ await agent.close();
48
+ }
49
+ return observations;
50
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Drive an ACP agent process over raw ndjson stdio — no SDK on our side — so a test sees exactly the
3
+ * bytes each side wrote. Used by the processor's equivalence and large-id echo tests.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ const FAKE_AGENT = fileURLToPath(new URL('../../test/fakeAgent.ts', import.meta.url));
8
+ const FAKE_BOOTSTRAP = "eval(require('node:fs').readFileSync(process.argv[1], 'utf8'))";
9
+ /** `acp/test/fakeAgent.ts` as a real child process, configured exactly as `acpClient.test.ts` does. */
10
+ export function fakeAgentLaunch(config = {}) {
11
+ return { command: process.execPath, args: ['-e', FAKE_BOOTSTRAP, FAKE_AGENT, JSON.stringify(config)] };
12
+ }
13
+ export function startRawAgent(launch) {
14
+ const child = spawn(launch.command, [...launch.args], {
15
+ env: launch.inheritEnv === false ? { ...launch.env } : { ...process.env, ...launch.env },
16
+ stdio: ['pipe', 'pipe', 'pipe'],
17
+ });
18
+ const lines = [];
19
+ const waiters = [];
20
+ let buffer = '';
21
+ let stderr = '';
22
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
23
+ child.stdout.on('data', (chunk) => {
24
+ buffer += chunk.toString('utf8');
25
+ let newline;
26
+ while ((newline = buffer.indexOf('\n')) >= 0) {
27
+ const line = buffer.slice(0, newline);
28
+ buffer = buffer.slice(newline + 1);
29
+ if (line.trim())
30
+ lines.push(line);
31
+ }
32
+ for (const wake of waiters.splice(0))
33
+ wake();
34
+ });
35
+ return {
36
+ send(line) {
37
+ child.stdin.write(`${line}\n`);
38
+ },
39
+ async nextLine(match, timeoutMs = 10_000) {
40
+ const deadline = Date.now() + timeoutMs;
41
+ for (;;) {
42
+ const index = lines.findIndex((line) => {
43
+ try {
44
+ return match(JSON.parse(line));
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ });
50
+ if (index >= 0)
51
+ return lines.splice(index, 1)[0];
52
+ const remaining = deadline - Date.now();
53
+ if (remaining <= 0 || child.exitCode !== null) {
54
+ throw new Error(`no matching line from ${launch.command} (exit ${child.exitCode}); stderr: ${stderr.slice(-500)}`);
55
+ }
56
+ await new Promise((resolve) => {
57
+ const timer = setTimeout(resolve, Math.min(remaining, 200));
58
+ waiters.push(() => { clearTimeout(timer); resolve(); });
59
+ });
60
+ }
61
+ },
62
+ async close() {
63
+ if (child.exitCode !== null || child.signalCode !== null)
64
+ return;
65
+ const exited = new Promise((resolve) => child.once('exit', () => resolve()));
66
+ child.kill();
67
+ await exited;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * keeper-acp generations, as the daemon sees them (Task 21, spec §5.5, §6.3): one keeper-acp per build ("generation") may be running,
3
+ * and after a CLI upgrade an **older** generation can still host live adapters. Those sessions are adopted where they are — the daemon
4
+ * attaches to each through the generation that hosts it and routes that session's IPC there — and stay there until §6.3 switching
5
+ * (Task 24) moves one at an `agent_open`. New sessions always go to the current build's generation.
6
+ *
7
+ * An older generation is never stopped while it hosts a running adapter; once it hosts none ({@link KeeperGenerations.stopIfIdle})
8
+ * it is asked to stop gracefully, and the current generation adopts the session directories it leaves (keeper-acp's own periodic
9
+ * adoption, or on the spot when a spawn needs one) — their outboxes survive.
10
+ */
11
+ import { readdir, stat } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { AcpKeeperClient } from '../../../keepers/acp/client.js';
14
+ import { sessionDirName } from '../../../keepers/acp/host.js';
15
+ import { KeeperTransport } from './keeper.js';
16
+ /** A process that is (or may still be) running an adapter: never stop the generation that hosts it. */
17
+ export function hostsRunningAdapter(summary) {
18
+ return summary.process.state === 'starting' || summary.process.state === 'running' || summary.process.state === 'exiting';
19
+ }
20
+ export function keeperGenerations(options) {
21
+ const { launcher } = options;
22
+ const log = options.log ?? ((line) => console.warn(line));
23
+ const older = new Map();
24
+ let currentGen;
25
+ const current = () => {
26
+ currentGen ??= launcher.currentGen('acp').catch((error) => { currentGen = undefined; throw error; });
27
+ return currentGen;
28
+ };
29
+ const transportFor = async (gen) => {
30
+ if (gen === await current())
31
+ return options.current;
32
+ let transport = older.get(gen);
33
+ if (!transport)
34
+ older.set(gen, transport = new KeeperTransport(new AcpKeeperClient({ launcher, gen })));
35
+ return transport;
36
+ };
37
+ return {
38
+ current,
39
+ live: () => launcher.liveGenerations('acp'),
40
+ async hasSessionData() {
41
+ const dir = path.join(launcher.pathsFor('acp', await current()).dataDir, 'sessions');
42
+ return (await readdir(dir).catch(() => [])).length > 0;
43
+ },
44
+ async hasSessionDir(sessionKey) {
45
+ const dir = path.join(launcher.pathsFor('acp', await current()).dataDir, 'sessions', sessionDirName(sessionKey));
46
+ return stat(dir).then(() => true, (error) => { if (error.code === 'ENOENT')
47
+ return false; throw error; });
48
+ },
49
+ transportFor,
50
+ async stopIfIdle(gen, held) {
51
+ if (gen === await current())
52
+ return false;
53
+ const transport = await transportFor(gen);
54
+ let sessions;
55
+ try {
56
+ sessions = await transport.list();
57
+ }
58
+ catch {
59
+ return false;
60
+ } // unreachable: nothing to decide on
61
+ if (sessions.some(hostsRunningAdapter) || held?.())
62
+ return false;
63
+ transport.close();
64
+ older.delete(gen);
65
+ const result = await launcher.stop('acp', gen, 'graceful');
66
+ log(`[acp] keeper-acp generation ${gen} hosts no running adapter: ${result.stopped ? 'stopped' : `could not be stopped${result.reason ? ` (${result.reason})` : ''}`}` +
67
+ (sessions.length > 0 ? `; the current generation adopts its ${sessions.length} kept session(s)` : ''));
68
+ return result.stopped;
69
+ },
70
+ close() {
71
+ for (const transport of older.values())
72
+ transport.close();
73
+ older.clear();
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,130 @@
1
+ import { CauseIndex, RawTruncatedError, SessionNotAttachedError, checkpointBlob, rawRecordOf, } from './types.js';
2
+ /** Raw records in flight per subscription. */
3
+ const RAW_WINDOW = 32;
4
+ /**
5
+ * Raw bytes in flight per subscription (Task 20): one record can be a ~10 MB image prompt, so a window of records alone could
6
+ * park 32 of them in daemon memory. keeper-acp pushes at most this many bytes plus one record ahead of the processor loop.
7
+ */
8
+ export const RAW_WINDOW_BYTES = 8 * 1024 * 1024;
9
+ const toOutbox = (record) => ({ o: record.o, t: record.t, seq: record.seq, src: record.src, frame: record.frame });
10
+ export class KeeperTransport {
11
+ client;
12
+ mode = 'keeper';
13
+ indexes = new Map();
14
+ constructor(client) {
15
+ this.client = client;
16
+ }
17
+ async spawn(sessionKey, spec, options) {
18
+ const result = await this.client.spawn({ sessionKey, spec, label: options.label, ...(options.baseOffset !== undefined ? { baseOffset: options.baseOffset } : {}) });
19
+ // A new session (its spawn record is its first record) has no downs yet; an existing one is indexed by `attach`.
20
+ if (result.o === (options.baseOffset ?? 0) && !this.indexes.has(sessionKey))
21
+ this.indexes.set(sessionKey, new CauseIndex());
22
+ return result;
23
+ }
24
+ async list() { return (await this.client.list()).sessions; }
25
+ async attach(sessionKey) {
26
+ const attached = await this.client.attach(sessionKey);
27
+ const { downs } = await this.client.downs(sessionKey, 0);
28
+ const index = new CauseIndex();
29
+ for (const down of downs)
30
+ index.add(down.cause, down.o);
31
+ this.indexes.set(sessionKey, index); // anything an earlier index knew is in raw, hence in `downs`
32
+ return attached;
33
+ }
34
+ hasDown(sessionKey, cause) {
35
+ const index = this.indexes.get(sessionKey);
36
+ if (!index)
37
+ throw new SessionNotAttachedError(sessionKey);
38
+ return index.has(cause);
39
+ }
40
+ observe(sessionKey, records) {
41
+ const index = this.indexes.get(sessionKey);
42
+ if (index)
43
+ for (const record of records)
44
+ index.observe(record);
45
+ }
46
+ async subscribeRaw(sessionKey, from, handler, options = {}) {
47
+ let expected = from;
48
+ let failed = false;
49
+ let subscription;
50
+ const fail = (error) => { if (failed)
51
+ return; failed = true; void subscription?.unsubscribe(); options.onClosed?.(error); };
52
+ subscription = await this.client.subscribe(sessionKey, 'raw', async (batch) => {
53
+ if (failed)
54
+ return;
55
+ if (batch.length > 0 && batch[0].o !== expected) {
56
+ fail(new RawTruncatedError(expected, batch[0].o));
57
+ return;
58
+ }
59
+ let records;
60
+ try {
61
+ records = batch.map((record) => rawRecordOf(record));
62
+ }
63
+ catch (error) {
64
+ fail(error instanceof Error ? error : new Error(String(error)));
65
+ return;
66
+ }
67
+ for (let i = 1; i < records.length; i++)
68
+ if (records[i].o !== records[i - 1].o + 1) {
69
+ fail(new RawTruncatedError(records[i - 1].o + 1, records[i].o));
70
+ return;
71
+ }
72
+ if (records.length === 0)
73
+ return;
74
+ expected = records[records.length - 1].o + 1;
75
+ this.observe(sessionKey, records);
76
+ await handler(records);
77
+ }, { from, window: options.window ?? RAW_WINDOW, windowBytes: options.windowBytes ?? RAW_WINDOW_BYTES, onClosed: (error) => fail(error) });
78
+ const inner = subscription;
79
+ return { get nextOffset() { return expected; }, unsubscribe: () => inner.unsubscribe() };
80
+ }
81
+ async readRaw(sessionKey, from, limit) {
82
+ const result = await this.client.read({ sessionKey, stream: 'raw', from, ...(limit !== undefined ? { limit } : {}) });
83
+ if (result.truncated)
84
+ throw new RawTruncatedError(from, result.first);
85
+ const records = result.records.map((record) => rawRecordOf(record));
86
+ this.observe(sessionKey, records);
87
+ return records;
88
+ }
89
+ async readOutbox(sessionKey, from, limit) {
90
+ const result = await this.client.read({ sessionKey, stream: 'outbox', from, ...(limit !== undefined ? { limit } : {}) });
91
+ return result.records.map(toOutbox);
92
+ }
93
+ async write(sessionKey, line, options) {
94
+ const result = await this.client.write({
95
+ sessionKey, spawnId: options.spawnId, line, annotation: { ...options.annotation },
96
+ ...(options.redact && options.redact.length > 0 ? { redact: options.redact.map((r) => ({ ref: r.ref, value: r.value })) } : {}),
97
+ });
98
+ if (result.status !== 'refused')
99
+ this.indexes.get(sessionKey)?.add(options.annotation.cause, result.o);
100
+ return result;
101
+ }
102
+ note(sessionKey, note) {
103
+ return this.client.note({ sessionKey, note: { ...note } });
104
+ }
105
+ appendOutbox(sessionKey, entries) {
106
+ return this.client.outboxAppend({ sessionKey, entries: [...entries] });
107
+ }
108
+ async commit(sessionKey, params) {
109
+ const result = await this.client.commit({
110
+ sessionKey,
111
+ ...(params.checkpoint ? { checkpoint: { offset: params.checkpoint.offset, blob: checkpointBlob(params.checkpoint.blob) } } : {}),
112
+ ...(params.outbox ? { outbox: { through: params.outbox.through, tail: { seq: params.outbox.tail.seq, src: { ...params.outbox.tail.src } } } } : {}),
113
+ ...(params.rawThrough !== undefined ? { rawThrough: params.rawThrough } : {}),
114
+ });
115
+ if (params.rawThrough !== undefined)
116
+ this.indexes.get(sessionKey)?.forgetThrough(params.rawThrough);
117
+ return result;
118
+ }
119
+ async terminate(sessionKey, graceMs) {
120
+ return (await this.client.terminate({ sessionKey, ...(graceMs !== undefined ? { graceMs } : {}) })).exit;
121
+ }
122
+ async release(sessionKey) {
123
+ await this.client.release(sessionKey);
124
+ this.indexes.delete(sessionKey);
125
+ }
126
+ close() { this.client.disconnect(); }
127
+ onConnectionLost(listener) {
128
+ return this.client.onConnectionLost?.(listener) ?? (() => { });
129
+ }
130
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The fallback transport (spec 2026-10-11 §5.7: keeper-acp unavailable or protocol-incompatible): the same session mechanism as
3
+ * keeper-acp (`keepers/acp/session.ts`), in the daemon. The adapter is a plain daemon child (signalled alone, as today's
4
+ * `closeAgentTransport` does), raw lives in memory, and the outbox is a kit stream log under
5
+ * `<configDir>/agents/local/<sessionKey>/` — today's journal guarantee: unacknowledged frames survive a daemon crash. Sessions do not
6
+ * survive the daemon (as today); an outbox left behind by a previous daemon is listed (`raw: null`) so its frames can be offered once
7
+ * and then released.
8
+ *
9
+ * Because the session code is shared, the same adapter traffic yields the same records as through keeper-acp.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { sessionKeyOf, spawnSpecOf } from '../../../keepers/acp/params.js';
16
+ import { MemoryStream } from '../../../keepers/acp/memoryStream.js';
17
+ import { AcpErrorCode } from '../../../keepers/acp/protocol.js';
18
+ import { AcpSession } from '../../../keepers/acp/session.js';
19
+ import { KeeperMethodError } from '../../../keepers/kit/protocol.js';
20
+ import { StreamLog } from '../../../keepers/kit/streamLog.js';
21
+ import { DAEMON_FRAME_INDEX } from '../../outbox.js';
22
+ import { RawTruncatedError, checkpointBlob, rawRecordOf, } from './types.js';
23
+ const DEFAULT_WINDOW = 32;
24
+ /** Longest directory name used as is (file systems cap a name at 255 bytes); longer keys get a hashed name. */
25
+ const MAX_DIR_NAME = 200;
26
+ /** The key of a hashed directory, stored beside the outbox's own files (the stream log ignores it). */
27
+ const KEY_FILE = 'session-key';
28
+ /**
29
+ * A session key as a directory name: percent-encoded (keys are daemon strings, not paths), never `.`/`..`; a key whose encoding
30
+ * is longer than {@link MAX_DIR_NAME} becomes `~<sha256(key)[:32]>` and its key is kept in `session-key`.
31
+ */
32
+ export function localSessionDir(root, sessionKey) {
33
+ const name = encodeURIComponent(sessionKey).replace(/^\.+$/, (dots) => '%2E'.repeat(dots.length));
34
+ return path.join(root, name.length <= MAX_DIR_NAME ? name : `~${createHash('sha256').update(sessionKey).digest('hex').slice(0, 32)}`);
35
+ }
36
+ const toOutbox = (record) => ({ o: record.o, t: record.t, seq: record.seq, src: record.src, frame: record.frame });
37
+ export class LocalTransport {
38
+ options;
39
+ mode = 'local';
40
+ root;
41
+ entries = new Map();
42
+ now;
43
+ logLine;
44
+ constructor(options) {
45
+ this.options = options;
46
+ this.root = path.join(options.configDir, 'agents', 'local');
47
+ this.now = options.now ?? Date.now;
48
+ this.logLine = options.log ?? (() => { });
49
+ }
50
+ open(sessionKey, label, baseOffset, orphan) {
51
+ const dir = localSessionDir(this.root, sessionKey);
52
+ if (path.basename(dir).startsWith('~')) {
53
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
54
+ writeFileSync(path.join(dir, KEY_FILE), sessionKey, { mode: 0o600 });
55
+ }
56
+ const meta = { v: 1, sessionKey, label, createdAtMs: this.now(), spawn: null, exited: false, removeOnExit: [], lastExit: null };
57
+ const outbox = StreamLog.open({ dir, now: this.now, immediateKinds: [], onError: (error) => this.logLine(`acp local ${sessionKey}: ${error.message}`) });
58
+ const session = new AcpSession({
59
+ meta, raw: new MemoryStream({ now: this.now, ...(baseOffset !== undefined ? { baseOffset } : {}) }), outbox, freshRaw: true, detached: false,
60
+ saveMeta: () => { }, now: this.now, log: this.logLine,
61
+ });
62
+ const entry = { session, dir, orphan };
63
+ this.entries.set(sessionKey, entry);
64
+ return entry;
65
+ }
66
+ /**
67
+ * Writes an outbox for `sessionKey` holding `records` (a legacy journal's entries, Task 20) — listed afterwards like any outbox a
68
+ * previous daemon left. Built under a staging name and renamed into place, so a crash leaves either nothing or the whole outbox;
69
+ * a key that already has a directory is left as it is (an import that already happened, or a session's own outbox).
70
+ */
71
+ async importOutbox(key, records) {
72
+ const sessionKey = sessionKeyOf(key);
73
+ const dir = localSessionDir(this.root, sessionKey);
74
+ if (this.entries.has(sessionKey) || existsSync(dir))
75
+ return false;
76
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
77
+ const staging = path.join(this.root, `.staging-${randomUUID()}`);
78
+ try {
79
+ mkdirSync(staging, { mode: 0o700 });
80
+ if (path.basename(dir).startsWith('~'))
81
+ writeFileSync(path.join(staging, KEY_FILE), sessionKey, { mode: 0o600 });
82
+ const log = StreamLog.open({ dir: staging, now: this.now, immediateKinds: [] });
83
+ try {
84
+ let last = -Infinity;
85
+ for (const record of records) {
86
+ if (!(record.seq > last))
87
+ continue; // strictly increasing, as `acp.outbox.append` requires
88
+ last = record.seq;
89
+ log.append({ k: 'frame', seq: record.seq, src: record.src ?? { o: 0, i: DAEMON_FRAME_INDEX }, frame: record.frame });
90
+ }
91
+ }
92
+ finally {
93
+ log.close();
94
+ }
95
+ renameSync(staging, dir);
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ rmSync(staging, { recursive: true, force: true });
100
+ throw error;
101
+ }
102
+ }
103
+ /** The outboxes a previous daemon left behind (no live session). */
104
+ orphans() {
105
+ let names;
106
+ try {
107
+ names = readdirSync(this.root);
108
+ }
109
+ catch {
110
+ return [];
111
+ }
112
+ const keys = [];
113
+ for (const name of names) {
114
+ if (name.startsWith('.staging-'))
115
+ continue;
116
+ let key;
117
+ try {
118
+ key = name.startsWith('~') ? readFileSync(path.join(this.root, name, KEY_FILE), 'utf8') : decodeURIComponent(name);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ try {
124
+ if (!statSync(path.join(this.root, name)).isDirectory())
125
+ continue;
126
+ }
127
+ catch {
128
+ continue;
129
+ }
130
+ if (!this.entries.has(key))
131
+ keys.push(key);
132
+ }
133
+ return keys.sort();
134
+ }
135
+ entry(key) {
136
+ const sessionKey = sessionKeyOf(key);
137
+ const existing = this.entries.get(sessionKey);
138
+ if (existing)
139
+ return existing;
140
+ if (this.orphans().includes(sessionKey))
141
+ return this.open(sessionKey, {}, undefined, true);
142
+ throw new KeeperMethodError(AcpErrorCode.notFound, 'ACP session not found');
143
+ }
144
+ async spawn(key, spec, options) {
145
+ const sessionKey = sessionKeyOf(key);
146
+ const checked = spawnSpecOf(spec, [this.root]);
147
+ const existing = this.entries.get(sessionKey);
148
+ if (existing && !existing.orphan) {
149
+ if (options.baseOffset !== undefined)
150
+ throw new KeeperMethodError(AcpErrorCode.badRequest, 'baseOffset only applies to a new session');
151
+ return existing.session.spawn(checked);
152
+ }
153
+ if (existing) {
154
+ existing.session.close();
155
+ this.entries.delete(sessionKey);
156
+ } // an orphan's outbox is reopened by the new session
157
+ const existed = existsSync(localSessionDir(this.root, sessionKey));
158
+ const entry = this.open(sessionKey, options.label, options.baseOffset, false);
159
+ try {
160
+ return await entry.session.spawn(checked);
161
+ }
162
+ catch (error) {
163
+ // A new session whose first spawn failed leaves nothing behind (an older outbox found there is kept).
164
+ this.entries.delete(sessionKey);
165
+ entry.session.close();
166
+ if (!existed)
167
+ rmSync(entry.dir, { recursive: true, force: true });
168
+ throw error;
169
+ }
170
+ }
171
+ async list() {
172
+ const live = [...this.entries.values()].map((e) => (e.orphan ? { ...e.session.summary(), raw: null } : e.session.summary()));
173
+ const left = this.orphans().map((key) => { const e = this.open(key, {}, undefined, true); return { ...e.session.summary(), raw: null }; });
174
+ return [...live, ...left];
175
+ }
176
+ async attach(sessionKey) {
177
+ const entry = this.entry(sessionKey);
178
+ const attached = entry.session.attach();
179
+ return entry.orphan ? { ...attached, raw: null } : attached;
180
+ }
181
+ hasDown(sessionKey, cause) { return this.entry(sessionKey).session.hasDown(cause); }
182
+ async subscribeRaw(sessionKey, from, handler, options = {}) {
183
+ const source = this.entry(sessionKey).session.source('raw');
184
+ const window = options.window ?? DEFAULT_WINDOW;
185
+ let next = from;
186
+ let closed = false;
187
+ let pumping = false;
188
+ let dirty = true;
189
+ const fail = (error) => { if (closed)
190
+ return; closed = true; unwatch(); options.onClosed?.(error); };
191
+ const pump = async () => {
192
+ if (pumping) {
193
+ dirty = true;
194
+ return;
195
+ }
196
+ pumping = true;
197
+ try {
198
+ while (dirty && !closed) {
199
+ dirty = false;
200
+ for (;;) {
201
+ const batch = await source.read(next, window);
202
+ if (closed || batch.length === 0)
203
+ break;
204
+ if (batch[0].o !== next) {
205
+ fail(new RawTruncatedError(next, batch[0].o));
206
+ return;
207
+ }
208
+ const records = batch.map(rawRecordOf);
209
+ next = records[records.length - 1].o + 1;
210
+ await handler(records);
211
+ }
212
+ }
213
+ }
214
+ catch (error) {
215
+ fail(error instanceof Error ? error : new Error(String(error)));
216
+ }
217
+ finally {
218
+ pumping = false;
219
+ }
220
+ };
221
+ const unwatch = source.watch(() => { dirty = true; void pump(); });
222
+ queueMicrotask(() => { void pump(); });
223
+ return { get nextOffset() { return next; }, unsubscribe: async () => { closed = true; unwatch(); } };
224
+ }
225
+ async readRaw(sessionKey, from, limit) {
226
+ const result = this.entry(sessionKey).session.read('raw', from, limit);
227
+ if (result.truncated)
228
+ throw new RawTruncatedError(from, result.first);
229
+ return result.records.map((record) => rawRecordOf(record));
230
+ }
231
+ async readOutbox(sessionKey, from, limit) {
232
+ return this.entry(sessionKey).session.read('outbox', from, limit).records.map(toOutbox);
233
+ }
234
+ async write(sessionKey, line, options) {
235
+ return this.entry(sessionKey).session.write(options.spawnId, line, { ...options.annotation }, (options.redact ?? []).map((r) => ({ ref: r.ref, value: r.value })));
236
+ }
237
+ async note(sessionKey, note) {
238
+ return this.entry(sessionKey).session.note({ ...note });
239
+ }
240
+ async appendOutbox(sessionKey, entries) {
241
+ return this.entry(sessionKey).session.outboxAppend([...entries]);
242
+ }
243
+ async commit(sessionKey, params) {
244
+ return this.entry(sessionKey).session.commit({
245
+ ...(params.checkpoint ? { checkpoint: { offset: params.checkpoint.offset, blob: checkpointBlob(params.checkpoint.blob) } } : {}),
246
+ ...(params.outbox ? { outbox: { through: params.outbox.through, tail: { seq: params.outbox.tail.seq, src: { ...params.outbox.tail.src } } } } : {}),
247
+ ...(params.rawThrough !== undefined ? { rawThrough: params.rawThrough } : {}),
248
+ });
249
+ }
250
+ terminate(sessionKey, graceMs) { return this.entry(sessionKey).session.terminate(graceMs); }
251
+ async release(sessionKey) {
252
+ const entry = this.entry(sessionKey);
253
+ if (entry.session.busy)
254
+ throw new KeeperMethodError(AcpErrorCode.busy, 'Terminate the process before releasing the session');
255
+ this.entries.delete(sessionKey);
256
+ entry.session.close();
257
+ rmSync(entry.dir, { recursive: true, force: true });
258
+ }
259
+ /** Fallback sessions die with the daemon: every adapter is terminated and every outbox closed (its frames stay on disk). */
260
+ async dispose(graceMs) {
261
+ const entries = [...this.entries.values()];
262
+ await Promise.all(entries.map((e) => e.session.terminate(graceMs).catch(() => null)));
263
+ for (const e of entries)
264
+ e.session.close();
265
+ this.entries.clear();
266
+ }
267
+ /** The daemon exits: same as {@link dispose} (fallback sessions do not outlive it). */
268
+ close() { void this.dispose(); }
269
+ }
@@ -0,0 +1,78 @@
1
+ import { materializeLine } from '../commands.js';
2
+ import { causeKey, decodeStreamRecord } from '../record.js';
3
+ /** Raw records are retained only from `first`: a read or subscription below it cannot give the processor a contiguous log. */
4
+ export class RawTruncatedError extends Error {
5
+ from;
6
+ first;
7
+ constructor(from, first) {
8
+ super(`Raw records ${from}..${first - 1} are no longer retained`);
9
+ this.from = from;
10
+ this.first = first;
11
+ this.name = 'RawTruncatedError';
12
+ }
13
+ }
14
+ /** `hasDown` was asked about a session the transport has not attached (the index is built by `attach`). */
15
+ export class SessionNotAttachedError extends Error {
16
+ constructor(sessionKey) { super(`ACP session ${sessionKey} is not attached`); this.name = 'SessionNotAttachedError'; }
17
+ }
18
+ /**
19
+ * The kit record as the processor sees it. Explicit losses the keeper wrote (`gap`: a stdout line over the record cap;
20
+ * `discontinuity`: a torn tail repaired at open) keep their offset and become a `keeper_<kind>` note, which the processor counts as an
21
+ * unknown note: offsets stay contiguous and nothing is silently skipped.
22
+ */
23
+ export function rawRecordOf(record) {
24
+ if (record.k === 'gap' || record.k === 'discontinuity') {
25
+ const { o, t, k, ...rest } = record;
26
+ return { o, t, kind: 'note', note: { ...rest, type: `keeper_${k}` } };
27
+ }
28
+ return decodeStreamRecord(record);
29
+ }
30
+ /** A down-cause index: `causeKey` → offset of its `down`. */
31
+ export class CauseIndex {
32
+ downs = new Map();
33
+ add(cause, o) { this.downs.set(causeKey(cause), o); }
34
+ has(cause) { return this.downs.has(causeKey(cause)); }
35
+ observe(record) { if (record.kind === 'down' && record.annotation)
36
+ this.add(record.annotation.cause, record.o); }
37
+ /** Raw was truncated through `through`: those downs can never be asked about again. */
38
+ forgetThrough(through) { for (const [key, o] of this.downs)
39
+ if (o <= through)
40
+ this.downs.delete(key); }
41
+ get size() { return this.downs.size; }
42
+ }
43
+ /**
44
+ * `executeDownCommands` (commands.ts) over a transport: the same rules — a cause already in raw is dropped; a `fill` whose blocks
45
+ * are gone is `unresolved` (so is a command whose `secretRef` the daemon no longer holds); a refusal (another spawn runs) and a
46
+ * keeper-side duplicate count as dropped/refused — but writes are awaited one by one, in order.
47
+ */
48
+ export async function executeDownCommandsVia(transport, sessionKey, commands, resolve, secrets) {
49
+ const written = [];
50
+ const dropped = [];
51
+ const refused = [];
52
+ const unresolved = [];
53
+ for (const command of commands) {
54
+ if (transport.hasDown(sessionKey, command.cause)) {
55
+ dropped.push(command);
56
+ continue;
57
+ }
58
+ const line = materializeLine(command, resolve);
59
+ const redact = (command.redact ?? []).map((ref) => ({ ref, value: secrets?.(ref) }));
60
+ if (line === undefined || redact.some((r) => r.value === undefined)) {
61
+ unresolved.push(command);
62
+ continue;
63
+ }
64
+ const result = await transport.write(sessionKey, line, {
65
+ annotation: { ...command.annotation, cause: command.cause, spawnId: command.spawnId },
66
+ ...(redact.length > 0 ? { redact: redact } : {}),
67
+ spawnId: command.spawnId,
68
+ });
69
+ if (result.status === 'written')
70
+ written.push(command);
71
+ else if (result.status === 'duplicate')
72
+ dropped.push(command);
73
+ else
74
+ refused.push(command);
75
+ }
76
+ return { written, dropped, refused, unresolved };
77
+ }
78
+ export const checkpointBlob = (blob) => (typeof blob === 'string' ? Buffer.from(blob, 'utf8') : blob).toString('base64');