@platless/axom-cli 0.3.13 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. package/dist/workstation/tools/terminal.js +0 -214
@@ -0,0 +1,211 @@
1
+ // Execute the real published adapter classes, without models or credentials.
2
+ // node <probe> <pi-acp-package-directory> [--expect-no-subagents]
3
+ //
4
+ // Pristine pi-acp emits no subagent variants and drops custom messages; the patched
5
+ // adapter normalizes pi-subagents children into draft ACP subagent variants with
6
+ // ids derived only from the root session id and toolCallId/runId, so the same input
7
+ // always produces byte-identical frames.
8
+ import assert from 'node:assert/strict';
9
+ import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import path from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const directory = path.resolve(process.argv[2]);
16
+ const expectNone = process.argv.includes('--expect-no-subagents');
17
+ const require = createRequire(path.join(directory, 'package.json'));
18
+ const scratch = await mkdtemp(path.join(tmpdir(), 'axom-pi-subagents-'));
19
+ try {
20
+ const original = await readFile(path.join(directory, 'dist/index.js'), 'utf8');
21
+ let source = original.slice(0, original.lastIndexOf('// src/index.ts')).replace(/^#![^\n]*\n/, '');
22
+ source = source.replace(/from "([^"\n]+)"/g, (whole, specifier) => specifier.startsWith('@') || specifier === 'cross-spawn'
23
+ ? `from ${JSON.stringify(pathToFileURL(require.resolve(specifier)).href)}` : whole);
24
+ source += '\nexport { PiAcpAgent, PiAcpSession };\n';
25
+ await writeFile(path.join(scratch, 'package.json'), await readFile(path.join(directory, 'package.json')));
26
+ await writeFile(path.join(scratch, 'runtime.mjs'), source);
27
+ const { PiAcpSession } = await import(pathToFileURL(path.join(scratch, 'runtime.mjs')));
28
+
29
+ const COLLECT = () => {
30
+ const updates = [];
31
+ return { updates, conn: { async sessionUpdate(event) { updates.push(event); } } };
32
+ };
33
+ const newSession = () => {
34
+ const collect = COLLECT();
35
+ let events;
36
+ const proc = { onEvent(handler) { events = handler; } };
37
+ const session = new PiAcpSession({ sessionId: 'fixture', cwd: scratch, mcpServers: [], proc, conn: collect.conn });
38
+ return { session, collect, fire: ev => { events(ev); return session.flushEmits(); } };
39
+ };
40
+
41
+ // One full drive; every awaited fire drains the ordered emit chain first.
42
+ // verify=false collects the frames without the per-step assertions, for the pristine run.
43
+ const drive = async (verify = true) => {
44
+ const { session, collect, fire } = newSession();
45
+ const updates = collect.updates;
46
+ const names = () => updates.map(u => `${u.sessionId}:${u.update.sessionUpdate}`);
47
+
48
+ // Foreground launch: spawned precedes the adapter's own tool_call frame.
49
+ await fire({ type: 'tool_execution_start', toolCallId: 't1', toolName: 'subagent',
50
+ args: { agent: 'scout', task: 'map the repo' } });
51
+ if (verify) {
52
+ assert.deepEqual(updates[0].update, { sessionUpdate: 'subagent_spawned', subagentSessionId: 'pi-agent:fixture:t1',
53
+ name: 'scout', task: 'map the repo', capabilities: {}, _meta: { pi: { toolCallId: 't1' } } });
54
+ assert.equal(updates[0].sessionId, 'fixture');
55
+ assert.equal(updates[1].update.sessionUpdate, 'tool_call');
56
+ }
57
+
58
+ // Foreground completion: child preview then terminal state, then the adapter's frame.
59
+ await fire({ type: 'tool_execution_end', toolCallId: 't1', isError: false,
60
+ result: { content: [{ type: 'text', text: 'all clear' }], details: { mode: 'single', runId: 'fg1' } } });
61
+ if (verify) {
62
+ const afterEnd = names();
63
+ assert.deepEqual(afterEnd.slice(2, 4), ['pi-agent:fixture:t1:agent_message_chunk', 'fixture:subagent_state_update']);
64
+ assert.equal(updates[3].update.state, 'completed');
65
+ }
66
+
67
+ // Management and steering calls are not launches.
68
+ await fire({ type: 'tool_execution_start', toolCallId: 't2', toolName: 'subagent', args: { action: 'list' } });
69
+ await fire({ type: 'tool_execution_end', toolCallId: 't2', isError: false,
70
+ result: { content: [{ type: 'text', text: 'agents: ...' }], details: {} } });
71
+ await fire({ type: 'tool_execution_start', toolCallId: 't3', toolName: 'subagent',
72
+ args: { id: 'x', message: 'steer' } });
73
+ await fire({ type: 'tool_execution_end', toolCallId: 't3', isError: false,
74
+ result: { content: [{ type: 'text', text: 'queued' }], details: {} } });
75
+ if (verify) assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_spawned' && u.update.name !== 'scout'));
76
+
77
+ // Async launch: the receipt is not a terminal state.
78
+ await fire({ type: 'tool_execution_start', toolCallId: 't4', toolName: 'subagent',
79
+ args: { agent: 'researcher', task: 'study', async: true } });
80
+ if (verify) assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_spawned' && u.update._meta?.pi?.toolCallId === 't4')?.update.subagentSessionId, 'pi-agent:fixture:t4');
81
+ await fire({ type: 'tool_execution_end', toolCallId: 't4', isError: false,
82
+ result: { content: [{ type: 'text', text: 'Async single: researcher [r9]' }],
83
+ details: { mode: 'single', runId: 'r9', asyncId: 'r9', asyncDir: '/tmp/async' } } });
84
+ if (verify) assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t4'));
85
+
86
+ // The completion notice closes the async child by run id.
87
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
88
+ content: 'researcher finished', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed',
89
+ summary: 'study done', resultPath: '/tmp/async/r9/result.json' }] } } });
90
+ await fire({ type: 'message_start', message: { role: 'custom', customType: 'subagent-notify',
91
+ content: 'duplicate pair member', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
92
+ if (verify) {
93
+ const closed = updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t4');
94
+ assert.equal(closed.update.state, 'completed');
95
+ assert.equal(closed.update._meta.pi.runId, 'r9');
96
+ assert.equal(closed.update._meta.pi.resultPath, '/tmp/async/r9/result.json');
97
+ }
98
+
99
+ // A repeated notice is idempotent.
100
+ const countBefore = updates.length;
101
+ if (verify) {
102
+ const beforePair = updates.length;
103
+ await fire({ type: 'message_start', message: { role: 'custom', customType: 'subagent-notify',
104
+ content: 'pair member', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
105
+ assert.equal(updates.length, beforePair);
106
+ }
107
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
108
+ content: 'researcher finished', details: { runs: [{ runId: 'r9', agent: 'researcher', status: 'completed' }] } } });
109
+ if (verify) assert.equal(updates.length, countBefore + 0);
110
+
111
+ // Unknown runs (adapter restart) get an adopted spawn plus their state.
112
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
113
+ content: 'worker stopped', details: { runs: [{ runId: 'zz', agent: 'worker', status: 'stopped', summary: '' }] } } });
114
+ if (verify) {
115
+ const adopted = updates.find(u => u.update.sessionUpdate === 'subagent_spawned' && u.update._meta?.pi?.adopted);
116
+ assert.equal(adopted.update.subagentSessionId, 'pi-agent:fixture:run:zz');
117
+ assert.equal(adopted.update.name, 'worker');
118
+ assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:run:zz').update.state, 'cancelled');
119
+ }
120
+
121
+ // Paused is not terminal; other custom messages are ignored.
122
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'subagent-notify',
123
+ content: 'paused', details: { runs: [{ runId: 'pp', agent: 'worker', status: 'paused' }] } } });
124
+ if (verify) {
125
+ assert.ok(updates.some(u => u.update.sessionUpdate === 'subagent_spawned' && u.update.subagentSessionId === 'pi-agent:fixture:run:pp'));
126
+ assert.ok(!updates.some(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:run:pp'));
127
+ }
128
+ const beforeOther = updates.length;
129
+ await fire({ type: 'message_end', message: { role: 'custom', customType: 'something-else',
130
+ content: 'x', details: {} } });
131
+ await fire({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'hi' }] } });
132
+ assert.equal(updates.length, beforeOther);
133
+
134
+ // Progress dedup: identical-heartbeat tool_execution_update frames are dropped, real
135
+ // changes pass, other tools are never deduplicated.
136
+ await fire({ type: 'tool_execution_start', toolCallId: 't5', toolName: 'subagent',
137
+ args: { agent: 'worker', task: 'long job', async: false } });
138
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
139
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
140
+ details: { progress: [{ agent: 'worker', durationMs: 1000, lastActivityAt: 1791891393036, status: 'running tool bash' }] } } });
141
+ const afterFirstUpdate = updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length;
142
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
143
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
144
+ details: { progress: [{ agent: 'worker', durationMs: 2000, lastActivityAt: 1791891394047,
145
+ recentOutput: ['(no output)', '(no output)'], status: 'running tool bash' }] } } });
146
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
147
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
148
+ details: { progress: [{ agent: 'worker', durationMs: 3000, lastActivityAt: 1791891395047,
149
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash' }] } } });
150
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
151
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
152
+ details: { progress: [{ agent: 'worker', durationMs: 3100, lastActivityAt: 1791891396047,
153
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
154
+ inputTokens: 2730, outputTokens: 69, cacheRead: 160, tokens: 2799, window: 2890, windowPeak: 2890 }],
155
+ results: [{ usage: { input: 2730, output: 69, cost: 0.0009, turns: 1 } }] } } });
156
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
157
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
158
+ details: { progress: [{ agent: 'worker', durationMs: 3200, lastActivityAt: 1791891397047,
159
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
160
+ inputTokens: 5483, outputTokens: 1852, cacheRead: 3049, tokens: 7335, window: 5642, windowPeak: 5642 }],
161
+ results: [{ usage: { input: 5483, output: 1852, cost: 0.0040, turns: 1 } }] } } });
162
+ if (verify) {
163
+ assert.equal(updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length, afterFirstUpdate,
164
+ 'heartbeat frames differing only in timers/recentOutput/token telemetry are dropped');
165
+ }
166
+ await fire({ type: 'tool_execution_update', toolCallId: 't5', toolName: 'subagent',
167
+ partialResult: { content: [{ type: 'text', text: '(running...)' }],
168
+ details: { progress: [{ agent: 'worker', durationMs: 3500, lastActivityAt: 1791891396047,
169
+ recentOutput: ['(no output)', '(no output)', '(no output)'], status: 'running tool bash',
170
+ recentTools: [{ tool: 'bash', args: 'ls', endMs: 1 }] }] } } });
171
+ if (verify) {
172
+ assert.equal(updates.filter(u => u.update.sessionUpdate === 'tool_call_update').length, afterFirstUpdate + 1,
173
+ 'a real milestone (recentTools growth / status change) still passes');
174
+ }
175
+ const updatesBeforeOther = updates.length;
176
+ await fire({ type: 'tool_execution_update', toolCallId: 'tx', toolName: 'read',
177
+ partialResult: { content: [{ type: 'text', text: 'x' }] } });
178
+ await fire({ type: 'tool_execution_update', toolCallId: 'tx', toolName: 'read',
179
+ partialResult: { content: [{ type: 'text', text: 'x' }] } });
180
+ if (verify) assert.equal(updates.length, updatesBeforeOther + 2, 'other tools are never deduplicated');
181
+ await fire({ type: 'tool_execution_end', toolCallId: 't5', isError: false,
182
+ result: { content: [{ type: 'text', text: 'long job done' }], details: { mode: 'single', runId: 'fg5' } } });
183
+ if (verify) {
184
+ assert.equal(updates.find(u => u.update.sessionUpdate === 'subagent_state_update' && u.update.subagentSessionId === 'pi-agent:fixture:t5')?.update.state, 'completed',
185
+ 'a deduplicated run still closes normally');
186
+ }
187
+
188
+ return updates;
189
+ };
190
+
191
+ if (expectNone) {
192
+ const first = await drive(false);
193
+ const spawns = first.filter(u => u.update.sessionUpdate === 'subagent_spawned').length;
194
+ const states = first.filter(u => u.update.sessionUpdate === 'subagent_state_update').length;
195
+ assert.equal(spawns, 0);
196
+ assert.equal(states, 0);
197
+ assert.ok(first.some(u => u.update.sessionUpdate === 'tool_call'), 'adapter still maps tool calls');
198
+ console.log('PASS pristine pi-acp emits no subagent variants and ignores custom messages');
199
+ } else {
200
+ const first = await drive();
201
+ const spawns = first.filter(u => u.update.sessionUpdate === 'subagent_spawned').length;
202
+ const states = first.filter(u => u.update.sessionUpdate === 'subagent_state_update').length;
203
+ assert.equal(spawns, 5); // t1, t4, adopted zz, paused pp, t5
204
+ assert.equal(states, 4); // t1, r9(t4), zz, t5
205
+ const second = await drive();
206
+ assert.deepEqual(second, first); // deterministic: no ids beyond the inputs
207
+ console.log('PASS subagent variants, ordering, idempotence and determinism');
208
+ }
209
+ } finally {
210
+ await rm(scratch, { recursive: true, force: true });
211
+ }
@@ -83,7 +83,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
83
83
  const directory = process.argv[2];
84
84
  if (!directory) throw new Error('Usage: node qoder-steering.mjs <patched-qoder-package>');
85
85
  const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
86
- assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.61');
86
+ assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.71');
87
87
  await verifyQoderSteering(await readFile(join(directory, 'bundle/qodercli.js'), 'utf8'));
88
88
  console.log('Native Qoder steering: host-turn lifetime, interruption, idle, validation, Stop and failure checks passed');
89
89
  }
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  const directory = process.argv[2];
7
7
  if (!directory) throw new Error('Usage: node qoder-subagent-origin.mjs <patched-package-directory>');
8
8
  const metadata = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
9
- assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.61');
9
+ assert.equal(metadata.name, '@qoder-ai/qodercli'); assert.equal(metadata.version, '1.1.71');
10
10
  const bundle = await readFile(join(directory, 'bundle/qodercli.js'), 'utf8');
11
11
  const method = (start, end) => {
12
12
  const from = bundle.indexOf(start), to = bundle.indexOf(end, from);
@@ -15,14 +15,21 @@ const method = (start, end) => {
15
15
  };
16
16
  const helper = method('axomQoderId(value)', 'supportsTerminalOutput(){');
17
17
  const dispatch = method('async dispatchSDKMessage(A)', 'createAcpSlashCommandHandler(){');
18
- const Session = new Function('brr', 'kRs', 'PRs', 'xc', 'Qze', `return class {
18
+ // 1.1.71 moved tool-call emission behind a `toolCallPublisher` whose constructor callback is
19
+ // `A => this.sendUpdate(A)` — still the patched sendUpdate, so the meta injection survives; the
20
+ // stub reproduces that wiring. `dkA`/`Fc`/`CtA`/`Y9` are the slice's free variables.
21
+ const Session = new Function('dkA', 'Fc', 'CtA', 'Y9', `return class {
19
22
  axomQoderToolOrigins=new Map;toolCallNames=new Map;permissionToolCallIds=new Set;
20
- constructor(connection){this.connection=connection;this.id='root';this.context={toolRegistry:{getTool:()=>({kind:'read'})},config:{isTasksEnabled:()=>false}}}
23
+ toolCallPublisher;
24
+ constructor(connection){this.connection=connection;this.id='root';this.context={toolRegistry:{getTool:()=>({kind:'read'})},config:{isTasksEnabled:()=>false}};
25
+ this.toolCallPublisher={
26
+ declare: async u => this.sendUpdate(u),
27
+ result: async u => this.sendUpdate({ sessionUpdate: 'tool_call_update', toolCallId: u.toolCallId, content: u.content, isError: u.isError }),
28
+ }}
21
29
  supportsTerminalOutput(){return false} async sendPlanUpdate(){}
22
30
  ${helper} ${dispatch}
23
- }`)(v => ({ toolCallId: v.toolCallId, kind: 'read', rawInput: v.input, status: v.status,
24
- _meta: { qoder: { toolName: v.name } } }), () => null,
25
- v => ({ toolCallId: v.toolCallId, status: v.isError ? 'failed' : 'completed', rawOutput: v.content }), 'Plan', new Set());
31
+ }`)(v => ({ sessionUpdate: 'tool_call', toolCallId: v.toolCallId, kind: v.kind, rawInput: v.input, status: v.status }),
32
+ 'Plan', new Set(), () => false);
26
33
  const events = [];
27
34
  const session = new Session({ sessionUpdate: async value => events.push(value) });
28
35
  const tool = (id, parent) => ({ type: 'assistant', parent_tool_use_id: parent,
@@ -44,6 +51,11 @@ await session.dispatchSDKMessage({ type: 'stream_event', parent_tool_use_id: nul
44
51
  assert.equal(events.at(-1).update._meta, undefined);
45
52
  await session.dispatchSDKMessage(tool('root-read', null));
46
53
  assert.equal(events.at(-1).update._meta.qoder.parentToolUseId, undefined);
54
+ // 1.1.61 skipped a re-declared tool id entirely; 1.1.71 re-declares it, so the conflict shows up
55
+ // as a freshly emitted tool_call whose origin was invalidated — the intent (a conflicting parent
56
+ // must not tag the tool as a child) is asserted on whatever the conflict emitted.
47
57
  await session.dispatchSDKMessage(tool('read-a', 'wrong-agent'));
48
- assert.equal(events.at(-1).update._meta.qoder.parentToolUseId, undefined);
49
- console.log('Official Qoder 1.1.61 ACP dispatch: parallel tool/result, child text, root and conflict checks passed');
58
+ const conflict = events.at(-1).update;
59
+ assert.equal(conflict.toolCallId, 'read-a');
60
+ assert.ok(!conflict._meta?.qoder?.parentToolUseId, 'conflicting re-declare must not carry a parent');
61
+ console.log('Official Qoder 1.1.71 ACP dispatch: parallel tool/result, child text, root and conflict checks passed');
@@ -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)');
@@ -54,7 +54,8 @@ JSONL 字段及通知格式,再显式扩充版本 gate;不对未知版本自
54
54
 
55
55
  ## 本地回归
56
56
 
57
- `npx vitest run src/workstation/qoderAdapter/backgroundTerminal.test.ts src/workstation/acp/acpSession.test.ts`
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 删除)
58
59
  以及 CLI 全量测试、typecheck/build/补丁台账校验。此次随 CLI 0.3.2(整合主干 0.3.1)交付,无 npm 发布操作。
59
60
 
60
61
  ## 旧会话详情配套投影
@@ -23,15 +23,16 @@
23
23
  模型/权限配置来自恢复响应。拒绝恢复时直接报错,不偷偷新建另一段会话。
24
24
  - 真正历史接管/backfill、显式 settings load、无 resume 能力继续旧路径。本补丁不以模糊
25
25
  文本去重掩盖这些路径的迟到历史;没有证据声称所有 session/load 都已彻底解决。
26
- - resume 不回放 opening,故 backgroundTerminalRepair 可从只读根 JSONL 恢复精确后台
26
+ - resume 不回放 opening,故后台修复(daemon 的 `BackgroundSideNotes` 读取 + processor 折叠 `qoder_side` note)可从只读根 JSONL 恢复精确后台
27
27
  Agent 输入与启动关系,继续按 tool-use-id 补回明确通知。只发送同一个工具的终态更新,
28
28
  不发送历史 opening、不生成子会话。既有通知可能重新确认相同任务;不是重复执行任务。
29
29
  初次增量扫描完成后,没有待确认任务即停止文件轮询读操作。原有读限制/身份校验仍适用。
30
30
 
31
31
  ## 回归与退役
32
32
 
33
- qoderForegroundBaseline.test.ts 使用真实 ACP transport、原始交错抓包和三前台任务边界;
34
- 覆盖省略类型、失败/取消后续轮、完整结果、主回答、已标识文字、background 不过滤。
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 输出。
35
36
  acpAgents.test.ts 覆盖 cached resume / 真正 backfill / settings load 与其他 provider 边界;
36
37
  backgroundTerminal.test.ts 覆盖无 opening 回放的后台精确结果恢复。
37
38