@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
@@ -72,7 +72,7 @@
72
72
  // // before `.connect()`, since it only works if the
73
73
  // // handler exists in time to answer this call
74
74
  // pingResultSink?: string,
75
- // requestPermissionDuringPrompt?: { toolCall: ToolCallUpdate, options: PermissionOption[] },
75
+ // requestPermissionDuringPrompt?: { sessionId?: string, toolCall: ToolCallUpdate, options: PermissionOption[] },
76
76
  // // if set, session/prompt calls the client's
77
77
  // // session/request_permission with this payload
78
78
  // // before returning, and writes the raw response to
@@ -85,6 +85,24 @@
85
85
  // // before returning, and writes the raw response to
86
86
  // // createElicitationResultSink.
87
87
  // createElicitationResultSink?: string,
88
+ // createElicitationsDuringPrompt?: object[],
89
+ // // the plural of the above: session/prompt calls the
90
+ // // client's elicitation/create once per element, IN
91
+ // // ORDER and ONE AT A TIME — each call is awaited
92
+ // // before the next is sent, so exactly one card is
93
+ // // ever open. That sequencing is the point: it is how
94
+ // // a real agent asks a follow-up question, and it is
95
+ // // the only way to observe one property kind's answer
96
+ // // without the others' cards confusing the timeline.
97
+ // // Each raw response is appended to the JSON array at
98
+ // // createElicitationResultsSink, which is REWRITTEN
99
+ // // after every answer rather than once at the end —
100
+ // // a driver (a test, or a human clicking through a
101
+ // // browser) has to be able to read the answer to card
102
+ // // N while card N+1 is still open, and a single
103
+ // // write at the end of the turn would only be
104
+ // // readable after the last card was answered.
105
+ // createElicitationResultsSink?: string,
88
106
  // authenticateSink?: string, // if set, a file this process writes (the raw JSON
89
107
  // // `authenticate` params, i.e. `{ methodId }`) the
90
108
  // // moment it receives that request — lets a test
@@ -124,6 +142,8 @@
124
142
  // // stderr-only login shape (antigravity), and it
125
143
  // // starts at spawn on purpose, which is exactly the
126
144
  // // window `ConnectAgentOptions.present` warns about.
145
+ // newSessionDelayMs?: number, // delays the session/new response
146
+ // hangOnSessionLoad?: boolean, // session/load never resolves, for startup cleanup tests
127
147
  // hangOnSessionNew?: boolean, // if true, session/new never resolves — the agent
128
148
  // // answered initialize and then went quiet on the
129
149
  // // very next request. Distinct from hangOnInitialize:
@@ -145,14 +165,41 @@
145
165
  // // 这么干的:setTimeout 0,见 acp-agent.js:756)。
146
166
  // // 用数组的数组是因为「推几次」本身就是被测行为:
147
167
  // // claude 会推两次(70 → 68),codex 只推一次。
168
+ // rejectLaterSessionNew?: string, // p3-fix7c 3: a marker path; the first session/new (across
169
+ // // processes) succeeds and creates it, every later one throws —
170
+ // // a probe retry that fails.
171
+ // commandsBeforeLaterSessionReply?: { commands: AvailableCommand[], markerPath: string }, // p3-fix7b M-4:
172
+ // // like commandsBeforeNewSessionReply, but only from the second
173
+ // // session/new on (counted across processes through markerPath) —
174
+ // // a first probe that saw nothing, then a retry the table races.
175
+ // commandsBeforeNewSessionReply?: AvailableCommand[], // p3-fix7 N9: one available_commands_update
176
+ // // sent INSIDE the session/new handler, before its reply —
177
+ // // the order a fast Claude adapter produced in run 5, which
178
+ // // reached the client before it knew its own session id.
148
179
  // newSessionCommandsDelayMs?: number, // 每次推送之间的间隔,默认 0。让测试能把一条晚到的
149
180
  // // 推送放在 settle 窗口的里面或外面。
181
+ // newSessionSink?: string, // if set, a file this process writes (the raw JSON
182
+ // // session/new params, including `_meta`) the moment
183
+ // // it receives that request — Task 5's proof that
184
+ // // `AcpSession.open` actually sent
185
+ // // `_meta.claudeCode.emitRawSDKMessages` (or did not,
186
+ // // for an agent that is not claude)
187
+ // extNotifications?: { method: string, params: unknown }[], // sent, in order, as raw
188
+ // // extNotification calls after session/new answers —
189
+ // // same "after the reply" timing as
190
+ // // `newSessionCommands` above. Task 5 uses this to
191
+ // // model claude-agent-acp's
192
+ // // `extNotification("_claude/sdkMessage", …)`.
193
+ // extNotificationDelayMs?: number, // delay between each of the above; default 0.
150
194
  // sessions?: SessionInfo[], // session/list's response `sessions` array; default []
151
195
  // nextCursor?: string, // session/list's response `nextCursor`
152
196
  // sessionListSink?: string, // if set, a file this process writes (the raw JSON
153
197
  // // session/list params, i.e. `{cwd, cursor}`) the
154
198
  // // moment it receives that request — Task 7's proof
155
199
  // // that listNativeSessions really sent cwd/cursor
200
+ // deleteSessionSink?: string, // if set, a file each session/delete request's params
201
+ // // are appended to as a JSON line
202
+ // deleteSessionFailures?: string[], // session ids whose session/delete throws internalError
156
203
  // loadUpdates?: SessionUpdate[], // Task 7 (adoption.ts): sent as session/update
157
204
  // // notifications during session/load, before it
158
205
  // // returns — this is the replay mechanism ACP v1
@@ -172,6 +219,9 @@
172
219
  // // the shape credentials expiring mid-conversation
173
220
  // // actually takes, and the only failure the login
174
221
  // // card may be drawn for
222
+ // exitOnPrompt?: boolean, // simulate transport loss after accepting a prompt
223
+ // promptRpcError?: { code: number, message: string }, // explicit remote JSON-RPC failure
224
+ // promptCallsSink?: string, // append-only request log for no-replay assertions
175
225
  // promptSink?: string, // if set, a file this process overwrites with the raw
176
226
  // // JSON session/prompt params on every prompt — the
177
227
  // // only way to observe WHICH sessionId a prompt was
@@ -179,12 +229,18 @@
179
229
  // // to a loaded session rather than opening a new one
180
230
  // loadSessionResponse?: LoadSessionResponse, // session/load's response; default {}
181
231
  // loadSessionSink?: string, // if set, a file this process writes (the raw JSON
182
- // // session/load params) the moment it receives that
183
- // // request — lets a test prove the right
184
- // // sessionId/cwd were sent, and (by NOT configuring
185
- // // this and asserting the file never appears) that
186
- // // adoption.ts never calls session/load when the
187
- // // capability isn't advertised
232
+ // // session/load params, including `_meta`) the
233
+ // // moment it receives that request — lets a test
234
+ // // prove the right sessionId/cwd/`_meta` were sent,
235
+ // // and (by NOT configuring this and asserting the
236
+ // // file never appears) that adoption.ts never calls
237
+ // // session/load when the capability isn't advertised
238
+ // loadExtNotifications?: { method: string, params: unknown }[], // Task 9's equivalent of
239
+ // // `extNotifications` above, but pushed after
240
+ // // session/load answers instead of session/new — the
241
+ // // shape claude-agent-acp's activity push actually
242
+ // // takes on the adoption/reconnect path, where no
243
+ // // session/new is ever sent
188
244
  // resumeSessionResponse?: ResumeSessionResponse, // session/resume's response; default {}
189
245
  // resumeSessionSink?: string, // same idea as loadSessionSink, for session/resume
190
246
  // // — Task 7's proof that adoption.ts NEVER calls
@@ -206,6 +262,16 @@
206
262
  // // `promptRequired`, so seeing it back means the
207
263
  // // agent ignored the opt-in we always send
208
264
  // steerRejectsWith?: string, // if set, the handler throws with this message
265
+ // asyncTaskStopSink?: string, // if set, a file this process writes (the raw JSON
266
+ // // `_session/async_task/stop` params) the moment it
267
+ // // receives that request. Also an EXTENSION, not ACP
268
+ // // (`claude-agent-acp` `acp-agent.js@0.79.0:165`),
269
+ // // routed by its literal method name for the same
270
+ // // reason `_session/steering` is
271
+ // asyncTaskStopStopped?: boolean, // what `{ stopped }` the handler answers with;
272
+ // // default true. `false` is what the adapter answers
273
+ // // for an id it has no live task under
274
+ // // (`acp-agent.js@0.79.0:1788`)
209
275
  // newSessionConfigOptions?: SessionConfigOption[], // returned verbatim on session/new (types.gen.d.ts:2546)
210
276
  // newSessionModes?: SessionModeState, // returned verbatim on session/new (:2542)
211
277
  // setConfigOptionSink?: string, // file receiving the JSON params of each session/set_config_option
@@ -225,7 +291,7 @@
225
291
  // fixture that can prove "honour the advertised capability, not the router" end to end, not just
226
292
  // assert a boolean.
227
293
  async function main() {
228
- const { writeFileSync } = require('node:fs');
294
+ const { writeFileSync, appendFileSync, existsSync } = require('node:fs');
229
295
  // Parsed and wired up before the (slow, cold-start) dynamic import below, so a caller that kills
230
296
  // this process almost immediately after spawning it (exercising connectAgent's initialize
231
297
  // timeout with a short deadline) still gets a SIGTERM handler in place. Registering this only
@@ -307,16 +373,56 @@ async function main() {
307
373
  return {};
308
374
  })
309
375
  .onRequest(methods.agent.session.new, async (ctx) => {
376
+ if (config.echoMcpAuthorizationToStderr) {
377
+ const authorization = ctx.params.mcpServers.find((server) => server.name === 'axom-delegation')?.headers?.[0]?.value;
378
+ process.stderr.write('adapter diagnostic: started\n');
379
+ process.stderr.write(`configured Authorization: ${authorization?.slice(0, 9) ?? ''}`);
380
+ await new Promise((resolve) => setTimeout(resolve, 20));
381
+ process.stderr.write(`${authorization?.slice(9) ?? ''}\n`);
382
+ process.stderr.write(`configured token: ${authorization?.slice(7) ?? ''}\n`);
383
+ process.stderr.write(`Open https://auth.example.test/device?token=${authorization?.slice(7) ?? ''} to continue\n`);
384
+ process.stderr.write('adapter diagnostic: finished\n');
385
+ }
310
386
  if (config.newSessionRejectsWith)
311
387
  throw new Error(config.newSessionRejectsWith);
312
388
  // Checked after the rejection so the two flags stay independently exercisable; a promise that
313
389
  // never settles is the only faithful way to model an agent that simply stops answering.
314
390
  if (config.hangOnSessionNew)
315
391
  return new Promise(() => { });
392
+ if (config.newSessionDelayMs)
393
+ await new Promise((resolve) => setTimeout(resolve, config.newSessionDelayMs));
394
+ if (config.newSessionSink)
395
+ writeFileSync(config.newSessionSink, JSON.stringify(ctx.params));
396
+ for (const { method, params } of config.beforeSessionReplyNotifications ?? []) {
397
+ await ctx.client.notify(method, params);
398
+ }
316
399
  // Pushed AFTER this handler returns, not before: the real adapter schedules its first
317
400
  // `available_commands_update` on a `setTimeout(…, 0)` that runs once `session/new` has been
318
401
  // answered (`acp-agent.js:752-757`). Sending them inline would let a client that only reads
319
402
  // updates *before* the reply still pass, which is the bug this fixture exists to catch.
403
+ if (config.rejectLaterSessionNew) {
404
+ if (existsSync(config.rejectLaterSessionNew))
405
+ throw new Error('second session refused');
406
+ writeFileSync(config.rejectLaterSessionNew, 'seen');
407
+ }
408
+ if (config.commandsBeforeLaterSessionReply) {
409
+ const { commands, markerPath } = config.commandsBeforeLaterSessionReply;
410
+ if (existsSync(markerPath)) {
411
+ await ctx.client.notify(methods.client.session.update, {
412
+ sessionId,
413
+ update: { sessionUpdate: 'available_commands_update', availableCommands: commands },
414
+ });
415
+ }
416
+ else {
417
+ writeFileSync(markerPath, 'seen');
418
+ }
419
+ }
420
+ if (config.commandsBeforeNewSessionReply) {
421
+ await ctx.client.notify(methods.client.session.update, {
422
+ sessionId,
423
+ update: { sessionUpdate: 'available_commands_update', availableCommands: config.commandsBeforeNewSessionReply },
424
+ });
425
+ }
320
426
  const pushes = config.newSessionCommands ?? [];
321
427
  if (pushes.length > 0) {
322
428
  void (async () => {
@@ -329,6 +435,18 @@ async function main() {
329
435
  }
330
436
  })();
331
437
  }
438
+ // Same "after the reply, not before" reasoning as `newSessionCommands` above — Task 5
439
+ // (agent-reported activity) uses this to model claude-agent-acp's
440
+ // `extNotification("_claude/sdkMessage", …)`, which only ever arrives once a session exists.
441
+ const extNotifications = config.extNotifications ?? [];
442
+ if (extNotifications.length > 0) {
443
+ void (async () => {
444
+ for (const { method, params } of extNotifications) {
445
+ await new Promise((r) => setTimeout(r, config.extNotificationDelayMs ?? 0));
446
+ await ctx.client.notify(method, params);
447
+ }
448
+ })();
449
+ }
332
450
  return {
333
451
  sessionId,
334
452
  ...(config.newSessionConfigOptions ? { configOptions: config.newSessionConfigOptions } : {}),
@@ -339,20 +457,49 @@ async function main() {
339
457
  if (config.sessionListSink)
340
458
  writeFileSync(config.sessionListSink, JSON.stringify(ctx.params));
341
459
  return { sessions: config.sessions ?? [], ...(config.nextCursor ? { nextCursor: config.nextCursor } : {}) };
460
+ })
461
+ .onRequest(methods.agent.session.delete, (ctx) => {
462
+ if (config.deleteSessionSink)
463
+ appendFileSync(config.deleteSessionSink, JSON.stringify(ctx.params) + '\n');
464
+ if (config.deleteSessionFailures?.includes(ctx.params.sessionId))
465
+ throw RequestError.internalError(undefined, 'boom');
466
+ return {};
342
467
  })
343
468
  .onRequest(methods.agent.session.load, async (ctx) => {
344
469
  if (config.loadSessionSink)
345
470
  writeFileSync(config.loadSessionSink, JSON.stringify(ctx.params));
346
471
  if (config.loadSessionRejectsWith)
347
472
  throw new Error(config.loadSessionRejectsWith);
473
+ if (config.hangOnSessionLoad)
474
+ return new Promise(() => { });
475
+ for (const { method, params } of config.beforeSessionReplyNotifications ?? []) {
476
+ await ctx.client.notify(method, params);
477
+ }
478
+ for (const notification of config.scopedLoadUpdates ?? []) {
479
+ await ctx.client.notify(methods.client.session.update, notification);
480
+ }
348
481
  for (const update of config.loadUpdates ?? []) {
349
482
  await ctx.client.notify(methods.client.session.update, { sessionId: ctx.params.sessionId, update });
350
483
  }
484
+ // Same "after the reply, not before" reasoning as `extNotifications` on session/new above —
485
+ // Task 9 uses this to model claude-agent-acp's `extNotification("_claude/sdkMessage", …)`
486
+ // arriving on the session/load (adoption/reconnect) path, where no session/new is ever sent.
487
+ const loadExtNotifications = config.loadExtNotifications ?? [];
488
+ if (loadExtNotifications.length > 0) {
489
+ void (async () => {
490
+ for (const { method, params } of loadExtNotifications) {
491
+ await new Promise((r) => setTimeout(r, config.extNotificationDelayMs ?? 0));
492
+ await ctx.client.notify(method, params);
493
+ }
494
+ })();
495
+ }
351
496
  return config.loadSessionResponse ?? {};
352
497
  })
353
498
  .onRequest(methods.agent.session.resume, async (ctx) => {
354
499
  if (config.resumeSessionSink)
355
500
  writeFileSync(config.resumeSessionSink, JSON.stringify(ctx.params));
501
+ if (config.resumeSessionRejectsWith)
502
+ throw new Error(config.resumeSessionRejectsWith);
356
503
  return config.resumeSessionResponse ?? {};
357
504
  })
358
505
  .onRequest(methods.agent.session.setConfigOption, (ctx) => {
@@ -384,10 +531,21 @@ async function main() {
384
531
  throw new Error(config.steerRejectsWith);
385
532
  const outcome = config.steerOutcome ?? 'injected';
386
533
  return outcome === 'promptRequired' ? { outcome, reason: 'noRunningTurn' } : { outcome };
534
+ })
535
+ .onRequest('_session/async_task/stop', (params) => params, (ctx) => {
536
+ if (config.asyncTaskStopSink)
537
+ writeFileSync(config.asyncTaskStopSink, JSON.stringify(ctx.params));
538
+ return { stopped: config.asyncTaskStopStopped ?? true };
387
539
  })
388
540
  .onRequest(methods.agent.session.prompt, async (ctx) => {
541
+ if (config.promptCallsSink)
542
+ appendFileSync(config.promptCallsSink, JSON.stringify(ctx.params) + '\n');
389
543
  if (config.promptSink)
390
544
  writeFileSync(config.promptSink, JSON.stringify(ctx.params));
545
+ if (config.exitOnPrompt)
546
+ process.exit(1);
547
+ if (config.promptRpcError)
548
+ throw new RequestError(config.promptRpcError.code, config.promptRpcError.message);
391
549
  // A plain Error is masked by the SDK as JSON-RPC -32603; `RequestError.authRequired()` is
392
550
  // the real thing an agent whose credentials expired answers with (-32000). The two shapes are
393
551
  // what the login card has to tell apart, and only a real agent process on a real wire proves
@@ -396,6 +554,11 @@ async function main() {
396
554
  throw RequestError.authRequired();
397
555
  if (config.promptRejectsWith)
398
556
  throw new Error(config.promptRejectsWith);
557
+ if (config.foregroundWriteSink && ctx.params.prompt.some(block => block.type === 'text' && block.text === 'partial-write')) {
558
+ appendFileSync(config.foregroundWriteSink, 'partial\n');
559
+ await cancelled;
560
+ return { stopReason: 'cancelled' };
561
+ }
399
562
  for (const update of updates) {
400
563
  await ctx.client.notify(methods.client.session.update, { sessionId: ctx.params.sessionId, update });
401
564
  }
@@ -405,7 +568,7 @@ async function main() {
405
568
  // OS process speaking real ACP, not just callable in-process.
406
569
  if (config.requestPermissionDuringPrompt) {
407
570
  const result = await ctx.client.request(methods.client.session.requestPermission, {
408
- sessionId: ctx.params.sessionId,
571
+ sessionId: config.requestPermissionDuringPrompt.sessionId ?? ctx.params.sessionId,
409
572
  toolCall: config.requestPermissionDuringPrompt.toolCall,
410
573
  options: config.requestPermissionDuringPrompt.options,
411
574
  });
@@ -416,6 +579,19 @@ async function main() {
416
579
  const result = await ctx.client.request(methods.client.elicitation.create, config.createElicitationDuringPrompt);
417
580
  writeFileSync(config.createElicitationResultSink, JSON.stringify(result));
418
581
  }
582
+ // The plural form. `await` inside the loop is load-bearing, not incidental: the whole point is
583
+ // that card N is answered before card N+1 is raised. The sink is rewritten on every iteration
584
+ // so the answer to card N is readable while card N+1 is still open.
585
+ if (Array.isArray(config.createElicitationsDuringPrompt)) {
586
+ const results = [];
587
+ for (const payload of config.createElicitationsDuringPrompt) {
588
+ const result = await ctx.client.request(methods.client.elicitation.create, payload);
589
+ results.push(result);
590
+ if (config.createElicitationResultsSink) {
591
+ writeFileSync(config.createElicitationResultsSink, JSON.stringify(results));
592
+ }
593
+ }
594
+ }
419
595
  if (config.hangUntilCancel) {
420
596
  await cancelled;
421
597
  return { stopReason: config.stopReasonAfterCancel ?? 'end_turn' };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * The shared world of the keeper-acp end-to-end tests (Task 21's adoption tests, Task 23's fault-injection gate): a temp config dir
3
+ * with a real keeper-acp (the built bundle in `dist/keepers`), a real adapter process (`scriptAgent.ts`, driven by marker files), and
4
+ * the server's half played by {@link FakeServer} (frames applied once per seq — the server's de-duplication — plus the answers it
5
+ * recorded). A daemon life is either in-process ({@link KeeperWorld.life}: its own keeper connection, AcpAgents, manager and
6
+ * TaskRegistry; "killing" it drops the connection without any goodbye) or a real process ({@link KeeperWorld.daemonProcess}:
7
+ * `daemonLife.ts` under vite-node, killed with SIGKILL). {@link KeeperWorld.dispose} kills every keeper, adapter and daemon process it
8
+ * started, by pid.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { rm } from 'node:fs/promises';
13
+ import path, { join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { expect, vi } from 'vitest';
16
+ import { killKeepersUnder, tempDir } from '../../../test/keeperKit.js';
17
+ import { AcpKeeperClient } from '../../keepers/acp/client.js';
18
+ import { computeBuildVersion } from '../../keepers/kit/buildVersion.js';
19
+ import { keeperGen } from '../../keepers/kit/identity.js';
20
+ import { KeeperLauncher } from '../../keepers/kit/launcher.js';
21
+ import { AgentSessionManager } from '../../agentSessions.js';
22
+ import { AGENT_WIRE_VERSION } from '../../protocol/agent.js';
23
+ import { TaskRegistry } from '../../update/taskRegistry.js';
24
+ import { AcpAgents } from '../acpAgents.js';
25
+ import { keeperGenerations } from '../processor/transport/generations.js';
26
+ import { KeeperTransport } from '../processor/transport/keeper.js';
27
+ import { LocalTransport } from '../processor/transport/local.js';
28
+ const here = path.dirname(fileURLToPath(import.meta.url));
29
+ export const packageRoot = path.join(here, '..', '..', '..', '..');
30
+ export const realDist = path.join(packageRoot, 'dist', 'keepers');
31
+ export const SCRIPT_AGENT = join(here, 'scriptAgent.ts');
32
+ export const DAEMON_LIFE = join(here, 'daemonLife.ts');
33
+ export const BOOTSTRAP = "eval(require('node:fs').readFileSync(process.argv[1], 'utf8'))";
34
+ export const touch = (dir, name) => writeFileSync(join(dir, name), '');
35
+ export const agentPid = (dir) => Number(readFileSync(join(dir, 'pid'), 'utf8'));
36
+ export const stdinLines = (dir) => existsSync(join(dir, 'stdin.jsonl')) ? readFileSync(join(dir, 'stdin.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
37
+ export const downMethods = (records) => records.filter((r) => r.kind === 'down').map((r) => JSON.parse(r.line).method);
38
+ /** The server's half: applies each seq once, in order (bridge_seq), per ref; remembers the answers it gave (spec §5.5.4). */
39
+ export class FakeServer {
40
+ applied = new Map();
41
+ lastSeen = new Map();
42
+ received = [];
43
+ /** ref → requestId → outcome: what the user answered (the card's `answer`). */
44
+ answers = new Map();
45
+ apply = (frame) => {
46
+ this.received.push(frame);
47
+ if ((frame.t === 'agent_event' || frame.t === 'agent_turn_done') && typeof frame.seq === 'number') {
48
+ const last = this.lastSeen.get(frame.sessionRef) ?? 0;
49
+ if (frame.seq <= last)
50
+ return; // the server's de-duplication
51
+ this.lastSeen.set(frame.sessionRef, frame.seq);
52
+ const list = this.applied.get(frame.sessionRef) ?? [];
53
+ list.push(frame);
54
+ this.applied.set(frame.sessionRef, list);
55
+ }
56
+ };
57
+ timeline(ref) { return this.applied.get(ref) ?? []; }
58
+ seqs(ref) { return this.timeline(ref).map((frame) => frame.seq); }
59
+ turnDones(ref) { return this.timeline(ref).filter((frame) => frame.t === 'agent_turn_done'); }
60
+ texts(ref) {
61
+ return this.timeline(ref).filter((f) => f.t === 'agent_event' && f.eventName === 'agent_message_chunk')
62
+ .map((f) => (f.payload.content?.text ?? ''));
63
+ }
64
+ cards(ref) {
65
+ return this.timeline(ref).filter((f) => f.eventName === 'permission_request').map((f) => f.payload.requestId);
66
+ }
67
+ /** The user answers a card: recorded first (the server's item), then sent if a daemon is there. */
68
+ answer(ref, requestId, outcome) {
69
+ const answered = this.answers.get(ref) ?? new Map();
70
+ answered.set(requestId, outcome);
71
+ this.answers.set(ref, answered);
72
+ return { t: 'agent_answer', sessionRef: ref, requestId, outcome };
73
+ }
74
+ /** Task 22's resend: the answers this side recorded for cards a durable reopen still reports pending. */
75
+ resends(ref, opened) {
76
+ const answered = this.answers.get(ref);
77
+ return (opened.pendingRequestIds ?? []).filter((id) => answered?.has(id))
78
+ .map((id) => ({ t: 'agent_answer', sessionRef: ref, requestId: id, outcome: answered.get(id) }));
79
+ }
80
+ }
81
+ /** What the server's timeline says, without the ref: compared between two runs. */
82
+ export const comparable = (frames) => frames.map(({ sessionRef: _ref, ...rest }) => rest);
83
+ /** Seqs 1..n, no duplicate, no gap. */
84
+ export function expectContiguous(seqs) {
85
+ expect(seqs).toEqual(Array.from({ length: seqs.length }, (_, k) => k + 1));
86
+ }
87
+ export class KeeperWorld {
88
+ configDir;
89
+ agentDir;
90
+ launcher;
91
+ lives = [];
92
+ processes = [];
93
+ extraDists = [];
94
+ agentDirs = [];
95
+ constructor(prefix) {
96
+ this.configDir = tempDir(prefix);
97
+ this.agentDir = this.newAgentDir(`${prefix}agent-`);
98
+ this.launcher = this.launcherFor(realDist);
99
+ }
100
+ launcherFor(distDir) {
101
+ return new KeeperLauncher({ configDir: this.configDir, workstationId: 'ws-t21', server: 'http://test.invalid', distDir, useSystemd: false,
102
+ startTimeoutMs: 20_000, umask: 0o022 });
103
+ }
104
+ /** Another adapter's marker directory (its pid is killed on dispose). */
105
+ newAgentDir(prefix = 'axom-t23-agent-') {
106
+ const dir = tempDir(prefix);
107
+ this.agentDirs.push(dir);
108
+ return dir;
109
+ }
110
+ scriptArgs(config, dir = this.agentDir) {
111
+ return ['-e', BOOTSTRAP, SCRIPT_AGENT, JSON.stringify({ dir, ...config })];
112
+ }
113
+ /** One daemon life: its own keeper-acp connection, AcpAgents (with the generations), manager and TaskRegistry, bound to `server`. */
114
+ life(server, over = {}) {
115
+ const use = over.launcher ?? this.launcher;
116
+ const real = new KeeperTransport(new AcpKeeperClient({ launcher: use }));
117
+ const keeper = (over.wrapKeeper ? over.wrapKeeper(real) : real);
118
+ const generations = keeperGenerations({ launcher: use, current: keeper, log: () => { } });
119
+ const local = new LocalTransport({ configDir: this.configDir });
120
+ const acp = new AcpAgents({ cacheDir: this.configDir, registry: { load: async () => [] }, transports: { keeper, local }, keeperGenerations: generations,
121
+ sessionStartTimeoutMs: 15_000, ...over.acp });
122
+ const tasks = new TaskRegistry();
123
+ const sent = [];
124
+ const manager = new AgentSessionManager(acp, null, join(this.configDir, 'agents'), tasks);
125
+ manager.bindSend((frame) => { sent.push(frame); server.apply(frame); }, AGENT_WIRE_VERSION);
126
+ let killed = false;
127
+ const result = {
128
+ keeper, generations, acp, tasks, manager, sent,
129
+ // kill -9 as keeper-acp sees it: the connection drops, nothing is detached, nothing is said.
130
+ kill() { if (killed)
131
+ return; killed = true; keeper.close(); generations.close(); void local.dispose(0); },
132
+ };
133
+ this.lives.push(result);
134
+ return result;
135
+ }
136
+ openFrame(ref, config, over = {}) {
137
+ return {
138
+ t: 'agent_open', id: `open-${ref}-${Math.random().toString(36).slice(2, 8)}`, sessionRef: ref, agentKey: 'script', bridge: 'script-acp',
139
+ cwd: this.configDir, shell: '/bin/sh', command: process.execPath, args: this.scriptArgs(config), ...over,
140
+ };
141
+ }
142
+ /** Opens `ref` on life `l` and waits for the reply. */
143
+ async openOn(l, ref, config, over = {}) {
144
+ const frame = this.openFrame(ref, config, over);
145
+ await l.manager.handleFrame(frame);
146
+ const reply = opened(l, frame.id)[0];
147
+ expect(reply, `no agent_opened for ${ref}`).toBeDefined();
148
+ expect(reply.ok, reply.error ?? '').toBe(true);
149
+ return reply;
150
+ }
151
+ /** The server reopens `ref` on life `l` after a reconnect: `agent_open` with its watermark, then Task 22's answer resend. */
152
+ async reopenOn(l, server, ref, config, over = {}) {
153
+ const reply = await this.openOn(l, ref, config, { resumeFromSeq: server.lastSeen.get(ref) ?? 0, ...over });
154
+ for (const frame of server.resends(ref, reply))
155
+ await l.manager.handleFrame(frame);
156
+ return reply;
157
+ }
158
+ /** Every raw record of `ref`, read over a connection of its own. */
159
+ async rawOf(ref, launcher = this.launcher) {
160
+ const reader = new KeeperTransport(new AcpKeeperClient({ launcher }));
161
+ try {
162
+ await reader.attach(ref);
163
+ const records = [];
164
+ const summary = (await reader.list()).find((s) => s.sessionKey === ref);
165
+ for (let at = summary.raw.first;;) {
166
+ const page = await reader.readRaw(ref, at);
167
+ if (page.length === 0)
168
+ break;
169
+ records.push(...page);
170
+ at = page[page.length - 1].o + 1;
171
+ }
172
+ return records;
173
+ }
174
+ finally {
175
+ reader.close();
176
+ }
177
+ }
178
+ /** keeper-acp's listing of `ref`, over a connection of its own. */
179
+ async summaryOf(ref) {
180
+ const reader = new KeeperTransport(new AcpKeeperClient({ launcher: this.launcher }));
181
+ try {
182
+ return (await reader.list()).find((s) => s.sessionKey === ref);
183
+ }
184
+ finally {
185
+ reader.close();
186
+ }
187
+ }
188
+ /**
189
+ * A daemon life in a process of its own (`daemonLife.ts` under vite-node): it adopts what keeper-acp holds, then handles the frames
190
+ * it is given on stdin (one JSON per line; `{cmd:'forceBegin'}` runs a force update's `TaskRegistry.stopAll()`, `{cmd:'forceStop'}`
191
+ * also the restart path's detach, and exits). Every
192
+ * frame it sends lands in a file; its stderr is kept in `<configDir>/daemon-<n>.stderr` (the sentinel grep reads it).
193
+ */
194
+ daemonProcess(faults = {}) {
195
+ const n = this.processes.length + 1;
196
+ const framesOut = join(this.configDir, `frames-daemon-${n}.jsonl`);
197
+ const stderrFile = join(this.configDir, `daemon-${n}.stderr`);
198
+ const child = spawn(join(packageRoot, 'node_modules', '.bin', 'vite-node'), [DAEMON_LIFE,
199
+ JSON.stringify({ configDir: this.configDir, distDir: realDist, framesOut, faults })], { cwd: packageRoot, stdio: ['pipe', 'ignore', 'pipe'] });
200
+ let stderr = '';
201
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); appendFileSync(stderrFile, chunk); });
202
+ const exited = new Promise((resolve) => child.once('exit', () => resolve()));
203
+ const result = {
204
+ child,
205
+ frames: () => existsSync(framesOut) ? readFileSync(framesOut, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line)) : [],
206
+ send: (frame) => { child.stdin.write(`${JSON.stringify(frame)}\n`); },
207
+ stderr: () => stderr,
208
+ kill: async () => { if (child.exitCode === null && child.signalCode === null)
209
+ child.kill('SIGKILL'); await exited; },
210
+ exited,
211
+ };
212
+ this.processes.push(result);
213
+ return result;
214
+ }
215
+ /** Opens `ref` in a daemon process and waits for its `agent_opened{ok}` (the server never prompts before it). */
216
+ async openIn(proc, frame) {
217
+ proc.send(frame);
218
+ let reply;
219
+ await vi.waitFor(() => {
220
+ expect(proc.child.exitCode, proc.stderr()).toBeNull();
221
+ reply = proc.frames().find((f) => f.t === 'agent_opened' && f.id === frame.id);
222
+ expect(reply).toBeDefined();
223
+ }, { timeout: 45_000, interval: 100 });
224
+ expect(reply.ok, reply.error ?? '').toBe(true);
225
+ return reply;
226
+ }
227
+ /** An "older build" of keeper-acp: the same bundle with one byte more (its own buildVersion, so its own generation). */
228
+ olderGeneration() {
229
+ const oldDist = tempDir('axom-t21-old-dist-');
230
+ this.extraDists.push(oldDist);
231
+ mkdirSync(join(oldDist, 'acp'), { recursive: true });
232
+ writeFileSync(join(oldDist, 'acp', 'keeper.mjs'), `${readFileSync(join(realDist, 'acp', 'keeper.mjs'), 'utf8')}\n// an older generation\n`);
233
+ const manifest = JSON.parse(readFileSync(join(realDist, 'acp', 'manifest.json'), 'utf8'));
234
+ const oldBuild = computeBuildVersion(join(oldDist, 'acp'));
235
+ writeFileSync(join(oldDist, 'acp', 'manifest.json'), JSON.stringify({ ...manifest, buildVersion: oldBuild }));
236
+ return { oldLauncher: this.launcherFor(oldDist), oldGen: keeperGen(oldBuild) };
237
+ }
238
+ /** Kills every daemon process, life, keeper-acp generation and adapter this world started (by pid); the directories stay. */
239
+ async stop() {
240
+ for (const proc of this.processes.splice(0))
241
+ await proc.kill().catch(() => { });
242
+ for (const life of this.lives.splice(0))
243
+ life.kill();
244
+ for (const gen of await this.launcher.liveGenerations('acp').catch(() => []))
245
+ await this.launcher.stop('acp', gen, 'force').catch(() => { });
246
+ for (const dir of this.agentDirs) {
247
+ const pidFile = join(dir, 'pid');
248
+ if (existsSync(pidFile)) {
249
+ try {
250
+ process.kill(Number(readFileSync(pidFile, 'utf8')), 'SIGKILL');
251
+ }
252
+ catch { /* gone */ }
253
+ }
254
+ }
255
+ }
256
+ /** {@link stop}, then removes every directory (killing any keeper whose pid manifest is still under the config dir). */
257
+ async dispose() {
258
+ await this.stop();
259
+ for (const dist of this.extraDists.splice(0))
260
+ await rm(dist, { recursive: true, force: true });
261
+ for (const dir of this.agentDirs.splice(0))
262
+ await rm(dir, { recursive: true, force: true });
263
+ killKeepersUnder(this.configDir);
264
+ }
265
+ }
266
+ export const opened = (l, id) => l.sent.filter((frame) => frame.t === 'agent_opened' && (id === undefined || frame.id === id));
267
+ export const liveSessionOf = (l, ref) => l.manager.sessions.get(ref)?.session;