@platless/axom-cli 0.1.0 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,257 @@
1
+ // Execute the real published adapter modules, without models or credentials.
2
+ // node <probe> <zcode-acp-server-package-directory> [--expect-no-subagents]
3
+ //
4
+ // Pristine zcode-acp-server emits no subagent/async-task variants (the backend's
5
+ // sub-agent lifecycle events carry no taskId, so every listener drops them) and
6
+ // stamps `_meta.subagent` on every Agent dispatch; the patched adapter forwards
7
+ // the backend's own `session.updated` agentId/taskId events as draft ACP
8
+ // subagent variants (RFD #1992) and AIR async-task events, re-scopes the
9
+ // sub-agent's internal `tool_subagent_*` calls onto its `sess_subagent_*` child
10
+ // session, and drops the mark when the client negotiated native subagents.
11
+ // Every emitted frame derives from backend-supplied ids only — no randomness,
12
+ // so the same event replay produces byte-identical output.
13
+ import assert from 'node:assert/strict';
14
+ import path from 'node:path';
15
+ import { pathToFileURL } from 'node:url';
16
+
17
+ const directory = path.resolve(process.argv[2]);
18
+ const expectNone = process.argv.includes('--expect-no-subagents');
19
+
20
+ const serverUrl = pathToFileURL(path.join(directory, 'dist/server.js')).href;
21
+ const dispatchUrl = pathToFileURL(path.join(directory, 'dist/handlers/dispatch.js')).href;
22
+ const bgUrl = pathToFileURL(path.join(directory, 'dist/handlers/background-tasks.js')).href;
23
+ const serverMod = await import(serverUrl);
24
+ const dispatchMod = await import(dispatchUrl);
25
+ const bgMod = await import(bgUrl);
26
+
27
+ if (expectNone) {
28
+ assert.equal(serverMod.AxomZcodeSubagents, undefined, 'pristine package must not export AxomZcodeSubagents');
29
+ // Pristine dispatch: no child re-scoping, mark always stamped.
30
+ const cx = recorder();
31
+ await dispatchMod.dispatchEvent(fakeServer({ subagents: true }), cx, 'root-1', {
32
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'tool_subagent_agent_a_call_b', title: 'inner', status: 'pending',
33
+ });
34
+ assert.equal(cx.sent.length, 1);
35
+ assert.equal(cx.sent[0].params.sessionId, 'root-1', 'pristine dispatch addresses sub-agent frames to the root');
36
+ assert.equal(cx.sent[0].params.update._meta.subagent, true, 'pristine dispatch stamps the subagent mark unconditionally');
37
+ console.log('PASS zcode-subagent-subwork (pristine: no variants, no re-scoping, mark unconditional)');
38
+ process.exit(0);
39
+ }
40
+
41
+ assert.ok(serverMod.AxomZcodeSubagents, 'patched package must export AxomZcodeSubagents');
42
+
43
+ function recorder() {
44
+ const sent = [];
45
+ return { sent, notify: async (method, params) => { sent.push({ method, params }); } };
46
+ }
47
+
48
+ function fakeServer(caps) {
49
+ const childCx = recorder();
50
+ return {
51
+ clientCapabilities: caps,
52
+ sessionAliases: (acpSid) => [acpSid],
53
+ notifyByZcodeSid: async (sid, update) => { void sid; rootCx.sent.push({ method: 'session/update', params: { sessionId: 'root-1', update } }); return true; },
54
+ clients: { size: 1, broadcast: () => childCx },
55
+ supportsTerminalOutput: () => false,
56
+ noteDispatchedToolCall: () => {},
57
+ dispatchedToolCalls: new Map(),
58
+ terminalSentData: new Map(),
59
+ workflowRunNames: new Map(),
60
+ sessionPlanActive: new Set(),
61
+ notifyTurnActiveSince: new Map(),
62
+ lastMode: new Map(),
63
+ __childCx: childCx,
64
+ };
65
+ }
66
+
67
+ const CAPS_NATIVE = { subagents: {}, _meta: { jetbrains: { air: { version: 1, capabilities: ['nativeSubagentSessions', 'asyncTasks'] } } } };
68
+ const CAPS_PLAIN = {};
69
+
70
+ const rootCx = recorder();
71
+
72
+ // ---------------------------------------------------------------- scenarios
73
+ const agentSpawn = (over = {}) => ({ type: 'session.updated', payload: {
74
+ agentId: 'agent_u1', agentType: 'general-purpose', childSessionId: 'sess_subagent_agent_u1',
75
+ description: 'Count kts files', prompt: 'Count the .kts files.', parentToolCallId: 'call_p1', status: 'running', ...over } });
76
+ const agentTerminal = (over = {}) => agentSpawn({ status: 'completed', totalDurationMs: 4477, totalToolUseCount: 1, totalTokens: 21127, ...over });
77
+ const parentResult = (result) => ({ type: 'tool.updated', payload: { toolCallId: 'call_p1', kind: 'result', result } });
78
+ const taskEvent = (over = {}) => ({ type: 'session.updated', payload: {
79
+ taskId: 'exec_t1', toolCallId: 'call_b1', toolName: 'Bash', taskKind: 'bash', cancellable: true,
80
+ command: 'sleep 6', description: 'Sleep 6 seconds', status: 'running', outputPath: '/tmp/out.log', ...over } });
81
+
82
+ let rootCxReset = () => { rootCx.sent.length = 0; };
83
+
84
+ // 1. Foreground: spawned at running; state held until the parent tool result,
85
+ // then child-scoped preview precedes the terminal state.
86
+ {
87
+ const server = fakeServer(CAPS_NATIVE);
88
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
89
+ obs.handleEvent(agentSpawn());
90
+ let names = rootCx.sent.map((s) => s.params.update.sessionUpdate);
91
+ assert.deepEqual(names, ['subagent_spawned']);
92
+ const spawned = rootCx.sent[0].params.update;
93
+ assert.equal(spawned.subagentSessionId, 'sess_subagent_agent_u1');
94
+ assert.equal(spawned.name, 'Count kts files');
95
+ assert.equal(spawned.task, 'Count the .kts files.');
96
+ assert.deepEqual(spawned.capabilities, {});
97
+ obs.handleEvent(agentTerminal()); // terminal arrives first (measured order)
98
+ assert.equal(rootCx.sent.length, 1, 'foreground completion is held for the parent result');
99
+ obs.handleEvent(parentResult({ success: true, content: '8\nagentId: agent_u1 (continue)\n<usage>subagent_tokens: 21127\ntool_uses: 1\nduration_ms: 4477</usage>' }));
100
+ names = rootCx.sent.map((s) => s.params.update.sessionUpdate);
101
+ assert.deepEqual(names, ['subagent_spawned', 'subagent_state_update']);
102
+ assert.equal(rootCx.sent[1].params.update.state, 'completed');
103
+ assert.equal(server.__childCx.sent.length, 1, 'the preview lands on the child session');
104
+ assert.equal(server.__childCx.sent[0].params.sessionId, 'sess_subagent_agent_u1');
105
+ assert.equal(server.__childCx.sent[0].params.update.content.text, '8');
106
+ }
107
+
108
+ // 1b. Foreground with a string-form result payload (both encodings measured).
109
+ rootCxReset();
110
+ {
111
+ const server = fakeServer(CAPS_NATIVE);
112
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
113
+ obs.handleEvent(agentSpawn());
114
+ obs.handleEvent(parentResult(JSON.stringify({ success: true, content: 'nine\nagentId: agent_u1 (x)\n<usage>d</usage>' })));
115
+ obs.handleEvent(agentTerminal());
116
+ assert.equal(server.__childCx.sent[0].params.update.content.text, 'nine');
117
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
118
+ }
119
+
120
+ // 2. Background agent: state fires at the agent terminal, no preview chunk.
121
+ rootCxReset();
122
+ {
123
+ const server = fakeServer(CAPS_NATIVE);
124
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
125
+ obs.handleEvent(agentSpawn({ background: true, outputFile: '/tmp/agent/out.txt' }));
126
+ obs.handleEvent({ type: 'session.updated', payload: { taskId: 'agent_u1', taskKind: 'subagent', status: 'running', toolCallId: 'call_p1' } });
127
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned'],
128
+ 'taskKind=subagent events belong to the agent path, never async_task');
129
+ obs.handleEvent(agentTerminal({ background: true, outputFile: '/tmp/agent/out.txt' }));
130
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
131
+ assert.equal(server.__childCx.sent.length, 0, 'background agents stream no child preview');
132
+ }
133
+
134
+ // 3. Adoption: first sighting already terminal emits spawned(adopted) then state.
135
+ rootCxReset();
136
+ {
137
+ const server = fakeServer(CAPS_NATIVE);
138
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
139
+ obs.handleEvent(agentTerminal());
140
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['subagent_spawned', 'subagent_state_update']);
141
+ assert.equal(rootCx.sent[0].params.update._meta.zcode.adopted, true);
142
+ }
143
+
144
+ // 4. Failed / cancelled mapping; unknown statuses ignored; replay idempotent.
145
+ rootCxReset();
146
+ {
147
+ const server = fakeServer(CAPS_NATIVE);
148
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
149
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', parentToolCallId: 'call_pf' }));
150
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', status: 'failed' }));
151
+ obs.handleEvent(agentSpawn({ agentId: 'agent_c', childSessionId: 'sess_subagent_agent_c', parentToolCallId: 'call_pc', status: 'cancelled' }));
152
+ obs.handleEvent(agentSpawn({ agentId: 'agent_r', childSessionId: 'sess_subagent_agent_r', status: 'paused' }));
153
+ const states = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'subagent_state_update').map((s) => s.params.update.state);
154
+ assert.deepEqual(states.sort(), ['cancelled', 'failed'], 'paused is not terminal; failed maps directly (foreground, non-completed)');
155
+ obs.handleEvent(agentSpawn({ agentId: 'agent_f', childSessionId: 'sess_subagent_agent_f', status: 'completed' }));
156
+ const statesAfter = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'subagent_state_update').map((s) => s.params.update.state);
157
+ assert.equal(statesAfter.filter((s) => s === 'completed').length, 0, 'terminal is single-shot');
158
+ }
159
+
160
+ // 5. Background bash task → AIR async_task pair.
161
+ rootCxReset();
162
+ {
163
+ const server = fakeServer(CAPS_NATIVE);
164
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
165
+ obs.handleEvent(taskEvent());
166
+ obs.handleEvent(taskEvent({ status: 'running', pid: 1 })); // duplicate sighting
167
+ assert.deepEqual(rootCx.sent.map((s) => s.params.update.sessionUpdate), ['async_task_spawned']);
168
+ const spawned = rootCx.sent[0].params.update;
169
+ assert.equal(spawned.asyncTaskId, 'exec_t1');
170
+ assert.equal(spawned.toolCallId, 'call_b1');
171
+ assert.equal(spawned.outputFilePath, '/tmp/out.log');
172
+ assert.equal(spawned.canStop, true);
173
+ assert.equal(spawned.showInTranscript, false);
174
+ obs.handleEvent(taskEvent({ status: 'failed' }));
175
+ obs.handleEvent(taskEvent({ status: 'completed' })); // single-shot terminal
176
+ const states = rootCx.sent.filter((s) => s.params.update.sessionUpdate === 'async_task_state_update').map((s) => s.params.update.state);
177
+ assert.deepEqual(states, ['failed']);
178
+ }
179
+
180
+ // 6. Workflow tasks and non-session.updated events are ignored.
181
+ rootCxReset();
182
+ {
183
+ const server = fakeServer(CAPS_NATIVE);
184
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
185
+ obs.handleEvent(taskEvent({ taskId: 'wf1', taskKind: 'workflow' }));
186
+ obs.handleEvent({ type: 'turn.started', payload: { inputSource: 'background_task' } });
187
+ obs.handleEvent({ type: 'model.streaming', payload: { kind: 'text_delta', delta: 'x' } });
188
+ obs.handleEvent(undefined);
189
+ assert.equal(rootCx.sent.length, 0);
190
+ }
191
+
192
+ // 7. Client without the draft capability: nothing is emitted (upstream behavior).
193
+ rootCxReset();
194
+ {
195
+ const server = fakeServer(CAPS_PLAIN);
196
+ const obs = new serverMod.AxomZcodeSubagents(server, 'z-1');
197
+ obs.handleEvent(agentSpawn());
198
+ obs.handleEvent(agentTerminal());
199
+ obs.handleEvent(taskEvent({ status: 'completed' }));
200
+ assert.equal(rootCx.sent.length, 0);
201
+ }
202
+
203
+ // 8. Dispatch re-scoping + mark gating against the patched dispatch module.
204
+ {
205
+ const server = fakeServer(CAPS_NATIVE);
206
+ const cx = recorder();
207
+ await dispatchMod.dispatchEvent(server, cx, 'root-1', {
208
+ kind: 'ToolCallNew', tool: 'Bash', callId: 'tool_subagent_agent_u1_call_inner1', title: 'inner bash', status: 'pending',
209
+ input: { command: 'ls' },
210
+ });
211
+ assert.equal(cx.sent.length, 1);
212
+ assert.equal(cx.sent[0].params.sessionId, 'sess_subagent_agent_u1', 'internal sub-agent frames are re-scoped to the child session');
213
+ assert.equal(cx.sent[0].params.update.toolCallId, 'tool_subagent_agent_u1_call_inner1');
214
+ assert.equal(cx.sent[0].params.update._meta.subagent, undefined, 'Bash frames never carry the mark');
215
+
216
+ const cxAgent = recorder();
217
+ await dispatchMod.dispatchEvent(server, cxAgent, 'root-1', {
218
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'call_p1', title: 'Agent: x', status: 'pending', input: {},
219
+ });
220
+ assert.equal(cxAgent.sent[0].params.update._meta.subagent, undefined,
221
+ 'with native subagents negotiated the mark is suppressed (the native events own the projection)');
222
+
223
+ const plainServer = fakeServer(CAPS_PLAIN);
224
+ const cxPlain = recorder();
225
+ await dispatchMod.dispatchEvent(plainServer, cxPlain, 'root-1', {
226
+ kind: 'ToolCallNew', tool: 'Agent', callId: 'call_p1', title: 'Agent: x', status: 'pending', input: {},
227
+ });
228
+ assert.equal(cxPlain.sent[0].params.update._meta.subagent, true, 'clients without the draft capability still get the mark');
229
+ }
230
+
231
+ // 9. Background-task card suppression: with the AIR async-task capability declared,
232
+ // the upstream [background] card must not mint beside the async_task events.
233
+ rootCxReset();
234
+ {
235
+ const server = fakeServer(CAPS_NATIVE);
236
+ const listener = new bgMod.BackgroundTaskListener(server, 'z-1');
237
+ listener.handleEvent(taskEvent());
238
+ listener.handleEvent(taskEvent({ status: 'completed' }));
239
+ assert.equal(rootCx.sent.length, 0, 'with asyncTasks declared the bg listener mints no card — AxomZcodeSubagents owns the task');
240
+ // Sub-agent background tasks are equally owned by the subagent path.
241
+ listener.handleEvent({ type: 'session.updated', payload: { taskId: 'agent_u1', taskKind: 'subagent', toolCallId: 'call_p1', status: 'running', description: 'd' } });
242
+ assert.equal(rootCx.sent.length, 0);
243
+ // Workflow runs keep the upstream fold/poller path — terminal status, so no poller
244
+ // arms; an unfolded terminal-first sighting still mints the upstream fallback card.
245
+ listener.handleEvent({ type: 'session.updated', payload: { taskId: 'wf1', taskKind: 'workflow', toolCallId: 'call_w1', status: 'completed', description: 'w' } });
246
+ assert.equal(rootCx.sent.length, 1, 'workflow tasks keep the upstream background-card path (not suppressed)');
247
+
248
+ rootCxReset();
249
+ const plainServer = fakeServer(CAPS_PLAIN);
250
+ const plainListener = new bgMod.BackgroundTaskListener(plainServer, 'z-1');
251
+ plainListener.handleEvent(taskEvent());
252
+ assert.equal(rootCx.sent.length, 1, 'clients without the AIR capability still get the upstream [background] card');
253
+ assert.equal(rootCx.sent[0].params.update.sessionUpdate, 'tool_call');
254
+ assert.ok(rootCx.sent[0].params.update.toolCallId.startsWith('bg_'), 'the upstream card keeps its bg_* id');
255
+ }
256
+
257
+ console.log('PASS zcode-subagent-subwork (patched: variants, child scoping, mark gating, no double task cards)');
@@ -0,0 +1,82 @@
1
+ # qoder-background-terminal
2
+
3
+ ## 为什么需要
4
+
5
+ 测试环境 Qoder CLI 1.1.61 的后台 Agent 调用在 ACP 中只返回 `async_launched`。
6
+ 原生根会话 JSONL 随后收到 `origin.kind=task-notification` 的系统完成通知,但 live ACP
7
+ 和 `session/load` 均未提供可供平台确认任务终态的更新。因此任务实际完成,Activity 仍显示
8
+ Waiting / Task result unconfirmed。不是根据主 Agent 的自然语言回答判断完成。
9
+
10
+ ## 补丁边界
11
+
12
+ 实现位于 Workstation 的 ACP 会话边界;不修改 Qoder 二进制,不引入 SDK、数据库表或平台委派。
13
+ 仅 initialize 身份 `qoder-cli@1.1.61` 的真实托管会话启用,配置探测不启用。
14
+ 按进程有效 HOME、cwd、native session ID 定位 `~/.qoder/projects/<encoded-cwd>/<session>.jsonl`。
15
+ 只读当前根会话历史,逐行校验精确 session ID / cwd / 非 sidechain;不打开 `tasks/*.output`
16
+ 或其他子任务完整日志。路径中的叶子文件不跟随 symlink。
17
+
18
+ 同时满足以下三项,才沿原有 WireEvent / journal / seq / ack 发出同一 Agent 工具的
19
+ `rawOutput.kind=agent-result` 更新:
20
+
21
+ 1. ACP 已观测到该根会话的后台 Agent 调用,且工具调用返回;cached resume 不重放
22
+ opening 时,从精确匹配的根 JSONL Agent tool_use 与结构化启动回执恢复相同关系。
23
+ 2. 原生结构化启动回执的 tool-use-id 对应该调用,并提供 agentId。
24
+ 3. 原生系统 task-notification 的 tool-use-id 对应该调用,含明确终态及结果。
25
+
26
+ 原生 ACP 已提供结构化终态时优先使用它。普通用户文字、其他会话、未知版本/格式不能触发补回。
27
+ 不根据标题、消息顺序、主 Agent 回答或超时推测完成。成功、失败、取消按明确通知状态处理;
28
+ 失败/取消格式当前由确定性用例验证,真机覆盖以验收报告为准。
29
+
30
+ 每次最多增量读取 4 MiB,读取块 64 KiB,单行上限 2 MiB;最多保存 512 个关联,单个结果
31
+ 128 KiB,全部结果 4 MiB,超过限制维持原始状态。当前没有待确认调用时不读文件。
32
+ 关闭、连接 abort、读取循环终止即停止定时器。没有文件、格式未知或读取失败不阻塞 ACP。
33
+ 重复轮询和同一文件重读不重复发布;重建运行时可重新确认同一工具,但不会创建第二个任务。
34
+ 历史补回走正常会话恢复路径,不直接修改数据库。
35
+
36
+ 2026-10-03 真机验收补充:默认 Agent 的输入可不带 `subagent_type`。它只是可选的类型
37
+ 文案,不能作为识别任务或跟踪完成通知的必填条件。识别仍要求 Qoder 的工具元数据、
38
+ `kind=think`、prompt 和明确后台标记;非字符串类型仍拒绝。服务端的 Qoder 专用摘要
39
+ 投影同步允许该字段缺失,不推测子会话身份。升级验收必须覆盖默认 Agent(省略类型)
40
+ 和显式类型两种调用,避免测试只覆盖手工指定 `subagent_type` 的请求。
41
+
42
+ ## 上游升级后如何判断是否保留
43
+
44
+ 版本增加不会自动证明修复。新版本默认不启用此补丁,先使用无补丁 CLI 抓取原始 ACP:
45
+
46
+ - 后台成功、失败、取消均有可对应 toolCallId 的明确终态及结果;启动回执不是任务终态。
47
+ - 两个同时运行的后台任务乱序完成,状态和结果不会串任务。
48
+ - 主轮次结束后迟到的后台完成仍会发送。
49
+ - 重开旧会话的 session/load 能恢复相同终态,断线恢复不创建重复任务。
50
+
51
+ 全部通过后登记修复版本、原始抓包、无补丁回归和删除提交,再删除模块、调用接线及测试。
52
+ 只解决 live 而不解决 load 时不能整体退役。需要对新版本继续兼容时,先重新核验目录编码、
53
+ JSONL 字段及通知格式,再显式扩充版本 gate;不对未知版本自动尝试。
54
+
55
+ ## 本地回归
56
+
57
+ `npx vitest run src/workstation/qoderAdapter/backgroundTerminal.test.ts src/workstation/acp/processor/sideChannel.test.ts src/workstation/acp/processor/equivalence.test.ts`
58
+ (修复折叠逻辑、经 raw 日志的 side note、golden 源 `qoder-background-terminal{,-large}`;旧 live 路径 `acpSession.test.ts` 已随 Task 29 删除)
59
+ 以及 CLI 全量测试、typecheck/build/补丁台账校验。此次随 CLI 0.3.2(整合主干 0.3.1)交付,无 npm 发布操作。
60
+
61
+ ## 旧会话详情配套投影
62
+
63
+ 真实 session/load 把同一个 Qoder opening 再次导入为无 turnId 的记录,原调用有唯一 turnId。
64
+ 详情原先要求恰好一个 opening,所以即使终态补回也会报 identity ambiguous。
65
+ 配套 `qoder-native-replay-detail` 只在 UI 折叠一个有 turn 的原调用及其更晚、无 turn、
66
+ 同 owner/call/input/title/locations 和完整 Qoder metadata 的精确副本。数据原样保留,
67
+ 多个真实 opening、跨 turn 的重复创建、任何输入或 vendor identity 冲突、其他 provider 的 opening 副本继续拒绝。
68
+ 待平台历史导入不再制造重复 opening 且无例外回归通过,再删除这个单独的投影兼容分支。
69
+ 此配套修改需要构建并部署 Web / Server,与仅 CLI 的终态修复分开核验。
70
+
71
+ Cached resume 的补充规则与限制见 [qoder-foreground-output.md](qoder-foreground-output.md)。
72
+
73
+
74
+ ## 后台结果的到达轮次
75
+
76
+ 2026-10-04 真机复验:session/resume 后按旧 toolCallId 补回两个已完成后台结果,
77
+ 不产生重复 opening,但详情控制器曾把更新的到达轮次也当作另一份调用身份,报 ambiguous。
78
+ 配套通用详情校验仅允许完整历史中唯一、明确 run_in_background=true 的原调用接收更晚更新;
79
+ 更新须同 owner/call/name,kind 不冲突、输入缺省或与原输入完全相等。
80
+ 真实重复 opening、前台跨轮复用、修改输入、未知/截断身份与更新早于创建仍拒绝。
81
+ 不新增厂商分支,不改任何 provider 的路由或执行控制。新增两个红绿功能测试并真机检查详情。
82
+ 这是后台异步结果的正常生命周期校验,独立于 Qoder 补丁,不应随上游补丁退役一并删除。
@@ -0,0 +1,42 @@
1
+ # qoder-foreground-output / qoder-cached-resume
2
+
3
+ ## 为什么需要
4
+
5
+ 2026-10-03 原生 Qoder CLI 1.1.61 抓包证实:两个前台 Agent 同时运行时,子任务
6
+ 文字/思考全部以根 sessionId 发送,没有 messageId 或归属 metadata。字符串在 ACP
7
+ 上线已交错,不能按标题、完成顺序或时间可靠拆回。每个 Agent 的工具最终结果却完整。
8
+ 另一个独立问题是 session/load 可能迟到重放历史,平台把它写到新轮次,制造重复 opening。
9
+ 本补丁不恢复暂存表,也不把未知来源的文字猜成子会话。
10
+
11
+ ## 行为与限制
12
+
13
+ 只启用 initialize 明确报告 qoder-cli@1.1.61 的进程;Claude、Codex、未知版本原样通过。
14
+ 实现位于 qoderAdapter/foregroundOutput.ts,ACP 层只接线。
15
+
16
+ - 根会话明确发出 Agent 前台调用(background=false 或省略)至所有前台调用终止期间,
17
+ 不投递没有 messageId/metadata 的根文字和思考。工具调用、命令、完整工具结果、权限、
18
+ 配置和有身份的子会话事件原样保留。任务完成后的主回答正常展示。
19
+ - 这是结果优先的降级:无法辨别的实时文字(包括此时可能出现的主 Agent 插话)不会展示;
20
+ **不宣称恢复完整子任务过程,也不宣称能独立操作这些摘要任务的 Stop/Resume**。
21
+ 出错、取消、轮次结束会解除过滤;不会因 unfinished Agent 永久屏蔽下一轮。
22
+ - 已有平台历史的普通 reconnect 使用官方 session/resume,保留同一个 native sessionId,
23
+ 模型/权限配置来自恢复响应。拒绝恢复时直接报错,不偷偷新建另一段会话。
24
+ - 真正历史接管/backfill、显式 settings load、无 resume 能力继续旧路径。本补丁不以模糊
25
+ 文本去重掩盖这些路径的迟到历史;没有证据声称所有 session/load 都已彻底解决。
26
+ - resume 不回放 opening,故后台修复(daemon 的 `BackgroundSideNotes` 读取 + processor 折叠 `qoder_side` note)可从只读根 JSONL 恢复精确后台
27
+ Agent 输入与启动关系,继续按 tool-use-id 补回明确通知。只发送同一个工具的终态更新,
28
+ 不发送历史 opening、不生成子会话。既有通知可能重新确认相同任务;不是重复执行任务。
29
+ 初次增量扫描完成后,没有待确认任务即停止文件轮询读操作。原有读限制/身份校验仍适用。
30
+
31
+ ## 回归与退役
32
+
33
+ acp/processor/foreground.test.ts(取代已删除的 qoderForegroundBaseline.test.ts)在 processor 上覆盖原始交错抓包和三前台任务边界、
34
+ 省略类型、失败/取消后续轮、完整结果、主回答、已标识文字、background 不过滤,以及 1.1.62/codex/claude 不过滤;
35
+ processor/sideChannel.test.ts 与 golden 等价测试(acp/processor/equivalence.test.ts,含 qoder-parallel-foreground)锁定 processor 输出。
36
+ acpAgents.test.ts 覆盖 cached resume / 真正 backfill / settings load 与其他 provider 边界;
37
+ backgroundTerminal.test.ts 覆盖无 opening 回放的后台精确结果恢复。
38
+
39
+ 上游升级后默认不启用。先裸 ACP 测并行前台长文字:必须提供稳定的来源/session/message
40
+ 标识,且终态与实际结果正确;再测 reconnect 不产生旧 opening,backfill 仍能取全历史,
41
+ 迟到后台通知仍能补全。通过原始抓包、无补丁回归及平台真机后记录版本再删除补丁。
42
+ 仅增加 CLI 版本号或最终任务成功不能证明可以退役。
@@ -0,0 +1,52 @@
1
+ {
2
+ "id": "qoder-steering",
3
+ "package": "@qoder-ai/qodercli",
4
+ "versions": [
5
+ "1.1.71"
6
+ ],
7
+ "bin": "qodercli",
8
+ "edits": [
9
+ {
10
+ "file": "bundle/qodercli.js",
11
+ "before_sha256": "a7ff3d5883231c1add98d266d5f860813c6fa7e498ae075384b7c558e8531c88",
12
+ "after_sha256": "b43037a4df32734a47bd5eb0f937583252bbbe637c36514cc8aba7fced3acfe9",
13
+ "before": "protocolVersion:_vs,authMethods:e,agentInfo:{name:d0,title:Nn,version:t},agentCapabilities:",
14
+ "after": "protocolVersion:xbs,authMethods:e,agentInfo:{name:HQ,title:Hn,version:t},_meta:{steering:{supported:true,timings:[\"now\"]}},agentCapabilities:"
15
+ },
16
+ {
17
+ "file": "bundle/qodercli.js",
18
+ "before_sha256": "b43037a4df32734a47bd5eb0f937583252bbbe637c36514cc8aba7fced3acfe9",
19
+ "after_sha256": "f1ec60df59dce8dc241fa504d947e5eb5aad7c098dbaa5e89a28d3fd8bb9000c",
20
+ "before": "async prompt(A){await this.ensureAuthenticated();let e=this.sessions.get(A.sessionId);",
21
+ "after": "async extMethod(method,params){\n if(method !== \"_session/steering\") throw as.methodNotFound(method);\n await this.ensureAuthenticated();\n if(!params || typeof params.sessionId !== \"string\" || !params.sessionId) throw as.invalidParams(undefined,\"Missing sessionId\");\n const session=this.sessions.get(params.sessionId);\n if(!session) throw as.invalidParams(undefined,\"Unknown sessionId\");\n return session.axomQoderSteer(params);\n}\nasync prompt(A){await this.ensureAuthenticated();let e=this.sessions.get(A.sessionId);"
22
+ },
23
+ {
24
+ "file": "bundle/qodercli.js",
25
+ "before_sha256": "f1ec60df59dce8dc241fa504d947e5eb5aad7c098dbaa5e89a28d3fd8bb9000c",
26
+ "after_sha256": "ea26b18b8e10d410c6eaf03d9df720d6b061979dd9cbc62ff4a1a98682b2f3e4",
27
+ "before": "promptRunning=!1;pendingMessages=[];",
28
+ "after": "promptRunning=!1;pendingMessages=[];axomQoderSteeringMessages=[];"
29
+ },
30
+ {
31
+ "file": "bundle/qodercli.js",
32
+ "before_sha256": "ea26b18b8e10d410c6eaf03d9df720d6b061979dd9cbc62ff4a1a98682b2f3e4",
33
+ "after_sha256": "3660901654aecf58b04ac669f2dc945c8ba16376e204ccef53f499a13d1d69e9",
34
+ "before": "async promptViaQueryEngine(A,e,t,i,n){",
35
+ "after": "axomQoderSteer(A){\n const steering=A._meta?.steering;\n if(steering?.priority !== undefined && steering.priority !== \"now\") throw as.invalidParams(undefined,\"Qoder supports only now steering\");\n if(steering?.idleBehavior !== undefined && steering.idleBehavior !== \"promptRequired\") throw as.invalidParams(undefined,\"Unsupported steering idleBehavior\");\n if(!Array.isArray(A.prompt) || !A.prompt.length || A.prompt.some(block=>!block || block.type !== \"text\" || typeof block.text !== \"string\")) throw as.invalidParams(undefined,\"Qoder steering requires text blocks\");\n const text=A.prompt.map(block=>block.text).join(\"\\n\");\n if(!text.trim() || text.length > 131072) throw as.invalidParams(undefined,\"Invalid steering text length\");\n if(this.disposed || !this.active || !this.promptRunning || !this.queryEngine?.activeAbortController || !this.pendingPrompt || this.pendingPrompt.signal.aborted) return {outcome:\"promptRequired\",reason:\"noRunningTurn\"};\n if(this.axomQoderSteeringMessages.length >= 32) throw as.invalidParams(undefined,\"Too many pending steering messages\");\n const message={prompt:A.prompt.map(block=>({type:\"text\",text:block.text})),id:mte()};\n this.axomQoderSteeringMessages.push(message);\n try{this.queryEngine.abort({source:\"sdk_interrupt\",origin:\"user\"});}\n catch(error){this.axomQoderSteeringMessages.pop();throw error;}\n return {outcome:\"injected\"};\n}\nasync promptViaQueryEngine(A,e,t,i,n){\n let result;\n try{\n result=await this.axomQoderPromptOnce(A,e,t,i,n);\n const originalId=result.userMessageId;\n while(!e.aborted && !this.disposed && this.active && this.axomQoderSteeringMessages.length){\n const message=this.axomQoderSteeringMessages.shift();\n const next=await this.axomQoderPromptOnce(message.prompt,e,mte(),message.id);\n if(result.usage && next.usage){\n const inputTokens=result.usage.inputTokens+next.usage.inputTokens;\n const outputTokens=result.usage.outputTokens+next.usage.outputTokens;\n next.usage={inputTokens,outputTokens,totalTokens:inputTokens+outputTokens};\n const models=new Map;\n for(const item of [...result._meta?.quota?.model_usage??[],...next._meta?.quota?.model_usage??[]]){\n const prior=models.get(item.model)??{input_tokens:0,output_tokens:0};\n models.set(item.model,{input_tokens:prior.input_tokens+item.token_count.input_tokens,output_tokens:prior.output_tokens+item.token_count.output_tokens});\n }\n next._meta={...next._meta,quota:{token_count:{input_tokens:inputTokens,output_tokens:outputTokens},model_usage:Array.from(models,([model,token_count])=>({model,token_count}))}};\n }\n result=next;\n }\n return {...result,...(originalId?{userMessageId:originalId}:{})};\n }finally{this.axomQoderSteeringMessages.length=0;}\n}\nasync axomQoderPromptOnce(A,e,t,i,n){"
36
+ },
37
+ {
38
+ "file": "bundle/qodercli.js",
39
+ "before_sha256": "3660901654aecf58b04ac669f2dc945c8ba16376e204ccef53f499a13d1d69e9",
40
+ "after_sha256": "7b031034151e76daccbf26c7dbf08779b3e75a281da97b8a56673efdf8433956",
41
+ "before": "if(!A.includes(\"Operation aborted\"))throw new cs(500,A);",
42
+ "after": "if(!A.includes(\"Operation aborted\")&&!(this.axomQoderSteeringMessages.length&&[\"aborted_tools\",\"aborted_streaming\"].includes(c.terminal_reason)))throw new as(500,A);"
43
+ },
44
+ {
45
+ "file": "bundle/qodercli.js",
46
+ "before_sha256": "7b031034151e76daccbf26c7dbf08779b3e75a281da97b8a56673efdf8433956",
47
+ "after_sha256": "a2949de7f397a904753d5d1d838d637cc93b3cf43e8b51af8e58a188d6c2e282",
48
+ "before": "else if(\"error_during_execution\"===A.subtype&&\"refusal\"!==A.stop_reason){let e=A.errors??[];",
49
+ "after": "else if(\"error_during_execution\"===A.subtype&&\"refusal\"!==A.stop_reason&&!(this.axomQoderSteeringMessages.length&&[\"aborted_tools\",\"aborted_streaming\"].includes(A.terminal_reason))){let e=A.errors??[];"
50
+ }
51
+ ]
52
+ }
@@ -0,0 +1,30 @@
1
+ # Qoder 原生 ACP 插话
2
+
3
+ Qoder 1.1.61 的原生 ACP 将运行中的 `session/prompt` 排队,没有声明或实现
4
+ `_session/steering`。只在实验 SDK adapter 中支持插话,不能让普通 `qoder` 入口出现按钮。
5
+
6
+ `qoder-steering.json` 在 `qoder-subagent-origin.json` 之后应用,逐步核验官方 npm
7
+ 包的 SHA-256。只修改 Axom 拥有的不可变副本,不改用户安装的 CLI 或 npm 缓存。
8
+ 原生 ACP 的 new/load/resume、模型、权限、MCP、工具和子任务归属继续由 Qoder 处理。
9
+
10
+ 补丁在 initialize 顶层声明 `_meta.steering={supported:true,timings:["now"]}`,
11
+ 并实现 `_session/steering`。文本通过校验后加入当前会话的专用插话队列,调用原生
12
+ QueryEngine.abort 中断当前执行;原有平台 prompt 保持待完成。原生执行收束后,在
13
+ 同一个 QueryEngine、历史、配置与平台回合内处理插话,新的回复完成才返回 prompt。
14
+ 这会中断尚未完成的工具;不承诺撤销已经完成的文件或命令副作用。
15
+
16
+ 没有运行回合时返回 `promptRequired`,不自行启动无人跟踪的回合。当前仅支持文本和
17
+ `now`;无效输入不打断执行。Stop 与关闭会话优先于插话,真实执行错误继续上报。
18
+ 消息队列限 32 条、每条文本限 128 KiB;每次 prompt 收束清理专用队列。它不增加
19
+ 数据库字段或第二份 transcript。通用 ACP、服务端和 UI 不增加 Qoder 判断。
20
+
21
+ 目前只认证 npm 1.1.61 这份原文件。新包必须重新核验源文件和运行时语义,不按版本号
22
+ 猜兼容。显式 command 覆盖继续尊重用户设置,不偷偷把本地 CLI 换成 managed 包。
23
+
24
+ 当无补丁的原生 Qoder ACP 自己声明并实现相同插话协议,且普通入口的运行中工具、
25
+ 消息落点、Stop、load/resume、模型/权限、子任务与完整基线验收通过时,可移除这份
26
+ 配方、runtime 引用和台账项;单独 SDK 探针或版本升级不构成移除依据。
27
+
28
+ 验证包括 CLI 的 `nativeSteeringPatch.test.ts`、对官方补丁包执行
29
+ `node axom-patch/cli/probes/qoder-steering.mjs <package>`,以及测试环境普通 `qoder`
30
+ 入口的真实模型、REST 和 canvas 界面验收。测试入口名称及具体会话必须写入验收记录。
@@ -0,0 +1,73 @@
1
+ {
2
+ "id": "qoder-subagent-origin",
3
+ "package": "@qoder-ai/qodercli",
4
+ "versions": [
5
+ "1.1.71"
6
+ ],
7
+ "edits": [
8
+ {
9
+ "file": "bundle/qodercli.js",
10
+ "before_sha256": "bbac475858838d609241881d1ee89b26c32a2eae8697672522778b18ed78a94a",
11
+ "after_sha256": "4807a4c1cfdb57a6bd9102ec3924185e3e2649935164fed3e98ca353063ff5f0",
12
+ "before": "toolCallNames=new Map;",
13
+ "after": "toolCallNames=new Map;axomQoderToolOrigins=new Map;"
14
+ },
15
+ {
16
+ "file": "bundle/qodercli.js",
17
+ "before_sha256": "4807a4c1cfdb57a6bd9102ec3924185e3e2649935164fed3e98ca353063ff5f0",
18
+ "after_sha256": "6485062df36f164432cfe8d8a7adbf32f0bf85053500c11b61b8fce15bcdcbed",
19
+ "before": "async sendUpdate(A){let e={sessionId:this.id,update:A};await this.connection.sessionUpdate(e)}",
20
+ "after": "axomQoderId(value){return typeof value === \"string\" && value.length > 0 && value.length <= 256 && !/[\\x00-\\x1f\\x7f]/.test(value)}\naxomQoderObserveSdk(message){\n if(message.type !== \"assistant\" || !Array.isArray(message.message?.content)) return;\n const parent=message.parent_tool_use_id;\n if(parent !== null && !this.axomQoderId(parent)) return;\n for(const block of message.message.content){\n if(block?.type !== \"tool_use\" || !this.axomQoderId(block.id) || !this.axomQoderId(block.name)) continue;\n const prior=this.axomQoderToolOrigins.get(block.id);\n if(this.axomQoderToolOrigins.has(block.id)){\n if(prior && (prior.parent !== parent || prior.name !== block.name)) this.axomQoderToolOrigins.set(block.id,null);\n continue;\n }\n if(this.axomQoderToolOrigins.size >= 4096) continue;\n this.axomQoderToolOrigins.set(block.id,{parent,name:block.name});\n }\n}\naxomQoderSdkUpdate(message,update){\n if(this.axomQoderId(message.parent_tool_use_id)) update={...update,_meta:{...update._meta,qoder:{...update._meta?.qoder,version:1,source:\"native-acp-patch\",parentToolUseId:message.parent_tool_use_id,route:\"child\"}}};\n return this.sendUpdate(update);\n}\nasync sendUpdate(A){\n if(A.sessionUpdate === \"tool_call\" || A.sessionUpdate === \"tool_call_update\"){\n const origin=this.axomQoderToolOrigins.get(A.toolCallId);\n if(origin) A={...A,_meta:{...A._meta,qoder:{...A._meta?.qoder,version:1,source:\"native-acp-patch\",toolName:origin.name,...(origin.parent ? {parentToolUseId:origin.parent,route:\"child\"} : {})}}};\n }\n let e={sessionId:this.id,update:A};await this.connection.sessionUpdate(e)\n}"
21
+ },
22
+ {
23
+ "file": "bundle/qodercli.js",
24
+ "before_sha256": "6485062df36f164432cfe8d8a7adbf32f0bf85053500c11b61b8fce15bcdcbed",
25
+ "after_sha256": "2acc4d22cf66ca8c2c73543ba6bd1023d64c7730204521444ce2386c724bb0fd",
26
+ "before": "async dispatchSDKMessage(A){switch(A.type){",
27
+ "after": "async dispatchSDKMessage(A){this.axomQoderObserveSdk(A);switch(A.type){"
28
+ },
29
+ {
30
+ "file": "bundle/qodercli.js",
31
+ "before_sha256": "2acc4d22cf66ca8c2c73543ba6bd1023d64c7730204521444ce2386c724bb0fd",
32
+ "after_sha256": "7908a80dee46d50fcf8d57ba94c45f52ac3261b358a105719e07c9d4db0b0538",
33
+ "before": "await this.sendUpdate({sessionUpdate:\"agent_message_chunk\",content:{type:\"text\",text:e.delta.text}})",
34
+ "after": "await this.axomQoderSdkUpdate(A,{sessionUpdate:\"agent_message_chunk\",content:{type:\"text\",text:e.delta.text}})"
35
+ },
36
+ {
37
+ "file": "bundle/qodercli.js",
38
+ "before_sha256": "7908a80dee46d50fcf8d57ba94c45f52ac3261b358a105719e07c9d4db0b0538",
39
+ "after_sha256": "d01e482a334b7973c40bc61f99482da0ccfda08984c289148cc1ca30866f7286",
40
+ "before": "await this.sendUpdate({sessionUpdate:\"agent_thought_chunk\",content:{type:\"text\",text:e.delta.thinking}})",
41
+ "after": "await this.axomQoderSdkUpdate(A,{sessionUpdate:\"agent_thought_chunk\",content:{type:\"text\",text:e.delta.thinking}})"
42
+ },
43
+ {
44
+ "file": "bundle/qodercli.js",
45
+ "before_sha256": "d01e482a334b7973c40bc61f99482da0ccfda08984c289148cc1ca30866f7286",
46
+ "after_sha256": "24113eca8518c82ed990ba68e1728cac9bd55f57fb2026030cfa5be430b85e85",
47
+ "before": "async streamHistory(A){let e=Ocd(A);for(let t of A){",
48
+ "after": "async streamHistory(A){let e=Ocd(A);for(let t of A){this.axomQoderObserveSdk(t);"
49
+ },
50
+ {
51
+ "file": "bundle/qodercli.js",
52
+ "before_sha256": "24113eca8518c82ed990ba68e1728cac9bd55f57fb2026030cfa5be430b85e85",
53
+ "after_sha256": "2337caf11a15d6f5f08dd0fade9968d3caaf2c4f4d370df4b86b28d7e8838bd2",
54
+ "before": "await this.sendUpdate({sessionUpdate:\"user_message_chunk\",content:{type:\"text\",text:A},messageId:t.uuid})",
55
+ "after": "await this.axomQoderSdkUpdate(t,{sessionUpdate:\"user_message_chunk\",content:{type:\"text\",text:A},messageId:t.uuid})"
56
+ },
57
+ {
58
+ "file": "bundle/qodercli.js",
59
+ "before_sha256": "2337caf11a15d6f5f08dd0fade9968d3caaf2c4f4d370df4b86b28d7e8838bd2",
60
+ "after_sha256": "628db161081afd5a44573e916de1afc096c0f2451c0f62f774f5116ecbfd414a",
61
+ "before": "await this.sendUpdate({sessionUpdate:\"agent_message_chunk\",content:{type:\"text\",text:A},messageId:t.uuid})",
62
+ "after": "await this.axomQoderSdkUpdate(t,{sessionUpdate:\"agent_message_chunk\",content:{type:\"text\",text:A},messageId:t.uuid})"
63
+ },
64
+ {
65
+ "file": "bundle/qodercli.js",
66
+ "before_sha256": "628db161081afd5a44573e916de1afc096c0f2451c0f62f774f5116ecbfd414a",
67
+ "after_sha256": "a7ff3d5883231c1add98d266d5f860813c6fa7e498ae075384b7c558e8531c88",
68
+ "before": "await this.sendUpdate({sessionUpdate:\"agent_thought_chunk\",content:{type:\"text\",text:A.thinking},messageId:t.uuid})",
69
+ "after": "await this.axomQoderSdkUpdate(t,{sessionUpdate:\"agent_thought_chunk\",content:{type:\"text\",text:A.thinking},messageId:t.uuid})"
70
+ }
71
+ ],
72
+ "bin": "qodercli"
73
+ }
@@ -0,0 +1,73 @@
1
+ # qoder-subagent-origin / qoder-subagent-subwork
2
+
3
+ ## 为什么需要
4
+
5
+ Qoder CLI 1.1.61 的内部运行流已经带 `parent_tool_use_id`,但它自己的 ACP
6
+ `dispatchSDKMessage` / `streamHistory` 在生成文字、思考和工具事件时丢掉了这个字段。
7
+ 因此原生 `qodercli --acp` 的子任务 Read / Bash 出现在主会话。ACP 上层无法从时间、
8
+ 标题或返回顺序补出可靠关系,尤其不能拆解并发任务。
9
+
10
+ 此前只在 Axom 的 SDK adapter 里加 `_meta.qoder`,没有覆盖用户正在用的原生入口。
11
+ 那个实验能证明 SDK 归属信息存在,但不能证明原生入口已经修好。
12
+
13
+ ## 补丁的位置与行为
14
+
15
+ 1. `qoder-subagent-origin.json` 对官方 npm `@qoder-ai/qodercli@1.1.61` 的
16
+ `bundle/qodercli.js` 做精确版本、原文件及每一步产物 SHA-256 校验。
17
+ 在 Qoder 原生 ACP 转换层保留每个工具的父 ID,在文字/思考及历史事件上写
18
+ `_meta.qoder={version:1,source:"native-acp-patch",parentToolUseId,route:"child"}`。
19
+ 根工具保留工具名和补丁版本,但不伪造父 ID。冲突和容量不足不猜归属。
20
+ 2. `qoderAdapter/subwork.ts` 消费该元数据,生成已有的 ACP
21
+ `subagent_spawned` / `subagent_state_update`,并给 child item 填确定性地址:
22
+ `qoder-agent:<root native sessionId>:<Agent toolCallId>`。
23
+ 解析器仍在根进程;任务正文、执行工具、结果和带身份的文字/思考落在 subwork。
24
+ 3. Agent 工具调用被归一为同一个 subwork 派发与结果,不再额外产生委派摘要卡。
25
+ 后台 `async_launched` 只是启动回执,不能据此结束子工作;已存在的后台真实终态修补
26
+ 走同一归一入口。权限回调保持原生根会话的请求/回答路径,不启用独立子任务控制。
27
+
28
+ 服务端、数据库、公共工作模型及 App 不新增 Qoder 判断;此次新增的核心 Qoder
29
+ 元数据分支已经撤回。通用 ACP 会话只接线到 Qoder 自己的边界。
30
+
31
+ ## 分发和版本门槛
32
+
33
+ `runtime.json` 引用补丁配方。`managedPatches.ts` 在 Install/Update/首次 managed
34
+ launch 时复制完整 npm 包及依赖,校验并写收据,启动不可变副本。npm 原缓存和用户
35
+ 原有 Qoder 二进制保持原样。Qoder 包同时发布 `qodercli` 和 dispatcher 两个 bin,
36
+ 配方显式选择 `bin:"qodercli"`;通用补丁加载器只增加可信配方的 bin 选择能力。
37
+ 没有显式 bin 的其他补丁仍执行原来的单入口检查。
38
+
39
+ - 只认证原生 Qoder CLI 1.1.61 的这份发布包,不把 1.1.64 当作兼容版本。
40
+ - CLI 版本 bump 到 0.3.7,补丁跟随正常 Client release 分发。
41
+ - 显式自定义 command 按用户配置运行,不会偷偷替换用户二进制。要启用原生补丁,
42
+ 工作站 Qoder 项使用 catalog 管理的原生 npm ACP 入口(清除旧二进制 command/args)。
43
+ - 继续走 Qoder 原生 ACP 的 new/load/resume、模型和权限模式;不把原生会话切换到
44
+ 不支持恢复的 SDK adapter。已有历史不追溯猜分或改写;修复后的新事件才按证据分流。
45
+ - SDK adapter 的既有实验同样可经过本边界,但不是原生入口验收的替代品。
46
+
47
+ ## 什么时候可以移除
48
+
49
+ 当无补丁的 Qoder ACP 能发送稳定的派生关系、每帧子任务归属、真实终态以及可回放
50
+ 的同一关系,并通过并发乱序、失败/取消、后台迟到、load/resume/重连验收后:删除
51
+ 配方、runtime 引用、Qoder subwork 归一器和接线,台账标 retired。继续保留通用
52
+ ACP/Claude/Codex 基线。不以升级版本号或主 Agent 返回成功作为移除依据。
53
+
54
+ ## 验证
55
+
56
+ ```bash
57
+ cd axom-cli
58
+ npm test
59
+ cd ..
60
+ node axom-patch/cli/probes/qoder-subagent-origin.mjs /path/to/patched/qoder-package
61
+ AXOM_BASELINE_SQLITE_BIN=/path/to/new-sqlite/bin \
62
+ AXOM_BASELINE_PYTHON_BIN=/path/to/new-python/bin scripts/test-acp-baseline.sh
63
+ ```
64
+
65
+ 运行时探针执行官方 bundle 里实际的 ACP dispatch 方法,验证并发 child 的工具与
66
+ 结果、文字、根输出和冲突边界;不启动模型、不读取凭据。真实功能验收必须另经
67
+ 测试工作站、服务端、数据库、REST 和页面,记录具体会话。fixture 通过不等于上线验收。
68
+
69
+ ## 原生后台过程补齐
70
+
71
+ Qoder 1.1.61 不向主 ACP 流转发后台子任务的 SDK 消息。`backgroundProcess.ts` 只读轮询经过原生启动回执确认的 `agent-<agentId>.jsonl`;每行必须同时匹配 native session、cwd、agentId、parent_tool_use_id、isSidechain 与版本。只发出实际记录的工具调用、结果、思考和回复,经 `_meta.qoder` 进入同一 subwork 边界。父会话空闲后仍可接收这些项;完成状态继续采用原生 task-notification,不从最后一条文本推断完成。
72
+
73
+ 解析有单行、单轮字节与行数上限,拒绝符号链接、未知工具结果与不完整行,并按原生 uuid 去重。这一补齐针对本次已观察的后台调用;历史未观察任务不会被猜测迁移。上游转发后台消息后应移除 transcript 轮询,并保留并行归属和晚到事件验收。