@platless/axom-cli 0.1.0 → 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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -1,128 +1,70 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
1
3
  import path from 'node:path';
2
- import { binaryTargetFor, installedBinaryVersion, platformTriple } from '../binaryInstall.js';
3
- import { npmPackageName } from '../installProbe.js';
4
- import { uvPackageName, uvToolEntrypoint, uvToolEntrypointFrom } from '../uvxInstall.js';
5
- export class UnsupportedDistributionError extends Error {
6
- }
7
- /** `npx --yes <specifier> [dist args]`,两个调用方共用的一处构造:钉版本的和不钉版本的。 */
8
- function npxLaunch(specifier, npx, platform) {
9
- return {
10
- command: platform === 'win32' ? 'npx.cmd' : 'npx',
11
- // `--yes` 让首次拉包不停在交互确认上。
12
- args: ['--yes', specifier, ...(npx.args ?? [])],
13
- env: { ...(npx.env ?? {}) },
14
- };
15
- }
4
+ import { fileURLToPath } from 'node:url';
5
+ import { nativeExtensionFor } from '../patchAssets.js';
6
+ import { verifyPatchedFiles } from '../managedPatches.js';
7
+ import { AdapterNotInstalledError, inside, labelFor } from '../builds/model.js';
8
+ import { assertBuildRunsOn } from '../builds/nodeGate.js';
16
9
  /**
17
- * registry 只给原料,不给命令行 —— 没有 `command` 字段,启动方式要从 `distribution` 推导。
18
- *
19
- * 三种分发方式现在都支持:`npx`/`uvx` 走各自安装侧维护的缓存/工具目录(见 `installProbe.ts`、
20
- * `uvxInstall.ts`),`binary` 走 `binaryInstall.ts`(Task 8)—— 按六个平台三元组挑一个目标、
21
- * 从 `adapterRoot` 找已经装好的版本。三条路径共同的规则是:**从不猜**,找不到就抛
22
- * `UnsupportedDistributionError`,理由写清楚是"这个平台没有发行版"还是"这台机器还没装",而不是
23
- * 笼统的一句"不支持"。
10
+ * `env` with `prefix` first on its `PATH` — unless `PATH` already starts with exactly that prefix (the
11
+ * spec's own env survived the merge), so applying it twice is harmless.
24
12
  */
25
- export async function resolveLaunch(agent, platform,
26
- // 六个平台三元组里的一半;只有 binary 分支用到。
27
- arch,
28
- // `<workstation config dir>/acp-adapters` —— `binaryInstall.ts` 解包落地的根目录,只有 binary
29
- // 分支用到。npx 走 npm 自己的 `_npx` 缓存,uvx 走 `uv tool dir`,都不需要这个根目录。
30
- adapterRoot,
31
- // 仅测试注入:真实调用永远走 `uvxInstall.ts` 的默认 `uv` 实现。签名上留成可选,两个真实调用点
32
- // (`acpAgents.ts`)不用跟着改。下面的 `uvTools` 给出时,这个参数被忽略 —— 见它的说明。
33
- run,
34
- // 已经读好的 `uv tool list` 快照(`listUvTools`)。给了就直接查表,不再自己起 `uv` 子进程 ——
35
- // `AcpAgents.roster` 为整份 39 条 registry 只读一次,把结果传进这里,而不是让每个 uvx agent
36
- // 在这条路径上各自再起一次 `uv`。真正的启动路径(`resolveLaunchFor`)不传这个参数,继续用
37
- // `run`/默认实现现读一次,这里的现读对它而言正是期望行为。
38
- //
39
- // 这个函数整体是 async 的唯一原因:`uv` 现在是非阻塞调用(`RunUv` 返回 `Promise`,见
40
- // `uvxInstall.ts`),没有预取快照时这里必须 `await` 它。给了 `uvTools` 的那条路径
41
- // (`AcpAgents.resolvable`)完全同步、不等待任何东西,包成 `async` 只是让两条路径共用同一个
42
- // 签名——`Promise.resolve` 包一层同步返回值的开销,跟每次 roster 扫描本来就要付的成本比可以
43
- // 忽略不计。
44
- uvTools) {
45
- const { npx, uvx, binary } = agent.distribution;
46
- // specifier 里已内联精确版本,不会追 latest —— 除非 registry 钉的那个版本 npm 根本没有,
47
- // 见 {@link unpinnedFallbackFor}。
48
- if (npx)
49
- return npxLaunch(npx.package, npx, platform);
50
- if (uvx) {
51
- // 不走 `uvx <pkg>`:那条路用 uv 自己的临时环境缓存,目录名带哈希,"装没装"读不可靠。
52
- // 改成我们自己 `uv tool install`、启动时直接跑装出来的入口点 —— 与 binary 一样,
53
- // 落到"我们装的、我们知道装在哪"这一条统一规则上。
54
- const outcome = uvTools
55
- ? uvToolEntrypointFrom(uvPackageName(uvx.package), uvTools)
56
- : await uvToolEntrypoint(uvPackageName(uvx.package), run);
57
- if (outcome.ok) {
58
- return { command: outcome.path, args: [...(uvx.args ?? [])], env: { ...(uvx.env ?? {}) } };
59
- }
60
- if (outcome.reason === 'ambiguous') {
61
- // 装了,但不敢猜:多个可执行文件里没有一个跟包名同名,uv 的列出顺序又不是文档化契约——
62
- // 静默启动错的那个,换来的是一次没人能追回这里的 ACP 握手挂死或失败,比拒绝启动更糟。
63
- throw new UnsupportedDistributionError(`Agent '${agent.id}' is distributed through uvx and its tool is installed, but this build ` +
64
- `cannot tell which of its executables is the ACP adapter: ${outcome.candidates.join(', ')}.`);
65
- }
66
- throw new UnsupportedDistributionError(`Agent '${agent.id}' is distributed through uvx but its tool is not installed on this machine.`);
67
- }
68
- if (binary) {
69
- // 六个平台三元组里挑一个;`windows-aarch64` 在实测的 registry 快照上只有 9/18 的覆盖,
70
- // "这个平台没有发行版"是常态,不是异常——`installBinary`(`binaryInstall.ts`)在下载前就
71
- // 用同一个函数做同一个判断,这里只是启动路径上再问一次同一件事,答案必须一致。
72
- const triple = platformTriple(platform, arch);
73
- const target = triple ? binaryTargetFor(agent, triple) : undefined;
74
- if (!target) {
75
- throw new UnsupportedDistributionError(`Agent '${agent.id}' publishes no archive for ${triple ?? `${platform}-${arch}`}.`);
76
- }
77
- // 装没装是这台机器磁盘上的事实,不是 registry 条目的属性 —— 跟 npx/uvx 分支一样,没装就说
78
- // "没装",不是"不支持"。
79
- //
80
- // `agent.version` 必须传:同一个 agent 的多个版本可以并存(`<root>/<agent>/<version>/`,
81
- // 就是为此设计的),`pickInstalledVersion` 的规则是"钉的版本在就取它,否则取最大"。
82
- // `AcpAgents.roster` 报告 `adapterVersion` 时传了它,这里不传就会在两个版本并存时报告一个、
83
- // 启动另一个 —— 名册说 0.75.0、跑起来是 0.74.0,而且没有任何一处会说出这件事。
84
- const version = installedBinaryVersion(agent.id, adapterRoot, agent.version);
85
- if (!version) {
86
- throw new UnsupportedDistributionError(`Agent '${agent.id}' has no installed adapter on this machine.`);
87
- }
88
- return {
89
- command: path.join(adapterRoot, agent.id, version, target.cmd),
90
- args: [...(target.args ?? [])],
91
- env: { ...(target.env ?? {}) },
92
- };
93
- }
94
- throw new UnsupportedDistributionError(`Agent '${agent.id}' declares no launchable distribution.`);
13
+ export function withPathPrefix(env, prefix) {
14
+ if (!prefix?.length)
15
+ return env;
16
+ const head = prefix.join(path.delimiter);
17
+ const current = env.PATH ?? '';
18
+ if (current === head || current.startsWith(head + path.delimiter))
19
+ return env;
20
+ return { ...env, PATH: current ? head + path.delimiter + current : head };
95
21
  }
96
22
  /**
97
- * 同一个 agent,但把 registry 钉的版本从 specifier 上剥掉 —— 上游钉了一个 npm 上并不存在的版本时,
98
- * 唯一还能把这个 agent 救回来的启动方式。
99
- *
100
- * ## 为什么需要它
101
- *
102
- * 2026-09-04 实测:registry 的 `codex-acp` 条目声明
103
- * `@agentclientprotocol/codex-acp@1.10.0`,而 npm 上这个包的版本停在 `1.9.0`
104
- * (`dist-tags.latest` 也是 `1.9.0`)。于是每一次 codex 启动都是 `npm error code ETARGET`,npx 退出,
105
- * 子进程死掉 —— 两台真实工作站同时失去了 codex。这是上游的缺陷;这里能做的是不让它把 agent 砖掉。
106
- *
107
- * ## 代价,以及为什么仍然值得
23
+ * The spawn spec for an installed build — **no npx, no registry read, no network**: the build's own
24
+ * entry, run with a fixed absolute Node (`nodePath`, the daemon's `process.execPath`) when it is
25
+ * JavaScript, or executed directly when it is native.
108
26
  *
109
- * 不钉版本装回来的,是 npm 此刻叫 `latest` 的那一个 —— **registry 从来没有指定过它**。它可能比钉的
110
- * 版本新(常见,上游多半正是发版发歪了),也可能是一个协议行为不同、甚至跟这个客户端谈不拢
111
- * `initialize` 的版本。所以这条路只在 ETARGET 上走(见 `acpClient.ts` 的 `isMissingPackageVersion`),
112
- * 只走一次,而且两条 warn 日志会把「跑的不是 registry 要的那个版本」说穿。
113
- *
114
- * ## 三种「没有回退可言」的情况,都返回 undefined
27
+ * `launchEnv` is the running CLI's catalog/runtime.json launch environment for this agent, evaluated
28
+ * at the registry version the build was installed from. It goes underneath the build's own
29
+ * (distribution-declared) env, the same order `registry/catalog.ts`'s `withLaunchEnv` always used.
30
+ * A patched build's files are re-hashed first (the tamper check that used to run on every launch), and
31
+ * a JavaScript build must pass the Node gate (`builds/nodeGate.ts`) against `nodePath`.
32
+ */
33
+ /**
34
+ * Expand a leading `~` / `~/` in every env value against the user's home directory.
115
35
  *
116
- * 没有 npx 分发(`uvx` 走的是 uv 自己的缓存,`binary` 这个版本压根不支持)、specifier 本来就不带版本
117
- * (剥无可剥,npm 已经在追 latest 了)。剥版本用 `npmPackageName`,它按最后一个 `@` 切,所以
118
- * `@scope/name@1.10.0` → `@scope/name`,而前导的 `@` 不会被当成版本分隔符。
36
+ * Why here and nowhere else: the CLI-owned `launchEnv` (catalog / runtime.json) ships as a static
37
+ * file, so it cannot spell the per-machine absolute path some consumers need — Claude's
38
+ * `CLAUDE_SECURESTORAGE_CONFIG_DIR` is used as a literal path and never expands `~` itself. And the
39
+ * final merged env is the only point that covers both sources: a build's own distribution env
40
+ * overwrites same-key `launchEnv` values, so expanding the `launchEnv` alone would let an unexpanded
41
+ * build value win. Only a leading `~` is touched; a `~` elsewhere in a value is data, not a path.
119
42
  */
120
- export function unpinnedFallbackFor(agent, platform) {
121
- const npx = agent.distribution.npx;
122
- if (!npx)
123
- return undefined;
124
- const unpinned = npmPackageName(npx.package);
125
- if (unpinned === npx.package)
126
- return undefined;
127
- return { pinned: npx.package, unpinned, launch: npxLaunch(unpinned, npx, platform) };
43
+ function expandHomeInEnv(env) {
44
+ const home = homedir();
45
+ return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value === '~' ? home : value.startsWith('~/') ? path.join(home, value.slice(2)) : value]));
46
+ }
47
+ export async function launchForBuild(build, root, options) {
48
+ const dir = inside(root, build.dir);
49
+ const entry = inside(dir, build.launch.entry);
50
+ if (build.kind === 'npm-patched' && build.package && build.patchedFiles) {
51
+ await verifyPatchedFiles(inside(path.join(dir, 'node_modules'), build.package), build.patchedFiles);
52
+ }
53
+ if (!existsSync(entry))
54
+ throw new AdapterNotInstalledError(build.agentId, `build ${build.buildId} is missing its executable`);
55
+ // Every build launch passes the Node gate — patched and pristine alike, whatever the args look like.
56
+ await assertBuildRunsOn(build, root, options.nodePath);
57
+ const env = expandHomeInEnv({ ...options.launchEnv, ...build.launch.env });
58
+ const pathPrefix = build.launch.pathDirs?.map((relative) => inside(dir, relative));
59
+ if (pathPrefix?.length) {
60
+ env.PATH = pathPrefix.join(path.delimiter) + path.delimiter + (env.PATH ?? options.basePath ?? process.env.PATH ?? '');
61
+ }
62
+ const extra = pathPrefix?.length ? { pathPrefix } : {};
63
+ const extension = build.launch.runtime !== 'node' ? nativeExtensionFor(build.agentId, build.version) : undefined;
64
+ if (extension) {
65
+ return { command: options.nodePath, args: [fileURLToPath(extension), entry, ...build.launch.args], env, label: labelFor(build), ...extra };
66
+ }
67
+ return build.launch.runtime === 'node'
68
+ ? { command: options.nodePath, args: [entry, ...build.launch.args], env, label: labelFor(build), ...extra }
69
+ : { command: entry, args: [...build.launch.args], env, label: labelFor(build), ...extra };
128
70
  }
@@ -1,3 +1,7 @@
1
+ import { codexActivityRaw } from './activityReporting.js';
2
+ export function newMapperScopeData() {
3
+ return { byId: {}, lastReportedQueueDepth: 0 };
4
+ }
1
5
  /**
2
6
  * One session's memory, and the reason this file is no longer stateless: **what this session has
3
7
  * already been told**, for the two branches that cannot answer a frame from the frame alone.
@@ -30,7 +34,25 @@
30
34
  * already (every call's `rawInput`/`rawOutput` is journaled), so there is no eviction here.
31
35
  */
32
36
  export class SessionMapperState {
33
- byId = new Map();
37
+ onActivityReported;
38
+ data;
39
+ /**
40
+ * Where an agent-reported activity-state push (today, only `codexActivityRaw`'s extraction of
41
+ * `session_info_update._meta.codex.threadStatus`) actually goes once {@link reportActivity} is
42
+ * called — `agentSessions.ts`'s job, not this file's: this module stays "zero IO" (see the file
43
+ * doc comment) by taking the effect as a constructor dependency rather than reaching for a
44
+ * socket itself.
45
+ *
46
+ * Defaults to a no-op so every existing `new SessionMapperState()` — the tests, `adoption.ts`'s
47
+ * replay path, which has no live connection to report anything on — keeps compiling and keeps
48
+ * doing nothing, exactly as it always has.
49
+ */
50
+ constructor(onActivityReported = () => { },
51
+ /** The memory itself, mutated in place — the processor passes the checkpointed copy it owns. */
52
+ data = newMapperScopeData()) {
53
+ this.onActivityReported = onActivityReported;
54
+ this.data = data;
55
+ }
34
56
  /**
35
57
  * The agent queue depth this session has already reported to the server.
36
58
  *
@@ -39,7 +61,9 @@ export class SessionMapperState {
39
61
  * reader already assumes. Seeding it to `-1` (or `null`) would let the very first frame through,
40
62
  * which on `pi-acp` is `startTurn`'s, i.e. the defect this field exists to prevent.
41
63
  */
42
- lastReportedQueueDepth = 0;
64
+ get lastReportedQueueDepth() {
65
+ return this.data.lastReportedQueueDepth;
66
+ }
43
67
  /**
44
68
  * Is `depth` news? Records it if so. **This is the whole fix for a defect that shipped in the
45
69
  * previous commit**, so the reasoning is written out rather than summarised.
@@ -94,13 +118,26 @@ export class SessionMapperState {
94
118
  reportsQueueDepth(depth) {
95
119
  if (depth === this.lastReportedQueueDepth)
96
120
  return false;
97
- this.lastReportedQueueDepth = depth;
121
+ this.data.lastReportedQueueDepth = depth;
98
122
  return true;
99
123
  }
124
+ /**
125
+ * One agent-reported activity-state push, vendor-verbatim. Not a `WireEvent` and never routed
126
+ * through {@link mapSessionUpdate}'s return value — activity is a level, not an edge (see the
127
+ * `agent_session_state` frame's own doc comment in `protocol/agent.ts`), so it must not become a
128
+ * durable item, must not be journaled and must not advance any seq. Forwarded to whatever
129
+ * {@link onActivityReported} this instance was constructed with.
130
+ */
131
+ reportActivity(raw) {
132
+ this.onActivityReported(raw);
133
+ }
100
134
  /** Remember what this call is called. Later updates that carry no `name` fall back to it. */
101
135
  record(toolCallId, name) {
102
136
  if (name)
103
- this.byId.set(toolCallId, name);
137
+ this.remember(toolCallId, name);
138
+ }
139
+ remember(toolCallId, name) {
140
+ Object.defineProperty(this.data.byId, toolCallId, { value: name, enumerable: true, writable: true, configurable: true });
104
141
  }
105
142
  /**
106
143
  * The name to report for one update, in ACP's own order of authority: an explicit `name` on the
@@ -112,10 +149,10 @@ export class SessionMapperState {
112
149
  */
113
150
  resolve(toolCallId, name, title) {
114
151
  if (name) {
115
- this.byId.set(toolCallId, name);
152
+ this.remember(toolCallId, name);
116
153
  return name;
117
154
  }
118
- return this.byId.get(toolCallId) ?? title ?? 'unknown';
155
+ return (Object.hasOwn(this.data.byId, toolCallId) ? this.data.byId[toolCallId] : undefined) ?? title ?? 'unknown';
119
156
  }
120
157
  }
121
158
  /**
@@ -136,7 +173,7 @@ function compact(value) {
136
173
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
137
174
  }
138
175
  /**
139
- * `ToolCall.locations` → the wire `tool_call.locations` shape; omitted entirely when there are none.
176
+ * `ToolCall.locations` → the wire `tool_call.locations` shape; [] explicitly clears a snapshot.
140
177
  *
141
178
  * An identity copy of ACP's `ToolCallLocation` (`types.gen.d.ts:581`), which is `{path, line?}` —
142
179
  * `line` is `number | null | undefined` (`:586-589`) and used to be dropped here, which cost the
@@ -145,7 +182,7 @@ function compact(value) {
145
182
  * enclosing payload.
146
183
  */
147
184
  function mapLocations(locations) {
148
- if (!locations || locations.length === 0)
185
+ if (locations == null)
149
186
  return undefined;
150
187
  return locations.map((location) => compact({ path: location.path, line: location.line ?? undefined }));
151
188
  }
@@ -166,7 +203,7 @@ function toolCallCreatedEvent(update, state) {
166
203
  // only field guaranteed to give the card a name.
167
204
  name: update.name ?? update.title,
168
205
  toolCallId: update.toolCallId,
169
- rawInput: update.rawInput,
206
+ rawInput: update.rawInput ?? undefined,
170
207
  kind: mapToolKind(update.kind),
171
208
  title: update.title,
172
209
  locations: mapLocations(update.locations),
@@ -239,16 +276,10 @@ function toolCallUpdateEvent(update, state) {
239
276
  title: update.title ?? undefined,
240
277
  kind: update.kind ?? undefined,
241
278
  status: update.status ?? undefined,
242
- // An empty array is omitted rather than sent, and the behaviour is deliberate now that the
243
- // card's fold *does* honour ACP's "replace the content collection": `applyChunks` treats an
244
- // empty chunk list as "unchanged" at every call site, never as a clear. So an empty array
245
- // carries nothing either way — and the server drops it on the way back out anyway
246
- // (`TimelineEvent.ToolCallUpdated.payload` only writes `content` when it is non-empty), which
247
- // is why sending one could not express a clear even if a consumer wanted to act on it.
248
- content: update.content && update.content.length > 0 ? update.content : undefined,
279
+ content: update.content == null ? undefined : update.content,
249
280
  locations: mapLocations(update.locations),
250
- rawInput: update.rawInput,
251
- rawOutput: update.rawOutput,
281
+ rawInput: update.rawInput ?? undefined,
282
+ rawOutput: update.rawOutput ?? undefined,
252
283
  // See the doc comment above: ACP's extension slot, copied verbatim and read by nothing *here*
253
284
  // (the server's `decodeToolUpdate` does read it, also verbatim).
254
285
  _meta: update._meta ?? undefined,
@@ -257,16 +288,11 @@ function toolCallUpdateEvent(update, state) {
257
288
  }
258
289
  function mapToolCallEvents(update, state) {
259
290
  const events = [toolCallCreatedEvent(update, state)];
260
- // ACP permits a `tool_call` to arrive already finished — the agent reporting a call it both
261
- // started and completed in one notification, with no separate `tool_call_update` at all. A card
262
- // only ever leaves "Running" on an update reporting a non-pending status
263
- // (`WorkTimelineTools.foldToolUpdate`'s outcome branch), and the creation event carries no status,
264
- // so without this second event an already-terminal call would render as permanently "Running".
265
- //
291
+ // Creation carries initial update fields too. Emit them immediately under the same id so pending
292
+ // content and status, as well as an already-terminal result, survive a missing later update.
266
293
  // `ToolCall` is structurally a `ToolCallUpdate` (every field of the latter, with `title` and `kind`
267
- // merely narrower), so the same function emits both — the two cannot drift into disagreeing about
268
- // what a terminal update looks like.
269
- if (update.status === 'completed' || update.status === 'failed') {
294
+ // merely narrower), so the same function emits both.
295
+ if (update.status != null || update.content != null || update.rawOutput != null) {
270
296
  events.push(toolCallUpdateEvent(update, state));
271
297
  }
272
298
  return events;
@@ -476,6 +502,9 @@ export function mapSessionUpdate(update, state) {
476
502
  const queueNotice = agentQueueNotice(update._meta, state);
477
503
  if (queueNotice)
478
504
  events.push(queueNotice);
505
+ const activityRaw = codexActivityRaw(update._meta);
506
+ if (activityRaw)
507
+ state.reportActivity(activityRaw);
479
508
  return events;
480
509
  }
481
510
  case 'usage_update':
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The `_session/steering` and `_session/async_task/stop` extension vocabulary of the deterministic processor
3
+ * (`processor/control.ts`; the old live session that also used it was deleted in Task 29). Pure: no I/O,
4
+ * no clock — the processor's import guard walks this file.
5
+ */
6
+ /**
7
+ * The steering extension's JSON-RPC method. **Not an ACP method** — `grep steer` over the pinned
8
+ * SDK's `dist/schema/types.gen.d.ts` returns nothing. It is implemented by `claude-agent-acp`
9
+ * (`acp-agent.js@0.76.0:164`), whose own comment calls it a *"Custom (extension) request method ...
10
+ * Named `_session/steering` per the agreed ACP steering wire protocol"*; the leading underscore is
11
+ * ACP's extension namespace. It is therefore reached through the untyped `request<Response, Params>
12
+ * (method: string, …)` overload rather than `methods.agent.*`, and every caller must check
13
+ * {@link AcpConnection.supportsSteering} first.
14
+ */
15
+ export const STEER_METHOD = '_session/steering';
16
+ /**
17
+ * The adapter's *stop one background task* extension method. **Not an ACP method** either — it is
18
+ * `claude-agent-acp`'s own `ASYNC_TASK_STOP_METHOD` (`acp-agent.js@0.79.0:165`), whose own comment
19
+ * reads *"Stops one Claude background task without cancelling the parent prompt turn."*
20
+ * `codex-acp@1.12.0` registers the same literal name with the same params
21
+ * (`dist/index.js:31608`, `:35126-35129`, `:32395-32406`). Reached through the untyped `request`
22
+ * overload, for the reason {@link STEER_METHOD} documents.
23
+ */
24
+ export const ASYNC_TASK_STOP_METHOD = '_session/async_task/stop';
25
+ /**
26
+ * Narrow a `_session/steering` response onto {@link SteerOutcome}, or throw.
27
+ *
28
+ * The extension answers with one of three outcomes (`acp-agent.js@0.76.0:1737`, `:1751`, `:1779`):
29
+ *
30
+ * - `injected` — a turn was running and the message went into it.
31
+ * - `promptRequired` (with `reason: "noRunningTurn"`) — no turn was running and we opted in, so the
32
+ * agent did nothing at all and the content stays host-owned.
33
+ * - `startedNewTurn` — no turn was running and the client did *not* opt in, so the agent detached a
34
+ * `prompt()` of its own.
35
+ *
36
+ * **`startedNewTurn` throws rather than mapping to anything** (decision D13). We always send
37
+ * `idleBehavior: "promptRequired"` ({@link AcpSession.steer}), so receiving it means the agent
38
+ * ignored the opt-in — and what it handed back is a turn nobody awaits: it never enters our queue,
39
+ * never lands an item, and never produces a `turn_done`, in a system that accounts by turn.
40
+ * Absorbing it as "injected" would mark a message delivered to a turn we are not tracking;
41
+ * absorbing it as "promptRequired" would submit the same message a second time. Neither is a
42
+ * recovery, so this fails where the contract broke, loudly enough to name.
43
+ *
44
+ * An outcome nobody recognises throws for the same reason: silently reading it as `injected` would
45
+ * record a delivery that may never have happened.
46
+ */
47
+ export function mapSteerOutcome(result) {
48
+ const outcome = result?.outcome;
49
+ if (outcome === 'injected')
50
+ return 'injected';
51
+ if (outcome === 'promptRequired')
52
+ return 'promptRequired';
53
+ if (outcome === 'startedNewTurn') {
54
+ throw new Error(`[acp] the agent answered ${STEER_METHOD} with 'startedNewTurn', a turn this client never ` +
55
+ "opted into and does not track: we always send _meta.steering.idleBehavior = 'promptRequired'.");
56
+ }
57
+ throw new Error(`[acp] unrecognised ${STEER_METHOD} outcome: ${JSON.stringify(outcome)}`);
58
+ }
@@ -0,0 +1,114 @@
1
+ import { activityIsBusy } from './activityBusy.js';
2
+ /** Spec §14: a session must be quiet this long before it is switched. */
3
+ export const SWITCH_QUIET_MS = 30_000;
4
+ /** How long a switch waits for the session's record loop to reach a record boundary before it gives up (and retries later). */
5
+ export const SWITCH_SUSPEND_TIMEOUT_MS = 10_000;
6
+ /** A reopen request the server did not answer (busy, an older open won, …) is asked again after this long. */
7
+ export const REOPEN_REQUEST_RETRY_MS = 60_000;
8
+ /** The note a switch writes into a successor it gives up on (rollback): discovery never picks such a key (fix round 1). */
9
+ export const SWITCH_ABANDONED_NOTE = 'switch_abandoned';
10
+ /** A reopen for an adapter keeper-acp lost is asked at most this many times per lost spawn (60 s, 120 s, 240 s apart). */
11
+ export const ADAPTER_LOST_REOPEN_ATTEMPTS = 3;
12
+ /** The prefix of a switched key's suffix: `<sessionRef>#s<n>`. */
13
+ export const SWITCHED_KEY_MARK = '#s';
14
+ /** The transport key of the `keyGen`th life of `sessionRef` (0: the ref itself). */
15
+ export function sessionKeyFor(sessionRef, keyGen) {
16
+ return keyGen <= 0 ? sessionRef : `${sessionRef}${SWITCHED_KEY_MARK}${keyGen}`;
17
+ }
18
+ /** The key generation a spawn label records (`label.keyGen`; absent or invalid: 0). */
19
+ export function keyGenOf(label) {
20
+ const value = label?.keyGen;
21
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0;
22
+ }
23
+ /**
24
+ * The session ref a keeper-acp session belongs to: its label's `sessionRef`, provided the key is the one the label's `keyGen` derives
25
+ * from it (a key nothing could have spawned under that ref names none).
26
+ */
27
+ export function refOfSession(summary) {
28
+ const ref = summary.label.sessionRef;
29
+ if (typeof ref !== 'string' || ref === '')
30
+ return undefined;
31
+ return sessionKeyFor(ref, keyGenOf(summary.label)) === summary.sessionKey ? ref : undefined;
32
+ }
33
+ /**
34
+ * Several keeper-acp sessions of one ref (a switch a crash interrupted — the only way two exist): which one is the ref's session and
35
+ * which go. The newest key whose open completed wins (its import of the predecessor's outbox had landed: the import precedes the
36
+ * load); every other one is superseded. When no newer key completed its open, the switch never finished: the newer keys go and the
37
+ * oldest one stays the ref's session. `openCompleted(key)` reads the session's head checkpoint.
38
+ */
39
+ export async function resolveSuperseded(sessions, openCompleted) {
40
+ const byGen = [...sessions].sort((a, b) => keyGenOf(b.label) - keyGenOf(a.label));
41
+ for (const candidate of byGen.slice(0, -1)) {
42
+ if (await openCompleted(candidate))
43
+ return { winner: candidate, superseded: byGen.filter((s) => s !== candidate) };
44
+ }
45
+ const oldest = byGen[byGen.length - 1];
46
+ return { winner: oldest, superseded: byGen.filter((s) => s !== oldest) };
47
+ }
48
+ /**
49
+ * The scan card's wording of each {@link switchBlockers} sentence (Task 25, spec §6.4): those sentences stay as they are for the
50
+ * log; a reader looking at "why can't this session move now" gets these. Unknown input (none today) passes through unchanged.
51
+ */
52
+ export function plainSwitchBlocker(blocker) {
53
+ const quiet = /^not quiet for (\d+) s$/.exec(blocker);
54
+ if (quiet)
55
+ return `it was active less than ${quiet[1]} seconds ago`;
56
+ return PLAIN_BLOCKERS[blocker] ?? blocker;
57
+ }
58
+ const PLAIN_BLOCKERS = {
59
+ 'opening, retiring or switching': 'it is being opened or moved',
60
+ 'its adapter is not running': 'its agent process is not running right now',
61
+ 'a turn is in flight': 'it is working on a reply',
62
+ 'a permission or elicitation is unanswered': 'it is waiting for an answer',
63
+ 'the agent reports it is active': 'the agent says it is still working',
64
+ 'an async task or background terminal is still running': 'it has a background task or terminal still running',
65
+ 'a delegation refresh is pending': 'its delegated access is being renewed',
66
+ };
67
+ /** Why a session may not be switched now (spec §6.3 step 2); empty = strictly idle. */
68
+ export function switchBlockers(input) {
69
+ const { session } = input;
70
+ const blockers = [];
71
+ if (input.busyRef)
72
+ blockers.push('opening, retiring or switching');
73
+ const liveness = session.liveness;
74
+ if (!liveness.agentAlive)
75
+ blockers.push('its adapter is not running');
76
+ if (liveness.turnRunning)
77
+ blockers.push('a turn is in flight');
78
+ if ((session.pendingRequestIds?.length ?? 0) > 0)
79
+ blockers.push('a permission or elicitation is unanswered');
80
+ const raw = session.activityRaw;
81
+ // `activityIsBusy` calls Codex "only waiting on an approval" idle (correct for the heartbeat); a switch must not kill that wait.
82
+ if (activityIsBusy(raw) || raw?.type === 'active')
83
+ blockers.push('the agent reports it is active');
84
+ if ((session.liveBackgroundWork ?? 0) > 0)
85
+ blockers.push('an async task or background terminal is still running');
86
+ if (session.delegation?.refreshPending)
87
+ blockers.push('a delegation refresh is pending');
88
+ const quiet = input.quietMs ?? SWITCH_QUIET_MS;
89
+ const last = Math.max(input.lastActivityAt, session.lastFoldAt ?? 0);
90
+ if (input.now - last < quiet)
91
+ blockers.push(`not quiet for ${Math.round(quiet / 1000)} s`);
92
+ return blockers;
93
+ }
94
+ /**
95
+ * Spec §6.3 step 4 / analysis D §5.2 step 7: a load answers with the adapter's own idea of the session's config (most adapters do
96
+ * not persist a model or mode choice into the session file); every option whose value differs from what the session had before the
97
+ * switch is set back. Options the new process does not offer, or values it refuses, are reported and left as they are.
98
+ */
99
+ export async function restoreConfigOptions(session, before, idFor, log) {
100
+ const restored = [];
101
+ for (const option of before) {
102
+ const now = session.configOptions.find((candidate) => candidate.id === option.id);
103
+ if (!now || now.type !== option.type || now.currentValue === option.currentValue)
104
+ continue;
105
+ try {
106
+ await session.setConfigOption(option.id, option.currentValue, idFor(option.id));
107
+ restored.push(option.id);
108
+ }
109
+ catch (error) {
110
+ log(`[acp] switch: config option '${option.id}' could not be set back to its value before the switch: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+ }
113
+ return restored;
114
+ }