@platless/axom-cli 0.5.9 → 0.6.36

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 (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,8 +0,0 @@
1
- {
2
- "name": "axom-qoder-sdk-runtime",
3
- "version": "1.0.0",
4
- "private": true,
5
- "dependencies": {
6
- "@qoder-ai/qoder-agent-sdk": "1.0.50"
7
- }
8
- }
@@ -1,24 +0,0 @@
1
- export function buildSessionMcpServers(userServers = [], lease) {
2
- if (!lease)
3
- return [...userServers];
4
- const fail = () => { throw new Error('Delegation MCP configuration is unavailable'); };
5
- let url;
6
- try {
7
- url = new URL(lease.url);
8
- }
9
- catch {
10
- return fail();
11
- }
12
- if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash ||
13
- url.pathname !== '/mcp/delegation' || !Number.isSafeInteger(lease.expiresAtEpochMs) ||
14
- lease.expiresAtEpochMs <= Date.now() || userServers.some((server) => server.name === 'axom-delegation'))
15
- return fail();
16
- const headers = Object.entries(lease.headers);
17
- if (headers.length !== 1 || headers[0][0].toLowerCase() !== 'authorization' ||
18
- !/^Bearer [A-Za-z0-9_-]+$/.test(headers[0][1]))
19
- return fail();
20
- return [...userServers, {
21
- type: 'http', name: 'axom-delegation', url: url.toString(),
22
- headers: headers.map(([name, value]) => ({ name, value })),
23
- }];
24
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Side channels as notes (Task 17c, spec 2026-10-11 §5.4: "一切输入都进日志" — Qoder side-file events
3
- * included).
4
- *
5
- * Qoder's certified background terminal repair reads the agent's transcript files. The processor
6
- * never does: a file can change between the live run and a replay. The daemon reads them
7
- * (`qoderAdapter/backgroundTerminal.ts` `BackgroundSideNotes`, run by T19) and appends what it
8
- * derived as notes; the processor folds a fact only when it reads its note, with the pure fold
9
- * (`qoderAdapter/backgroundTerminalFold.ts`) over checkpointed state. A replay therefore gives the same
10
- * frames whatever the files say by then, or if they are gone.
11
- *
12
- * - `qoder_terminal_repair{nativeSessionId, restoreCalls?}` — the daemon enabled the repair where
13
- * today's path calls `enableBackgroundTerminalRepair`: after the open note for a load (so the
14
- * history is observed), after the open result for new/resume (`restoreCalls: true` on resume).
15
- * Refused (`droppedNotes`) unless connected, the runtime is the certified one, the id is the root
16
- * (or the session being loaded) and no repair is on yet. `includeProcess` follows the subwork
17
- * boundary (`supportsSubworkMeta`), as `AcpSession` decides it.
18
- * - `qoder_side{fact}` / `qoder_side{chunk}` — one derived fact (or the consecutive chunks of one too
19
- * large for a note). Its events — a child transcript row's, or the results a poll's end publishes —
20
- * are emitted the way `AcpSession.emitEvent` emits the repair's (`updates.ts` `emitSessionEvent`).
21
- * Facts are idempotent by their own fields (ids, row uuids), so a reader that restarted from the
22
- * start of a file repeats nothing. Dropped while no repair is on or the connection is closed.
23
- */
24
- import { certifiedRuntime } from '../../qoderAdapter/foregroundOutput.js';
25
- import { supportsSubworkMeta } from '../../qoderAdapter/subwork.js';
26
- import { newTerminalRepairData, readSideNote, TerminalRepairFold } from '../../qoderAdapter/backgroundTerminalFold.js';
27
- import { connected } from './session.js';
28
- import { emitSessionEvent } from './updates.js';
29
- function agentInfo(state) {
30
- return state.initialize.status === 'ready' ? state.initialize.facts.agentInfo : undefined;
31
- }
32
- /** `qoder_terminal_repair{nativeSessionId, restoreCalls?}`. */
33
- export function onRepairNote(state, note) {
34
- const { nativeSessionId, restoreCalls } = note;
35
- const root = state.rootSessionId ?? state.history?.rootSessionId ?? null;
36
- if (!connected(state) || state.boundaries.repair !== null || !certifiedRuntime(agentInfo(state)) || typeof nativeSessionId !== 'string'
37
- || nativeSessionId !== root || (restoreCalls !== undefined && typeof restoreCalls !== 'boolean')) {
38
- state.diagnostics.droppedNotes += 1;
39
- return;
40
- }
41
- state.boundaries.repair = newTerminalRepairData({
42
- nativeSessionId, restoreCalls: restoreCalls === true, includeProcess: supportsSubworkMeta(agentInfo(state)),
43
- });
44
- }
45
- /** `qoder_side{fact | chunk}`. */
46
- export function onSideNote(state, note, out) {
47
- const repair = state.boundaries.repair;
48
- if (repair === null || !connected(state)) {
49
- state.diagnostics.droppedNotes += 1;
50
- return;
51
- }
52
- const read = readSideNote(state.boundaries.sideChunk, note);
53
- state.boundaries.sideChunk = read.buffer;
54
- if (read.malformed) {
55
- state.diagnostics.droppedNotes += 1;
56
- return;
57
- }
58
- if (!read.fact)
59
- return;
60
- for (const event of new TerminalRepairFold(repair).apply(read.fact))
61
- emitSessionEvent(state, out, event);
62
- }
63
- /** The connection or the session is gone: the repair stops (`BackgroundTerminalRepair.close`). */
64
- export function closeRepair(state) {
65
- state.boundaries.repair = null;
66
- state.boundaries.sideChunk = null;
67
- }
68
- /**
69
- * What the daemon's reader asks before it reads (`BackgroundSideNotes.poll(demand)`), answered from the
70
- * processor's **current** state, or `undefined` while no repair is on (stop the reader). The answers
71
- * only steer which files are read; what was read reaches the processor through the notes alone.
72
- */
73
- export function sideDemand(state) {
74
- if (state.boundaries.repair === null)
75
- return undefined;
76
- return {
77
- active: () => state.boundaries.repair !== null && new TerminalRepairFold(state.boundaries.repair).active(),
78
- wantsProcess: (toolUseId) => state.boundaries.repair !== null && new TerminalRepairFold(state.boundaries.repair).wantsProcess(toolUseId),
79
- };
80
- }
@@ -1,45 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { buildSessionMcpServers } from './delegationMcp.js';
6
- export const QODER_MANAGED_VERSION = '1.1.61';
7
- /** Owned by one process; the bearer is never an argument or environment variable. */
8
- export async function prepareQoderStartup(launch, lease, cwd, env, agentEnv) {
9
- buildSessionMcpServers([], lease);
10
- if (launch.args.some((arg) => arg === '--' || /^(--mcp-config|--strict-mcp-config|--settings|--setting-sources)(=|$)/.test(arg))) {
11
- throw new Error('Managed Qoder startup has conflicting configuration arguments');
12
- }
13
- const version = await new Promise((resolve, reject) => {
14
- execFile(launch.command, [...launch.args, '--version'], {
15
- cwd, env: { ...process.env, ...launch.env, ...env, ...agentEnv },
16
- encoding: 'utf8', timeout: 10_000, maxBuffer: 64 * 1024, windowsHide: true,
17
- }, (error, stdout) => error ? reject(new Error('Managed Qoder version could not be verified')) : resolve(stdout.trim()));
18
- });
19
- if (version !== QODER_MANAGED_VERSION)
20
- throw new Error('Managed Qoder version is not certified');
21
- const dir = await mkdtemp(join(tmpdir(), 'axom-qoder-mcp-'));
22
- let disposal;
23
- const dispose = () => disposal ??= rm(dir, { recursive: true, force: true });
24
- try {
25
- await chmod(dir, 0o700);
26
- const config = join(dir, 'mcp.json');
27
- const authorization = Object.values(lease.headers)[0];
28
- await writeFile(config, JSON.stringify({ mcpServers: {
29
- 'axom-delegation': { type: 'http', url: lease.url, headers: { Authorization: authorization } },
30
- } }), { mode: 0o600, flag: 'wx' });
31
- // Validate again after the asynchronous version probe; an expired lease cannot start a process.
32
- buildSessionMcpServers([], lease);
33
- return {
34
- launch: { ...launch, args: [...launch.args, '--strict-mcp-config', '--mcp-config', config,
35
- '--setting-sources', '', '--settings', '{"autoMemoryEnabled":false}',
36
- '--disallowed-tools', 'Agent', '--cwd', cwd,
37
- '--append-system-prompt', 'Use the axom-delegation MCP tools for delegation. Native Agent execution is disabled in this session.'] },
38
- secrets: [authorization, authorization.slice('Bearer '.length)], dir, dispose,
39
- };
40
- }
41
- catch (error) {
42
- await dispose();
43
- throw error;
44
- }
45
- }
@@ -1,125 +0,0 @@
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
- }
@@ -1,118 +0,0 @@
1
- /** Native Qoder does not forward background child SDK messages through ACP. Read only the
2
- * exact transcript named by a validated root launch receipt; never scan or infer ownership.
3
- *
4
- * Each accepted row's derived events go to `onRow` (with the row's `uuid`) when it is given — the
5
- * side-file reader records them as one `child` fact (`backgroundTerminalFold.ts`) — and otherwise one
6
- * by one to `emit`. */
7
- import { constants } from 'node:fs';
8
- import { open } from 'node:fs/promises';
9
- const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
10
- const id = (value) => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,160}$/.test(value);
11
- const LIMIT = 2 * 1024 * 1024;
12
- export class BackgroundProcess {
13
- options;
14
- offset = 0;
15
- tail = Buffer.alloc(0);
16
- skipLine = false;
17
- seen = new Set();
18
- tools = new Map();
19
- constructor(options) {
20
- this.options = options;
21
- }
22
- async poll() {
23
- const file = await open(this.options.path, constants.O_RDONLY | constants.O_NOFOLLOW);
24
- try {
25
- const stat = await file.stat();
26
- if (!stat.isFile())
27
- return true;
28
- if (stat.size < this.offset) {
29
- this.offset = 0;
30
- this.tail = Buffer.alloc(0);
31
- this.skipLine = false;
32
- }
33
- const end = Math.min(stat.size, this.offset + 4 * 1024 * 1024);
34
- while (this.offset < end) {
35
- const chunk = Buffer.alloc(Math.min(64 * 1024, end - this.offset));
36
- const { bytesRead } = await file.read(chunk, 0, chunk.length, this.offset);
37
- if (!bytesRead)
38
- break;
39
- this.offset += bytesRead;
40
- this.consumeBytes(chunk.subarray(0, bytesRead));
41
- }
42
- return this.offset >= stat.size;
43
- }
44
- finally {
45
- await file.close();
46
- }
47
- }
48
- consumeBytes(bytes) {
49
- let start = 0;
50
- for (let end = bytes.indexOf(10); end >= 0; end = bytes.indexOf(10, start)) {
51
- const part = bytes.subarray(start, end);
52
- if (!this.skipLine && this.tail.length + part.length <= LIMIT) {
53
- try {
54
- this.consume(object(JSON.parse(Buffer.concat([this.tail, part]).toString('utf8'))));
55
- }
56
- catch { /* not evidence */ }
57
- }
58
- this.tail = Buffer.alloc(0);
59
- this.skipLine = false;
60
- start = end + 1;
61
- }
62
- const part = bytes.subarray(start);
63
- if (!this.skipLine && this.tail.length + part.length <= LIMIT)
64
- this.tail = Buffer.concat([this.tail, part]);
65
- else {
66
- this.tail = Buffer.alloc(0);
67
- this.skipLine = true;
68
- }
69
- }
70
- consume(row) {
71
- const o = this.options;
72
- if (row.sessionId !== o.sessionId || row.cwd !== o.cwd || row.agentId !== o.agentId ||
73
- row.parent_tool_use_id !== o.parentToolUseId || row.isSidechain !== true || row.version !== '1.1.61' ||
74
- !id(row.uuid) || this.seen.has(row.uuid) || this.seen.size >= 4096)
75
- return;
76
- this.seen.add(row.uuid);
77
- const content = object(row.message).content;
78
- if (!Array.isArray(content))
79
- return;
80
- const events = [];
81
- for (const block of content.map(object)) {
82
- if (row.type === 'assistant' && block.type === 'tool_use' && id(block.id) && typeof block.name === 'string') {
83
- if (this.tools.has(block.id) || this.tools.size >= 4096)
84
- continue;
85
- const input = object(block.input);
86
- this.tools.set(block.id, { name: block.name, input });
87
- this.derive(events, 'tool_call', { toolCallId: block.id, name: block.name, title: block.name,
88
- kind: block.name === 'Read' ? 'read' : block.name === 'Bash' ? 'execute' : block.name === 'Agent' ? 'think' : 'other',
89
- rawInput: input, status: 'pending' }, block.name);
90
- }
91
- else if (row.type === 'user' && block.type === 'tool_result' && id(block.tool_use_id)) {
92
- const tool = this.tools.get(block.tool_use_id);
93
- if (!tool)
94
- continue;
95
- const raw = row.toolUseResult ?? block.content;
96
- const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content ?? '');
97
- this.derive(events, 'tool_call_update', { toolCallId: block.tool_use_id, name: tool.name,
98
- status: block.is_error === true ? 'failed' : 'completed', rawOutput: raw,
99
- content: [{ type: 'content', content: { type: 'text', text } }] }, tool.name);
100
- }
101
- else if (row.type === 'assistant' && ((block.type === 'text' && typeof block.text === 'string') ||
102
- (block.type === 'thinking' && typeof block.thinking === 'string'))) {
103
- this.derive(events, block.type === 'text' ? 'agent_message_chunk' : 'agent_thought_chunk', { content: { type: 'text', text: block.type === 'text' ? block.text : block.thinking } });
104
- }
105
- }
106
- if (this.options.onRow) {
107
- if (events.length > 0)
108
- this.options.onRow(row.uuid, events);
109
- }
110
- else
111
- for (const event of events)
112
- this.options.emit(event);
113
- }
114
- derive(events, eventName, payload, toolName) {
115
- events.push({ eventName, payload: { ...payload, _meta: { qoder: { version: 1, source: 'correction',
116
- parentToolUseId: this.options.parentToolUseId, route: 'child', ...(toolName ? { toolName } : {}) } } } });
117
- }
118
- }
@@ -1,193 +0,0 @@
1
- /** Qoder 1.1.61 drops native background task notifications from both live ACP and session/load.
2
- * This read-only compatibility boundary repairs only a known root Agent tool, never a session.
3
- * Lifecycle/removal contract: axom-patch/cli/qoder-background-terminal.md.
4
- *
5
- * The impure half (Task 17c): this file reads the side files; everything the repair decides is the
6
- * pure fold in `backgroundTerminalFold.ts`. {@link BackgroundSideNotes} is the daemon side of the deterministic
7
- * processor: a poll returns the facts as `qoder_side` note payloads for the raw log; the processor folds them only when it
8
- * reads those notes, so a replay never touches the files. (The old live path's in-process driver, `BackgroundTerminalRepair`,
9
- * was deleted with that path in Task 29.)
10
- */
11
- import { constants } from 'node:fs';
12
- import { open } from 'node:fs/promises';
13
- import { dirname, isAbsolute, join, resolve } from 'node:path';
14
- import { BackgroundProcess } from './backgroundProcess.js';
15
- import { address, MAX_TASKS, object, rootRowFacts, sideNotes, } from './backgroundTerminalFold.js';
16
- export { TerminalRepairFold, newTerminalRepairData, sideNotes } from './backgroundTerminalFold.js';
17
- const CHUNK = 64 * 1024;
18
- const MAX_LINE = 2 * 1024 * 1024;
19
- const MAX_POLL = 4 * 1024 * 1024;
20
- export function nativeTerminalHistoryPath(identity, sessionId, cwd, historyHome) {
21
- if (identity?.name !== 'qoder-cli' || identity.version !== '1.1.61' ||
22
- !address(sessionId) || !isAbsolute(cwd) || !isAbsolute(historyHome))
23
- return;
24
- // Certified ~/.qoder/projects layout. Even a colliding directory name cannot cross session/cwd
25
- // identity: every accepted row is checked again against the exact native session and directory.
26
- return join(historyHome, '.qoder', 'projects', resolve(cwd).replace(/[^a-zA-Z0-9]/g, '-'), sessionId + '.jsonl');
27
- }
28
- /**
29
- * Reads the root transcript and the background children's transcripts incrementally (≤ 4 MiB per
30
- * poll and file, lines ≤ 2 MiB, `O_NOFOLLOW`, never written) and hands each derived {@link SideFact}
31
- * to the sink, in transcript order: root facts, then each followed child's rows and catch-up status,
32
- * then `poll_end` — only when the root read completed. Its offsets are its own (not checkpointed): a
33
- * new reader starts at 0 and the fold drops what it already has.
34
- */
35
- export class BackgroundSideReader {
36
- options;
37
- launches = new Map();
38
- processes = new Map();
39
- offset = 0;
40
- fileIdentity = '';
41
- pendingLine = Buffer.alloc(0);
42
- skipLine = false;
43
- closed = false;
44
- warned = false;
45
- restoring;
46
- constructor(options) {
47
- this.options = options;
48
- this.restoring = options.restoreCalls === true;
49
- }
50
- /** One poll. Never rejects: a missing, replaced or unreadable file is not evidence. */
51
- async read(demand, sink) {
52
- if (this.closed || (!this.restoring && !demand.active()))
53
- return;
54
- try {
55
- await this.readIncrement(demand, sink);
56
- }
57
- catch (error) {
58
- const code = object(error).code;
59
- if (!['ENOENT', 'ELOOP', 'ENOTDIR', 'EACCES'].includes(String(code)) && !this.warned) {
60
- this.warned = true;
61
- console.warn('[acp:qoder] background terminal repair unavailable; retaining native ACP state');
62
- }
63
- }
64
- }
65
- close() {
66
- this.closed = true;
67
- this.launches.clear();
68
- this.processes.clear();
69
- this.pendingLine = Buffer.alloc(0);
70
- }
71
- async readIncrement(demand, sink) {
72
- // O_NOFOLLOW avoids importing a transcript through a replaced symlink. File is never written.
73
- const file = await open(this.options.path, constants.O_RDONLY | constants.O_NOFOLLOW);
74
- try {
75
- const stat = await file.stat();
76
- if (!stat.isFile() || this.closed)
77
- return;
78
- const identity = `${stat.dev}:${stat.ino}`;
79
- if (identity !== this.fileIdentity || stat.size < this.offset) {
80
- this.offset = 0;
81
- this.pendingLine = Buffer.alloc(0);
82
- this.skipLine = false;
83
- this.fileIdentity = identity;
84
- }
85
- const end = Math.min(stat.size, this.offset + MAX_POLL);
86
- const chunk = Buffer.alloc(CHUNK);
87
- while (!this.closed && this.offset < end) {
88
- const { bytesRead } = await file.read(chunk, 0, Math.min(CHUNK, end - this.offset), this.offset);
89
- if (!bytesRead)
90
- break;
91
- this.offset += bytesRead;
92
- this.lines(chunk.subarray(0, bytesRead), sink);
93
- }
94
- if (this.offset >= stat.size)
95
- this.restoring = false;
96
- if (!this.closed) {
97
- await this.pollProcesses(demand, sink);
98
- if (!this.closed)
99
- sink({ kind: 'poll_end' });
100
- }
101
- }
102
- finally {
103
- await file.close();
104
- }
105
- }
106
- lines(chunk, sink) {
107
- let start = 0;
108
- for (let end = chunk.indexOf(10); end !== -1; end = chunk.indexOf(10, start)) {
109
- if (!this.skipLine && this.pendingLine.length + end - start <= MAX_LINE) {
110
- const line = Buffer.concat([this.pendingLine, chunk.subarray(start, end)]).toString('utf8');
111
- let row;
112
- try {
113
- row = object(JSON.parse(line));
114
- }
115
- catch { /* partial/corrupt non-evidence */ }
116
- if (row)
117
- this.consume(row, sink);
118
- }
119
- this.pendingLine = Buffer.alloc(0);
120
- this.skipLine = false;
121
- start = end + 1;
122
- }
123
- const tail = chunk.subarray(start);
124
- if (!this.skipLine && this.pendingLine.length + tail.length <= MAX_LINE) {
125
- this.pendingLine = Buffer.concat([this.pendingLine, tail]);
126
- }
127
- else {
128
- this.pendingLine = Buffer.alloc(0);
129
- this.skipLine = true;
130
- }
131
- }
132
- consume(row, sink) {
133
- const { nativeSessionId, cwd } = this.options;
134
- for (const fact of rootRowFacts(row, { nativeSessionId, cwd, restoreCalls: this.options.restoreCalls === true })) {
135
- // The reader's own copy of the receipts (same rule as the fold's): which child transcripts exist.
136
- if (fact.kind === 'launch' && fact.agentId && this.launches.size < MAX_TASKS && !this.launches.has(fact.toolUseId)) {
137
- this.launches.set(fact.toolUseId, fact.agentId);
138
- }
139
- sink(fact);
140
- }
141
- }
142
- async pollProcesses(demand, sink) {
143
- for (const [id, agentId] of this.launches) {
144
- if (this.closed || !demand.wantsProcess(id))
145
- continue;
146
- let process = this.processes.get(id);
147
- if (!process) {
148
- process = new BackgroundProcess({
149
- path: join(dirname(this.options.path), this.options.nativeSessionId, 'subagents', `agent-${agentId}.jsonl`),
150
- sessionId: this.options.nativeSessionId, cwd: this.options.cwd, agentId, parentToolUseId: id,
151
- emit: () => { },
152
- onRow: (uuid, events) => { if (!this.closed)
153
- sink({ kind: 'child', toolUseId: id, uuid, events }); },
154
- });
155
- this.processes.set(id, process);
156
- }
157
- let caughtUp;
158
- try {
159
- caughtUp = await process.poll();
160
- }
161
- catch {
162
- caughtUp = true; /* missing/corrupt transcript is not execution evidence */
163
- }
164
- sink({ kind: 'child_status', toolUseId: id, caughtUp });
165
- }
166
- }
167
- }
168
- /**
169
- * The daemon side of the processor (T19 runs it on its own timer, for a session whose processor
170
- * folded a `qoder_terminal_repair` note): one {@link poll} reads what is new and returns the note
171
- * payloads to append to the raw log, in order. `demand` is asked of the processor's current state
172
- * (`acp/processor/sideChannel.ts` `sideDemand`); what it answered is recorded only through the facts,
173
- * so the processor's fold never depends on when the daemon polled.
174
- */
175
- export class BackgroundSideNotes {
176
- reader;
177
- /** Facts over `MAX_SIDE_FACT_BYTES` that could not be recorded (diagnostics). */
178
- droppedFacts = 0;
179
- constructor(options) {
180
- this.reader = new BackgroundSideReader(options);
181
- }
182
- async poll(demand) {
183
- const notes = [];
184
- await this.reader.read(demand, (fact) => {
185
- const encoded = sideNotes(fact);
186
- if (encoded.length === 0)
187
- this.droppedFacts += 1;
188
- notes.push(...encoded);
189
- });
190
- return notes;
191
- }
192
- close() { this.reader.close(); }
193
- }