@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.
- package/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- 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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
98
|
-
*
|
|
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
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
|
|
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
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
152
|
+
this.remember(toolCallId, name);
|
|
116
153
|
return name;
|
|
117
154
|
}
|
|
118
|
-
return this.byId.
|
|
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;
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
261
|
-
//
|
|
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
|
|
268
|
-
|
|
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
|
+
}
|