@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,267 @@
1
+ /**
2
+ * The shared world of the keeper-acp end-to-end tests (Task 21's adoption tests, Task 23's fault-injection gate): a temp config dir
3
+ * with a real keeper-acp (the built bundle in `dist/keepers`), a real adapter process (`scriptAgent.ts`, driven by marker files), and
4
+ * the server's half played by {@link FakeServer} (frames applied once per seq — the server's de-duplication — plus the answers it
5
+ * recorded). A daemon life is either in-process ({@link KeeperWorld.life}: its own keeper connection, AcpAgents, manager and
6
+ * TaskRegistry; "killing" it drops the connection without any goodbye) or a real process ({@link KeeperWorld.daemonProcess}:
7
+ * `daemonLife.ts` under vite-node, killed with SIGKILL). {@link KeeperWorld.dispose} kills every keeper, adapter and daemon process it
8
+ * started, by pid.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { rm } from 'node:fs/promises';
13
+ import path, { join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { expect, vi } from 'vitest';
16
+ import { killKeepersUnder, tempDir } from '../../../test/keeperKit.js';
17
+ import { AcpKeeperClient } from '../../keepers/acp/client.js';
18
+ import { computeBuildVersion } from '../../keepers/kit/buildVersion.js';
19
+ import { keeperGen } from '../../keepers/kit/identity.js';
20
+ import { KeeperLauncher } from '../../keepers/kit/launcher.js';
21
+ import { AgentSessionManager } from '../../agentSessions.js';
22
+ import { AGENT_WIRE_VERSION } from '../../protocol/agent.js';
23
+ import { TaskRegistry } from '../../update/taskRegistry.js';
24
+ import { AcpAgents } from '../acpAgents.js';
25
+ import { keeperGenerations } from '../processor/transport/generations.js';
26
+ import { KeeperTransport } from '../processor/transport/keeper.js';
27
+ import { LocalTransport } from '../processor/transport/local.js';
28
+ const here = path.dirname(fileURLToPath(import.meta.url));
29
+ export const packageRoot = path.join(here, '..', '..', '..', '..');
30
+ export const realDist = path.join(packageRoot, 'dist', 'keepers');
31
+ export const SCRIPT_AGENT = join(here, 'scriptAgent.ts');
32
+ export const DAEMON_LIFE = join(here, 'daemonLife.ts');
33
+ export const BOOTSTRAP = "eval(require('node:fs').readFileSync(process.argv[1], 'utf8'))";
34
+ export const touch = (dir, name) => writeFileSync(join(dir, name), '');
35
+ export const agentPid = (dir) => Number(readFileSync(join(dir, 'pid'), 'utf8'));
36
+ export const stdinLines = (dir) => existsSync(join(dir, 'stdin.jsonl')) ? readFileSync(join(dir, 'stdin.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
37
+ export const downMethods = (records) => records.filter((r) => r.kind === 'down').map((r) => JSON.parse(r.line).method);
38
+ /** The server's half: applies each seq once, in order (bridge_seq), per ref; remembers the answers it gave (spec §5.5.4). */
39
+ export class FakeServer {
40
+ applied = new Map();
41
+ lastSeen = new Map();
42
+ received = [];
43
+ /** ref → requestId → outcome: what the user answered (the card's `answer`). */
44
+ answers = new Map();
45
+ apply = (frame) => {
46
+ this.received.push(frame);
47
+ if ((frame.t === 'agent_event' || frame.t === 'agent_turn_done') && typeof frame.seq === 'number') {
48
+ const last = this.lastSeen.get(frame.sessionRef) ?? 0;
49
+ if (frame.seq <= last)
50
+ return; // the server's de-duplication
51
+ this.lastSeen.set(frame.sessionRef, frame.seq);
52
+ const list = this.applied.get(frame.sessionRef) ?? [];
53
+ list.push(frame);
54
+ this.applied.set(frame.sessionRef, list);
55
+ }
56
+ };
57
+ timeline(ref) { return this.applied.get(ref) ?? []; }
58
+ seqs(ref) { return this.timeline(ref).map((frame) => frame.seq); }
59
+ turnDones(ref) { return this.timeline(ref).filter((frame) => frame.t === 'agent_turn_done'); }
60
+ texts(ref) {
61
+ return this.timeline(ref).filter((f) => f.t === 'agent_event' && f.eventName === 'agent_message_chunk')
62
+ .map((f) => (f.payload.content?.text ?? ''));
63
+ }
64
+ cards(ref) {
65
+ return this.timeline(ref).filter((f) => f.eventName === 'permission_request').map((f) => f.payload.requestId);
66
+ }
67
+ /** The user answers a card: recorded first (the server's item), then sent if a daemon is there. */
68
+ answer(ref, requestId, outcome) {
69
+ const answered = this.answers.get(ref) ?? new Map();
70
+ answered.set(requestId, outcome);
71
+ this.answers.set(ref, answered);
72
+ return { t: 'agent_answer', sessionRef: ref, requestId, outcome };
73
+ }
74
+ /** Task 22's resend: the answers this side recorded for cards a durable reopen still reports pending. */
75
+ resends(ref, opened) {
76
+ const answered = this.answers.get(ref);
77
+ return (opened.pendingRequestIds ?? []).filter((id) => answered?.has(id))
78
+ .map((id) => ({ t: 'agent_answer', sessionRef: ref, requestId: id, outcome: answered.get(id) }));
79
+ }
80
+ }
81
+ /** What the server's timeline says, without the ref: compared between two runs. */
82
+ export const comparable = (frames) => frames.map(({ sessionRef: _ref, ...rest }) => rest);
83
+ /** Seqs 1..n, no duplicate, no gap. */
84
+ export function expectContiguous(seqs) {
85
+ expect(seqs).toEqual(Array.from({ length: seqs.length }, (_, k) => k + 1));
86
+ }
87
+ export class KeeperWorld {
88
+ configDir;
89
+ agentDir;
90
+ launcher;
91
+ lives = [];
92
+ processes = [];
93
+ extraDists = [];
94
+ agentDirs = [];
95
+ constructor(prefix) {
96
+ this.configDir = tempDir(prefix);
97
+ this.agentDir = this.newAgentDir(`${prefix}agent-`);
98
+ this.launcher = this.launcherFor(realDist);
99
+ }
100
+ launcherFor(distDir) {
101
+ return new KeeperLauncher({ configDir: this.configDir, workstationId: 'ws-t21', server: 'http://test.invalid', distDir, useSystemd: false,
102
+ startTimeoutMs: 20_000, umask: 0o022 });
103
+ }
104
+ /** Another adapter's marker directory (its pid is killed on dispose). */
105
+ newAgentDir(prefix = 'axom-t23-agent-') {
106
+ const dir = tempDir(prefix);
107
+ this.agentDirs.push(dir);
108
+ return dir;
109
+ }
110
+ scriptArgs(config, dir = this.agentDir) {
111
+ return ['-e', BOOTSTRAP, SCRIPT_AGENT, JSON.stringify({ dir, ...config })];
112
+ }
113
+ /** One daemon life: its own keeper-acp connection, AcpAgents (with the generations), manager and TaskRegistry, bound to `server`. */
114
+ life(server, over = {}) {
115
+ const use = over.launcher ?? this.launcher;
116
+ const real = new KeeperTransport(new AcpKeeperClient({ launcher: use }));
117
+ const keeper = (over.wrapKeeper ? over.wrapKeeper(real) : real);
118
+ const generations = keeperGenerations({ launcher: use, current: keeper, log: () => { } });
119
+ const local = new LocalTransport({ configDir: this.configDir });
120
+ const acp = new AcpAgents({ cacheDir: this.configDir, registry: { load: async () => [] }, transports: { keeper, local }, keeperGenerations: generations,
121
+ sessionStartTimeoutMs: 15_000, ...over.acp });
122
+ const tasks = new TaskRegistry();
123
+ const sent = [];
124
+ const manager = new AgentSessionManager(acp, null, join(this.configDir, 'agents'), tasks);
125
+ manager.bindSend((frame) => { sent.push(frame); server.apply(frame); }, AGENT_WIRE_VERSION);
126
+ let killed = false;
127
+ const result = {
128
+ keeper, generations, acp, tasks, manager, sent,
129
+ // kill -9 as keeper-acp sees it: the connection drops, nothing is detached, nothing is said.
130
+ kill() { if (killed)
131
+ return; killed = true; keeper.close(); generations.close(); void local.dispose(0); },
132
+ };
133
+ this.lives.push(result);
134
+ return result;
135
+ }
136
+ openFrame(ref, config, over = {}) {
137
+ return {
138
+ t: 'agent_open', id: `open-${ref}-${Math.random().toString(36).slice(2, 8)}`, sessionRef: ref, agentKey: 'script', bridge: 'script-acp',
139
+ cwd: this.configDir, shell: '/bin/sh', command: process.execPath, args: this.scriptArgs(config), ...over,
140
+ };
141
+ }
142
+ /** Opens `ref` on life `l` and waits for the reply. */
143
+ async openOn(l, ref, config, over = {}) {
144
+ const frame = this.openFrame(ref, config, over);
145
+ await l.manager.handleFrame(frame);
146
+ const reply = opened(l, frame.id)[0];
147
+ expect(reply, `no agent_opened for ${ref}`).toBeDefined();
148
+ expect(reply.ok, reply.error ?? '').toBe(true);
149
+ return reply;
150
+ }
151
+ /** The server reopens `ref` on life `l` after a reconnect: `agent_open` with its watermark, then Task 22's answer resend. */
152
+ async reopenOn(l, server, ref, config, over = {}) {
153
+ const reply = await this.openOn(l, ref, config, { resumeFromSeq: server.lastSeen.get(ref) ?? 0, ...over });
154
+ for (const frame of server.resends(ref, reply))
155
+ await l.manager.handleFrame(frame);
156
+ return reply;
157
+ }
158
+ /** Every raw record of `ref`, read over a connection of its own. */
159
+ async rawOf(ref, launcher = this.launcher) {
160
+ const reader = new KeeperTransport(new AcpKeeperClient({ launcher }));
161
+ try {
162
+ await reader.attach(ref);
163
+ const records = [];
164
+ const summary = (await reader.list()).find((s) => s.sessionKey === ref);
165
+ for (let at = summary.raw.first;;) {
166
+ const page = await reader.readRaw(ref, at);
167
+ if (page.length === 0)
168
+ break;
169
+ records.push(...page);
170
+ at = page[page.length - 1].o + 1;
171
+ }
172
+ return records;
173
+ }
174
+ finally {
175
+ reader.close();
176
+ }
177
+ }
178
+ /** keeper-acp's listing of `ref`, over a connection of its own. */
179
+ async summaryOf(ref) {
180
+ const reader = new KeeperTransport(new AcpKeeperClient({ launcher: this.launcher }));
181
+ try {
182
+ return (await reader.list()).find((s) => s.sessionKey === ref);
183
+ }
184
+ finally {
185
+ reader.close();
186
+ }
187
+ }
188
+ /**
189
+ * A daemon life in a process of its own (`daemonLife.ts` under vite-node): it adopts what keeper-acp holds, then handles the frames
190
+ * it is given on stdin (one JSON per line; `{cmd:'forceBegin'}` runs a force update's `TaskRegistry.stopAll()`, `{cmd:'forceStop'}`
191
+ * also the restart path's detach, and exits). Every
192
+ * frame it sends lands in a file; its stderr is kept in `<configDir>/daemon-<n>.stderr` (the sentinel grep reads it).
193
+ */
194
+ daemonProcess(faults = {}) {
195
+ const n = this.processes.length + 1;
196
+ const framesOut = join(this.configDir, `frames-daemon-${n}.jsonl`);
197
+ const stderrFile = join(this.configDir, `daemon-${n}.stderr`);
198
+ const child = spawn(join(packageRoot, 'node_modules', '.bin', 'vite-node'), [DAEMON_LIFE,
199
+ JSON.stringify({ configDir: this.configDir, distDir: realDist, framesOut, faults })], { cwd: packageRoot, stdio: ['pipe', 'ignore', 'pipe'] });
200
+ let stderr = '';
201
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); appendFileSync(stderrFile, chunk); });
202
+ const exited = new Promise((resolve) => child.once('exit', () => resolve()));
203
+ const result = {
204
+ child,
205
+ frames: () => existsSync(framesOut) ? readFileSync(framesOut, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line)) : [],
206
+ send: (frame) => { child.stdin.write(`${JSON.stringify(frame)}\n`); },
207
+ stderr: () => stderr,
208
+ kill: async () => { if (child.exitCode === null && child.signalCode === null)
209
+ child.kill('SIGKILL'); await exited; },
210
+ exited,
211
+ };
212
+ this.processes.push(result);
213
+ return result;
214
+ }
215
+ /** Opens `ref` in a daemon process and waits for its `agent_opened{ok}` (the server never prompts before it). */
216
+ async openIn(proc, frame) {
217
+ proc.send(frame);
218
+ let reply;
219
+ await vi.waitFor(() => {
220
+ expect(proc.child.exitCode, proc.stderr()).toBeNull();
221
+ reply = proc.frames().find((f) => f.t === 'agent_opened' && f.id === frame.id);
222
+ expect(reply).toBeDefined();
223
+ }, { timeout: 45_000, interval: 100 });
224
+ expect(reply.ok, reply.error ?? '').toBe(true);
225
+ return reply;
226
+ }
227
+ /** An "older build" of keeper-acp: the same bundle with one byte more (its own buildVersion, so its own generation). */
228
+ olderGeneration() {
229
+ const oldDist = tempDir('axom-t21-old-dist-');
230
+ this.extraDists.push(oldDist);
231
+ mkdirSync(join(oldDist, 'acp'), { recursive: true });
232
+ writeFileSync(join(oldDist, 'acp', 'keeper.mjs'), `${readFileSync(join(realDist, 'acp', 'keeper.mjs'), 'utf8')}\n// an older generation\n`);
233
+ const manifest = JSON.parse(readFileSync(join(realDist, 'acp', 'manifest.json'), 'utf8'));
234
+ const oldBuild = computeBuildVersion(join(oldDist, 'acp'));
235
+ writeFileSync(join(oldDist, 'acp', 'manifest.json'), JSON.stringify({ ...manifest, buildVersion: oldBuild }));
236
+ return { oldLauncher: this.launcherFor(oldDist), oldGen: keeperGen(oldBuild) };
237
+ }
238
+ /** Kills every daemon process, life, keeper-acp generation and adapter this world started (by pid); the directories stay. */
239
+ async stop() {
240
+ for (const proc of this.processes.splice(0))
241
+ await proc.kill().catch(() => { });
242
+ for (const life of this.lives.splice(0))
243
+ life.kill();
244
+ for (const gen of await this.launcher.liveGenerations('acp').catch(() => []))
245
+ await this.launcher.stop('acp', gen, 'force').catch(() => { });
246
+ for (const dir of this.agentDirs) {
247
+ const pidFile = join(dir, 'pid');
248
+ if (existsSync(pidFile)) {
249
+ try {
250
+ process.kill(Number(readFileSync(pidFile, 'utf8')), 'SIGKILL');
251
+ }
252
+ catch { /* gone */ }
253
+ }
254
+ }
255
+ }
256
+ /** {@link stop}, then removes every directory (killing any keeper whose pid manifest is still under the config dir). */
257
+ async dispose() {
258
+ await this.stop();
259
+ for (const dist of this.extraDists.splice(0))
260
+ await rm(dist, { recursive: true, force: true });
261
+ for (const dir of this.agentDirs.splice(0))
262
+ await rm(dir, { recursive: true, force: true });
263
+ killKeepersUnder(this.configDir);
264
+ }
265
+ }
266
+ export const opened = (l, id) => l.sent.filter((frame) => frame.t === 'agent_opened' && (id === undefined || frame.id === id));
267
+ export const liveSessionOf = (l, ref) => l.manager.sessions.get(ref)?.session;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Test stand-in for a transport's outbox stream (Task 20): what `acp.outbox.append` / `acp.commit` do to one session's outbox,
3
+ * in memory — strictly increasing seqs, offsets, truncation through an offset with its `committedTail` watermark, checkpoints and
4
+ * raw truncation recorded. Kept per sessionRef by the manager tests' fake agents so a "reopen" reads back what an earlier life
5
+ * left, as `ProcessorSession.start` does from the real stream.
6
+ */
7
+ import { SessionOutbox } from '../outbox.js';
8
+ export class MemoryOutboxStream {
9
+ records = [];
10
+ next = 0;
11
+ committed = -1;
12
+ committedTail = null;
13
+ checkpoints = [];
14
+ rawTruncations = [];
15
+ commits = [];
16
+ /** Non-null: the next appends fail with it (a transport that went away). */
17
+ failAppend = null;
18
+ async append(entries) {
19
+ if (this.failAppend)
20
+ throw this.failAppend;
21
+ let last = this.records.at(-1)?.seq ?? this.committedTail?.seq ?? -Infinity;
22
+ for (const entry of entries) {
23
+ if (!(entry.seq > last))
24
+ throw new Error(`Outbox seq ${entry.seq} does not follow ${last}`);
25
+ last = entry.seq;
26
+ }
27
+ const first = this.next;
28
+ for (const entry of entries)
29
+ this.records.push({ o: this.next++, t: 0, seq: entry.seq, src: { ...entry.src }, frame: { ...entry.frame } });
30
+ return { first, last: this.next - 1 };
31
+ }
32
+ async commit(params) {
33
+ this.commits.push(params);
34
+ if (params.checkpoint)
35
+ this.checkpoints.push({ offset: params.checkpoint.offset, blob: String(params.checkpoint.blob) });
36
+ if (params.outbox) {
37
+ if (params.outbox.through >= this.next)
38
+ throw new Error('Outbox commit is beyond the outbox head');
39
+ this.committed = Math.max(this.committed, params.outbox.through);
40
+ this.committedTail = params.outbox.tail;
41
+ this.records = this.records.filter((r) => r.o > this.committed);
42
+ }
43
+ if (params.rawThrough !== undefined) {
44
+ const latest = this.checkpoints.at(-1)?.offset ?? -1;
45
+ if (params.rawThrough > latest)
46
+ throw new Error('Raw may only be truncated through the latest checkpoint');
47
+ this.rawTruncations.push(params.rawThrough);
48
+ }
49
+ return {};
50
+ }
51
+ /** What a new life reads back: the records past the watermark. */
52
+ leftovers() { return this.records.filter((r) => r.o > this.committed); }
53
+ /** A new life's {@link SessionOutbox} over this stream, as `ProcessorSession.start` builds it. */
54
+ outbox() { return new SessionOutbox(this, { leftovers: this.leftovers(), committedTail: this.committedTail }); }
55
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Real-capture fixtures (Task 17d) and the transducer that turns one into a processor raw log.
3
+ *
4
+ * ## What a fixture is
5
+ *
6
+ * A sanitised capture (`sanitize.ts`) in the kit-stream spelling: the wire between an ACP adapter and
7
+ * the client that captured it — `scripts/acp-capture.mjs` or the legacy capture harness. The `down`
8
+ * lines are **that client's**, with its own JSON-RPC ids; nothing in a fixture depends on the
9
+ * processor's id scheme, so a fixture stays valid when the processor changes.
10
+ *
11
+ * ## The transducer
12
+ *
13
+ * {@link transduce} plays the daemon against a fixture: every client decision the capture shows
14
+ * becomes the note the daemon would write (`session/new` → `agent_session_open`, `session/prompt` →
15
+ * `agent_prompt`, `session/set_mode` → `agent_set_config_option`, a permission answer →
16
+ * `agent_answer`, `session/cancel` → `agent_interrupt`, anything else → `rpc.request`), the processor
17
+ * writes its own `down` lines through the real record loop (`processor/test/harness.ts` `run`), and the
18
+ * adapter's `up` lines follow in capture order — a response's id rewritten from the capture client's
19
+ * request to the processor's. The result is the raw log keeper-acp would have recorded for the same
20
+ * conversation: the input of the determinism, batching, checkpoint and equivalence tests.
21
+ */
22
+ import { readdirSync, readFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { permissionRequestId } from '../../permission.js';
26
+ import { MemoryRawLog } from '../../processor/memoryLog.js';
27
+ import { decodeStreamRecord } from '../../processor/record.js';
28
+ import { createProcessor } from '../../processor/step.js';
29
+ import { run } from '../../processor/test/harness.js';
30
+ export const RAW_DIR = fileURLToPath(new URL('.', import.meta.url));
31
+ /** The spawn id every transduced log uses (card ids are `perm:<SPAWN_ID>:<agent id>`). */
32
+ export const SPAWN_ID = 'spawn-fixture';
33
+ /** Every committed fixture, by discovery (never a hand-kept list). */
34
+ export function fixtureNames() {
35
+ return readdirSync(RAW_DIR).filter((name) => name.endsWith('.jsonl')).sort();
36
+ }
37
+ export function readFixtureText(name) {
38
+ return readFileSync(join(RAW_DIR, name), 'utf8');
39
+ }
40
+ /** A fixture's records, decoded through the processor's own stream decoder. */
41
+ export function loadFixture(name) {
42
+ return readFixtureText(name).split('\n').filter((line) => line.trim()).map((line) => decodeStreamRecord(JSON.parse(line)));
43
+ }
44
+ const isResponse = (m) => m.method === undefined && m.id !== undefined && ('result' in m || 'error' in m);
45
+ /** Fold a capture into the processor raw log keeper-acp would have recorded for the same conversation. */
46
+ export function transduce(capture, options = {}) {
47
+ let clock = 0;
48
+ const stdin = [];
49
+ const log = new MemoryRawLog(() => clock, (line) => stdin.push(line));
50
+ const session = { log, stdin, outbox: [], blocks: new Map() };
51
+ const processor = createProcessor();
52
+ const actions = [];
53
+ /** capture request id → processor request id (both as JSON). */
54
+ const ids = new Map();
55
+ /** capture request id → method, for the agent's requests (to classify the client's answers). */
56
+ const agentRequests = new Map();
57
+ let next = 0;
58
+ let turns = 0;
59
+ /** An interrupt was decided since the last prompt: the cards it cancelled are answered by the processor itself. */
60
+ let interrupted = false;
61
+ let configs = 0;
62
+ /** The capture id of a `session/load`: its response is where the daemon closes the history window. */
63
+ let loadRequest;
64
+ const fold = () => { next = run(processor, session, next); };
65
+ /** Append a note (and any entries that must precede its fold), fold, and return the requests the processor wrote for it. */
66
+ const decide = (entry, ...with_) => {
67
+ const before = stdin.length;
68
+ log.append(entry);
69
+ for (const extra of with_)
70
+ log.append(extra);
71
+ fold();
72
+ return stdin.slice(before).map((line) => JSON.parse(line)).filter((m) => m.method !== undefined && m.id !== undefined);
73
+ };
74
+ const map = (captured, written) => {
75
+ const own = written.find((m) => m.method === captured.method);
76
+ if (!own)
77
+ throw new Error(`transduce: the processor wrote no ${captured.method} for capture request ${JSON.stringify(captured.id)}`);
78
+ ids.set(JSON.stringify(captured.id), own.id);
79
+ };
80
+ for (const record of capture) {
81
+ clock = record.t;
82
+ if (record.kind === 'proc') {
83
+ if (record.proc.type === 'spawn') {
84
+ decide({ kind: 'proc', proc: { ...record.proc, spawnId: SPAWN_ID } });
85
+ if (options.launch !== undefined)
86
+ decide({ kind: 'note', note: { type: 'agent_launch', launch: options.launch } });
87
+ }
88
+ else {
89
+ decide({ kind: 'proc', proc: record.proc });
90
+ }
91
+ }
92
+ else if (record.kind === 'err') {
93
+ decide({ kind: 'err', line: record.line });
94
+ }
95
+ else if (record.kind === 'note') {
96
+ decide({ kind: 'note', note: record.note });
97
+ }
98
+ else if (record.kind === 'up') {
99
+ let line = record.line;
100
+ let message;
101
+ try {
102
+ message = JSON.parse(line);
103
+ }
104
+ catch { /* a parse error is the processor's business */ }
105
+ if (message && isResponse(message)) {
106
+ const own = ids.get(JSON.stringify(message.id));
107
+ if (own !== undefined)
108
+ line = JSON.stringify({ ...message, id: own });
109
+ }
110
+ else if (message && message.method !== undefined && message.id !== undefined) {
111
+ agentRequests.set(JSON.stringify(message.id), message.method);
112
+ }
113
+ decide({ kind: 'up', line });
114
+ // T19: the daemon writes `historyEnd` as soon as it has seen the load's result (the open result
115
+ // the processor reports on this record) — every update after it is live, never history.
116
+ if (message && isResponse(message) && loadRequest !== undefined && JSON.stringify(message.id) === loadRequest) {
117
+ loadRequest = undefined;
118
+ decide({ kind: 'note', note: { type: 'historyEnd' } });
119
+ }
120
+ }
121
+ else {
122
+ const message = JSON.parse(record.line);
123
+ const params = message.params ?? {};
124
+ if (message.method === 'initialize') {
125
+ const own = stdin.map((l) => JSON.parse(l)).filter((m) => m.method === 'initialize').at(-1);
126
+ if (!own)
127
+ throw new Error('transduce: no initialize was written at spawn');
128
+ ids.set(JSON.stringify(message.id), own.id);
129
+ }
130
+ else if (message.method === 'session/new' || message.method === 'session/load' || message.method === 'session/resume') {
131
+ const mode = message.method.slice('session/'.length);
132
+ const mcpServers = Array.isArray(params.mcpServers) ? params.mcpServers : [];
133
+ actions.push({ kind: 'open', mode: mode, cwd: String(params.cwd), mcpServers,
134
+ ...(mode !== 'new' ? { sessionId: String(params.sessionId) } : {}) });
135
+ if (mode === 'load')
136
+ loadRequest = JSON.stringify(message.id);
137
+ map(message, decide({ kind: 'note', note: {
138
+ type: 'agent_session_open', mode, cwd: params.cwd, mcpServers,
139
+ ...(mode !== 'new' ? { sessionId: params.sessionId } : {}),
140
+ } }, ...(mode === 'load' && options.afterLoadOpen ? options.afterLoadOpen(String(params.sessionId)) : [])));
141
+ }
142
+ else if (message.method === 'session/prompt') {
143
+ turns += 1;
144
+ const blocksRef = `blocks-${turns}`;
145
+ const blocks = params.prompt;
146
+ const platformTurnId = `turn-${turns}`;
147
+ session.blocks.set(blocksRef, blocks);
148
+ interrupted = false;
149
+ actions.push({ kind: 'prompt', blocks, platformTurnId });
150
+ map(message, decide({ kind: 'note', note: { type: 'agent_prompt', blocksRef, platformTurnId } }));
151
+ }
152
+ else if (message.method === 'session/set_mode' || message.method === 'session/set_config_option') {
153
+ configs += 1;
154
+ const configId = message.method === 'session/set_mode' ? 'acp:modes' : String(params.configId);
155
+ const value = message.method === 'session/set_mode' ? params.modeId : params.value;
156
+ actions.push({ kind: 'config', configId, value });
157
+ map(message, decide({ kind: 'note', note: { type: 'agent_set_config_option', id: `cfg-${configs}`, configId, value } }));
158
+ }
159
+ else if (message.method === 'session/cancel') {
160
+ actions.push({ kind: 'interrupt', o: record.o });
161
+ interrupted = true;
162
+ decide({ kind: 'note', note: { type: 'agent_interrupt' } });
163
+ }
164
+ else if (message.method !== undefined && message.id !== undefined) {
165
+ actions.push({ kind: 'rpc', method: message.method });
166
+ map(message, decide({ kind: 'note', note: { type: 'rpc.request', method: message.method, ...(message.params !== undefined ? { params } : {}) } }));
167
+ }
168
+ else if (isResponse(message)) {
169
+ const method = agentRequests.get(JSON.stringify(message.id));
170
+ if (method === 'session/request_permission') {
171
+ const outcome = message.result?.outcome;
172
+ const optionId = outcome?.outcome === 'selected' && typeof outcome.optionId === 'string' ? outcome.optionId : null;
173
+ // A `cancelled` answer after the client's interrupt is the interrupt's own doing: the processor
174
+ // answered that card on the `agent_interrupt` note, as `AcpSession.interrupt` does.
175
+ const byInterrupt = optionId === null && interrupted;
176
+ actions.push({ kind: 'answer', agentRequestId: message.id, optionId, byInterrupt });
177
+ if (!byInterrupt) {
178
+ decide({ kind: 'note', note: {
179
+ type: 'agent_answer', requestId: permissionRequestId(SPAWN_ID, message.id), outcome: optionId ?? '__cancelled__',
180
+ } });
181
+ }
182
+ }
183
+ // Any other answer (the capture client refusing fs/terminal with -32601) the processor
184
+ // already gave itself on the request's own record.
185
+ }
186
+ else {
187
+ throw new Error(`transduce: no daemon decision for the capture's down line ${record.line.slice(0, 200)}`);
188
+ }
189
+ }
190
+ const extra = options.after?.get(record.o);
191
+ if (extra)
192
+ for (const entry of extra())
193
+ decide(entry);
194
+ }
195
+ return { session, records: log.read(0), frames: session.outbox, processor, actions };
196
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Qoder captures for Task 17d, built from the update-level Qoder fixtures — there is **no raw Qoder
3
+ * capture** (ruling: a real one is the T23 real-machine gate's).
4
+ *
5
+ * Each one is wrapped into the same raw-record spelling as a sanitised real capture, as the wire
6
+ * `qoder-cli` / the SDK adapter would put it between a client and itself: `initialize` (the adapter's
7
+ * identity), `session/new`, one `session/prompt`, the fixture's `session/update` lines, the prompt's
8
+ * response. Side-channel inputs (the root transcript rows the background terminal repair reads) are
9
+ * returned beside the capture: the live path reads them from a history home, the processor gets them
10
+ * as `qoder_side` notes from the daemon's reader.
11
+ *
12
+ * Sources: `acp/test/qoder-parallel-foreground.json`, `acp/test/qoder-delegation.json` (the `live`
13
+ * phase of both probes), `acp/test/qoder-background-terminal.json` (updates + native rows, plus a
14
+ * variant whose result is large enough to be chunked into several notes), and
15
+ * `qoderAdapter/fixtures/native-events.json` through the real SDK adapter `QoderMessageMapper` and
16
+ * `PermissionBridge` (SDK callback cards).
17
+ */
18
+ import { readFileSync } from 'node:fs';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { QoderMessageMapper } from '../../../qoderAdapter/mapper.js';
21
+ import { PermissionBridge } from '../../../qoderAdapter/permissions.js';
22
+ import { QODER_PROFILE } from '../../processor/test/fixtureLog.js';
23
+ const json = (path) => JSON.parse(readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8'));
24
+ export const CERTIFIED_QODER = { name: 'qoder-cli', version: '1.1.61' };
25
+ export const QODER_SDK = { name: 'axom-qoder-sdk', version: '1.0.50' };
26
+ /** Lines → kit-stream ndjson (a synthetic `proc spawn` first, `t` = position). */
27
+ export function captureText(lines) {
28
+ const records = [JSON.stringify({ o: 0, t: 0, k: 'proc', kind: 'spawn', spawnId: 'capture', pid: 0, pgid: 0, startTime: 0 })];
29
+ lines.forEach((line, index) => records.push(JSON.stringify({ o: index + 1, t: index + 1, k: line.k, l: JSON.stringify(line.message) })));
30
+ return `${records.join('\n')}\n`;
31
+ }
32
+ export const up = (message) => ({ k: 'up', message });
33
+ export const down = (message) => ({ k: 'down', message });
34
+ export const update = (sessionId, value) => up({ jsonrpc: '2.0', method: 'session/update', params: { sessionId, update: value } });
35
+ /** initialize → session/new → (between) → prompt → updates → response. */
36
+ function oneTurn(options) {
37
+ return captureText([
38
+ down({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: 1, clientCapabilities: {} } }),
39
+ up({ jsonrpc: '2.0', id: 1, result: { protocolVersion: 1, agentCapabilities: { loadSession: true }, authMethods: [],
40
+ agentInfo: options.agentInfo, ...(options.initializeMeta ? { _meta: options.initializeMeta } : {}) } }),
41
+ down({ jsonrpc: '2.0', id: 2, method: 'session/new', params: { cwd: options.cwd, mcpServers: [] } }),
42
+ up({ jsonrpc: '2.0', id: 2, result: { sessionId: options.sessionId } }),
43
+ ...(options.afterOpen ?? []),
44
+ down({ jsonrpc: '2.0', id: 3, method: 'session/prompt', params: { sessionId: options.sessionId, prompt: [{ type: 'text', text: 'xxxxx' }] } }),
45
+ ...options.updates.map((value) => update(options.sessionId, value)),
46
+ ...(options.inTurn ?? []),
47
+ up({ jsonrpc: '2.0', id: 3, result: { stopReason: options.stopReason ?? 'end_turn' } }),
48
+ ]);
49
+ }
50
+ function parallelForeground() {
51
+ const fixture = json('../qoder-parallel-foreground.json');
52
+ const sessionId = 'qoder-root';
53
+ const cwd = '/work/baseline';
54
+ return { name: 'qoder-parallel-foreground', sessionId, cwd,
55
+ text: oneTurn({ agentInfo: CERTIFIED_QODER, sessionId, cwd, updates: fixture.updates }) };
56
+ }
57
+ function delegationProbes() {
58
+ const fixture = json('../qoder-delegation.json');
59
+ return fixture.probes.map((probe) => {
60
+ const sessionId = probe.events[0].sessionId;
61
+ const cwd = '/work/delegation';
62
+ return { name: `qoder-delegation-${probe.name}`, sessionId, cwd, text: oneTurn({ agentInfo: CERTIFIED_QODER, sessionId, cwd,
63
+ updates: probe.events.filter((event) => event.phase === 'live').map((event) => event.update) }) };
64
+ });
65
+ }
66
+ function backgroundTerminal(large) {
67
+ const fixture = json('../qoder-background-terminal.json');
68
+ // The large variant: task B's result grows to ~96 KiB (under the 128 KiB result cap, over one
69
+ // 64 KiB note), so its `terminal` fact reaches the processor as several `qoder_side` chunks.
70
+ const rows = large
71
+ ? fixture.nativeRows.map((row) => JSON.parse(JSON.stringify(row).replace('QODER_TERMINAL_20261003_B_DONE', `QODER_TERMINAL_20261003_B_DONE ${'0123456789abcdef'.repeat(6 * 1024)}`)))
72
+ : fixture.nativeRows;
73
+ return { name: large ? 'qoder-background-terminal-large' : 'qoder-background-terminal', sessionId: fixture.nativeSessionId, cwd: fixture.cwd,
74
+ nativeRows: rows, text: oneTurn({ agentInfo: CERTIFIED_QODER, sessionId: fixture.nativeSessionId, cwd: fixture.cwd, updates: fixture.updates }) };
75
+ }
76
+ /**
77
+ * The SDK adapter's callback cards: `native-events.json`'s two child tool calls mapped by the real
78
+ * `QoderMessageMapper`, and one permission request per child raised through the real
79
+ * `PermissionBridge` (its random request id replaced by a fixed UUID), answered allow / reject.
80
+ */
81
+ async function sdkCards() {
82
+ const fixture = json('../../../qoderAdapter/fixtures/native-events.json');
83
+ const binding = { rootSessionId: fixture.rootSessionId, runtimeGeneration: fixture.runtimeGeneration };
84
+ const corrections = [];
85
+ const mapper = new QoderMessageMapper(binding, { correction: (value) => corrections.push(value) });
86
+ const updates = [];
87
+ const tools = fixture.hooks.filter((hook) => hook.event === 'tool');
88
+ for (const [index, hook] of tools.entries()) {
89
+ updates.push(...mapper.map({ type: 'assistant', session_id: fixture.rootSessionId, uuid: `assistant-${index}`, parent_tool_use_id: null,
90
+ message: { role: 'assistant', content: [{ type: 'tool_use', id: hook.toolUseId, name: 'Edit', input: { path: '/work/sdk/fixture.txt' } }] } }, `run-${hook.agentId}`));
91
+ mapper.observe({ ...binding, toolUseId: hook.toolUseId, agentId: hook.agentId, launchingToolUseId: hook.launchingToolUseId });
92
+ }
93
+ updates.push(...corrections);
94
+ mapper.dispose();
95
+ const requests = [];
96
+ const bridge = new PermissionBridge({ ...binding, request: async (request) => {
97
+ requests.push(request);
98
+ return { outcome: { outcome: 'cancelled' } };
99
+ } });
100
+ for (const hook of tools) {
101
+ await bridge.canUseTool('Edit', { path: '/work/sdk/fixture.txt' }, {
102
+ toolUseID: hook.toolUseId, agentID: hook.agentId, signal: new AbortController().signal,
103
+ permissionOptions: [{ outcome: 'proceed_once', allowed: true }],
104
+ });
105
+ }
106
+ bridge.close();
107
+ const cards = [];
108
+ requests.forEach((request, index) => {
109
+ const meta = request.toolCall._meta.qoderSdk;
110
+ const fixed = { ...request, toolCall: { ...request.toolCall, _meta: { qoderSdk: { ...meta, requestId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}` } } } };
111
+ cards.push(up({ jsonrpc: '2.0', id: 10 + index, method: 'session/request_permission', params: fixed }));
112
+ cards.push(down({ jsonrpc: '2.0', id: 10 + index, result: { outcome: { outcome: 'selected', optionId: index === 0 ? 'allow' : 'reject' } } }));
113
+ });
114
+ const sdkProfile = { ...QODER_PROFILE, mode: 'sdk-adapter' };
115
+ const sessionId = fixture.rootSessionId;
116
+ return { name: 'qoder-sdk-cards', sessionId, cwd: '/work/sdk', text: oneTurn({
117
+ agentInfo: QODER_SDK, initializeMeta: { qoderProfile: sdkProfile }, sessionId, cwd: '/work/sdk',
118
+ afterOpen: [up({ jsonrpc: '2.0', method: '_axom/qoder/profile', params: { sessionId, revision: 1,
119
+ profile: { ...sdkProfile, rootSessionId: sessionId, runtimeGeneration: fixture.runtimeGeneration, rootConfirmed: true, initialized: true } } })],
120
+ updates, inTurn: cards,
121
+ }) };
122
+ }
123
+ export async function qoderCaptures() {
124
+ return [parallelForeground(), ...delegationProbes(), backgroundTerminal(false), backgroundTerminal(true), await sdkCards()];
125
+ }