@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,101 @@
1
+ // Daemon-side: a typed `acp.*` client for keeper-acp (launch through KeeperLauncher, one long-lived KeeperClient per generation).
2
+ // Exempt from the keeper-side import guard (it runs in the daemon), though it imports nothing but the kit and this keeper's protocol.
3
+ import { KeeperClient } from '../kit/client.js';
4
+ import { protocolWindow } from '../kit/protocol.js';
5
+ import { ACP_PROTOCOL_VERSION, acpStream, } from './protocol.js';
6
+ /** keeper-acp could not be reached (launch, connect or hello failed): the request was never sent. */
7
+ export class AcpKeeperUnavailableError extends Error {
8
+ cause;
9
+ code = 'launch_failed';
10
+ constructor(cause) {
11
+ super(cause instanceof Error ? cause.message : String(cause));
12
+ this.cause = cause;
13
+ this.name = 'AcpKeeperUnavailableError';
14
+ }
15
+ }
16
+ export class AcpKeeperClient {
17
+ options;
18
+ client;
19
+ clientGen;
20
+ lostListeners = new Set();
21
+ constructor(options) {
22
+ this.options = options;
23
+ }
24
+ get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(ACP_PROTOCOL_VERSION); }
25
+ clientFor(paths) {
26
+ if (this.client && this.clientGen === paths.gen)
27
+ return this.client;
28
+ this.client?.close();
29
+ this.client = new KeeperClient({
30
+ kind: 'acp', socket: paths.socket, tokenFile: paths.tokenFile, gen: paths.gen, daemonProtocol: this.daemonProtocol,
31
+ requestTimeoutMs: this.options.requestTimeoutMs, connectTimeoutMs: this.options.connectTimeoutMs,
32
+ });
33
+ this.client.on('error', () => { });
34
+ // An established connection dropped (keeper-acp died or restarted): the kit client reconnects to the same socket by itself, but
35
+ // nothing relaunches a keeper that is gone until a request is made — the sessions re-attach (Task 21), which does.
36
+ this.client.on('disconnected', (error) => { for (const listener of [...this.lostListeners])
37
+ listener(error); });
38
+ this.clientGen = paths.gen;
39
+ return this.client;
40
+ }
41
+ /** Called when an established connection to keeper-acp drops (not when this client is closed). Returns the unsubscribe. */
42
+ onConnectionLost(listener) {
43
+ this.lostListeners.add(listener);
44
+ return () => { this.lostListeners.delete(listener); };
45
+ }
46
+ get hello() { return this.client?.state === 'ready' ? this.client.hello : undefined; }
47
+ /** The generation this client is bound to (`gen`), or `undefined` for the current build's. */
48
+ get gen() { return this.options.gen; }
49
+ /** The connected client of the current build, launching keeper-acp only when needed (a `gen`-bound client only connects). */
50
+ async ensure() {
51
+ const current = this.client;
52
+ if (current && current.state === 'ready')
53
+ return current;
54
+ if (this.options.gen !== undefined) {
55
+ if (!this.options.launcher.pathsFor)
56
+ throw new Error('A generation-bound keeper-acp client needs launcher.pathsFor');
57
+ const client = this.clientFor(this.options.launcher.pathsFor('acp', this.options.gen));
58
+ await client.connect();
59
+ return client;
60
+ }
61
+ const handle = await this.options.launcher.launch('acp');
62
+ const client = this.clientFor(handle.paths);
63
+ await client.connect();
64
+ return client;
65
+ }
66
+ async call(method, params, options = {}) {
67
+ let client;
68
+ try {
69
+ client = await this.ensure();
70
+ }
71
+ catch (error) {
72
+ throw new AcpKeeperUnavailableError(error);
73
+ }
74
+ return client.request(method, params, options);
75
+ }
76
+ spawn(params, options = {}) { return this.call('acp.spawn', params, options); }
77
+ list() { return this.call('acp.list', {}); }
78
+ attach(sessionKey) { return this.call('acp.attach', { sessionKey }); }
79
+ write(params) { return this.call('acp.write', params); }
80
+ note(params) { return this.call('acp.note', params); }
81
+ read(params) { return this.call('acp.read', params); }
82
+ downs(sessionKey, from = 0) { return this.call('acp.downs', { sessionKey, from }); }
83
+ outboxAppend(params) { return this.call('acp.outbox.append', params); }
84
+ commit(params) { return this.call('acp.commit', params); }
85
+ /** Waits for the exit record (TERM → grace → KILL → 2 s → pipes closed); allow for both graces. */
86
+ terminate(params) { return this.call('acp.terminate', params, { timeoutMs: (params.graceMs ?? 2000) + 10_000 }); }
87
+ release(sessionKey) { return this.call('acp.release', { sessionKey }); }
88
+ activity() { return this.call('keeper.activity', {}); }
89
+ async subscribe(sessionKey, stream, handler, options) {
90
+ let client;
91
+ try {
92
+ client = await this.ensure();
93
+ }
94
+ catch (error) {
95
+ throw new AcpKeeperUnavailableError(error);
96
+ }
97
+ return client.subscribe(acpStream(stream, sessionKey), handler, options);
98
+ }
99
+ /** Drops the connection (keeper-acp and its adapters keep running). */
100
+ disconnect() { this.client?.close(); this.client = undefined; this.clientGen = undefined; }
101
+ }
@@ -0,0 +1,62 @@
1
+ // Envelope diagnostics for keeper-acp (spec §5.3): sniff only a JSON-RPC line's `id` and `method`, and keep the requests each
2
+ // direction has not answered yet. For diagnostics and activity only — the processor's checkpoint plus replay is the truth.
3
+ // Keeper-side module: node builtins and kit siblings only (the import guard enforces it).
4
+ import { ACP_ENVELOPE_PENDING, ACP_ENVELOPE_SNIFF_BYTES } from './protocol.js';
5
+ /** `id`/`method` of a JSON-RPC object line, or undefined (not sniffable: too long, not JSON, not an object). */
6
+ export function sniffEnvelope(line) {
7
+ if (line.length > ACP_ENVELOPE_SNIFF_BYTES)
8
+ return undefined;
9
+ let value;
10
+ try {
11
+ value = JSON.parse(line);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
17
+ return undefined;
18
+ const message = value;
19
+ const id = typeof message.id === 'string' || typeof message.id === 'number' ? JSON.stringify(message.id) : undefined;
20
+ const method = typeof message.method === 'string' ? message.method.slice(0, 128) : undefined;
21
+ return { ...(id !== undefined ? { id } : {}), ...(method !== undefined ? { method } : {}) };
22
+ }
23
+ /** Unpaired requests per direction. A request is a line with both `id` and `method`; a response is `id` without `method`. */
24
+ export class EnvelopeTracker {
25
+ /** Daemon → adapter requests (down lines) awaiting an up response. */
26
+ outbound = new Map();
27
+ /** Adapter → daemon requests (up lines) awaiting a down response. */
28
+ inbound = new Map();
29
+ lastUp = null;
30
+ lastDown = null;
31
+ unsniffed = 0;
32
+ /** A new process: nothing from the previous one can be answered any more. */
33
+ reset() { this.outbound.clear(); this.inbound.clear(); }
34
+ up(line, at) { this.lastUp = at; this.observe(line, at, this.inbound, this.outbound); }
35
+ down(line, at) { this.lastDown = at; this.observe(line, at, this.outbound, this.inbound); }
36
+ observe(line, at, mine, theirs) {
37
+ const envelope = sniffEnvelope(line);
38
+ if (!envelope) {
39
+ this.unsniffed += 1;
40
+ return;
41
+ }
42
+ if (envelope.id === undefined)
43
+ return; // a notification
44
+ if (envelope.method !== undefined) {
45
+ mine.delete(envelope.id); // re-insert at the end (most recent)
46
+ mine.set(envelope.id, { id: envelope.id, method: envelope.method, sinceEpochMs: at });
47
+ while (mine.size > ACP_ENVELOPE_PENDING)
48
+ mine.delete(mine.keys().next().value);
49
+ }
50
+ else
51
+ theirs.delete(envelope.id);
52
+ }
53
+ get pendingInboundCount() { return this.inbound.size; }
54
+ get pendingOutboundCount() { return this.outbound.size; }
55
+ snapshot() {
56
+ return {
57
+ pendingOutbound: [...this.outbound.values()].map((p) => ({ ...p })),
58
+ pendingInbound: [...this.inbound.values()].map((p) => ({ ...p })),
59
+ lastUpEpochMs: this.lastUp, lastDownEpochMs: this.lastDown, unsniffed: this.unsniffed,
60
+ };
61
+ }
62
+ }
@@ -0,0 +1,419 @@
1
+ // keeper-acp's host: the sessions it holds under `<configDir>/keepers/acp/data/sessions/<id>/`, the `acp.*` methods on the kit
2
+ // server, start-up recovery, the disk guard and activity. Keeper-side module: node builtins and kit siblings only (guarded).
3
+ //
4
+ // On disk, per session (0700 directories, 0600 files, never /tmp):
5
+ // session.json SessionMeta: key, opaque label, owner keeper, latest spawn identity (pid, pgid, start time), exit, removeOnExit.
6
+ // Never env values or secrets. Atomically replaced.
7
+ // raw/ kit stream log: up / down / proc / note / err (+ gap, discontinuity); the processor's checkpoints live in its slots.
8
+ // outbox/ kit stream log: frame{seq, src, frame}; `committedTail` is its commit watermark.
9
+ // stderr.log redacted stderr, rotated to stderr.log.1 at 1 MiB.
10
+ import { createHash } from 'node:crypto';
11
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, statfs, writeSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { manifestProcessLive } from '../kit/lock.js';
14
+ import { KeeperMethodError } from '../kit/protocol.js';
15
+ import { StreamLog } from '../kit/streamLog.js';
16
+ import { annotationOf, bad, labelOf, noteOf, offsetOf, onlyKeys, outboxEntriesOf, redactionsOf, sessionKeyOf, spawnSpecOf, } from './params.js';
17
+ import { ACP_CAPABILITY, ACP_DISK_CHECK_MS, ACP_MIN_FREE_DISK_BYTES, ACP_SESSION_LIMIT, AcpErrorCode, parseAcpStream, } from './protocol.js';
18
+ import { SESSION_FILE, STDERR_FILE, listSessionDirs, readSessionMeta, settleLostAdapter, stderrTailOf } from './recover.js';
19
+ import { AcpSession } from './session.js';
20
+ const META_TMP = `${SESSION_FILE}.tmp`;
21
+ function privateDir(dir) { mkdirSync(dir, { recursive: true, mode: 0o700 }); }
22
+ function atomicJson(file, value) {
23
+ const temp = path.join(path.dirname(file), META_TMP);
24
+ const fd = openSync(temp, 'w', 0o600);
25
+ try {
26
+ const data = Buffer.from(JSON.stringify(value));
27
+ let written = 0;
28
+ while (written < data.length)
29
+ written += writeSync(fd, data, written, data.length - written);
30
+ fsyncSync(fd);
31
+ }
32
+ finally {
33
+ closeSync(fd);
34
+ }
35
+ renameSync(temp, file);
36
+ }
37
+ /** The session directory name: never the key itself (a daemon-chosen string), so no key can name a path. */
38
+ export const sessionDirName = (sessionKey) => createHash('sha256').update(sessionKey).digest('hex').slice(0, 32);
39
+ const labelText = (label) => {
40
+ for (const key of ['agentKey', 'agentId'])
41
+ if (typeof label[key] === 'string' && label[key])
42
+ return label[key].slice(0, 128);
43
+ return 'adapter';
44
+ };
45
+ export class AcpHost {
46
+ options;
47
+ sessions = new Map();
48
+ dirs = new Map();
49
+ sessionsDir;
50
+ now;
51
+ logLine;
52
+ diskTimer;
53
+ maintenanceTimer;
54
+ stdoutHeld = false;
55
+ stopping = false;
56
+ adopting;
57
+ /** Lost adapters of adopted sessions still being settled (SIGTERM, grace, SIGKILL), by key: a spawn into one waits (minor 8). */
58
+ settling = new Map();
59
+ constructor(options) {
60
+ this.options = options;
61
+ this.sessionsDir = path.join(options.dataDir, 'sessions');
62
+ this.now = options.now ?? Date.now;
63
+ this.logLine = options.log ?? (() => { });
64
+ }
65
+ report(error) { this.logLine(`acp: ${error instanceof Error ? error.message : String(error)}`); }
66
+ /** Start-up: adopt every session whose keeper is gone (keeper_lost), then start the disk guard and the periodic adoption. */
67
+ async open() {
68
+ privateDir(this.sessionsDir);
69
+ await this.adopt();
70
+ this.diskTimer = setInterval(() => { void this.checkDisk(); }, ACP_DISK_CHECK_MS);
71
+ this.diskTimer.unref();
72
+ void this.checkDisk();
73
+ this.maintenanceTimer = setInterval(() => { void this.adopt(); }, this.options.maintenanceMs ?? 60_000);
74
+ this.maintenanceTimer.unref();
75
+ }
76
+ async ownerAlive(owner) {
77
+ if (!owner || owner.pid === this.options.owner.pid)
78
+ return false; // ours (or nobody's): adopt
79
+ return (this.options.ownerAlive ?? manifestProcessLive)(owner);
80
+ }
81
+ /** Adopt now every session directory whose keeper is gone (what the periodic adoption does). */
82
+ adoptNow() { return this.adopt(); }
83
+ adopt() {
84
+ this.adopting ??= (async () => {
85
+ // One liveness probe per owner per pass (each may spawn `ps`): many directories of one older generation share its owner, and
86
+ // `acp.list` runs this pass every time (Task 21 fix round 2).
87
+ const alive = new Map();
88
+ const ownerAlive = (owner) => {
89
+ const key = JSON.stringify(owner ?? null);
90
+ let probe = alive.get(key);
91
+ if (!probe)
92
+ alive.set(key, probe = this.ownerAlive(owner));
93
+ return probe;
94
+ };
95
+ for (const { dir, meta } of listSessionDirs(this.sessionsDir, (message) => this.logLine(`acp: ${message}`))) {
96
+ if (this.sessions.has(meta.sessionKey) || this.stopping)
97
+ continue;
98
+ if (path.basename(dir) !== sessionDirName(meta.sessionKey)) {
99
+ this.logLine(`acp: ${path.basename(dir)}: misnamed session directory, left alone`);
100
+ continue;
101
+ }
102
+ try {
103
+ if (await ownerAlive(meta.owner))
104
+ continue;
105
+ if (this.sessions.has(meta.sessionKey))
106
+ continue;
107
+ await this.adoptOne(dir, meta);
108
+ }
109
+ catch (error) {
110
+ this.report(error);
111
+ }
112
+ }
113
+ })().finally(() => { this.adopting = undefined; });
114
+ return this.adopting;
115
+ }
116
+ async adoptOne(dir, meta) {
117
+ meta.owner = { ...this.options.owner };
118
+ // The lost adapter's removeOnExit, captured BEFORE the session is registered: once it is, a spawn into it replaces
119
+ // meta.removeOnExit with the new adapter's, which this cleanup must never remove (final review minor 8).
120
+ const lostRemoveOnExit = [...meta.removeOnExit];
121
+ const key = meta.sessionKey;
122
+ const session = this.openSession(dir, meta, { fresh: false });
123
+ this.sessions.set(key, session);
124
+ this.dirs.set(key, dir);
125
+ session.persist();
126
+ if (meta.spawn && !meta.exited) {
127
+ session.recordKeeperLost(stderrTailOf(dir));
128
+ // A spawn into the session waits until the lost adapter's group is settled (SIGKILL due) and its files are removed.
129
+ let settled;
130
+ const settling = new Promise((resolve) => { settled = resolve; });
131
+ this.settling.set(key, settling);
132
+ void settling.then(() => { if (this.settling.get(key) === settling)
133
+ this.settling.delete(key); });
134
+ let outcome;
135
+ try {
136
+ outcome = await settleLostAdapter(meta, () => { try {
137
+ session.removeOnExit(lostRemoveOnExit);
138
+ }
139
+ finally {
140
+ settled();
141
+ } }, this.options.sweep);
142
+ }
143
+ catch (error) {
144
+ settled();
145
+ throw error;
146
+ } // never leave a spawn waiting on a settle that failed
147
+ this.logLine(`acp: session ${key}: keeper lost; adapter group ${outcome}`);
148
+ }
149
+ else if (lostRemoveOnExit.length > 0)
150
+ session.removeOnExit(lostRemoveOnExit); // exited, but its keeper died before cleaning up
151
+ }
152
+ openSession(dir, meta, options) {
153
+ const onError = (error) => this.report(error);
154
+ const raw = StreamLog.open({ dir: path.join(dir, 'raw'), now: this.now, immediateKinds: ['proc'], onError, ...(options.baseOffset !== undefined ? { baseOffset: options.baseOffset } : {}) });
155
+ let outbox;
156
+ try {
157
+ outbox = StreamLog.open({ dir: path.join(dir, 'outbox'), now: this.now, immediateKinds: [], onError });
158
+ }
159
+ catch (error) {
160
+ raw.close();
161
+ throw error;
162
+ }
163
+ const session = new AcpSession({
164
+ meta, raw, outbox, freshRaw: options.fresh, detached: true, stderrFile: path.join(dir, STDERR_FILE),
165
+ saveMeta: (m) => atomicJson(path.join(dir, SESSION_FILE), m), now: this.now, log: this.logLine,
166
+ });
167
+ if (this.stdoutHeld)
168
+ session.holdStdout(true);
169
+ return session;
170
+ }
171
+ find(sessionKey) {
172
+ const session = this.sessions.get(sessionKey);
173
+ if (!session)
174
+ throw new KeeperMethodError(AcpErrorCode.notFound, 'ACP session not found');
175
+ return session;
176
+ }
177
+ // ---- methods ---------------------------------------------------------------------------------------------------------------
178
+ async spawn(params) {
179
+ onlyKeys(params, ['sessionKey', 'spec', 'label', 'baseOffset']);
180
+ const sessionKey = sessionKeyOf(params.sessionKey);
181
+ const spec = spawnSpecOf(params.spec, [this.options.dataDir]);
182
+ const label = labelOf(params.label);
183
+ const baseOffset = params.baseOffset === undefined ? undefined : offsetOf(params.baseOffset, 'baseOffset');
184
+ const existing = this.sessions.get(sessionKey);
185
+ if (existing) {
186
+ if (baseOffset !== undefined)
187
+ bad('baseOffset only applies to a new session');
188
+ await this.settling.get(sessionKey);
189
+ return existing.spawn(spec);
190
+ }
191
+ if (this.sessions.size >= ACP_SESSION_LIMIT)
192
+ throw new KeeperMethodError(AcpErrorCode.limit, 'Too many ACP sessions in keeper-acp');
193
+ const dir = path.join(this.sessionsDir, sessionDirName(sessionKey));
194
+ if (existsSync(dir)) {
195
+ // A directory this keeper does not hold. Another LIVE generation's session is never touched; a dead keeper's is adopted
196
+ // right here (keeper_lost, as the periodic adoption would, without waiting for it). A directory WITHOUT session.json (a start
197
+ // killed between mkdir and the metadata rename) names no owner and holds no acknowledged-anything: reclaimed. One whose
198
+ // session.json exists but cannot be read (corrupt, a future schema) is never deleted — its outbox may hold unacknowledged frames.
199
+ // Reclaiming relies on the daemon spawning a sessionKey in one keeper generation at a time (interface-contracts).
200
+ const found = readSessionMeta(dir);
201
+ if (found === 'unreadable')
202
+ throw new KeeperMethodError(AcpErrorCode.unreadable, 'This ACP session directory has unreadable metadata; it is left alone');
203
+ if (found !== 'absent') {
204
+ if (found.sessionKey !== sessionKey || await this.ownerAlive(found.owner))
205
+ throw new KeeperMethodError(AcpErrorCode.foreign, 'This ACP session belongs to another keeper-acp');
206
+ if (!this.sessions.has(sessionKey))
207
+ await this.adoptOne(dir, found);
208
+ const adopted = this.sessions.get(sessionKey);
209
+ if (baseOffset !== undefined)
210
+ bad('baseOffset only applies to a new session');
211
+ await this.settling.get(sessionKey);
212
+ return adopted.spawn(spec);
213
+ }
214
+ rmSync(dir, { recursive: true, force: true });
215
+ }
216
+ const meta = { v: 1, sessionKey, label, createdAtMs: this.now(), owner: { ...this.options.owner }, spawn: null, exited: false, removeOnExit: [], lastExit: null };
217
+ let session;
218
+ try {
219
+ privateDir(dir);
220
+ atomicJson(path.join(dir, SESSION_FILE), meta);
221
+ session = this.openSession(dir, meta, { fresh: true, baseOffset });
222
+ }
223
+ catch (error) {
224
+ rmSync(dir, { recursive: true, force: true });
225
+ throw error;
226
+ }
227
+ this.sessions.set(sessionKey, session);
228
+ this.dirs.set(sessionKey, dir);
229
+ try {
230
+ return await session.spawn(spec);
231
+ }
232
+ catch (error) {
233
+ // A new session whose first spawn failed leaves nothing behind.
234
+ if (this.sessions.get(sessionKey) === session) {
235
+ this.sessions.delete(sessionKey);
236
+ this.dirs.delete(sessionKey);
237
+ }
238
+ session.close();
239
+ rmSync(dir, { recursive: true, force: true });
240
+ throw error;
241
+ }
242
+ }
243
+ list() { return { sessions: [...this.sessions.values()].map((s) => s.summary()) }; }
244
+ attach(params) {
245
+ onlyKeys(params, ['sessionKey']);
246
+ return this.find(sessionKeyOf(params.sessionKey)).attach();
247
+ }
248
+ write(params) {
249
+ onlyKeys(params, ['sessionKey', 'spawnId', 'line', 'annotation', 'redact']);
250
+ const session = this.find(sessionKeyOf(params.sessionKey));
251
+ if (typeof params.spawnId !== 'string' || !params.spawnId)
252
+ bad('Invalid spawnId');
253
+ if (typeof params.line !== 'string')
254
+ bad('Invalid line');
255
+ return session.write(params.spawnId, params.line, annotationOf(params.annotation), redactionsOf(params.redact));
256
+ }
257
+ note(params) {
258
+ onlyKeys(params, ['sessionKey', 'note']);
259
+ return this.find(sessionKeyOf(params.sessionKey)).note(noteOf(params.note));
260
+ }
261
+ read(params) {
262
+ onlyKeys(params, ['sessionKey', 'stream', 'from', 'limit']);
263
+ const session = this.find(sessionKeyOf(params.sessionKey));
264
+ if (params.stream !== 'raw' && params.stream !== 'outbox')
265
+ bad('Invalid stream');
266
+ const limit = params.limit === undefined ? undefined : offsetOf(params.limit, 'limit', 1);
267
+ return session.read(params.stream, offsetOf(params.from, 'from'), limit);
268
+ }
269
+ downs(params) {
270
+ onlyKeys(params, ['sessionKey', 'from']);
271
+ return this.find(sessionKeyOf(params.sessionKey)).downsFrom(params.from === undefined ? 0 : offsetOf(params.from, 'from'));
272
+ }
273
+ outboxAppend(params) {
274
+ onlyKeys(params, ['sessionKey', 'entries']);
275
+ return this.find(sessionKeyOf(params.sessionKey)).outboxAppend(outboxEntriesOf(params.entries));
276
+ }
277
+ commit(params) {
278
+ onlyKeys(params, ['sessionKey', 'checkpoint', 'outbox', 'rawThrough']);
279
+ const session = this.find(sessionKeyOf(params.sessionKey));
280
+ const checkpoint = params.checkpoint;
281
+ const outbox = params.outbox;
282
+ if (checkpoint !== undefined && (typeof checkpoint !== 'object' || checkpoint === null || typeof checkpoint.blob !== 'string'))
283
+ bad('Invalid checkpoint');
284
+ if (outbox !== undefined && (typeof outbox !== 'object' || outbox === null || typeof outbox.tail !== 'object' || outbox.tail === null))
285
+ bad('Invalid outbox commit');
286
+ return session.commit({
287
+ ...(checkpoint ? { checkpoint: { offset: offsetOf(checkpoint.offset, 'checkpoint.offset', -1), blob: checkpoint.blob } } : {}),
288
+ ...(outbox ? { outbox: { through: offsetOf(outbox.through, 'outbox.through', -1), tail: outbox.tail } } : {}),
289
+ ...(params.rawThrough !== undefined ? { rawThrough: offsetOf(params.rawThrough, 'rawThrough', -1) } : {}),
290
+ });
291
+ }
292
+ async terminate(params) {
293
+ onlyKeys(params, ['sessionKey', 'graceMs']);
294
+ const session = this.find(sessionKeyOf(params.sessionKey));
295
+ const graceMs = params.graceMs === undefined ? undefined : Math.min(offsetOf(params.graceMs, 'graceMs'), 60_000);
296
+ return { exit: await session.terminate(graceMs) };
297
+ }
298
+ release(params) {
299
+ onlyKeys(params, ['sessionKey']);
300
+ const sessionKey = sessionKeyOf(params.sessionKey);
301
+ const session = this.find(sessionKey);
302
+ if (session.busy)
303
+ throw new KeeperMethodError(AcpErrorCode.busy, 'Terminate the process before releasing the session');
304
+ this.sessions.delete(sessionKey);
305
+ const dir = this.dirs.get(sessionKey);
306
+ this.dirs.delete(sessionKey);
307
+ session.close();
308
+ if (dir)
309
+ rmSync(dir, { recursive: true, force: true });
310
+ return { released: true };
311
+ }
312
+ source(stream) {
313
+ const parsed = parseAcpStream(stream);
314
+ return parsed ? this.sessions.get(parsed.sessionKey)?.source(parsed.stream) : undefined;
315
+ }
316
+ // ---- disk guard, activity, shutdown --------------------------------------------------------------------------------------------
317
+ async freeBytes() {
318
+ if (this.options.diskFree)
319
+ return this.options.diskFree();
320
+ return new Promise((resolve, reject) => statfs(this.sessionsDir, (error, stats) => (error ? reject(error) : resolve(Number(stats.bavail) * Number(stats.bsize)))));
321
+ }
322
+ /** Spec §3.4's hard limit: with less than 1 GiB free, adapter stdout is no longer read (the adapter blocks) until space returns. */
323
+ async checkDisk() {
324
+ let free;
325
+ try {
326
+ free = await this.freeBytes();
327
+ }
328
+ catch (error) {
329
+ this.report(error);
330
+ return;
331
+ }
332
+ const hold = free < ACP_MIN_FREE_DISK_BYTES;
333
+ if (hold === this.stdoutHeld)
334
+ return;
335
+ this.stdoutHeld = hold;
336
+ this.logLine(`acp: ${hold ? 'less than 1 GiB free: adapter stdout paused' : 'disk space back: adapter stdout resumed'}`);
337
+ for (const session of this.sessions.values())
338
+ session.holdStdout(hold);
339
+ }
340
+ /** Adapters this keeper runs now (any state but exited). */
341
+ get runningAdapters() { let count = 0; for (const session of this.sessions.values())
342
+ if (session.running)
343
+ count += 1; return count; }
344
+ /** Mechanical facts only (spec §3.5): sessions, running adapters, unanswered adapter requests; one item per session. */
345
+ activity() {
346
+ const items = [];
347
+ let running = 0;
348
+ let overQuota = 0;
349
+ let pendingInbound = 0;
350
+ for (const session of this.sessions.values()) {
351
+ const info = session.processInfo();
352
+ if (session.running)
353
+ running += 1;
354
+ if (session.overQuota)
355
+ overQuota += 1;
356
+ pendingInbound += session.pendingInbound;
357
+ const since = info.state === 'exited' ? (info.exit?.atMs ?? session.meta.createdAtMs) : info.state === 'none' ? session.meta.createdAtMs : info.sinceEpochMs;
358
+ items.push({ type: 'acp', id: session.sessionKey, label: labelText(session.meta.label), state: info.state, sinceEpochMs: since });
359
+ }
360
+ return { metrics: { sessions: this.sessions.size, sessionsRunning: running, sessionsOverQuota: overQuota, pendingInbound, stdoutPaused: this.stdoutHeld ? 1 : 0 }, items };
361
+ }
362
+ /** The keeper stops: every adapter is terminated (`keeper_stopped`; force: SIGKILL at once) and every stream closed. */
363
+ async dispose(mode = 'graceful') {
364
+ this.stopping = true;
365
+ clearInterval(this.diskTimer);
366
+ clearInterval(this.maintenanceTimer);
367
+ await Promise.all([...this.sessions.values()].map((s) => s.terminate(mode === 'force' ? 0 : 1500, 'keeper_stopped').catch((error) => this.report(error))));
368
+ for (const session of this.sessions.values())
369
+ session.close();
370
+ }
371
+ }
372
+ /**
373
+ * keeper-acp stops by itself once it has had no authenticated daemon connection AND no running adapter for this long (final review
374
+ * finding 4): a keeper an uninstalled or downgraded CLI left behind does not live forever. Any daemon connection, or any adapter
375
+ * running, restarts the clock.
376
+ */
377
+ export const ACP_IDLE_EXIT_MS = 24 * 60 * 60 * 1000;
378
+ /** How often the idle clock is looked at. */
379
+ export const ACP_IDLE_CHECK_MS = 60_000;
380
+ export function createAcpKeeper(host, options = {}) {
381
+ const now = options.now ?? Date.now;
382
+ let idleTimer;
383
+ return {
384
+ capabilities: [ACP_CAPABILITY],
385
+ install(server) {
386
+ // The idle self-exit: a graceful stop through the kit's stop path (shutdown, manifest and token removed, exit 0).
387
+ const idleExitMs = options.idleExitMs ?? ACP_IDLE_EXIT_MS;
388
+ let busyAt = now();
389
+ idleTimer = setInterval(() => {
390
+ if (server.authenticatedConnectionCount > 0 || host.runningAdapters > 0) {
391
+ busyAt = now();
392
+ return;
393
+ }
394
+ if (now() - busyAt < idleExitMs)
395
+ return;
396
+ clearInterval(idleTimer);
397
+ options.log?.(`acp: no daemon connection and no running adapter for ${Math.round(idleExitMs / 60_000)} min; stopping`);
398
+ server.stopped('graceful');
399
+ }, options.idleCheckMs ?? ACP_IDLE_CHECK_MS);
400
+ idleTimer.unref();
401
+ server.register('acp.spawn', (params) => host.spawn(params), { startsWork: true });
402
+ // A listing first adopts what a keeper that is gone left (an older generation the daemon just stopped, Task 21): the daemon sees
403
+ // those sessions at once instead of after the next periodic adoption.
404
+ server.register('acp.list', async () => { await host.adoptNow(); return host.list(); });
405
+ server.register('acp.attach', (params) => host.attach(params));
406
+ server.register('acp.write', (params) => host.write(params));
407
+ server.register('acp.note', (params) => host.note(params));
408
+ server.register('acp.read', (params) => host.read(params));
409
+ server.register('acp.downs', (params) => host.downs(params));
410
+ server.register('acp.outbox.append', (params) => host.outboxAppend(params));
411
+ server.register('acp.commit', (params) => host.commit(params));
412
+ server.register('acp.terminate', (params) => host.terminate(params));
413
+ server.register('acp.release', (params) => host.release(params));
414
+ server.setStreamResolver((stream) => host.source(stream));
415
+ server.addActivitySampler(() => host.activity());
416
+ },
417
+ shutdown: (mode) => { clearInterval(idleTimer); return host.dispose(mode); },
418
+ };
419
+ }
@@ -0,0 +1,18 @@
1
+ // keeper-acp: hosts ACP adapter processes so they survive daemon restarts, upgrades and crashes (spec §5.3). Started by the daemon's
2
+ // KeeperLauncher as `node <bundleDir>/keeper.mjs <launch config>`. It holds, per session, an ACP-agnostic ordered raw log, an outbox
3
+ // and checkpoint slots; it never interprets ACP beyond sniffing `id`/`method` for diagnostics.
4
+ // `scripts/bundle-keepers.mjs` reads `protocolVersion` below (the line must keep this exact shape) and `../daemonProtocol.json` for
5
+ // the daemon protocol range. It must equal ACP_PROTOCOL_VERSION (protocol.ts).
6
+ import { runKeeperMain } from '../kit/bootstrap.js';
7
+ import { AcpHost, createAcpKeeper } from './host.js';
8
+ export const protocolVersion = 1;
9
+ await runKeeperMain({
10
+ kind: 'acp',
11
+ protocolVersion,
12
+ create: async (config, { log }) => {
13
+ // Sessions live under the kind's data dir (shared by its generations; each session belongs to the keeper process that owns it).
14
+ const host = new AcpHost({ dataDir: config.dataDir, owner: { pid: process.pid, gen: config.gen, bundleDir: config.bundleDir }, log });
15
+ await host.open(); // start-up recovery: sessions whose keeper died are exited (keeper_lost) before anyone can ask
16
+ return createAcpKeeper(host, { log });
17
+ },
18
+ });