@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,114 @@
1
+ import { activityIsBusy } from './activityBusy.js';
2
+ /** Spec §14: a session must be quiet this long before it is switched. */
3
+ export const SWITCH_QUIET_MS = 30_000;
4
+ /** How long a switch waits for the session's record loop to reach a record boundary before it gives up (and retries later). */
5
+ export const SWITCH_SUSPEND_TIMEOUT_MS = 10_000;
6
+ /** A reopen request the server did not answer (busy, an older open won, …) is asked again after this long. */
7
+ export const REOPEN_REQUEST_RETRY_MS = 60_000;
8
+ /** The note a switch writes into a successor it gives up on (rollback): discovery never picks such a key (fix round 1). */
9
+ export const SWITCH_ABANDONED_NOTE = 'switch_abandoned';
10
+ /** A reopen for an adapter keeper-acp lost is asked at most this many times per lost spawn (60 s, 120 s, 240 s apart). */
11
+ export const ADAPTER_LOST_REOPEN_ATTEMPTS = 3;
12
+ /** The prefix of a switched key's suffix: `<sessionRef>#s<n>`. */
13
+ export const SWITCHED_KEY_MARK = '#s';
14
+ /** The transport key of the `keyGen`th life of `sessionRef` (0: the ref itself). */
15
+ export function sessionKeyFor(sessionRef, keyGen) {
16
+ return keyGen <= 0 ? sessionRef : `${sessionRef}${SWITCHED_KEY_MARK}${keyGen}`;
17
+ }
18
+ /** The key generation a spawn label records (`label.keyGen`; absent or invalid: 0). */
19
+ export function keyGenOf(label) {
20
+ const value = label?.keyGen;
21
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0;
22
+ }
23
+ /**
24
+ * The session ref a keeper-acp session belongs to: its label's `sessionRef`, provided the key is the one the label's `keyGen` derives
25
+ * from it (a key nothing could have spawned under that ref names none).
26
+ */
27
+ export function refOfSession(summary) {
28
+ const ref = summary.label.sessionRef;
29
+ if (typeof ref !== 'string' || ref === '')
30
+ return undefined;
31
+ return sessionKeyFor(ref, keyGenOf(summary.label)) === summary.sessionKey ? ref : undefined;
32
+ }
33
+ /**
34
+ * Several keeper-acp sessions of one ref (a switch a crash interrupted — the only way two exist): which one is the ref's session and
35
+ * which go. The newest key whose open completed wins (its import of the predecessor's outbox had landed: the import precedes the
36
+ * load); every other one is superseded. When no newer key completed its open, the switch never finished: the newer keys go and the
37
+ * oldest one stays the ref's session. `openCompleted(key)` reads the session's head checkpoint.
38
+ */
39
+ export async function resolveSuperseded(sessions, openCompleted) {
40
+ const byGen = [...sessions].sort((a, b) => keyGenOf(b.label) - keyGenOf(a.label));
41
+ for (const candidate of byGen.slice(0, -1)) {
42
+ if (await openCompleted(candidate))
43
+ return { winner: candidate, superseded: byGen.filter((s) => s !== candidate) };
44
+ }
45
+ const oldest = byGen[byGen.length - 1];
46
+ return { winner: oldest, superseded: byGen.filter((s) => s !== oldest) };
47
+ }
48
+ /**
49
+ * The scan card's wording of each {@link switchBlockers} sentence (Task 25, spec §6.4): those sentences stay as they are for the
50
+ * log; a reader looking at "why can't this session move now" gets these. Unknown input (none today) passes through unchanged.
51
+ */
52
+ export function plainSwitchBlocker(blocker) {
53
+ const quiet = /^not quiet for (\d+) s$/.exec(blocker);
54
+ if (quiet)
55
+ return `it was active less than ${quiet[1]} seconds ago`;
56
+ return PLAIN_BLOCKERS[blocker] ?? blocker;
57
+ }
58
+ const PLAIN_BLOCKERS = {
59
+ 'opening, retiring or switching': 'it is being opened or moved',
60
+ 'its adapter is not running': 'its agent process is not running right now',
61
+ 'a turn is in flight': 'it is working on a reply',
62
+ 'a permission or elicitation is unanswered': 'it is waiting for an answer',
63
+ 'the agent reports it is active': 'the agent says it is still working',
64
+ 'an async task or background terminal is still running': 'it has a background task or terminal still running',
65
+ 'a delegation refresh is pending': 'its delegated access is being renewed',
66
+ };
67
+ /** Why a session may not be switched now (spec §6.3 step 2); empty = strictly idle. */
68
+ export function switchBlockers(input) {
69
+ const { session } = input;
70
+ const blockers = [];
71
+ if (input.busyRef)
72
+ blockers.push('opening, retiring or switching');
73
+ const liveness = session.liveness;
74
+ if (!liveness.agentAlive)
75
+ blockers.push('its adapter is not running');
76
+ if (liveness.turnRunning)
77
+ blockers.push('a turn is in flight');
78
+ if ((session.pendingRequestIds?.length ?? 0) > 0)
79
+ blockers.push('a permission or elicitation is unanswered');
80
+ const raw = session.activityRaw;
81
+ // `activityIsBusy` calls Codex "only waiting on an approval" idle (correct for the heartbeat); a switch must not kill that wait.
82
+ if (activityIsBusy(raw) || raw?.type === 'active')
83
+ blockers.push('the agent reports it is active');
84
+ if ((session.liveBackgroundWork ?? 0) > 0)
85
+ blockers.push('an async task or background terminal is still running');
86
+ if (session.delegation?.refreshPending)
87
+ blockers.push('a delegation refresh is pending');
88
+ const quiet = input.quietMs ?? SWITCH_QUIET_MS;
89
+ const last = Math.max(input.lastActivityAt, session.lastFoldAt ?? 0);
90
+ if (input.now - last < quiet)
91
+ blockers.push(`not quiet for ${Math.round(quiet / 1000)} s`);
92
+ return blockers;
93
+ }
94
+ /**
95
+ * Spec §6.3 step 4 / analysis D §5.2 step 7: a load answers with the adapter's own idea of the session's config (most adapters do
96
+ * not persist a model or mode choice into the session file); every option whose value differs from what the session had before the
97
+ * switch is set back. Options the new process does not offer, or values it refuses, are reported and left as they are.
98
+ */
99
+ export async function restoreConfigOptions(session, before, idFor, log) {
100
+ const restored = [];
101
+ for (const option of before) {
102
+ const now = session.configOptions.find((candidate) => candidate.id === option.id);
103
+ if (!now || now.type !== option.type || now.currentValue === option.currentValue)
104
+ continue;
105
+ try {
106
+ await session.setConfigOption(option.id, option.currentValue, idFor(option.id));
107
+ restored.push(option.id);
108
+ }
109
+ catch (error) {
110
+ log(`[acp] switch: config option '${option.id}' could not be set back to its value before the switch: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+ }
113
+ return restored;
114
+ }
@@ -32,6 +32,21 @@
32
32
  import { spawn } from 'node:child_process';
33
33
  import { randomUUID } from 'node:crypto';
34
34
  import { createAuthUrlScanner } from './authUrlSniffer.js';
35
+ import { withPathPrefix } from './registry/launch.js';
36
+ export { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLoginSpec.js';
37
+ /**
38
+ * The launch a terminal sign-in runs under. A `_meta["terminal-auth"]` names its own command and
39
+ * args — but **not its own environment**: the resolved launch's env (and PATH prefix) still apply,
40
+ * because the launchEnv layer must reach the login child exactly as it reaches a session. Claude's
41
+ * `CLAUDE_SECURESTORAGE_CONFIG_DIR` decides which credential slot `--cli auth login` writes into;
42
+ * a meta login launched over an empty env would seed the shared slot and strand the isolated one —
43
+ * and every session on it — logged out. The append shape is the launch itself, unchanged.
44
+ */
45
+ export function launchForTerminalLogin(terminal, resolved) {
46
+ return terminal.kind === 'meta'
47
+ ? { command: terminal.command, args: [], env: resolved.env, ...(resolved.pathPrefix ? { pathPrefix: resolved.pathPrefix } : {}) }
48
+ : resolved;
49
+ }
35
50
  export class TerminalLoginFailedError extends Error {
36
51
  }
37
52
  export class TerminalLoginTimeoutError extends Error {
@@ -41,54 +56,13 @@ export class TerminalLoginCancelledError extends Error {
41
56
  /** How many of the process's last output lines a failure quotes. */
42
57
  const FAILURE_TAIL_LINES = 20;
43
58
  const DEFAULT_KILL_GRACE_MS = 2_000;
44
- function isRecord(value) {
45
- return typeof value === 'object' && value !== null && !Array.isArray(value);
46
- }
47
- function stringArray(value) {
48
- return Array.isArray(value) && value.every((item) => typeof item === 'string') ? [...value] : undefined;
49
- }
50
- function stringRecord(value) {
51
- if (!isRecord(value))
52
- return {};
53
- return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
54
- }
55
- function metaTerminalAuth(method) {
56
- if (!isRecord(method) || !isRecord(method._meta))
57
- return undefined;
58
- const entry = method._meta['terminal-auth'];
59
- return isRecord(entry) ? entry : undefined;
60
- }
61
- /** The `args` a `_meta["terminal-auth"]` entry carries, for display where the top-level `args` is absent. */
62
- export function metaTerminalAuthArgs(method) {
63
- return stringArray(metaTerminalAuth(method)?.args);
64
- }
65
- /**
66
- * How to run this advertised method as a terminal login, or `undefined` when it is not one (spec
67
- * D6: top-level `type: "terminal"`, or a `_meta["terminal-auth"]` object).
68
- */
69
- export function terminalLoginSpecOf(method) {
70
- if (!isRecord(method))
71
- return undefined;
72
- const meta = metaTerminalAuth(method);
73
- if (meta && typeof meta.command === 'string' && meta.command.trim() !== '') {
74
- return { kind: 'meta', command: meta.command, args: stringArray(meta.args) ?? [], env: stringRecord(meta.env) };
75
- }
76
- if (meta || method.type === 'terminal') {
77
- return {
78
- kind: 'append',
79
- args: stringArray(method.args) ?? stringArray(meta?.args) ?? [],
80
- env: { ...stringRecord(meta?.env), ...stringRecord(method.env) },
81
- };
82
- }
83
- return undefined;
84
- }
85
59
  /**
86
60
  * The concrete command line. `baseEnv` is the workstation's and the agent's configured environment
87
61
  * (`AcpListSpec.env` then `.agentEnv`), layered the way `connectAgent` layers a session's; the
88
62
  * method's own `env` goes last because ACP says it overrides the base launch configuration.
89
63
  */
90
64
  export function terminalInvocationFor(spec, launch, baseEnv) {
91
- const env = { ...launch.env, ...baseEnv, ...spec.env };
65
+ const env = withPathPrefix({ ...launch.env, ...baseEnv, ...spec.env }, launch.pathPrefix);
92
66
  if (spec.kind === 'meta')
93
67
  return { command: spec.command, args: [...spec.args], env };
94
68
  return { command: launch.command, args: [...launch.args, ...spec.args], env };
@@ -0,0 +1,41 @@
1
+ function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ function stringArray(value) {
5
+ return Array.isArray(value) && value.every((item) => typeof item === 'string') ? [...value] : undefined;
6
+ }
7
+ function stringRecord(value) {
8
+ if (!isRecord(value))
9
+ return {};
10
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
11
+ }
12
+ function metaTerminalAuth(method) {
13
+ if (!isRecord(method) || !isRecord(method._meta))
14
+ return undefined;
15
+ const entry = method._meta['terminal-auth'];
16
+ return isRecord(entry) ? entry : undefined;
17
+ }
18
+ /** The `args` a `_meta["terminal-auth"]` entry carries, for display where the top-level `args` is absent. */
19
+ export function metaTerminalAuthArgs(method) {
20
+ return stringArray(metaTerminalAuth(method)?.args);
21
+ }
22
+ /**
23
+ * How to run this advertised method as a terminal login, or `undefined` when it is not one (spec
24
+ * D6: top-level `type: "terminal"`, or a `_meta["terminal-auth"]` object).
25
+ */
26
+ export function terminalLoginSpecOf(method) {
27
+ if (!isRecord(method))
28
+ return undefined;
29
+ const meta = metaTerminalAuth(method);
30
+ if (meta && typeof meta.command === 'string' && meta.command.trim() !== '') {
31
+ return { kind: 'meta', command: meta.command, args: stringArray(meta.args) ?? [], env: stringRecord(meta.env) };
32
+ }
33
+ if (meta || method.type === 'terminal') {
34
+ return {
35
+ kind: 'append',
36
+ args: stringArray(method.args) ?? stringArray(meta?.args) ?? [],
37
+ env: { ...stringRecord(meta?.env), ...stringRecord(method.env) },
38
+ };
39
+ }
40
+ return undefined;
41
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Local, network-free adapter fixtures for the build tests (spec 2026-10-11 §6): npm package
3
+ * tarballs, a fake `npm` that installs them with the **real** npm (offline), and binary archives.
4
+ *
5
+ * The adapter in every fixture keeps lazily loading its own modules for as long as it runs — a
6
+ * fresh `require` of `lib/mod-<n>.js` every few milliseconds, printed to stdout — which is exactly
7
+ * the access pattern a later install must never disturb.
8
+ */
9
+ import { execFileSync } from 'node:child_process';
10
+ import { createHash } from 'node:crypto';
11
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ export const LAZY_MODULES = 4;
14
+ /** The adapter program: every 15 ms, drop and re-require one of its own modules and print what it says. */
15
+ export function lazyAdapterSource(version, shebang = '#!/usr/bin/env node') {
16
+ return `${shebang}
17
+ const path = require('node:path');
18
+ let i = 0;
19
+ setInterval(() => {
20
+ const file = path.join(__dirname, 'lib', 'mod-' + (i++ % ${LAZY_MODULES}) + '.js');
21
+ try {
22
+ delete require.cache[require.resolve(file)];
23
+ process.stdout.write(require(file) + '\\n');
24
+ } catch (error) {
25
+ process.stdout.write('LOAD-FAILED ' + error.message + '\\n');
26
+ }
27
+ }, 15);
28
+ process.stdout.write('started ${version}\\n');
29
+ `;
30
+ }
31
+ function lazyModules(version) {
32
+ return Object.fromEntries(Array.from({ length: LAZY_MODULES }, (_, n) => [
33
+ `lib/mod-${n}.js`, `module.exports = ${JSON.stringify(`${version}:mod-${n}`)};\n`,
34
+ ]));
35
+ }
36
+ function writeTree(root, files) {
37
+ for (const [relative, content] of Object.entries(files)) {
38
+ const file = path.join(root, relative);
39
+ mkdirSync(path.dirname(file), { recursive: true });
40
+ writeFileSync(file, content);
41
+ }
42
+ }
43
+ /** An npm tarball (`package/` root, as `npm pack` writes) for `name@version` whose bin is the lazy adapter. */
44
+ export function npmTarball(directory, name, version, options = {}) {
45
+ const tag = options.tag ? `-${options.tag}` : '';
46
+ const source = path.join(directory, `src-${name.replace(/[@/]/g, '_')}-${version}${tag}`, 'package');
47
+ writeTree(source, {
48
+ 'package.json': JSON.stringify({ name, version, bin: { [name.split('/').pop()]: 'bin/adapter.js' }, ...(options.engines ? { engines: options.engines } : {}) }),
49
+ 'bin/adapter.js': options.binSource ?? lazyAdapterSource(version).replace("path.join(__dirname, 'lib'", "path.join(__dirname, '..', 'lib'"),
50
+ ...lazyModules(version),
51
+ });
52
+ const tarball = path.join(directory, `${name.replace(/[@/]/g, '_')}-${version}${tag}.tgz`);
53
+ execFileSync('tar', ['-czf', tarball, '-C', path.dirname(source), 'package']);
54
+ return tarball;
55
+ }
56
+ /** A `.tar.gz` binary archive: `./adapter` (an executable script) and its lazily loaded `lib/`. */
57
+ export function binaryArchive(directory, version) {
58
+ const source = path.join(directory, `bin-src-${version}`);
59
+ writeTree(source, { adapter: lazyAdapterSource(version, `#!${process.execPath}`), ...lazyModules(version) });
60
+ chmodSync(path.join(source, 'adapter'), 0o755);
61
+ const archive = path.join(directory, `bin-${version}.tar.gz`);
62
+ execFileSync('tar', ['-czf', archive, '-C', source, '.']);
63
+ return readFileSync(archive);
64
+ }
65
+ /** The real npm this machine has, run offline by the fake below. */
66
+ export function realNpm() {
67
+ const sibling = path.join(path.dirname(process.execPath), 'npm');
68
+ return existsSync(sibling) ? sibling : 'npm';
69
+ }
70
+ /**
71
+ * A stand-in `npm` that only understands `install --prefix <dir> … <spec>`: records every spec it is
72
+ * asked for, then either fails the way npm would or installs the mapped local tarball with the real
73
+ * npm, `--offline` — never the network.
74
+ */
75
+ export function fakeNpm(directory, options) {
76
+ mkdirSync(directory, { recursive: true });
77
+ const log = path.join(directory, 'calls.txt');
78
+ const pidLog = path.join(directory, 'pids.txt');
79
+ const config = path.join(directory, 'fake-npm.json');
80
+ writeFileSync(config, JSON.stringify({ ...options, realNpm: realNpm(), log, pidLog, cache: path.join(directory, 'npm-cache') }));
81
+ const script = path.join(directory, 'npm');
82
+ writeFileSync(script, `#!${process.execPath}
83
+ const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process');
84
+ const cfg = JSON.parse(fs.readFileSync(${JSON.stringify(config)}, 'utf8'));
85
+ const argv = process.argv.slice(2);
86
+ if (argv[0] === 'view') {
87
+ // \`npm view <name> version --json\`: what \`latest\` names, from the same map an unpinned install uses.
88
+ fs.appendFileSync(cfg.log, 'view ' + argv[1] + '\\n');
89
+ const latest = cfg.latest && cfg.latest[argv[1]];
90
+ if (!latest || cfg.noView) { process.stderr.write('npm error code E404\\n'); process.exit(1); }
91
+ process.stdout.write(JSON.stringify(latest.slice(latest.lastIndexOf('@') + 1)) + '\\n');
92
+ process.exit(0);
93
+ }
94
+ const spec = argv[argv.length - 1];
95
+ const prefix = argv[argv.indexOf('--prefix') + 1];
96
+ fs.appendFileSync(cfg.log, spec + '\\n');
97
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
98
+ if (cfg.delayMs) sleep(cfg.delayMs);
99
+ if ((cfg.etarget || []).includes(spec)) {
100
+ process.stderr.write('npm error code ETARGET\\nnpm error notarget No matching version found for ' + spec + '.\\n');
101
+ process.exit(1);
102
+ }
103
+ if (cfg.fail && cfg.fail[spec] !== undefined) { process.stderr.write(cfg.fail[spec]); process.exit(1); }
104
+ if ((cfg.hang || []).includes(spec)) {
105
+ fs.appendFileSync(cfg.pidLog, process.pid + '\\n');
106
+ const name = spec.slice(0, spec.lastIndexOf('@') > 0 ? spec.lastIndexOf('@') : spec.length);
107
+ const dir = path.join(prefix, 'node_modules', name);
108
+ fs.mkdirSync(dir, { recursive: true });
109
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, version: spec.split('@').pop() }));
110
+ for (;;) sleep(1000);
111
+ }
112
+ const pinned = cfg.tarballs[spec] ? spec : cfg.latest && cfg.latest[spec];
113
+ const tarball = pinned && cfg.tarballs[pinned];
114
+ if (!tarball) { process.stderr.write('npm error code E404\\nnpm error 404 Not Found - ' + spec + '\\n'); process.exit(1); }
115
+ const real = spawnSync(cfg.realNpm, [...argv.slice(0, -1), '--offline', tarball], {
116
+ stdio: 'inherit', env: { ...process.env, npm_config_cache: cfg.cache },
117
+ });
118
+ process.exit(real.status === null ? 1 : real.status);
119
+ `);
120
+ chmodSync(script, 0o755);
121
+ const lines = (file) => (existsSync(file) ? readFileSync(file, 'utf8').split('\n').filter(Boolean) : []);
122
+ return { command: script, calls: () => lines(log), pids: () => lines(pidLog).map(Number) };
123
+ }
124
+ /** Every file, directory and symlink under `root` with its content hash / link target — byte-identity of a tree. */
125
+ export function treeDigest(root) {
126
+ const out = {};
127
+ const walk = (relative) => {
128
+ const full = path.join(root, relative);
129
+ const stat = lstatSync(full);
130
+ if (stat.isSymbolicLink())
131
+ out[relative] = `link:${readlinkSync(full)}`;
132
+ else if (stat.isDirectory()) {
133
+ out[relative || '.'] = `dir:${stat.mode.toString(8)}`;
134
+ for (const name of readdirSync(full).sort())
135
+ walk(relative ? path.join(relative, name) : name);
136
+ }
137
+ else
138
+ out[relative] = `file:${stat.mode.toString(8)}:${createHash('sha256').update(readFileSync(full)).digest('hex')}`;
139
+ };
140
+ walk('');
141
+ return out;
142
+ }
143
+ /**
144
+ * A `RunUv` that never spawns anything: `tool install` lays out what `uv tool install` really lays out
145
+ * (measured, `uvxInstall.ts`) under the `UV_TOOL_DIR`/`UV_TOOL_BIN_DIR` it is given — including the
146
+ * absolute paths, so a renamed tree really breaks — and `tool list --show-paths` reports it. Each
147
+ * executable is a shell script that prints `<name> <version> <exe> <args>`, and with `--wait` stays up.
148
+ */
149
+ export function fakeUv(options) {
150
+ const calls = [];
151
+ const run = async (args, _timeoutMs, env = {}) => {
152
+ calls.push({ args: [...args], env: { ...env } });
153
+ const toolDir = env.UV_TOOL_DIR;
154
+ const binDir = env.UV_TOOL_BIN_DIR;
155
+ if (!toolDir || !binDir)
156
+ return { ok: false, stdout: '', error: 'fake uv refuses to touch the user tool dir' };
157
+ if (args[0] === 'tool' && args[1] === 'list') {
158
+ if (!existsSync(toolDir))
159
+ return { ok: true, stdout: 'No tools installed\n' };
160
+ let out = '';
161
+ for (const name of readdirSync(toolDir).sort()) {
162
+ const receipt = path.join(toolDir, name, 'receipt.json');
163
+ if (!existsSync(receipt))
164
+ continue;
165
+ const { version, executables } = JSON.parse(readFileSync(receipt, 'utf8'));
166
+ out += `${name} v${version} (${path.join(toolDir, name)})\n`;
167
+ for (const exe of executables)
168
+ out += `- ${exe} (${path.join(binDir, exe)})\n`;
169
+ }
170
+ return { ok: true, stdout: out || 'No tools installed\n' };
171
+ }
172
+ if (args[0] !== 'tool' || args[1] !== 'install')
173
+ return { ok: false, stdout: '', error: `fake uv: unexpected ${args.join(' ')}` };
174
+ const requirement = args[args.length - 1];
175
+ const pinned = requirement.includes('==') ? requirement : options.latest?.[requirement];
176
+ const executables = pinned ? options.packages[pinned] : undefined;
177
+ if (!pinned || !executables)
178
+ return { ok: false, stdout: '', error: `uv: no solution for ${requirement}` };
179
+ const [name, version] = pinned.split('==');
180
+ const venv = path.join(toolDir, name);
181
+ mkdirSync(path.join(venv, 'bin'), { recursive: true });
182
+ mkdirSync(binDir, { recursive: true });
183
+ writeFileSync(path.join(venv, 'VERSION'), version);
184
+ if (options.hang?.includes(requirement))
185
+ return new Promise(() => { });
186
+ if (options.fail?.[requirement] !== undefined)
187
+ return { ok: false, stdout: '', error: options.fail[requirement] };
188
+ for (const exe of executables) {
189
+ const script = path.join(venv, 'bin', exe);
190
+ // Like a real console script: an absolute path to its own environment baked in.
191
+ writeFileSync(script, `#!/bin/sh\necho "${name} $(cat '${path.join(venv, 'VERSION')}') ${exe} $*"\n[ "$1" = --wait ] && exec sleep 30\nexit 0\n`);
192
+ chmodSync(script, 0o755);
193
+ symlinkSync(script, path.join(binDir, exe));
194
+ }
195
+ writeFileSync(path.join(venv, 'receipt.json'), JSON.stringify({ version, executables }));
196
+ return { ok: true, stdout: '' };
197
+ };
198
+ return { run, calls };
199
+ }
200
+ /**
201
+ * A pure-Python wheel `name-version-py3-none-any.whl` in `directory`, written with the machine's
202
+ * `python3` (stdlib `zipfile`, no network). Its console script `name` prints `<name> <version>
203
+ * <sys.executable>`, and with `--wait` keeps re-importing its own module (printing what it says) — the
204
+ * lazy-loading pattern a later install must not disturb.
205
+ */
206
+ export function pythonWheel(python, directory, name, version) {
207
+ mkdirSync(directory, { recursive: true });
208
+ const builder = path.join(directory, `make-wheel-${version}.py`);
209
+ writeFileSync(builder, String.raw `
210
+ import base64, hashlib, sys, zipfile
211
+ name, version, out = sys.argv[1], sys.argv[2], sys.argv[3]
212
+ mod = name.replace('-', '_')
213
+ files = {
214
+ mod + '/__init__.py': (
215
+ "import importlib, sys, time\n"
216
+ "def main():\n"
217
+ " print('%s', '%s', sys.executable, flush=True)\n"
218
+ " if '--wait' in sys.argv:\n"
219
+ " from . import lazy\n"
220
+ " for _ in range(3000):\n"
221
+ " importlib.reload(lazy)\n"
222
+ " print(lazy.VALUE, flush=True)\n"
223
+ " time.sleep(0.02)\n"
224
+ ) % (name, version),
225
+ mod + '/lazy.py': "VALUE = '%s:lazy'\n" % version,
226
+ '%s-%s.dist-info/METADATA' % (mod, version): 'Metadata-Version: 2.1\nName: %s\nVersion: %s\n' % (name, version),
227
+ '%s-%s.dist-info/WHEEL' % (mod, version): 'Wheel-Version: 1.0\nGenerator: axom-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n',
228
+ '%s-%s.dist-info/entry_points.txt' % (mod, version): '[console_scripts]\n%s = %s:main\n' % (name, mod),
229
+ }
230
+ record = '%s-%s.dist-info/RECORD' % (mod, version)
231
+ lines = []
232
+ for path, text in files.items():
233
+ digest = base64.urlsafe_b64encode(hashlib.sha256(text.encode()).digest()).rstrip(b'=').decode()
234
+ lines.append('%s,sha256=%s,%d' % (path, digest, len(text.encode())))
235
+ lines.append(record + ',,')
236
+ files[record] = '\n'.join(lines) + '\n'
237
+ wheel = '%s/%s-%s-py3-none-any.whl' % (out, mod, version)
238
+ with zipfile.ZipFile(wheel, 'w') as archive:
239
+ for path, text in files.items():
240
+ archive.writestr(path, text)
241
+ print(wheel)
242
+ `);
243
+ return execFileSync(python, ['-I', builder, name, version, directory], { encoding: 'utf8' }).trim();
244
+ }
245
+ /**
246
+ * What `npx --yes <name>@<version>` leaves in npm's cache: `<npmCache>/_npx/<hash>/node_modules/…`
247
+ * (with npm's hidden lockfile), made by the real npm, offline, from a local tarball.
248
+ */
249
+ export function npxCacheEntry(npmCache, hash, tarball) {
250
+ const prefix = path.join(npmCache, '_npx', hash);
251
+ mkdirSync(prefix, { recursive: true });
252
+ execFileSync(realNpm(), ['install', '--prefix', prefix, '--no-save', '--no-audit', '--no-fund', '--offline', tarball], {
253
+ env: { ...process.env, npm_config_cache: path.join(npmCache, '..', `npx-fixture-cache-${hash}`) }, stdio: 'ignore',
254
+ });
255
+ return path.join(prefix, 'node_modules');
256
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * One daemon life in its own process (Task 21's kill -9 test, Task 23's fault gate): AcpAgents + AgentSessionManager over keeper-acp,
3
+ * the way `runDaemon` wires them — adoption at boot, then the server's frames, one JSON per stdin line, handed to the manager in order.
4
+ * Every frame it sends is appended to `framesOut` (one JSON line each): the server's view of what this daemon sent. It never exits on
5
+ * its own: the test kills it with SIGKILL — `{cmd:'offer'}` appends the reconnect offer it would send now — or sends `{cmd:'forceStop'}`, a force update's
6
+ * `TaskRegistry.stopAll()` followed by the restart path's `detachDurableSessions()` (keeper-hosted sessions detach), after which it exits 0.
7
+ * `{cmd:'forceBegin'}` is only the start of a force update (`stopAll()`, before the download): the daemon keeps running.
8
+ *
9
+ * Fault seams (`faults`, Task 23), both hang forever so a SIGKILL lands exactly there:
10
+ * - `holdOutboxEvent` / `holdOutboxText`: `acp.outbox.append` of entries holding a frame with this `eventName` / whose JSON contains
11
+ * this text — the record's commands are already in raw (the driver writes before it records), its frames never reach the outbox
12
+ * ("written into keeper, not recorded in the outbox");
13
+ * - `holdNoteType`: `acp.note` of this type — the daemon took the server's frame, nothing reached keeper-acp.
14
+ * Reaching a seam writes `<configDir>/held-<seam>`.
15
+ *
16
+ * Run with vite-node: `vite-node daemonLife.ts <config JSON>`; config `{configDir, distDir, framesOut, faults?}`.
17
+ */
18
+ import { appendFileSync, writeFileSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { createInterface } from 'node:readline';
21
+ import { AcpKeeperClient } from '../../keepers/acp/client.js';
22
+ import { KeeperLauncher } from '../../keepers/kit/launcher.js';
23
+ import { AgentSessionManager } from '../../agentSessions.js';
24
+ import { AGENT_WIRE_VERSION } from '../../protocol/agent.js';
25
+ import { TaskRegistry } from '../../update/taskRegistry.js';
26
+ import { AcpAgents } from '../acpAgents.js';
27
+ import { keeperGenerations } from '../processor/transport/generations.js';
28
+ import { KeeperTransport } from '../processor/transport/keeper.js';
29
+ const never = new Promise(() => { });
30
+ /** The transport with the configured seams: every other call goes straight through. */
31
+ function withFaults(inner, configDir, faults) {
32
+ if (!faults.holdOutboxEvent && !faults.holdOutboxText && !faults.holdNoteType)
33
+ return inner;
34
+ return new Proxy(inner, {
35
+ get(target, prop) {
36
+ const value = target[prop];
37
+ if (prop === 'appendOutbox' && (faults.holdOutboxEvent || faults.holdOutboxText)) {
38
+ return async (...args) => {
39
+ if (args[1].some((entry) => entry.frame.eventName === faults.holdOutboxEvent) ||
40
+ (faults.holdOutboxText !== undefined && JSON.stringify(args[1]).includes(faults.holdOutboxText))) {
41
+ writeFileSync(path.join(configDir, 'held-outbox'), JSON.stringify(args[1]));
42
+ return never;
43
+ }
44
+ return target.appendOutbox(...args);
45
+ };
46
+ }
47
+ if (prop === 'note' && faults.holdNoteType) {
48
+ return async (...args) => {
49
+ if (args[1].type === faults.holdNoteType) {
50
+ writeFileSync(path.join(configDir, 'held-note'), JSON.stringify(args[1]));
51
+ return never;
52
+ }
53
+ return target.note(...args);
54
+ };
55
+ }
56
+ return typeof value === 'function' ? value.bind(target) : value;
57
+ },
58
+ });
59
+ }
60
+ async function main() {
61
+ const config = JSON.parse(process.argv[process.argv.length - 1]);
62
+ const launcher = new KeeperLauncher({ configDir: config.configDir, workstationId: 'ws-t21', server: 'http://test.invalid', distDir: config.distDir,
63
+ useSystemd: false, startTimeoutMs: 20_000, umask: 0o022 });
64
+ const keeper = withFaults(new KeeperTransport(new AcpKeeperClient({ launcher })), config.configDir, config.faults ?? {});
65
+ const acp = new AcpAgents({ cacheDir: config.configDir, registry: { load: async () => [] }, transports: { keeper },
66
+ keeperGenerations: keeperGenerations({ launcher, current: keeper, log: () => { } }) });
67
+ const tasks = new TaskRegistry();
68
+ const manager = new AgentSessionManager(acp, null, path.join(config.configDir, 'agents'), tasks);
69
+ manager.bindSend((frame) => appendFileSync(config.framesOut, `${JSON.stringify(frame)}\n`), AGENT_WIRE_VERSION);
70
+ await manager.adoptSessions();
71
+ // Frames in wire order, handled without waiting for each other (as the agent channel's dispatcher does).
72
+ for await (const line of createInterface({ input: process.stdin, crlfDelay: Infinity })) {
73
+ if (!line.trim())
74
+ continue;
75
+ const message = JSON.parse(line);
76
+ if ('cmd' in message) {
77
+ if (message.cmd === 'offer') {
78
+ // What this daemon would put in a connection's reconnect offer now (`resumeOfferSessions`), as a frame.
79
+ appendFileSync(config.framesOut, `${JSON.stringify({ t: 'agent_resume_offer', sessions: await manager.resumeOfferSessions() })}\n`);
80
+ continue;
81
+ }
82
+ if (message.cmd === 'forceBegin') {
83
+ // A forced update starts (UpdateCoordinator: `stopAll()` before DOWNLOADING); this daemon keeps running while it downloads.
84
+ await tasks.stopAll();
85
+ appendFileSync(config.framesOut, `${JSON.stringify({ t: '__force_begun__' })}\n`);
86
+ continue;
87
+ }
88
+ if (message.cmd === 'forceStop') {
89
+ // The whole forced update as the sessions see it: `stopAll()`, then the restart path's detach right before the hand-off.
90
+ await tasks.stopAll();
91
+ await tasks.detachDurableSessions();
92
+ appendFileSync(config.framesOut, `${JSON.stringify({ t: '__force_stopped__' })}\n`);
93
+ process.exit(0);
94
+ }
95
+ continue;
96
+ }
97
+ void manager.handleFrame(message).catch((error) => {
98
+ process.stderr.write(`handling ${message.t} failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
99
+ });
100
+ }
101
+ setInterval(() => { }, 1_000);
102
+ }
103
+ main().catch((error) => {
104
+ process.stderr.write(`daemon life failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
105
+ process.exit(1);
106
+ });