@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,222 @@
1
+ import { accessSync, constants, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { CREATE_KEYS, PTY_OPERATIONS } from '../keepers/terminal/pty.js';
5
+ import { PTY_SESSION_LIMIT } from '../keepers/terminal/protocol.js';
6
+ import { TERMINAL_NOT_FOUND } from '../keepers/legacy.js';
7
+ import { identifier, integer, keys } from './validation.js';
8
+ const LEGACY = 'legacy';
9
+ /** The single-keeper form of {@link KitGenerations}: one generation named `kit`, running when it answers a hello. */
10
+ function singleKit(kit) {
11
+ return {
12
+ servingGeneration: async () => ({ gen: 'kit', client: kit }),
13
+ running: async () => [{ gen: 'kit', client: kit }],
14
+ };
15
+ }
16
+ export class TerminalRouter {
17
+ deps;
18
+ owners = new Map();
19
+ kits;
20
+ /** Clients of the generations seen in the last listing, by generation. */
21
+ clients = new Map();
22
+ constructor(deps) {
23
+ this.deps = deps;
24
+ if (!deps.kits && !deps.kit)
25
+ throw new Error('TerminalRouter needs keeper-terminal');
26
+ this.kits = deps.kits ?? singleKit(deps.kit);
27
+ }
28
+ /** The current owner map (tests and diagnostics): `legacy`, or a generation (`kit` with a single keeper-terminal). */
29
+ ownerOf(id) { return this.owners.get(id); }
30
+ async request(method, params) {
31
+ const op = method.startsWith('terminal.') ? method.slice('terminal.'.length) : '';
32
+ if (op === 'list') {
33
+ keys(params, []);
34
+ return { sessions: await this.list() };
35
+ }
36
+ if (op === 'create')
37
+ return { ...await this.create(params) };
38
+ if (!PTY_OPERATIONS.has(op))
39
+ throw new Error('Unsupported workstation tool');
40
+ const id = identifier(params, 'id');
41
+ const owner = await this.resolveOwner(id);
42
+ if (!owner)
43
+ throw new Error(TERMINAL_NOT_FOUND);
44
+ const kit = owner === LEGACY ? undefined : this.clients.get(owner);
45
+ if (owner !== LEGACY && !kit)
46
+ throw new Error(TERMINAL_NOT_FOUND);
47
+ try {
48
+ const result = owner === LEGACY
49
+ ? await this.deps.legacy.request(method, params)
50
+ : { ...await kit[op].call(kit, params) };
51
+ if (op === 'close')
52
+ this.owners.delete(id);
53
+ return result;
54
+ }
55
+ catch (error) {
56
+ // The owner no longer has it (closed elsewhere, keeper restarted): forget it; the next request looks it up again.
57
+ if (error instanceof Error && error.message === TERMINAL_NOT_FOUND)
58
+ this.owners.delete(id);
59
+ throw error;
60
+ }
61
+ }
62
+ async list() {
63
+ const { legacy, kits } = await this.refresh();
64
+ const seen = new Set((legacy ?? []).map((session) => session.id));
65
+ const out = [...(legacy ?? [])];
66
+ for (const sessions of kits.values())
67
+ for (const session of sessions ?? [])
68
+ if (!seen.has(session.id)) {
69
+ seen.add(session.id);
70
+ out.push(session);
71
+ }
72
+ return out;
73
+ }
74
+ /** Lists every side once (keeper-terminal generations only if running) and refreshes the owners of every side that answered. */
75
+ async refresh() {
76
+ const [legacy, kits] = await Promise.all([this.legacySessions(), this.kitRefresh()]);
77
+ if (legacy)
78
+ this.replaceOwners(LEGACY, legacy);
79
+ else if (!this.deps.legacy?.live)
80
+ this.replaceOwners(LEGACY, []);
81
+ return { legacy, kits };
82
+ }
83
+ /**
84
+ * Lists every running keeper-terminal generation (never starts one) and refreshes their owners: a generation that answered gets
85
+ * exactly its sessions; one that did not answer keeps its owners; one that is no longer running loses them (its PTYs are gone).
86
+ */
87
+ async kitRefresh() {
88
+ const listed = new Map();
89
+ let running;
90
+ try {
91
+ running = await this.kits.running();
92
+ }
93
+ catch {
94
+ return listed;
95
+ }
96
+ await Promise.all(running.map(async ({ gen, client }) => {
97
+ this.clients.set(gen, client);
98
+ listed.set(gen, await this.sessionsOf(client));
99
+ }));
100
+ for (const [gen, sessions] of listed)
101
+ if (sessions)
102
+ this.replaceOwners(gen, sessions);
103
+ for (const [id, owner] of [...this.owners])
104
+ if (owner !== LEGACY && !listed.has(owner))
105
+ this.owners.delete(id);
106
+ for (const gen of [...this.clients.keys()])
107
+ if (!listed.has(gen))
108
+ this.clients.delete(gen);
109
+ return listed;
110
+ }
111
+ replaceOwners(side, sessions) {
112
+ for (const [id, owner] of this.owners)
113
+ if (owner === side)
114
+ this.owners.delete(id);
115
+ for (const session of sessions)
116
+ if (side === LEGACY || !this.owners.has(session.id))
117
+ this.owners.set(session.id, side);
118
+ }
119
+ /** The legacy keeper's sessions; `sessions()` probes it first (read-only), so a daemon that just started finds it too. */
120
+ async legacySessions() {
121
+ return this.deps.legacy ? this.deps.legacy.sessions() : null;
122
+ }
123
+ /** One generation's sessions if it is already running; null when it is not (or did not answer): nothing is started here. */
124
+ async sessionsOf(client) {
125
+ try {
126
+ if (!await client.runningHello())
127
+ return null;
128
+ return (await client.list()).sessions;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ async resolveOwner(id) {
135
+ const known = this.owners.get(id);
136
+ if ((known !== undefined && known !== LEGACY && this.clients.has(known)) || (known === LEGACY && this.deps.legacy?.live))
137
+ return known;
138
+ await this.refresh();
139
+ return this.owners.get(id);
140
+ }
141
+ async create(params) {
142
+ // The legacy keeper's checks, wording and order (tools/terminal.ts at the merge-base: id, clientId, keys, cols, rows, name,
143
+ // then existing, limit, cwd), all before the cwd is resolved.
144
+ const id = identifier(params, 'id');
145
+ identifier(params, 'clientId');
146
+ keys(params, [...CREATE_KEYS]);
147
+ integer(params, 'cols', 20, 300, 100);
148
+ integer(params, 'rows', 5, 100, 30);
149
+ const name = params.name;
150
+ if (name !== undefined && (typeof name !== 'string' || !name.length || name.length > 128 || /[\x00-\x1f]/.test(name)))
151
+ throw new Error('Invalid terminal name');
152
+ const legacy = this.deps.legacy;
153
+ const knownOwner = this.owners.get(id);
154
+ if (legacy && (knownOwner === undefined || knownOwner === LEGACY)) {
155
+ // The legacy keeper never gains a session after the upgrade (creates go to keeper-terminal), so the ids of its first
156
+ // successful list answer "is this a retry of one of its sessions?" for good — even while it is busy or hung.
157
+ if (legacy.knownIds === null) {
158
+ const sessions = await legacy.sessions(); // bounded; null at once when it is busy or absent
159
+ if (sessions)
160
+ this.replaceOwners(LEGACY, sessions);
161
+ }
162
+ if (legacy.live && legacy.knownIds?.has(id))
163
+ return legacy.request('terminal.create', params);
164
+ }
165
+ // Duplicate and limit first (the legacy order), against the keeper-terminal generations already running: an idempotent create
166
+ // (in whichever generation has the id) costs no files-host round trip. keeper-terminal re-checks both when it really creates.
167
+ const listed = await this.kitRefresh();
168
+ for (const [gen, sessions] of listed) {
169
+ const existing = sessions?.find((session) => session.id === id);
170
+ if (existing) {
171
+ this.owners.set(id, gen);
172
+ return existing;
173
+ }
174
+ }
175
+ const serving = await this.kits.servingGeneration();
176
+ const running = listed.get(serving.gen);
177
+ if (running && running.length >= PTY_SESSION_LIMIT)
178
+ throw new Error('Terminal session limit exceeded');
179
+ if (params.cwd !== undefined && typeof params.cwd !== 'string')
180
+ throw new Error('Invalid cwd');
181
+ const launch = await this.deps.launch(params.cwd);
182
+ const created = await serving.client.create({ ...params, ...launch });
183
+ this.clients.set(serving.gen, serving.client);
184
+ this.owners.set(id, serving.gen);
185
+ return created;
186
+ }
187
+ }
188
+ /**
189
+ * The shell a new PTY runs (as a login shell, `-l`): what the legacy keeper chose (`$SHELL`, else the account's shell, else
190
+ * `/bin/sh`), restricted to an absolute path to an executable file, which keeper-terminal requires.
191
+ */
192
+ export function terminalShell(env = process.env) {
193
+ let account;
194
+ try {
195
+ account = os.userInfo().shell ?? undefined;
196
+ }
197
+ catch { /* no passwd entry (some containers) */ }
198
+ for (const candidate of [env.SHELL, account, '/bin/sh']) {
199
+ if (!candidate || !path.isAbsolute(candidate))
200
+ continue;
201
+ try {
202
+ if (statSync(candidate).isFile()) {
203
+ accessSync(candidate, constants.X_OK);
204
+ return candidate;
205
+ }
206
+ }
207
+ catch { /* next */ }
208
+ }
209
+ return '/bin/sh';
210
+ }
211
+ /**
212
+ * The environment a new PTY gets: the daemon's own (the legacy keeper gave its own process.env, which was the daemon's when it
213
+ * was spawned), never the access token (commands/workstation.ts already drops it from the daemon; dropped again here so a
214
+ * future spawn path cannot leak it). keeper-terminal adds TERM and COLORTERM.
215
+ */
216
+ export function terminalEnv(env = process.env) {
217
+ const out = {};
218
+ for (const [name, value] of Object.entries(env))
219
+ if (value !== undefined && name !== 'AXOM_ACCESS_TOKEN')
220
+ out[name] = value;
221
+ return out;
222
+ }
@@ -6,7 +6,9 @@ export const TOOL_METHODS = new Set([
6
6
  'browser.list', 'browser.create', 'browser.attach', 'browser.frame', 'browser.navigate', 'browser.input', 'browser.takeover', 'browser.detach', 'browser.close',
7
7
  'tools.info', 'terminal.list', 'terminal.create', 'terminal.attach', 'terminal.poll',
8
8
  'terminal.write', 'terminal.resize', 'terminal.takeover', 'terminal.detach', 'terminal.close',
9
- 'files.list', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
9
+ 'files.media.prepare', 'files.media.read', 'files.media.cancel',
10
+ 'files.media.open', 'files.media.status', 'files.media.seek', 'files.media.interest', 'files.media.object', 'files.media.release',
11
+ 'files.list', 'files.find', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
10
12
  'files.upload.begin', 'files.upload.chunk', 'files.upload.finish', 'files.upload.cancel',
11
13
  ]);
12
14
  export function object(value) {
@@ -47,4 +49,38 @@ export function validateRequest(method, params) {
47
49
  throw new Error('Unsupported workstation tool');
48
50
  if (!object(params) || Buffer.byteLength(JSON.stringify(params)) > TOOL_REQUEST_BYTES)
49
51
  throw new Error('Invalid or oversized tool parameters');
52
+ const shape = MEDIA_SHAPES[method];
53
+ if (shape) {
54
+ keys(params, [...shape, 'readOnly', 'filesystem']);
55
+ string(params, 'path');
56
+ identifier(params, 'consumerId');
57
+ if (method !== 'files.media.open' && !/^[a-f0-9]{64}$/.test(string(params, 'version', 64)))
58
+ throw new Error('Invalid version');
59
+ for (const flag of ['readOnly', 'filesystem', 'hls', 'playing']) {
60
+ if (params[flag] !== undefined && typeof params[flag] !== 'boolean')
61
+ throw new Error(`Invalid ${flag}`);
62
+ }
63
+ if (method === 'files.media.open' && typeof params.hls !== 'boolean')
64
+ throw new Error('Invalid hls');
65
+ if (method === 'files.media.seek' || method === 'files.media.interest')
66
+ integer(params, 'positionMs', 0, 21_600_000);
67
+ if (method === 'files.media.seek')
68
+ integer(params, 'revision', 0, Number.MAX_SAFE_INTEGER);
69
+ if (method === 'files.media.interest' && typeof params.playing !== 'boolean')
70
+ throw new Error('Invalid playing');
71
+ if (method === 'files.media.object') {
72
+ if (!/^[A-Za-z0-9_-]+$/.test(string(params, 'objectId', 128)))
73
+ throw new Error('Invalid objectId');
74
+ integer(params, 'offset', 0, Number.MAX_SAFE_INTEGER);
75
+ integer(params, 'length', 1, 256 * 1024);
76
+ }
77
+ }
50
78
  }
79
+ const MEDIA_SHAPES = {
80
+ 'files.media.open': ['path', 'consumerId', 'hls'],
81
+ 'files.media.status': ['path', 'version', 'consumerId'],
82
+ 'files.media.seek': ['path', 'version', 'consumerId', 'positionMs', 'revision'],
83
+ 'files.media.interest': ['path', 'version', 'consumerId', 'positionMs', 'playing'],
84
+ 'files.media.object': ['path', 'version', 'consumerId', 'objectId', 'offset', 'length'],
85
+ 'files.media.release': ['path', 'version', 'consumerId'],
86
+ };
@@ -253,6 +253,9 @@ export class UpdateCoordinator {
253
253
  this.options.tasks.accepting = true;
254
254
  return this.transition(this.base(command, 'SUCCEEDED'));
255
255
  }
256
+ // Keeper-hosted agent sessions outlive this process: detach them first (head checkpoint, adapter untouched — no
257
+ // `session/cancel`, no failed turn), so the next daemon adopts them where they are (spec §5.5.5).
258
+ await this.options.tasks.detachDurableSessions();
256
259
  await this.options.restart();
257
260
  return restarting;
258
261
  }
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
+ import { PTY_UNAVAILABLE_ERROR } from '../keepers/terminal/protocol.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  /**
5
6
  * Says once, at daemon start, when the installed CLI's optional `node-pty` does not load. There is
@@ -22,3 +23,23 @@ export async function preflightOptionalPty(context, deps = {}) {
22
23
  (deps.warn ?? console.warn)('[daemon] Optional node-pty does not load on this platform; the terminal tool stays unavailable.');
23
24
  }
24
25
  }
26
+ export { PTY_UNAVAILABLE_ERROR };
27
+ /**
28
+ * A cached load check of the node-pty copy the terminal would use (the same `require('node-pty')` from this package),
29
+ * done at most once per daemon. Answering `tools.info` must never start a keeper just to learn this.
30
+ */
31
+ export function cachedPtyStatus(load = () => require('node-pty')) {
32
+ let status;
33
+ return () => {
34
+ if (status)
35
+ return status;
36
+ try {
37
+ load();
38
+ status = { terminal: true };
39
+ }
40
+ catch {
41
+ status = { terminal: false, terminalError: PTY_UNAVAILABLE_ERROR };
42
+ }
43
+ return status;
44
+ };
45
+ }
@@ -10,6 +10,14 @@ export class TaskRegistry {
10
10
  idleWaiters = new Set();
11
11
  stopping = null;
12
12
  accepting = true;
13
+ /** Keeper-terminal execs also counted in `activeShellTasks` right now (see {@link countKeeperExec}). */
14
+ keeperExecsCounted = 0;
15
+ /**
16
+ * Work owned by another process that a daemon restart would interrupt (the files host's in-progress uploads plus
17
+ * running media encodes), sampled **now** on every snapshot — never an edge counter. Its owner calls
18
+ * [externalActivityChanged] when the level may have dropped, so a drain waiting on it wakes.
19
+ */
20
+ externalActivity = () => 0;
13
21
  /**
14
22
  * 忙碌是**现在采出来的**,不是攒出来的。
15
23
  *
@@ -24,6 +32,11 @@ export class TaskRegistry {
24
32
  snapshot() {
25
33
  const activeAgentRefs = new Set();
26
34
  for (const [sessionRef, session] of this.agentSessions) {
35
+ // Spec §8.2: `activeAgentTasks` counts only agent work a daemon restart would cut. A keeper-hosted (durable) session is not
36
+ // such work — its turn goes on in keeper-acp and the next daemon adopts it — so a busy one neither holds a graceful drain nor
37
+ // blocks an automatic patch. Fallback sessions (in this process) and in-flight open reservations are counted.
38
+ if (session.durable)
39
+ continue;
27
40
  // 一个抛异常的采样器不得让心跳和更新准入一起垮掉;读不出来就当它不主张忙碌,
28
41
  // 与未知 vendor 值的处置同一条推理。
29
42
  let busy = false;
@@ -38,7 +51,16 @@ export class TaskRegistry {
38
51
  }
39
52
  for (const sessionRef of this.agentOpenReservations.keys())
40
53
  activeAgentRefs.add(sessionRef);
41
- return { activeShellTasks: this.shellTasks.size, activeAgentTasks: activeAgentRefs.size };
54
+ let external = 0;
55
+ // Same rule as the session sampler above: a failing sampler claims nothing rather than taking the heartbeat down.
56
+ try {
57
+ const sampled = this.externalActivity();
58
+ external = Number.isSafeInteger(sampled) && sampled > 0 ? sampled : 0;
59
+ }
60
+ catch {
61
+ external = 0;
62
+ }
63
+ return { activeShellTasks: this.shellTasks.size + external, activeAgentTasks: activeAgentRefs.size };
42
64
  }
43
65
  canUpdate() {
44
66
  const snapshot = this.snapshot();
@@ -79,7 +101,11 @@ export class TaskRegistry {
79
101
  for (const waiter of [...this.idleWaiters])
80
102
  waiter();
81
103
  }
82
- /** Run one computer task under an abort signal which force-update owns. The method name preserves old callers. */
104
+ /**
105
+ * Run one computer task under an abort signal which force-update owns. The method name preserves old callers.
106
+ * `release()` stops counting the task before it ends (work a daemon restart would no longer interrupt, spec §8.2): from then on it
107
+ * is not in `activeShellTasks`, a drain does not wait for it and `stopAll()` does not abort it. Idempotent.
108
+ */
83
109
  async withShellTask(run) {
84
110
  if (!this.accepting)
85
111
  throw new Error('Workstation is not accepting new work');
@@ -87,18 +113,61 @@ export class TaskRegistry {
87
113
  let complete;
88
114
  const cleanedUp = new Promise((resolve) => { complete = resolve; });
89
115
  this.shellTasks.set(controller, cleanedUp);
116
+ let counted = true;
117
+ const release = () => {
118
+ if (!counted)
119
+ return;
120
+ counted = false;
121
+ this.shellTasks.delete(controller);
122
+ complete();
123
+ this.notifyIfIdle();
124
+ };
90
125
  try {
91
- return await run(controller.signal);
126
+ return await run(controller.signal, release);
92
127
  }
93
128
  finally {
129
+ release();
130
+ }
131
+ }
132
+ /**
133
+ * Counts a short window of work in `activeShellTasks` WITHOUT the admission gate and without being stoppable: a drain (graceful
134
+ * or forced) waits for it, nothing aborts it. For work admitted while draining that a restart must not cut in half (a wire v5
135
+ * exec between `exec.start` and its first checkpoint, spec §8.2). Returns the release (idempotent).
136
+ */
137
+ holdShellTask() {
138
+ const controller = new AbortController();
139
+ let complete;
140
+ this.shellTasks.set(controller, new Promise((resolve) => { complete = resolve; }));
141
+ let held = true;
142
+ return () => {
143
+ if (!held)
144
+ return;
145
+ held = false;
94
146
  this.shellTasks.delete(controller);
95
147
  complete();
96
148
  this.notifyIfIdle();
97
- }
149
+ };
98
150
  }
99
- /** Register a live native agent for force-stop, plus how to ask whether it is busy right now. */
100
- registerAgentSession(sessionRef, close, busy) {
101
- this.agentSessions.set(sessionRef, { close, busy });
151
+ /**
152
+ * Marks one exec that runs in keeper-terminal AND is counted in `activeShellTasks` right now — a wire ≤ 4 exec (a shell task for
153
+ * its whole run) or a wire v5 exec before its first checkpoint (held). keeper-terminal's `execRunning` counts it too: the heartbeat
154
+ * reports this number (`execCounted`) so the Workstations page does not count it twice (final review minor 7). Returns the
155
+ * release (idempotent).
156
+ */
157
+ countKeeperExec() {
158
+ this.keeperExecsCounted += 1;
159
+ let counted = true;
160
+ return () => { if (!counted)
161
+ return; counted = false; this.keeperExecsCounted -= 1; };
162
+ }
163
+ /** How many keeper-terminal execs `activeShellTasks` includes now (see {@link countKeeperExec}). */
164
+ get countedKeeperExecs() { return this.keeperExecsCounted; }
165
+ /**
166
+ * Register a live native agent for force-stop, plus how to ask whether it is busy right now. A durable session's `close` must be
167
+ * its detach (see {@link AgentSessionRegistration}).
168
+ */
169
+ registerAgentSession(sessionRef, close, busy, registration = {}) {
170
+ this.agentSessions.set(sessionRef, { close, busy, durable: registration.durable === true });
102
171
  }
103
172
  /**
104
173
  * 采样式判据没有"变空闲"这个事件可以挂钩,所以持有者在可能变空闲时喊一声。
@@ -107,6 +176,10 @@ export class TaskRegistry {
107
176
  sessionBusyChanged() {
108
177
  this.notifyIfIdle();
109
178
  }
179
+ /** The external activity level may have changed (see [externalActivity]). */
180
+ externalActivityChanged() {
181
+ this.notifyIfIdle();
182
+ }
110
183
  /** Atomically admits and counts an asynchronous open before any journal or subprocess await. */
111
184
  tryReserveAgentOpen(sessionRef) {
112
185
  if (!this.accepting)
@@ -126,6 +199,10 @@ export class TaskRegistry {
126
199
  isForceStopping() {
127
200
  return this.stopping !== null;
128
201
  }
202
+ /** Whether `sessionRef` is registered as a keeper-hosted (durable) session: a force stop leaves it admitted (see {@link stopAll}). */
203
+ isDurableSession(sessionRef) {
204
+ return this.agentSessions.get(sessionRef)?.durable === true;
205
+ }
129
206
  unregisterAgentSession(sessionRef) {
130
207
  this.agentSessions.delete(sessionRef);
131
208
  this.notifyIfIdle();
@@ -134,12 +211,29 @@ export class TaskRegistry {
134
211
  * 排干期间拒收新回合。**只管准入**——它不再登记任何东西,忙碌由 [snapshot] 现场采样。
135
212
  * 这两件事以前挤在一个方法里,删计数时极易把准入一起删掉,那会让 drain 期间照收新活。
136
213
  */
137
- tryAdmitTurn(_sessionRef) {
138
- return this.accepting;
214
+ tryAdmitTurn(sessionRef) {
215
+ // Spec §8.2: a keeper-hosted session's prompt is admitted while draining — it enters keeper-acp's log first and a restart
216
+ // continues it; a fallback session's would be cut by the restart.
217
+ return this.accepting || this.agentSessions.get(sessionRef)?.durable === true;
218
+ }
219
+ /**
220
+ * The restart path of an update (Task 21, spec §5.5.5): every durable session is detached (its registered close), so the next daemon
221
+ * adopts it with a head checkpoint; fallback sessions are left as they are (they end with this process, their outboxes kept).
222
+ * Bounded by `timeoutMs`: a restart never waits on a session longer than that. All are attempted even when one fails.
223
+ */
224
+ async detachDurableSessions(timeoutMs = 5_000) {
225
+ const durable = [...this.agentSessions.values()].filter((session) => session.durable);
226
+ const all = Promise.allSettled(durable.map(async (session) => session.close()));
227
+ let timer;
228
+ await Promise.race([all, new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); })]);
229
+ clearTimeout(timer);
139
230
  }
140
231
  /**
141
232
  * Atomically refuse future work, signal every shell process group through its owning executor,
142
- * and close every native agent. All closes are attempted even when one bridge fails.
233
+ * and close every native agent that runs in this process. A durable (keeper-hosted) session is left alone: it stays registered,
234
+ * attached and admitted (spec §8.2, the same as a graceful drain) — the restart path detaches it right before the hand-off
235
+ * ({@link detachDurableSessions}), and a forced update that fails before that leaves it exactly as it was. All closes are
236
+ * attempted even when one bridge fails.
143
237
  */
144
238
  async stopAll() {
145
239
  if (this.stopping)
@@ -154,7 +248,7 @@ export class TaskRegistry {
154
248
  this.agentOpenReservations.clear();
155
249
  this.notifyIfIdle();
156
250
  const [outcomes] = await Promise.all([
157
- Promise.allSettled([...this.agentSessions.values()].map(async (session) => session.close())),
251
+ Promise.allSettled([...this.agentSessions.values()].filter((session) => !session.durable).map(async (session) => session.close())),
158
252
  Promise.all(shells.map(([, cleanedUp]) => cleanedUp)),
159
253
  ]);
160
254
  const failures = outcomes.filter((outcome) => outcome.status === 'rejected');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platless/axom-cli",
3
- "version": "0.3.13",
3
+ "version": "0.5.9",
4
4
  "description": "Axomwork CLI - manage Axomwork structures, records and executions over MCP",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,33 +11,36 @@
11
11
  "skills"
12
12
  ],
13
13
  "engines": {
14
- "node": ">=20"
14
+ "node": ">=24"
15
15
  },
16
16
  "scripts": {
17
- "build": "rm -rf dist && tsc && node scripts/copy-patches.mjs",
17
+ "build": "rm -rf dist && tsc && node scripts/copy-patches.mjs && node scripts/copy-runtime-deps.mjs && node scripts/bundle-keepers.mjs",
18
18
  "pretest": "npm run build",
19
19
  "test": "vitest run",
20
20
  "typecheck": "tsc --noEmit",
21
- "prepack": "node scripts/copy-patches.mjs --check"
21
+ "prepack": "node scripts/copy-patches.mjs --check && node scripts/copy-runtime-deps.mjs --check && node scripts/bundle-keepers.mjs --check"
22
22
  },
23
23
  "dependencies": {
24
- "@agentclientprotocol/sdk": "^1.4.0",
24
+ "@agentclientprotocol/sdk": "1.4.0",
25
25
  "@modelcontextprotocol/sdk": "^1.29.0",
26
26
  "@napi-rs/keyring": "^1.1.3",
27
- "@qoder-ai/qoder-agent-sdk": "1.0.50",
28
27
  "@xterm/addon-serialize": "^0.14.0",
29
28
  "@xterm/headless": "^6.0.0",
30
29
  "commander": "^15.0.0",
31
30
  "js-yaml": "^4.1.0",
31
+ "proper-lockfile": "4.1.2",
32
32
  "semver": "^7.8.5",
33
33
  "ws": "^8.18.0",
34
34
  "zod": "^4.4.3"
35
35
  },
36
36
  "devDependencies": {
37
+ "@qoder-ai/qoder-agent-sdk": "1.0.50",
37
38
  "@types/js-yaml": "^4.0.9",
38
- "@types/node": "^20.11.0",
39
+ "@types/node": "^24.19.2",
40
+ "@types/proper-lockfile": "4.1.4",
39
41
  "@types/semver": "^7.8.0",
40
42
  "@types/ws": "^8.5.13",
43
+ "esbuild": "0.27.7",
41
44
  "typescript": "^5.2.2",
42
45
  "vitest": "^2.1.9"
43
46
  },