@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,47 @@
1
+ /** Bounded, plain-JS wire validation: usable inside the deterministic ACP processor. */
2
+ export const ACCOUNT_LIMITS_READ_TIMEOUT_MS = 180_000;
3
+ const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
4
+ const text = (v, min = 0, max = 160) => typeof v === 'string' && v.length >= min && v.length <= max;
5
+ const time = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(v) && Number.isFinite(new Date(v).getTime());
6
+ const oneOf = (v, values) => typeof v === 'string' && values.includes(v);
7
+ function parse(value) {
8
+ const valid = object(value) && value.schemaVersion === 1 && text(value.provider, 1, 64)
9
+ && (value.updateMode === undefined || oneOf(value.updateMode, ['snapshot', 'delta']))
10
+ && oneOf(value.billingMode, ['subscription', 'non_subscription', 'unknown'])
11
+ && oneOf(value.availability, ['available', 'unavailable', 'unknown', 'not_applicable']) && time(value.observedAt)
12
+ && (value.plan === null || object(value.plan) && text(value.plan.id) && text(value.plan.label))
13
+ && (value.accountKey === undefined || text(value.accountKey, 0, 128))
14
+ && Array.isArray(value.limits) && value.limits.length <= 32 && value.limits.every(row => object(row)
15
+ && text(row.id, 1) && text(row.label) && oneOf(row.kind, ['subscription', 'spend', 'rate'])
16
+ && (row.windowSeconds === null || typeof row.windowSeconds === 'number' && Number.isInteger(row.windowSeconds) && row.windowSeconds > 0)
17
+ && (row.period == null || text(row.period))
18
+ && (row.key === undefined || row.key === null || text(row.key, 1, 64))
19
+ && (row.usedPercent === null || typeof row.usedPercent === 'number' && Number.isFinite(row.usedPercent) && row.usedPercent >= 0 && row.usedPercent <= 100)
20
+ && (row.resetsAt === null || time(row.resetsAt)) && oneOf(row.status, ['allowed', 'warning', 'blocked', 'unknown'])
21
+ && (row.active == null || typeof row.active === 'boolean') && (row.scope == null || object(row.scope))
22
+ && ['used', 'limit', 'unit', 'currency'].every(key => row[key] === undefined || text(row[key]))
23
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
24
+ && new Set(value.limits.map(row => row.id)).size === value.limits.length
25
+ && Array.isArray(value.balances) && value.balances.length <= 16 && value.balances.every(row => object(row)
26
+ && text(row.id, 1) && text(row.label) && text(row.unit) && (row.remaining === null || text(row.remaining))
27
+ && (row.currency === undefined || text(row.currency)) && (row.unlimited === undefined || typeof row.unlimited === 'boolean')
28
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
29
+ && new Set(value.balances.map(row => row.id)).size === value.balances.length
30
+ && (value._meta === undefined || object(value._meta)) && JSON.stringify(value).length <= 65_536;
31
+ if (!valid)
32
+ throw new Error('Invalid account quota snapshot');
33
+ return { ...value, _meta: value._meta ?? {} };
34
+ }
35
+ export const accountLimitsSchema = {
36
+ parse,
37
+ safeParse(value) {
38
+ try {
39
+ return { success: true, data: parse(value) };
40
+ }
41
+ catch {
42
+ return { success: false, error: new Error('Invalid account quota snapshot') };
43
+ }
44
+ },
45
+ };
46
+ export const ACCOUNT_LIMITS_READ = '_axom/account/rateLimits/read';
47
+ export const ACCOUNT_LIMITS_UPDATED = '_axom/account/rateLimits/updated';
@@ -1,7 +1,15 @@
1
+ import { accountLimitsSchema } from './accountLimits.js';
1
2
  import { isRuntimeDiagnostics } from '../qoderAdapter/profile.js';
2
3
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
3
4
  /** Current version and feature floors for the independently negotiated agent protocol. */
4
- export const AGENT_WIRE_VERSION = 24;
5
+ export const AGENT_WIRE_VERSION = 27;
6
+ /**
7
+ * `agent_switch_idle` / `agent_switch_idle_result` (spec 2026-10-11 §6.4, Task 25): "move idle sessions" of one agent to another
8
+ * adapter build. A server-to-client request: the server sends it only to a client advertising this version (an older one has no
9
+ * case for it and the request would only die at its timeout).
10
+ */
11
+ export const AGENT_WIRE_ACCOUNT_LIMITS_VERSION = 27;
12
+ export const AGENT_WIRE_SWITCH_IDLE_VERSION = 26;
5
13
  /** `agent_delete_native` / `agent_native_deleted`: purge a session Axom created on an external agent. */
6
14
  export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
7
15
  export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
@@ -207,7 +215,7 @@ export const AGENT_WIRE_ELICITATION_VERSION = 16;
207
215
  *
208
216
  * Declarative on purpose: the required-field half of this table is the *only* statement anywhere of
209
217
  * which fields the Kotlin twin will refuse to decode without, and the contract suite
210
- * (`acp/contract.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
218
+ * (`protocol/agentRequiredFields.test.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
211
219
  * enforced. Left as inline `hasRequired({...})` calls inside a `switch`, that list is unreadable from
212
220
  * a test, so the only way to check it would be a hand-maintained copy in the test file — which is
213
221
  * precisely how `agent_authenticate.methodId` came to have no guard at all while a report still
@@ -229,6 +237,7 @@ export const AGENT_FRAME_SHAPES = {
229
237
  optional: { sessions: (sessions) => Array.isArray(sessions) && sessions.every(isAgentResumeSession) },
230
238
  },
231
239
  heartbeat_ack: { required: { seq: isNumber } },
240
+ agent_reopen_request: { required: { sessionRef: isString, reason: isString } },
232
241
  agent_open: {
233
242
  required: {
234
243
  id: isString, sessionRef: isString, agentKey: isString, bridge: isString, cwd: isString, shell: isString,
@@ -272,6 +281,8 @@ export const AGENT_FRAME_SHAPES = {
272
281
  // Vendor-verbatim and uninterpreted here (decision L1) -- any object shape is accepted, the
273
282
  // same lenience `agent_event.payload` gets via `isRecord`.
274
283
  activityRaw: isRecord,
284
+ // Task 21 (spec §5.5.4): `true`-or-absent, and the two id lists.
285
+ durable: isVerifiedTrue, pendingRequestIds: isStringArray, steeredIds: isStringArray,
275
286
  },
276
287
  },
277
288
  agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
@@ -319,6 +330,9 @@ export const AGENT_FRAME_SHAPES = {
319
330
  },
320
331
  // Same lenience as `agent_list_native` above, for the same reason: the session manager wants to
321
332
  // answer a malformed query with its own `ok: false` rather than have the transport drop it.
333
+ agent_account_read: { required: { id: isString, bridge: isString, agentKey: isString, query: () => true } },
334
+ agent_account_read_result: { required: { id: isString, ok: isBoolean }, optional: { snapshot: (value) => accountLimitsSchema.safeParse(value).success, error: isString } },
335
+ agent_account_update: { required: { agentKey: isString, snapshot: (value) => accountLimitsSchema.safeParse(value).success } },
322
336
  agent_verify: {
323
337
  required: { id: isString, bridge: isString, query: () => true },
324
338
  // 与 `query` 那条**刻意的宽松**相反,这里是严格的:一份畸形的 query 还有个能回
@@ -398,8 +412,35 @@ export const AGENT_FRAME_SHAPES = {
398
412
  agent_provider_task_stopped: { required: { id: isTaskId, state: isProviderStopState },
399
413
  optional: { error: isTaskSummary } },
400
414
  agent_async_task_stop: { required: { sessionRef: isString, asyncTaskId: isString } },
415
+ agent_switch_idle: { required: { id: isString, bridge: isString }, optional: { toBuild: isString } },
416
+ agent_switch_idle_result: {
417
+ required: { id: isString, ok: isBoolean },
418
+ optional: { registered: isCount, immediate: isCount, unmovable: isSwitchIdleUnmovableArray, error: isNullableString },
419
+ },
401
420
  };
402
421
  const DISCRIMINATORS = new Set(Object.keys(AGENT_FRAME_SHAPES));
422
+ /**
423
+ * `agent_reopen_request{sessionRef, reason}` (spec 2026-10-11 §6.3, C→S): a session that is idle and has a pending switch (an adapter
424
+ * update, a keeper-acp generation change) asks the server to send `agent_open{requireSessionLoad:true}` with the session's fresh
425
+ * environment — a switch needs the spawn secrets only an `agent_open` carries. Task 24 decides when; the manager's
426
+ * `requestReopen` sends it, and only to a server advertising {@link AGENT_WIRE_REOPEN_REQUEST_VERSION}: an older server cannot
427
+ * decode it, and eight undecodable frames in a row close the agent channel (spec §10: there the switch waits for a natural reopen).
428
+ */
429
+ export const AGENT_WIRE_REOPEN_REQUEST_VERSION = 25;
430
+ /**
431
+ * The same version (Task 22): the server acknowledges as soon as a subagent relation releases the watermark it capped, and stops
432
+ * wasting its 32-event ack count on capped attempts — so a sent relation-pinned outbox entry may count against the sending
433
+ * window again (`SessionOutbox.inflight`). Below it the entries keep not counting once sent (fix round 1 of Task 20).
434
+ */
435
+ export const AGENT_WIRE_RELATION_ACK_VERSION = 25;
436
+ export const AGENT_REOPEN_REQUEST_SHAPE = AGENT_FRAME_SHAPES.agent_reopen_request;
437
+ export function agentReopenRequest(sessionRef, reason) {
438
+ return { t: 'agent_reopen_request', sessionRef, reason };
439
+ }
440
+ function isCount(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
441
+ function isSwitchIdleUnmovableArray(value) {
442
+ return Array.isArray(value) && value.every((entry) => isObject(entry) && isString(entry.sessionRef) && isString(entry.reason));
443
+ }
403
444
  function isRecord(value) { return isObject(value); }
404
445
  function isTaskId(value) {
405
446
  return typeof value === 'string' && value.length > 0 &&
@@ -422,12 +463,13 @@ function isProviderStopState(value) {
422
463
  }
423
464
  function isAgentResumeSession(value) {
424
465
  return isObject(value)
425
- && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean });
466
+ && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean })
467
+ && (value.pendingRequestIds === undefined || isStringArray(value.pendingRequestIds));
426
468
  }
427
469
  /**
428
470
  * Accepts only the two outcomes this client can produce. `startedNewTurn` is refused on purpose: it
429
471
  * is the extension's third outcome, and receiving one means a peer invented a disposition for a
430
- * message the server has already put on its timeline — see `acpSession.ts`'s `mapSteerOutcome`.
472
+ * message the server has already put on its timeline — see `acp/steering.ts`'s `mapSteerOutcome`.
431
473
  */
432
474
  function isSteerOutcome(value) { return value === 'injected' || value === 'promptRequired'; }
433
475
  /** Accepts `true` and nothing else — the whole point of `authenticated` having no `false`. */
@@ -729,8 +771,10 @@ export function outboundFailureReply(frame, reason) {
729
771
  case 'agent_authenticated': return { t: 'agent_authenticated', id, ok: false, error };
730
772
  case 'agent_installed': return { t: 'agent_installed', id, ok: false, error };
731
773
  case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
774
+ case 'agent_account_read_result': return { t: 'agent_account_read_result', id, ok: false, error };
732
775
  case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
733
776
  case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
777
+ case 'agent_switch_idle_result': return { t: 'agent_switch_idle_result', id, ok: false, error };
734
778
  case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
735
779
  case 'agent_backfill_done':
736
780
  case 'agent_backfill_events': return { t: 'agent_backfill_done', id, ok: false, count: 0, error };
@@ -26,6 +26,18 @@ export const isNullableAuthMethodInfoArray = (value) => value === null || (Array
26
26
  export function isMissingPrerequisite(value) {
27
27
  return isObject(value) && hasRequired(value, { label: isString }) && hasOptional(value, { hint: isString });
28
28
  }
29
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
30
+ export function isBridgeCurrentBuild(value) {
31
+ return isObject(value) && hasRequired(value, { id: isString, version: isString }) && hasOptional(value, { patched: isTrueOnly });
32
+ }
33
+ export function isBridgeBuild(value) {
34
+ return isObject(value)
35
+ && hasRequired(value, { id: isString, version: isString, sessions: isCount, idle: isCount, busy: isCount, unmovable: isCount })
36
+ && hasOptional(value, { patched: isTrueOnly, current: isTrueOnly, busyReasons: isStringArray, unmovableReasons: isStringArray });
37
+ }
38
+ export function isBridgeSwitchPending(value) {
39
+ return isObject(value) && hasRequired(value, { remaining: isCount }) && hasOptional(value, { toBuild: isString });
40
+ }
29
41
  export function isBridgeInfo(value) {
30
42
  return isObject(value)
31
43
  && hasRequired(value, { type: isString })
@@ -66,6 +78,11 @@ export function isBridgeInfo(value) {
66
78
  // A peer that sent one would be publishing "we looked and it cannot steer" through a key
67
79
  // whose absence already says exactly that, so refusing it keeps one spelling per fact.
68
80
  supportsSteering: isTrueOnly,
81
+ supportsAccountLimits: isTrueOnly,
82
+ // Task 25 (spec §6.4): scan-time build data. Optional: absent is "not looked at".
83
+ currentBuild: isBridgeCurrentBuild,
84
+ builds: (value) => Array.isArray(value) && value.every(isBridgeBuild),
85
+ switchPending: isBridgeSwitchPending,
69
86
  });
70
87
  }
71
88
  export function isBridgeInfoArray(value) {
@@ -103,6 +120,19 @@ export function bridgesForWire(bridges, frame) {
103
120
  }
104
121
  return kept;
105
122
  }
123
+ /**
124
+ * The roster as a hello carries it: without the scan-only build fields (`currentBuild`/`builds`/`switchPending`, spec §6.4). The
125
+ * daemon keeps one cached roster, which a scan refreshes with those fields; a later reconnect's hello would otherwise carry a
126
+ * snapshot of sessions and pending moves that is stale by then. Only the scan answers them, fresh.
127
+ */
128
+ export function helloRoster(bridges) {
129
+ return bridges.map((bridge) => {
130
+ if (bridge.currentBuild === undefined && bridge.builds === undefined && bridge.switchPending === undefined)
131
+ return bridge;
132
+ const { currentBuild: _current, builds: _builds, switchPending: _pending, ...rest } = bridge;
133
+ return rest;
134
+ });
135
+ }
106
136
  function partitionWireSafeBridges(bridges) {
107
137
  let parsed;
108
138
  try {
@@ -6,6 +6,7 @@ const DISCRIMINATORS = new Set([
6
6
  'desktop_capability', 'desktop_open', 'desktop_opened', 'desktop_data', 'desktop_credit', 'desktop_close',
7
7
  'computer_hello', 'computer_ready', 'heartbeat', 'heartbeat_ack',
8
8
  'exec_request', 'exec_output', 'exec_exit', 'exec_cancel', 'tool_request', 'tool_result',
9
+ 'exec_started', 'exec_ack', 'exec_attach', 'exec_status',
9
10
  'file_read_request', 'file_read_result', 'file_write_request', 'file_write_result',
10
11
  'file_ls_request', 'file_ls_result', 'file_mkdir_request', 'file_mkdir_result',
11
12
  ]);
@@ -16,6 +17,9 @@ const fileErrorCodes = new Set([
16
17
  ]);
17
18
  const isNonnegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
18
19
  const isPositiveInteger = (value) => Number.isSafeInteger(value) && value >= 1;
20
+ /** A record offset, or -1 for "none" (exec_status / exec_ack). */
21
+ const isOffsetOrNone = (value) => Number.isSafeInteger(value) && value >= -1;
22
+ const EXEC_STATES = new Set(['running', 'exited', 'unknown']);
19
23
  const isSha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
20
24
  const onlyKeys = (value, keys) => Object.keys(value).every(key => keys.includes(key));
21
25
  function isFileError(value) {
@@ -66,12 +70,19 @@ function isComputerFrame(value) {
66
70
  case 'exec_request': return hasRequired(value, {
67
71
  id: isString, command: isString, shell: isString, cwd: isString, env: isStringRecord,
68
72
  pathPrepend: isStringArray, streaming: isBoolean,
69
- }) && hasOptional(value, { timeoutSeconds: isNullableNumber });
70
- case 'exec_output': return hasRequired(value, { id: isString, stream: isString, chunk: isString });
73
+ }) && hasOptional(value, { timeoutSeconds: isNullableNumber, label: isString, jobId: isString });
74
+ case 'exec_started': return hasRequired(value, { id: isString, durable: isBoolean, startedAtMs: isNumber })
75
+ && hasOptional(value, { pid: isNonnegativeInteger });
76
+ case 'exec_output': return hasRequired(value, { id: isString, stream: isString, chunk: isString })
77
+ && hasOptional(value, { offset: isNonnegativeInteger, recvAtMs: isNumber });
78
+ case 'exec_ack': return hasRequired(value, { id: isString, throughOffset: isOffsetOrNone });
71
79
  case 'exec_exit': return hasRequired(value, {
72
80
  id: isString, exitCode: isNullableNumber, stdout: isString, stderr: isString,
73
81
  timedOut: isBoolean, truncated: isBoolean,
74
- });
82
+ }) && hasOptional(value, { signal: isString, cancelled: isBoolean, gaps: isNonnegativeInteger });
83
+ case 'exec_attach': return hasRequired(value, { id: isString, fromOffset: isNonnegativeInteger });
84
+ case 'exec_status': return hasRequired(value, { id: isString, state: isString, headOffset: isOffsetOrNone, committedOffset: isOffsetOrNone })
85
+ && EXEC_STATES.has(value.state);
75
86
  case 'tool_request': return hasRequired(value, { id: isString, method: isString, params: isObject })
76
87
  && value.id.length > 0 && value.id.length <= 128
77
88
  && value.method.length > 0 && value.method.length <= 64
@@ -82,7 +93,7 @@ function isComputerFrame(value) {
82
93
  && (value.ok ? value.error === null : typeof value.error === 'string' && value.error.length > 0)
83
94
  && Buffer.byteLength(JSON.stringify(value)) <= TOOL_RESULT_BYTES
84
95
  && onlyKeys(value, ['t', 'id', 'ok', 'result', 'error']);
85
- case 'exec_cancel': return hasRequired(value, { id: isString });
96
+ case 'exec_cancel': return hasRequired(value, { id: isString }) && hasOptional(value, { signal: isString });
86
97
  case 'file_read_request': return hasRequired(value, {
87
98
  id: isString, root: isString, path: isString, startLine: isPositiveInteger, maxLines: isPositiveInteger,
88
99
  }) && value.maxLines <= 2000 && onlyKeys(value, ['t', 'id', 'root', 'path', 'startLine', 'maxLines']);
@@ -8,11 +8,20 @@ export const MANAGEMENT_WIRE_METRICS_VERSION = 4;
8
8
  export const MANAGEMENT_WIRE_PRESSURE_VERSION = 5;
9
9
  /** Replaces normalized CPU load with raw 1/5/15-minute load averages and CPU capacity. */
10
10
  export const MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION = 6;
11
- export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION;
11
+ /** Adds per-GPU utilization and GPU memory to heartbeat metrics. */
12
+ export const MANAGEMENT_WIRE_GPU_VERSION = 7;
13
+ /** Adds `keepers` to heartbeats and the `keeper_control` / `keeper_control_result` pair (spec §8.1, Task 26). */
14
+ export const MANAGEMENT_WIRE_KEEPERS_VERSION = 8;
15
+ export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_KEEPERS_VERSION;
16
+ /** Bounds of one heartbeat's `keepers` (spec §8.1). */
17
+ export const KEEPER_STATUS_MAX_ITEMS = 32;
18
+ export const KEEPER_LABEL_MAX_CHARS = 64;
19
+ export const KEEPER_STATUS_MAX_ENTRIES = 16;
20
+ export const KEEPER_AFFECTED_MAX_ITEMS = 256;
12
21
  const DISCRIMINATORS = new Set([
13
22
  'management_hello', 'management_ready', 'heartbeat', 'heartbeat_ack',
14
23
  'channel_ticket_request', 'channel_ticket', 'client_update', 'client_update_status',
15
- 'external_agents_scan', 'external_agents_scan_result',
24
+ 'external_agents_scan', 'external_agents_scan_result', 'keeper_control', 'keeper_control_result',
16
25
  ]);
17
26
  const CHANNEL_KINDS = new Set(['MANAGEMENT', 'COMPUTER', 'AGENT']);
18
27
  const CHANNEL_STATES = new Set(['DISCONNECTED', 'CONNECTING', 'CONNECTED', 'RECONNECTING']);
@@ -63,6 +72,15 @@ function isStoragePoolMetrics(value) {
63
72
  availableBytes: isNumber,
64
73
  });
65
74
  }
75
+ function isGpuMetrics(value) {
76
+ return isObject(value) && hasRequired(value, { id: isString, name: isString })
77
+ && hasOptional(value, {
78
+ utilization: isNullable(isNumber),
79
+ memoryTotalBytes: isNullable(isNumber),
80
+ memoryUsedBytes: isNullable(isNumber),
81
+ coreCount: isNullable(isNumber),
82
+ });
83
+ }
66
84
  function isWorkstationMetrics(value) {
67
85
  return isObject(value) && hasRequired(value, {
68
86
  cpuUtilization: isNullable(isNumber),
@@ -76,8 +94,31 @@ function isWorkstationMetrics(value) {
76
94
  cpuLoad15: isNullable(isNumber),
77
95
  cpuLogicalCount: isNullable(isNumber),
78
96
  memoryPressure: isNullable(isNumber),
97
+ gpus: isNullable((gpus) => Array.isArray(gpus) && gpus.every(isGpuMetrics)),
79
98
  });
80
99
  }
100
+ const KEEPER_KINDS = new Set(['terminal', 'acp']);
101
+ const KEEPER_STATES = new Set(['running', 'starting', 'stopped', 'unavailable']);
102
+ const KEEPER_REPLACEMENTS = new Set(['none', 'available', 'required', 'mandatory']);
103
+ const KEEPER_ITEM_TYPES = new Set(['pty', 'exec', 'session']);
104
+ const KEEPER_ACTIONS = new Set(['replace-when-idle', 'replace-now', 'cancel']);
105
+ const oneOf = (set) => (value) => typeof value === 'string' && set.has(value);
106
+ const isPending = (value) => value === null || value === 'when-idle';
107
+ function isKeeperItem(value) {
108
+ return isObject(value) && hasRequired(value, { type: oneOf(KEEPER_ITEM_TYPES), id: isString, label: isString, state: isString, sinceEpochMs: isNumber })
109
+ && hasOptional(value, { jobId: isString });
110
+ }
111
+ function isKeeperStatus(value) {
112
+ return isObject(value) && hasRequired(value, {
113
+ kind: oneOf(KEEPER_KINDS), gen: isString, state: oneOf(KEEPER_STATES), protocolVersion: isNumber, buildVersion: isString,
114
+ replacement: oneOf(KEEPER_REPLACEMENTS),
115
+ metrics: (metrics) => isObject(metrics) && Object.values(metrics).every(isNumber),
116
+ items: (items) => Array.isArray(items) && items.length <= KEEPER_STATUS_MAX_ITEMS && items.every(isKeeperItem),
117
+ }) && hasOptional(value, { legacy: isBoolean, pending: isPending });
118
+ }
119
+ function isKeeperAffected(value) {
120
+ return isObject(value) && hasRequired(value, { type: oneOf(KEEPER_ITEM_TYPES), id: isString, label: isString, gen: isString });
121
+ }
81
122
  function isManagementFrame(value) {
82
123
  switch (value.t) {
83
124
  case 'management_hello': return hasRequired(value, {
@@ -93,10 +134,12 @@ function isManagementFrame(value) {
93
134
  channels: (channels) => isObject(channels) && Object.entries(channels).every(([kind, state]) => CHANNEL_KINDS.has(kind) && isChannelState(state)),
94
135
  activeShellTasks: isNumber, activeAgentTasks: isNumber,
95
136
  }) && hasOptional(value, {
137
+ playgroundCwd: isString,
96
138
  protocols: (protocols) => isProtocolRangeMap(protocols, false),
97
139
  os: isNullable(isOsInfo),
98
140
  update: isNullable(isUpdateStatus),
99
141
  metrics: isNullable(isWorkstationMetrics),
142
+ keepers: isNullable((keepers) => Array.isArray(keepers) && keepers.every(isKeeperStatus)),
100
143
  });
101
144
  case 'heartbeat_ack': return hasRequired(value, { seq: isNumber });
102
145
  case 'channel_ticket_request': return hasRequired(value, { channel: isChannelKind });
@@ -105,6 +148,11 @@ function isManagementFrame(value) {
105
148
  case 'client_update_status': return hasRequired(value, { status: isUpdateStatus });
106
149
  case 'external_agents_scan': return hasRequired(value, { commandId: isString });
107
150
  case 'external_agents_scan_result': return hasRequired(value, { commandId: isString, bridges: isBridgeInfoArray });
151
+ case 'keeper_control': return hasRequired(value, { commandId: isString, kind: oneOf(KEEPER_KINDS), action: oneOf(KEEPER_ACTIONS) });
152
+ case 'keeper_control_result': return hasRequired(value, {
153
+ commandId: isString, kind: oneOf(KEEPER_KINDS), action: oneOf(KEEPER_ACTIONS), ok: isBoolean,
154
+ affected: (affected) => Array.isArray(affected) && affected.every(isKeeperAffected),
155
+ }) && hasOptional(value, { pending: isPending, error: isString, affectedIncomplete: (flag) => flag === true });
108
156
  default: return false;
109
157
  }
110
158
  }
@@ -1,5 +1,9 @@
1
1
  /** Native Qoder does not forward background child SDK messages through ACP. Read only the
2
- * exact transcript named by a validated root launch receipt; never scan or infer ownership. */
2
+ * exact transcript named by a validated root launch receipt; never scan or infer ownership.
3
+ *
4
+ * Each accepted row's derived events go to `onRow` (with the row's `uuid`) when it is given — the
5
+ * side-file reader records them as one `child` fact (`backgroundTerminalFold.ts`) — and otherwise one
6
+ * by one to `emit`. */
3
7
  import { constants } from 'node:fs';
4
8
  import { open } from 'node:fs/promises';
5
9
  const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -73,13 +77,14 @@ export class BackgroundProcess {
73
77
  const content = object(row.message).content;
74
78
  if (!Array.isArray(content))
75
79
  return;
80
+ const events = [];
76
81
  for (const block of content.map(object)) {
77
82
  if (row.type === 'assistant' && block.type === 'tool_use' && id(block.id) && typeof block.name === 'string') {
78
83
  if (this.tools.has(block.id) || this.tools.size >= 4096)
79
84
  continue;
80
85
  const input = object(block.input);
81
86
  this.tools.set(block.id, { name: block.name, input });
82
- this.emit('tool_call', { toolCallId: block.id, name: block.name, title: block.name,
87
+ this.derive(events, 'tool_call', { toolCallId: block.id, name: block.name, title: block.name,
83
88
  kind: block.name === 'Read' ? 'read' : block.name === 'Bash' ? 'execute' : block.name === 'Agent' ? 'think' : 'other',
84
89
  rawInput: input, status: 'pending' }, block.name);
85
90
  }
@@ -89,18 +94,25 @@ export class BackgroundProcess {
89
94
  continue;
90
95
  const raw = row.toolUseResult ?? block.content;
91
96
  const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content ?? '');
92
- this.emit('tool_call_update', { toolCallId: block.tool_use_id, name: tool.name,
97
+ this.derive(events, 'tool_call_update', { toolCallId: block.tool_use_id, name: tool.name,
93
98
  status: block.is_error === true ? 'failed' : 'completed', rawOutput: raw,
94
99
  content: [{ type: 'content', content: { type: 'text', text } }] }, tool.name);
95
100
  }
96
101
  else if (row.type === 'assistant' && ((block.type === 'text' && typeof block.text === 'string') ||
97
102
  (block.type === 'thinking' && typeof block.thinking === 'string'))) {
98
- this.emit(block.type === 'text' ? 'agent_message_chunk' : 'agent_thought_chunk', { content: { type: 'text', text: block.type === 'text' ? block.text : block.thinking } });
103
+ this.derive(events, block.type === 'text' ? 'agent_message_chunk' : 'agent_thought_chunk', { content: { type: 'text', text: block.type === 'text' ? block.text : block.thinking } });
99
104
  }
100
105
  }
106
+ if (this.options.onRow) {
107
+ if (events.length > 0)
108
+ this.options.onRow(row.uuid, events);
109
+ }
110
+ else
111
+ for (const event of events)
112
+ this.options.emit(event);
101
113
  }
102
- emit(eventName, payload, toolName) {
103
- this.options.emit({ eventName, payload: { ...payload, _meta: { qoder: { version: 1, source: 'correction',
114
+ derive(events, eventName, payload, toolName) {
115
+ events.push({ eventName, payload: { ...payload, _meta: { qoder: { version: 1, source: 'correction',
104
116
  parentToolUseId: this.options.parentToolUseId, route: 'child', ...(toolName ? { toolName } : {}) } } } });
105
117
  }
106
118
  }