@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
@@ -35,20 +35,29 @@
35
35
  *
36
36
  * `elicitation/create`'s `mode` union is **open** (`"form" | "url" | string`, per
37
37
  * `CreateElicitationRequest`'s schema comment: "Custom or future elicitation mode" — a bare `string`
38
- * arm, unlike `session/update`'s closed 15-member union). Axom's wire vocabulary now *has* the
38
+ * arm, unlike `session/update`'s closed 15-member union). Axom's wire vocabulary has the
39
39
  * `elicitation_request`/`elicitation` pair (Plan B renamed the platform's own interaction events onto
40
- * ACP's words). The two names are not in the same state: `elicitation_request` (`InteractionRequested`)
41
- * still has no bridge decode path — `TimelineEventWire.decode` has no branch for it, so a bridge
42
- * emitting it is rejected outright — but bare `elicitation` (`UserInteractionRequested`) has had a named
43
- * decode branch since `TimelineEventWire.kt:127`, and it is answerable end to end: decode rebuilds the
44
- * typed event, `CoworkEngine` lands a `USER_INTERACTION` item and parks the turn, and once the user
45
- * answers, `ExternalAgentSessionRuntime.execute` forwards the reply through `channel.prompt(runId,
46
- * context.userText, ...)`. `'form'` still declines — it ships an arbitrary JSON-Schema
47
- * `requestedSchema` (`ElicitationSchema`) that nothing on Axom's wire translates into that fixed Q&A
48
- * shape — and the open `default` arm still declines everything else the SDK or a nonstandard agent
49
- * might send, for the same reason: neither has anywhere to go yet, and declining is what keeps them
50
- * defensible — it never hangs the agent and it never fabricates a user's consent for input nobody was
51
- * ever shown.
40
+ * ACP's words); both are answerable end to end now, but along different paths. `elicitation_request`
41
+ * (`TimelineEvent.ElicitationRequested`) is what {@link handleFormElicitation} raises for `'form'`
42
+ * mode (this task, Task 18) — `TimelineEventWire.decodeElicitationRequested` rebuilds it, lands a
43
+ * `WorkItemKind.USER_INTERACTION` item, parks the run, and the user's answer travels back over its own
44
+ * frame, `AgentFrame.AgentElicitationAnswer` (Task 17), through `AcpSession.answerElicitation`. Bare
45
+ * `elicitation` (`UserInteractionRequested`) is the *platform's own* agents asking a question — a
46
+ * named decode branch since `TimelineEventWire.kt:127` — and answers through `ExternalAgentSessionRuntime
47
+ * .execute` forwarding the reply as a normal `channel.prompt(runId, context.userText, ...)`; a bridged
48
+ * agent's `elicitation/create` never produces this one.
49
+ *
50
+ * `'form'`'s wire capability (`acpClient.ts`'s `clientCapabilities.elicitation.form`) is what makes the
51
+ * whole thing reachable in the first place: `claude-agent-acp` puts `AskUserQuestion` into
52
+ * `disallowedTools` (`dist/acp-agent.js:5959`) unless it is declared, so before that line existed the
53
+ * model had no tool to call `elicitation/create` from at all. {@link handleFormElicitation} answers it
54
+ * following {@link handleRequestPermission}'s own shape — see that function's doc comment for the
55
+ * four properties carried over deliberately, and {@link handleFormElicitation}'s own doc comment for
56
+ * the one place the two diverge (a schema outside Axom's profile still raises the card before
57
+ * declining, rather than being refused in silence the way an empty form is). The open `default` arm
58
+ * still declines everything the SDK or a nonstandard agent sends outside `'form'`/`'url'`, for the
59
+ * same reason it always has: it has nowhere to go, and declining is what keeps it defensible — it
60
+ * never hangs the agent and it never fabricates a user's consent for input nobody was ever shown.
52
61
  *
53
62
  * `'url'` is different: it is the shape some agents use to hand a sign-in link to the reader (Codex's
54
63
  * device-code flow reaches it through `elicitation/create` once `elicitation.url` is declared in
@@ -113,10 +122,9 @@ function toolCallPayload(toolCall) {
113
122
  * ACP `RequestPermissionRequest` (`types.gen.d.ts:108`) on Axom's wire: `{toolCall, options}` plus
114
123
  * `requestId`.
115
124
  *
116
- * `requestId` is the one admitted extension — ACP correlates the answer through the JSON-RPC request
117
- * id, which lives in the envelope (`ClientRequestContext.requestId`), and an event stream has no
118
- * envelope. `sessionId`, ACP's third field (`:112`), is deliberately not carried: the event is
119
- * already being delivered on a specific run.
125
+ * `requestId` is a host-derived resolver token — {@link permissionRequestId}, the same on the live
126
+ * path and in the ACP processor. The raw JSON-RPC ID stays local to its connection; exact `sessionId`
127
+ * travels in the wire event's `acpSessionId` origin field.
120
128
  *
121
129
  * **`toolCall` is nested, not flattened.** The flattened version lifted the tool's
122
130
  * `name`/`title`/`kind`/`arguments` to the top level, which put `name` (the tool's programmatic name,
@@ -127,9 +135,10 @@ function toolCallPayload(toolCall) {
127
135
  * `toolCall.title`, and the renderer computes it from the same field now
128
136
  * (`WorkTimelineInteractions.foldPermission`) instead of storing a second, drifting copy.
129
137
  */
130
- function buildPermissionRequestedEvent(input) {
138
+ export function buildPermissionRequestedEvent(input) {
131
139
  return {
132
140
  eventName: 'permission_request',
141
+ ...(input.sessionId !== undefined ? { acpSessionId: input.sessionId } : {}),
133
142
  payload: {
134
143
  requestId: input.requestId,
135
144
  toolCall: toolCallPayload(input.toolCall),
@@ -155,7 +164,7 @@ function buildPermissionRequestedEvent(input) {
155
164
  * `sink.answer` — the agent gave up on this specific request, so nothing should still expect an
156
165
  * answer for it. Session-level abandonment (the session closing, or the connection dying) is instead
157
166
  * `AcpSession`'s own job (`registerPendingApproval` resolving immediately when already closed, and
158
- * `runReadLoop` draining on an aborted connection) — see `acpSession.ts`.
167
+ * `runReadLoop` draining on an aborted connection) — see the old `acpSession.ts` (deleted in Task 29).
159
168
  *
160
169
  * The eventual answer is whatever string reaches `sink`'s registered resolver: normally one of
161
170
  * `input.options`' own `optionId`s (relayed from the user through `AcpSession.answer`), but both
@@ -194,14 +203,17 @@ export async function handleRequestPermission(input, sink) {
194
203
  };
195
204
  sink.registerPendingApproval(input.requestId, (outcome) => {
196
205
  settle(optionIds.has(outcome) ? { outcome: 'answered', optionId: outcome } : { outcome: 'rejected' });
197
- });
206
+ }, input.sessionId === undefined ? undefined : { originNativeSessionId: input.sessionId, rpcRequestId: input.rpcRequestId ?? input.requestId });
207
+ if (settled)
208
+ return;
198
209
  input.signal?.addEventListener('abort', () => {
199
210
  settle({ outcome: 'rejected' });
200
211
  // Purge the now-moot pending entry so a stray later answer doesn't linger against a request
201
212
  // the agent already gave up on. The `settle` above has already fixed this promise's outcome
202
213
  // (the `settled` guard makes the resolver this triggers a no-op), so the value passed here
203
214
  // only has to avoid accidentally matching a real option id.
204
- sink.answer(input.requestId, unmatchableAnswerFor(input.options));
215
+ const cancel = sink.cancelPendingApproval?.bind(sink) ?? sink.answer.bind(sink);
216
+ cancel(input.requestId, unmatchableAnswerFor(input.options));
205
217
  }, { once: true });
206
218
  sink.emitEvent(event);
207
219
  });
@@ -211,26 +223,35 @@ function declineElicitation() {
211
223
  return { action: 'decline' };
212
224
  }
213
225
  /**
214
- * Answer `elicitation/create`. `'url'` accepts and hands the URL out through `deps.present` **only
215
- * when both hold**: the request itself actually carries a string `elicitationId`/`url` (a
216
- * nonconforming agent that omits either gets declined, never `present(undefined, undefined)`), and a
217
- * real `deps.present` is wired (see {@link PresentElicitationUrl}'s doc comment — its absence means
218
- * declining, not accepting into a void). Every other arm still declines — see the class doc comment
219
- * for why. Accepting here does **not** mean the user finished anything; it means "the URL has been put
226
+ * Answer `elicitation/create`, dispatching on `request.mode`. `'url'` accepts and hands the URL out
227
+ * through `ctx.present` **only when both hold**: the request itself actually carries a string
228
+ * `elicitationId`/`url` (a nonconforming agent that omits either gets declined, never
229
+ * `present(undefined, undefined)`), and a real `ctx.present` is wired (see
230
+ * {@link PresentElicitationUrl}'s doc comment — its absence means declining, not accepting into a
231
+ * void). Accepting here does **not** mean the user finished anything; it means "the URL has been put
220
232
  * in front of them", matching ACP's own `accept` semantics for this mode — which is exactly why both
221
- * guards exist: an `accept` this module cannot back with a shown URL would make that claim false.
233
+ * guards exist: an `accept` this module cannot back with a shown URL would make that claim false. This
234
+ * branch, and this doc paragraph, are unchanged by Task 18 — only `'form'` moved.
235
+ *
236
+ * `'form'` is answered by {@link handleFormElicitation} — see its own doc comment, which is also
237
+ * where the four properties it deliberately carries over from {@link handleRequestPermission} are
238
+ * listed, and the one place it diverges.
222
239
  *
223
240
  * The `switch` has a real `default`, not just `'form'`/`'url'` cases, because `CreateElicitationRequest['mode']`
224
241
  * is `"form" | "url" | string` — an **open** union (unlike `session/update`'s closed 15-member one).
225
242
  * Falling through with no default would mean the client's response is `undefined` for any mode the SDK
226
243
  * or a nonstandard agent invents next, sending a malformed JSON-RPC result the agent may wait on
227
244
  * forever instead of a clean, immediate decline.
245
+ *
246
+ * `sink` is threaded through every mode uniformly (`registerPermissionHandlers` always has one, one
247
+ * per connection — see `PermissionSink`'s own doc comment) even though only `'form'` reads it; passing
248
+ * it unconditionally here is simpler than a second entry point that exists solely to omit an unused
249
+ * parameter, and every arm below is still free to ignore it.
228
250
  */
229
- export function handleCreateElicitation(request, deps) {
251
+ export async function handleCreateElicitation(request, ctx, sink) {
230
252
  switch (request.mode) {
231
253
  case 'form':
232
- // No form-rendering UI exists on Axom's wire yet — see the class doc comment.
233
- return declineElicitation();
254
+ return handleFormElicitation(request, ctx, sink);
234
255
  case 'url': {
235
256
  const { elicitationId, url } = request;
236
257
  if (typeof elicitationId !== 'string' || typeof url !== 'string') {
@@ -239,13 +260,13 @@ export function handleCreateElicitation(request, deps) {
239
260
  // path. The elicitation still gets answered (declined), never left to hang.
240
261
  return declineElicitation();
241
262
  }
242
- if (!deps.present) {
263
+ if (!ctx.present) {
243
264
  // Nobody to show it to yet — see `PresentElicitationUrl`'s doc comment. Declining here is
244
265
  // what an agent (or an MCP server proxied through one) raising this mid-turn can actually
245
266
  // act on, unlike an `accept` that then goes nowhere.
246
267
  return declineElicitation();
247
268
  }
248
- deps.present(elicitationId, url);
269
+ ctx.present(elicitationId, url);
249
270
  return { action: 'accept' };
250
271
  }
251
272
  default:
@@ -255,6 +276,241 @@ export function handleCreateElicitation(request, deps) {
255
276
  return declineElicitation();
256
277
  }
257
278
  }
279
+ /**
280
+ * Is `key` a free-text companion the server will *fold away* (`<key>_custom`,
281
+ * `ElicitationRequest.CUSTOM_SUFFIX` on the Kotlin side, `claude-agent-acp`'s own
282
+ * `question_<n>_custom` shape) rather than something the reader gets a row for? A folded companion
283
+ * must not be counted when deciding whether a schema has anything to draw at all.
284
+ *
285
+ * **The suffix alone is not the rule — the owner has to exist.** This used to return `true` for any
286
+ * `_custom`-suffixed key, which matched a server that *dropped* an ownerless one. Task 9 (ruling
287
+ * R15) changed that: `elicitationRequestFromPayload` now lands an ownerless `_custom` key as a
288
+ * read-only `UNPRESENTABLE` entry instead of discarding it, so a schema made *entirely* of such keys
289
+ * decodes into a real (if all-read-only) card. Measured against the real decoder: `{"foo_custom":
290
+ * {"type":"string"}}` alone → `fields=[foo_custom:UNPRESENTABLE]`, not `null`. With the old
291
+ * suffix-only rule this function returned `[]` for that schema and {@link handleFormElicitation}'s
292
+ * empty-form guard declined it **without ever raising a card** — the card silently never appears and
293
+ * the agent gets a decline it did not earn, which is the "too narrow" half of the drift R24 is about.
294
+ *
295
+ * Checking that the owner is *also a property* is deliberately weaker than the server's own rule
296
+ * (which additionally requires the owner to be a single-/multi-select, ruling R13), and the weakness
297
+ * only ever runs in the safe direction: it can under-count fields (a `_custom` key whose owner is a
298
+ * scalar is folded here but is a read-only row server-side) and never over-count. Under-counting
299
+ * cannot wrongly reach zero — for it to fold a key away, the owner has to be a property, and that
300
+ * owner is itself counted. Zero here therefore still means zero server-side, which is the only thing
301
+ * the empty-form guard reads.
302
+ */
303
+ function isFoldedCompanionKey(key, properties) {
304
+ if (!key.endsWith('_custom'))
305
+ return false;
306
+ const owner = key.slice(0, -'_custom'.length);
307
+ return owner.length > 0 && Object.prototype.hasOwnProperty.call(properties, owner);
308
+ }
309
+ /**
310
+ * The property keys of a `'form'` request's `requestedSchema.properties` that the reader will
311
+ * actually get a row for — `[]` for anything that isn't a legible `{properties: {...}}` object,
312
+ * including a missing `requestedSchema` altogether. This alone decides
313
+ * {@link handleFormElicitation}'s empty-form guard: zero keys here is the same "nothing legible
314
+ * could ever be raised" shape as `handleRequestPermission`'s `options.length === 0`, regardless of
315
+ * *why* there are zero (no schema, an empty `properties`, or a `properties` that is entirely folded
316
+ * companions).
317
+ *
318
+ * All three of those are the shapes the server-side decoder still drops outright — measured, not
319
+ * inferred: `{"properties":{}}`, `properties` that isn't an object, and a missing `requestedSchema`
320
+ * each return `null` from `elicitationRequestFromPayload`, because `MODE_FORM` requires
321
+ * `fields.isNotEmpty()`. Everything else it now decodes (see {@link handleFormElicitation}).
322
+ */
323
+ export function elicitationFieldKeys(requestedSchema) {
324
+ const properties = requestedSchema?.properties;
325
+ if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
326
+ return [];
327
+ const record = properties;
328
+ return Object.keys(record).filter((key) => !isFoldedCompanionKey(key, record));
329
+ }
330
+ /**
331
+ * Kotlin's `String.isBlank()`, restated: empty, or entirely whitespace. `validate.ts`'s
332
+ * `nonEmptyString` (`.length > 0`) does **not** catch `" "` — `ElicitationRequest.init`'s
333
+ * `require(message.isNotBlank())` does, and so must this: a blank
334
+ * `message` throws inside `elicitationRequestFromPayload`'s `runCatching`. It is a **request-level**
335
+ * invariant, so unlike a malformed property it has no read-only fallback to degrade into — the whole
336
+ * request is dropped server-side, which is why this check survived Task 9's removal of the
337
+ * per-property profile predicate (see the ruling-R24 block below).
338
+ */
339
+ export function isBlank(value) {
340
+ return value.trim().length === 0;
341
+ }
342
+ /**
343
+ * **Ruling R24, restated after Task 9 (D5′) — the predicate this block used to hold is gone because
344
+ * the server-side gap it predicted is gone.**
345
+ *
346
+ * There used to be an `isAxomProfileProperty`/`isAxomProfileSchema` pair here, a hand-written mirror
347
+ * of every `error()`/`require()` on `elicitationRequestFromPayload`'s answerable path. It existed
348
+ * because **one** malformed property took down the **whole** `elicitation_request` server-side: the
349
+ * throw escaped to that function's outer `runCatching`, the event was dropped, and the agent blocked
350
+ * on `elicitation/create` forever with nothing on screen to explain it. Declining before sending was
351
+ * the lesser evil — the agent at least unblocked — but it bought that by making the card never
352
+ * appear, which is the *other* silent failure.
353
+ *
354
+ * Task 9 removed the cause. Every one of those shapes now degrades to a single read-only
355
+ * `UNPRESENTABLE` entry inside an otherwise normal card (`ElicitationField`'s new kind; the
356
+ * per-property `runCatching` in `elicitationRequestFromPayload`), so there is nothing left at the
357
+ * *property* level for this client to predict. **Measured against the real Kotlin decoder, not read
358
+ * off the parser** — every payload `permission.test.ts` exercises was run through
359
+ * `elicitationRequestFromPayload` and only these still return `null`, all of them request-level and
360
+ * all of them already covered by the two guards {@link handleFormElicitation} keeps:
361
+ *
362
+ * | shape | verdict | guard that still catches it |
363
+ * |---|---|---|
364
+ * | **whitespace-only** `message` (`" "`) | `null` (`ElicitationRequest.init`) | {@link isBlank}, emit-then-decline |
365
+ * | **empty** `message` (`""`) | `null`, same requirement | `validateWireEvent` rejects the event first → decline, **no card** |
366
+ * | `properties: {}` | `null` (`MODE_FORM` needs `fields.isNotEmpty()`) | {@link elicitationFieldKeys} → `[]` |
367
+ * | `properties` not an object | `null`, same requirement | {@link elicitationFieldKeys} → `[]` |
368
+ * | `requestedSchema` absent | `null`, same requirement | {@link elicitationFieldKeys} → `[]` |
369
+ *
370
+ * Final review F7 split that first row in two, because merging them wrote the verdict backwards for
371
+ * half of it: `""` never reaches emit-then-decline at all. {@link validateWireEvent} runs first
372
+ * (`decodeElicitationRequested`'s `nonEmptyString`, `validate.ts:164`) and rejects the event, so the
373
+ * reader gets **no card** — precisely the outcome emit-then-decline exists to avoid. Only
374
+ * *whitespace* makes it past that gate and into the path this table describes; the {@link isBlank}
375
+ * KDoc had the reason right all along (`nonEmptyString` does not see `" "`), it was this row that
376
+ * folded the two cases into one sentence.
377
+ *
378
+ * Everything else decodes, including every shape this file used to decline: duplicate `const`s, a
379
+ * blank option `title`, a candidate-less `type: "array"`, an unknown or `_`-prefixed `type`, a
380
+ * scalar carrying candidates or a malformed `oneOf`/`items`/`anyOf`, a duplicate untitled `enum`
381
+ * value, a property that isn't an object at all, a blank property key, and an ownerless `_custom`
382
+ * key. Each becomes a visible read-only row; the reader can answer the rest of the card and submit,
383
+ * or press Skip (ruling D4/R19) to send `{action: 'decline'}` themselves.
384
+ *
385
+ * **The rule itself has not changed, only the answer it currently produces.** It is keyed on server
386
+ * behaviour: whenever whatever decodes a `CreateElicitationRequest` server-side starts *rejecting* a
387
+ * shape this client forwards, a predicate has to come back here in the same commit — otherwise the
388
+ * agent parks on `elicitation/create` forever waiting for an answer that cannot be produced. And if
389
+ * a reintroduced predicate is ever narrower than the decoder, the card silently never appears and
390
+ * the agent gets a decline it did not earn. Both directions have shipped bugs (Task 8, Task 8.5);
391
+ * measure them against the real decoder rather than against a reading of this file.
392
+ */
393
+ /**
394
+ * ACP `CreateElicitationRequest`'s `'form'` arm folded onto Axom's `elicitation_request` wire shape —
395
+ * the mirror of `buildPermissionRequestedEvent` above, and the payload
396
+ * `TimelineEventWire.decodeElicitationRequested`/`elicitationRequestFromPayload` expect.
397
+ *
398
+ * `requestId` is `ctx.requestId` — the JSON-RPC envelope id — not ACP's own optional
399
+ * `ElicitationRequestScope.requestId` field on `request`, for the identical reason
400
+ * `buildPermissionRequestedEvent` reads its `requestId` off `PermissionRequestInput` rather than the
401
+ * request body: ACP correlates the eventual answer through the envelope, and an event stream has none.
402
+ *
403
+ * `requestedSchema` is forwarded **verbatim** — Global Constraint 3, "transport, not translation" (see
404
+ * `toolCallPayload`'s doc comment for the same rule applied to `ToolCallUpdate`): this function does
405
+ * not narrow a field to fit Axom's profile, and it does not strip a field outside it either. The wire
406
+ * event always carries exactly what the agent asked; {@link handleFormElicitation} is what decides,
407
+ * from that same schema, whether to wait for an answer or decline right after raising the card.
408
+ */
409
+ export function buildElicitationRequestedEvent(request, requestId) {
410
+ return {
411
+ eventName: 'elicitation_request',
412
+ payload: {
413
+ requestId,
414
+ ...(typeof request.toolCallId === 'string' ? { toolCallId: request.toolCallId } : {}),
415
+ request: {
416
+ mode: request.mode,
417
+ message: request.message,
418
+ requestedSchema: request.requestedSchema,
419
+ },
420
+ answer: null,
421
+ },
422
+ };
423
+ }
424
+ /**
425
+ * `'form'`-mode `elicitation/create` — the counterpart to {@link handleRequestPermission} this
426
+ * module's class doc comment promises. Four properties carried over deliberately:
427
+ *
428
+ * 1. **Every path settles.** The `Promise` below always resolves — including the two guard clauses
429
+ * above it, both still inside this `async function` — because the agent is blocked on
430
+ * `session/prompt` until it does.
431
+ * 2. **An empty form is refused before it is raised**, mirroring `handleRequestPermission`'s
432
+ * `options.length === 0` hard refusal: a `requestedSchema` with zero answerable fields (missing
433
+ * entirely, `properties: {}`, or `properties` that is entirely `_custom` companions — see
434
+ * {@link elicitationFieldKeys}) is a card nobody could ever answer, which is worse than no card.
435
+ * 3. **`ctx.signal` aborting settles `{action: 'cancel'}` and purges the pending entry** — ACP's own
436
+ * real cancellation outcome (see `AcpSession`'s `ABANDONED_ELICITATION_ANSWER` doc comment for why
437
+ * this needs no randomized sentinel the way `handleRequestPermission`'s purge value does), so a
438
+ * card nobody is listening to answer does not linger in `AcpSession`'s pending map.
439
+ * 4. **The open `default` arm stays reachable** — one level up, in {@link handleCreateElicitation}'s
440
+ * own `switch`, not repeated here.
441
+ *
442
+ * **Where this diverges from the template**, deliberately: a blank `message` ({@link isBlank}) still
443
+ * **raises the card** before declining — the question is legible, `ElicitationRequest.init` just
444
+ * rejects the message server-side. It declines only *after* raising, instead of the guard-clause
445
+ * silence `handleRequestPermission` uses for its one hard-refusal case. Dropping the event here
446
+ * instead would leave the user seeing neither the question the agent asked nor any reason nothing
447
+ * happened (spec §12 D5′). This is the *opposite* of the empty-form case above, which has nothing
448
+ * legible to draw at all — raising a card for it would show the user a form with no fields on it,
449
+ * not a degraded but real question.
450
+ *
451
+ * **A schema with fields Axom cannot render no longer takes this exit** (Task 9, ruling R24): those
452
+ * decode into read-only rows server-side, so this handler waits for a real answer — which the reader
453
+ * supplies either by answering the rest of the card or by pressing Skip (`{action: 'decline'}`,
454
+ * ruling D4). See the ruling-R24 block above for the measured list of what still fails to decode.
455
+ */
456
+ async function handleFormElicitation(request, ctx, sink) {
457
+ const requestId = ctx.requestId;
458
+ if (!sink || !requestId) {
459
+ // Defensive only: `registerPermissionHandlers` — the one real caller — always supplies both,
460
+ // reading `requestId` off the JSON-RPC envelope. Nothing to raise a card through or correlate an
461
+ // answer with, so there is nothing left to do but decline, same as the empty-form case below.
462
+ return declineElicitation();
463
+ }
464
+ if (ctx.signal?.aborted) {
465
+ // Mirrors `handleRequestPermission`'s identical early check: a listener added *after* `abort()`
466
+ // already fired never sees it (`AbortSignal` does not replay past events), so without this guard
467
+ // the mid-flight listener below would never fire and this would hang forever instead of settling.
468
+ return { action: 'cancel' };
469
+ }
470
+ const fieldKeys = elicitationFieldKeys(request.requestedSchema);
471
+ if (fieldKeys.length === 0) {
472
+ return declineElicitation();
473
+ }
474
+ const event = buildElicitationRequestedEvent(request, requestId);
475
+ const rejection = validateWireEvent(event);
476
+ if (rejection) {
477
+ // Defensive only, same reasoning as `handleRequestPermission`'s identical check: every field here
478
+ // is derived from ACP's own typed request, so a conforming agent can never actually trip this.
479
+ console.error(`[acp] refusing to raise an elicitation card that fails validateWireEvent: ${rejection.reason}`);
480
+ return declineElicitation();
481
+ }
482
+ if (isBlank(request.message)) {
483
+ // Legible, just not collectible here — raise it, then decline. See this function's own doc
484
+ // comment for why this is not the same as the empty-form guard above. A blank `message` is the
485
+ // **only** reason left for this branch: `ElicitationRequest.init`'s `require(message.isNotBlank())`
486
+ // is a request-level invariant, so the throw takes the whole request down with no read-only
487
+ // fallback to catch it, and `validate.ts`'s own `nonEmptyString` (`.length > 0`) does not see
488
+ // `" "`. Every *property*-level reason that used to route here now decodes into a read-only row
489
+ // instead — see the ruling-R24 block above for the measured table.
490
+ sink.emitEvent(event);
491
+ return declineElicitation();
492
+ }
493
+ return new Promise((resolve) => {
494
+ let settled = false;
495
+ const settle = (response) => {
496
+ if (settled)
497
+ return;
498
+ settled = true;
499
+ resolve(response);
500
+ };
501
+ sink.registerPendingElicitation(requestId, settle);
502
+ ctx.signal?.addEventListener('abort', () => {
503
+ settle({ action: 'cancel' });
504
+ // Purge the now-moot pending entry, exactly like `handleRequestPermission`'s own purge call —
505
+ // the `settled` guard above already makes a later real answer a no-op for this promise, but
506
+ // without this the entry would linger in `AcpSession.pendingElicitations` forever. `'cancel'`
507
+ // is ACP's own real cancellation outcome, so — unlike `handleRequestPermission`'s
508
+ // `unmatchableAnswerFor` — there is no collision to guard against here.
509
+ sink.answerElicitation(requestId, { action: 'cancel' });
510
+ }, { once: true });
511
+ sink.emitEvent(event);
512
+ });
513
+ }
258
514
  /**
259
515
  * `elicitation/complete` fires only for an elicitation the client previously accepted — now the `'url'`
260
516
  * mode, once the agent considers that round trip finished. This turns it into a `dismiss` call so
@@ -265,6 +521,35 @@ export function handleCreateElicitation(request, deps) {
265
521
  export function handleElicitationComplete(notification, deps) {
266
522
  deps.dismiss(notification.elicitationId);
267
523
  }
524
+ /**
525
+ * The card id of one `session/request_permission`: `perm:<hostGeneration>:<JSON agent id>` (spec
526
+ * 2026-10-11 §5.4).
527
+ *
528
+ * ## Why derived, not random
529
+ *
530
+ * It used to be `randomUUID()` per invocation. A card id the server is already showing must still
531
+ * resolve after the daemon restarts underneath a live adapter, and a random id cannot: the re-presented
532
+ * request would get a new one, `settleApproval` ignores unknown ids, and the user's answer to the
533
+ * visible card would be lost (analysis A §4). Derived from the request itself, the same request gets
534
+ * the same id in every daemon generation, and the ACP processor — which may not draw randomness at
535
+ * all (`processor/guard.test.ts`) — computes it with this very function.
536
+ *
537
+ * ## Why it is still unique
538
+ *
539
+ * - `hostGeneration` identifies one adapter process: the keeper's `spawnId` on the processor path,
540
+ * `AgentRuntimeDiagnostics.generation` (fresh per `connectAgent`) on today's live path — the same
541
+ * value published as `callbackContext.hostGeneration`. A reconnect is a new process, so a new
542
+ * generation: the agent's ids restart at 0 there without colliding with an earlier card.
543
+ * - Within one process the agent never reuses a pending JSON-RPC id. `JSON.stringify` keeps the
544
+ * number `1` and the string `"1"` apart.
545
+ *
546
+ * Kept short enough for `AcpSession.registerPendingApproval`'s 256-byte rule for every id a real
547
+ * adapter sends; a pathological id longer than ~200 bytes is refused there, as an overlong
548
+ * `rpcRequestId` already was.
549
+ */
550
+ export function permissionRequestId(hostGeneration, agentRequestId) {
551
+ return `perm:${hostGeneration}:${JSON.stringify(agentRequestId)}`;
552
+ }
268
553
  /**
269
554
  * Register this module's three handlers onto `app`. Must run inside `connectAgent`'s `configure`
270
555
  * hook — see the file doc comment for why a post-connect `.onRequest`/`.onNotification` would compile
@@ -278,16 +563,23 @@ export function handleElicitationComplete(notification, deps) {
278
563
  * the call site instead. Its own `present` may still be omitted (see {@link PresentElicitationUrl}) —
279
564
  * that is `acpAgents.ts`'s session connection saying, on purpose, that a `url` elicitation raised
280
565
  * outside a login has nowhere to go, and it declines rather than accepting into nothing.
566
+ *
567
+ * `connection.generation` is the adapter process's generation — `connectAgent` hands it to its
568
+ * `configure` hook — and scopes the card ids {@link permissionRequestId} derives.
281
569
  */
282
- export function registerPermissionHandlers(app, sink, elicitationDeps) {
570
+ export function registerPermissionHandlers(app, sink, elicitationDeps, connection) {
283
571
  app
284
572
  .onRequest(methods.client.session.requestPermission, async (ctx) => {
285
- const decision = await handleRequestPermission({ requestId: String(ctx.requestId), toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
573
+ if (typeof ctx.params.sessionId !== 'string' || !ctx.params.sessionId.trim()) {
574
+ console.error(JSON.stringify({ component: 'acp', error: 'invalid_permission_origin', rpcRequestId: String(ctx.requestId) }));
575
+ return { outcome: { outcome: 'cancelled' } };
576
+ }
577
+ const decision = await handleRequestPermission({ requestId: permissionRequestId(connection.generation, ctx.requestId), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
286
578
  return {
287
579
  outcome: decision.outcome === 'answered' ? { outcome: 'selected', optionId: decision.optionId } : { outcome: 'cancelled' },
288
580
  };
289
581
  })
290
- .onRequest(methods.client.elicitation.create, (ctx) => handleCreateElicitation(ctx.params, elicitationDeps))
582
+ .onRequest(methods.client.elicitation.create, (ctx) => handleCreateElicitation(ctx.params, { present: elicitationDeps.present, requestId: String(ctx.requestId), signal: ctx.signal }, sink))
291
583
  .onNotification(methods.client.elicitation.complete, (ctx) => handleElicitationComplete(ctx.params, elicitationDeps));
292
584
  }
293
585
  /**
@@ -309,8 +601,14 @@ export function createDeferredPermissionSink() {
309
601
  return {
310
602
  sink: {
311
603
  emitEvent: (event) => requireBound().emitEvent(event),
312
- registerPendingApproval: (requestId, resolve) => requireBound().registerPendingApproval(requestId, resolve),
604
+ registerPendingApproval: (requestId, resolve, address) => requireBound().registerPendingApproval(requestId, resolve, address),
605
+ cancelPendingApproval: (requestId, outcome) => {
606
+ const target = requireBound();
607
+ (target.cancelPendingApproval?.bind(target) ?? target.answer.bind(target))(requestId, outcome);
608
+ },
313
609
  answer: (requestId, outcome) => requireBound().answer(requestId, outcome),
610
+ registerPendingElicitation: (requestId, resolve) => requireBound().registerPendingElicitation(requestId, resolve),
611
+ answerElicitation: (requestId, response) => requireBound().answerElicitation(requestId, response),
314
612
  },
315
613
  bind(target) {
316
614
  // Review finding (task-5, item 4): silently replacing an already-bound sink on a second call
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Processor checkpoints (spec 2026-10-11 §5.5): the state after record `offset`, as JSON.
3
+ *
4
+ * ## Two versions, two questions
5
+ *
6
+ * - `schema` answers "can this daemon **read** it?". An unknown schema throws
7
+ * {@link CheckpointUnreadableError}; the caller then replays from the session's start (outbox and
8
+ * watermark cover the frames already sent), or, if the log was truncated, marks the session
9
+ * degraded and switches it at the next open (§5.5.6). Readability of older schemas is a test-guarded
10
+ * promise: never change the meaning of an existing field without bumping `schema` and adding a
11
+ * reader for the old one.
12
+ * - `processorVersion` answers "would **this** code have produced the same frames?". Bump it whenever
13
+ * `step` can emit different output for the same records. A readable checkpoint from another
14
+ * processor version is still restored — that is the whole point of cutting a head checkpoint at
15
+ * upgrade — but the caller must not count replay-vs-outbox differences as divergence then.
16
+ *
17
+ * Checkpoints are cut only on record boundaries (a synchronous `step` leaves no half-processed record)
18
+ * and carry no secrets (`state.ts`).
19
+ */
20
+ import { PROCESSOR_STATE_SCHEMA } from './state.js';
21
+ /**
22
+ * Bump when `step`'s output for the same records changes. 2: Task 17a (turns, cards, `seq`; prompt
23
+ * methods refused by `rpc.request`). 3: Task 17b (`session/update` mapped, the open session bound
24
+ * from its response, config/steer/levels; events validated before numbering). 4: commands carry
25
+ * their `spawnId`; `processor.history_end` carries `droppedCount`; a prompt closes open history.
26
+ * 5: Task 17c (the callback snapshot in the runtime diagnostics and its revision bumps; Qoder SDK
27
+ * cards carry a `callbackContext`; the Qoder adapter boundaries; side-channel notes). 6: a process call's
28
+ * result waits for its child's first status; ended/delivered calls keep no dead data.
29
+ * 7: Task 20 — the `agent_install_outcome` note becomes a `step` event (it was an unknown note).
30
+ * 8: Task 21 — a level update after a load's response is folded live, not as history; the `agent_output_gap` note is a `step`.
31
+ */
32
+ export const PROCESSOR_VERSION = 8;
33
+ export const CHECKPOINT_SCHEMA = 1;
34
+ export class CheckpointUnreadableError extends Error {
35
+ }
36
+ function clone(value) {
37
+ return JSON.parse(JSON.stringify(value));
38
+ }
39
+ /** A detached copy of `state`, so later steps cannot reach into a checkpoint already handed out. */
40
+ export function checkpointOf(state) {
41
+ return { schema: CHECKPOINT_SCHEMA, processorVersion: PROCESSOR_VERSION, offset: state.offset, state: clone(state) };
42
+ }
43
+ export function encodeCheckpoint(checkpoint) {
44
+ return JSON.stringify(checkpoint);
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+ /** Parse and check a checkpoint blob (string or already-parsed JSON). */
50
+ export function decodeCheckpoint(blob) {
51
+ let value = blob;
52
+ if (typeof blob === 'string') {
53
+ try {
54
+ value = JSON.parse(blob);
55
+ }
56
+ catch {
57
+ throw new CheckpointUnreadableError('checkpoint is not JSON');
58
+ }
59
+ }
60
+ if (!isRecord(value))
61
+ throw new CheckpointUnreadableError('checkpoint is not an object');
62
+ if (value.schema !== CHECKPOINT_SCHEMA)
63
+ throw new CheckpointUnreadableError(`unknown checkpoint schema ${JSON.stringify(value.schema)}`);
64
+ const { processorVersion, offset, state } = value;
65
+ if (typeof processorVersion !== 'number' || !Number.isInteger(processorVersion))
66
+ throw new CheckpointUnreadableError('checkpoint has no processorVersion');
67
+ if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < -1)
68
+ throw new CheckpointUnreadableError('checkpoint offset is invalid');
69
+ if (!isRecord(state) || state.schema !== PROCESSOR_STATE_SCHEMA)
70
+ throw new CheckpointUnreadableError('checkpoint state schema is unknown');
71
+ if (state.offset !== offset)
72
+ throw new CheckpointUnreadableError('checkpoint offset disagrees with its state');
73
+ for (const key of ['process', 'initialize', 'outbound', 'inbound', 'diagnostics', 'permissions', 'elicitations',
74
+ 'permissionOrigins', 'permissionParents', 'unresolvedPermissionOrigins', 'open', 'mapperScopes', 'message', 'steers',
75
+ 'asyncTasks', 'runtime', 'callbacks', 'boundaries']) {
76
+ if (!isRecord(state[key]))
77
+ throw new CheckpointUnreadableError(`checkpoint state has no ${key}`);
78
+ }
79
+ if (typeof state.seq !== 'number' || !Array.isArray(state.turns) || !Array.isArray(state.settledPermissions)) {
80
+ throw new CheckpointUnreadableError('checkpoint state has no session layer (seq/turns)');
81
+ }
82
+ if (!Array.isArray(state.steered) || !('configOptions' in state) || !('delegation' in state) || !('history' in state)
83
+ || !('activity' in state)) {
84
+ throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
85
+ }
86
+ return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone(state) };
87
+ }
@@ -0,0 +1,35 @@
1
+ /** The line to write for `command`: `fill`ed from `resolve`, or `undefined` if its blocks are gone. */
2
+ export function materializeLine(command, resolve) {
3
+ if (command.fill === undefined)
4
+ return command.line;
5
+ const blocks = resolve?.(command.fill.blocksRef);
6
+ if (blocks === undefined)
7
+ return undefined;
8
+ const message = JSON.parse(command.line);
9
+ // Assigning an existing key keeps its position: the line keeps the SDK's key order.
10
+ message.params.prompt = blocks;
11
+ return JSON.stringify(message);
12
+ }
13
+ export function executeDownCommands(sink, commands, resolve) {
14
+ const written = [];
15
+ const dropped = [];
16
+ const refused = [];
17
+ const unresolved = [];
18
+ for (const command of commands) {
19
+ if (sink.hasDown(command.cause)) {
20
+ dropped.push(command);
21
+ continue;
22
+ }
23
+ const line = materializeLine(command, resolve);
24
+ if (line === undefined) {
25
+ unresolved.push(command);
26
+ continue;
27
+ }
28
+ const { fill: _fill, ...rest } = command;
29
+ if (sink.writeDown({ ...rest, line }) === 'refused')
30
+ refused.push(command);
31
+ else
32
+ written.push(command);
33
+ }
34
+ return { written, dropped, refused, unresolved };
35
+ }