@platless/axom-cli 0.3.13 → 0.6.36
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 +18 -5
- package/dist/axom-patch/cli/README.md +119 -9
- 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 +123 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -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/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +194 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +39 -15
- package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/zcode-models.json +37 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
- package/dist/cli.js +4 -9
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- 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 +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2192 -636
- package/dist/workstation/acp/acpClient.js +84 -495
- package/dist/workstation/acp/adoption.js +10 -396
- 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 +324 -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/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +290 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +239 -159
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +60 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +78 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +282 -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 +105 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +496 -0
- package/dist/workstation/acp/processor/state.js +67 -0
- package/dist/workstation/acp/processor/step.js +442 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +58 -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 +390 -0
- package/dist/workstation/acp/processorSession.js +1348 -0
- package/dist/workstation/acp/registry/catalog.js +189 -41
- package/dist/workstation/acp/registry/launch.js +76 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +111 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- 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 +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +295 -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/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +135 -0
- package/dist/workstation/acp/test/realAdapter.js +331 -0
- package/dist/workstation/acp/test/scriptAgent.js +214 -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/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1289 -544
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +172 -8
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +393 -12
- package/dist/workstation/daemonctl.js +103 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -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/credentials.js +142 -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/{fileOps.js → files/text.js} +152 -39
- 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 +124 -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 +289 -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 +76 -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/managedInstall.js +57 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +50 -19
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +141 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +154 -93
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +11 -8
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
- package/dist/workstation/qoderAdapter/history.js +0 -87
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -93
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -77
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -1,94 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "Adoption" —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `session/
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* The plan text that spawned this task says "`session/resume(replayFrom)`". Both do not exist in
|
|
14
|
-
* `@agentclientprotocol/sdk@1.4.0`'s stable v1 entry point: `ResumeSessionRequest` has no
|
|
15
|
-
* `replayFrom` field (that name only appears under `@agentclientprotocol/sdk/experimental/v2`,
|
|
16
|
-
* which Global Constraint 1 forbids importing), and — more fundamentally — `resumeSession`'s own
|
|
17
|
-
* doc comment in `dist/acp.d.ts` says it "Resumes an existing session **without** replaying the
|
|
18
|
-
* message history (**unlike** `session/load`)". `session/load` (`loadSession`, gated by the
|
|
19
|
-
* top-level `AgentCapabilities.loadSession: boolean`, not `sessionCapabilities`) is the ACP v1
|
|
20
|
-
* method whose own doc comment says the agent should "Stream the entire conversation history back
|
|
21
|
-
* to the client via notifications" — i.e. it is the actual replay mechanism this file needs.
|
|
22
|
-
* `session/resume` exists for a client that already has a session's history cached elsewhere and
|
|
23
|
-
* wants to keep working without paying for a redundant replay; Axom has never seen an adopted
|
|
24
|
-
* session's history before, so that case never applies here.
|
|
25
|
-
*
|
|
26
|
-
* Verified both target adapters actually support the method this file calls: `claude-agent-acp`
|
|
27
|
-
* 0.70.0 and `codex-acp` 1.6.2 both declare `agentCapabilities.loadSession: true` and both
|
|
28
|
-
* implement `session/load` (`.superpowers/research/acp-sdk-registry.md`, "VERIFIED" section).
|
|
29
|
-
*
|
|
30
|
-
* ## Observing the replayed `session/update` notifications
|
|
31
|
-
*
|
|
32
|
-
* Unlike `session/new` (Task 4's `AcpSession`, built on the SDK's `ClientContext.buildSession(...)`
|
|
33
|
-
* convenience wrapper), the SDK has no `SessionBuilder`-equivalent helper for `session/load` — the
|
|
34
|
-
* only public, supported way to see raw `session/update` notifications outside of that helper is a
|
|
35
|
-
* `ClientApp`-level `onNotification(methods.client.session.update, ...)` handler (the pattern the
|
|
36
|
-
* SDK's own `acp.test.js` uses throughout for exactly this). That has to be registered before
|
|
37
|
-
* `ClientApp.connect()` copies its handler table (`acpClient.ts`'s `ConnectAgentOptions.configure`
|
|
38
|
-
* doc comment; `permission.ts`'s file doc comment says the same for `session/request_permission`).
|
|
39
|
-
* {@link createReplayCapture}/{@link ReplayCapture.register} exist for that: a caller passes
|
|
40
|
-
* `capture.register` into `connectAgent`'s `configure` hook — composable with `permission.ts`'s
|
|
41
|
-
* `registerPermissionHandlers` in the same hook, since `configure` is one function a caller can do
|
|
42
|
-
* several registrations inside of — and later, once the connection exists, passes the *same*
|
|
43
|
-
* `capture` into {@link replayNativeSession}. The registered handler sees *every* `session/update`
|
|
44
|
-
* notification on the connection, not just the one being replayed (verified against
|
|
45
|
-
* `SessionUpdateRouter.handleMessage`, which always returns `Handled.no(message)` — it never consumes
|
|
46
|
-
* the message, so dispatch keeps walking the handler chain to whatever else is registered, including
|
|
47
|
-
* the SDK's own internal `session/new`-scoped router for a live `AcpSession` on the *same* connection).
|
|
48
|
-
* That is exactly the axis review found this file getting wrong in fix round 1: a stray comment here
|
|
49
|
-
* used to say this "never interferes with a live `AcpSession` on some other connection" — true, but
|
|
50
|
-
* irrelevant, since the real conflict is a live `AcpSession` on the *same* connection (the shape a
|
|
51
|
-
* caller composing this with `registerPermissionHandlers` in one `configure` hook produces routinely).
|
|
52
|
-
* Without gating, the handler above would buffer *every* session's updates forever — only
|
|
53
|
-
* {@link ReplayCapture.take} ever releases a buffered entry, and it is only ever called for the one
|
|
54
|
-
* `sessionId` being replayed, so any other session's notifications arriving on this connection (most
|
|
55
|
-
* concretely: a live `AcpSession` opened on the same connection right after adoption, which Task 8
|
|
56
|
-
* needs) would accumulate unbounded for the life of the connection. {@link ReplayCapture.arm} exists
|
|
57
|
-
* to close that: the handler now only buffers a `sessionId` that has been armed, {@link
|
|
58
|
-
* replayNativeSession} arms its target *before* sending `session/load`, and `take()` disarms on its
|
|
59
|
-
* way out — so the buffer's upper bound is whatever is currently being replayed, never every session
|
|
60
|
-
* this connection has ever seen an update for.
|
|
61
|
-
*
|
|
62
|
-
* ## Replay goes through the same mapping and folding the live path uses — reimplemented, not shared
|
|
63
|
-
*
|
|
64
|
-
* {@link replayNativeSession} maps every replayed `SessionUpdate` through the exact same
|
|
65
|
-
* `mapSessionUpdate` (`sessionUpdateMapper.ts`) the live path (`AcpSession`) uses, and every mapped
|
|
66
|
-
* event through the same `validateWireEvent` (`validate.ts`) outbound check — so it can never
|
|
67
|
-
* emit anything the server can't decode, and (per `mapSessionUpdate`'s own contract) can never emit
|
|
68
|
-
* `final_result`. What it does NOT reuse is `AcpSession`'s own `agent_message` synthesis /
|
|
69
|
-
* synthesis: that lives in `AcpSession` as push-based, event-sink-driven instance state (see its
|
|
70
|
-
* class doc comment), a different execution shape than this file's pull-based `AsyncGenerator` over
|
|
71
|
-
* an already-complete update list. Task 4's report flagged this by name: "the replay path needs its
|
|
72
|
-
* own equivalent folding and message-completion logic — do not assume the live path's state machine
|
|
73
|
-
* is doing it for you." {@link foldReplayEvents} is that equivalent, reimplemented as a single pass
|
|
74
|
-
* over a finished array instead of instance fields mutated across live callbacks.
|
|
75
|
-
*
|
|
76
|
-
* **The `user_message` half of that duplication is gone.** This file used to carry its own copy of
|
|
77
|
-
* `AcpSession.foldUserMessageChunk`, and the two had already drifted: replay flushed a buffered user
|
|
78
|
-
* message on *any* intervening event, the live path only on a different `messageId` or turn end, so
|
|
79
|
-
* the same transcript produced a different number of cards depending on which path replayed it.
|
|
80
|
-
* Phase 7 moved the fold to `CoworkEngine`, which already buffered `agent_message_chunk` by exactly
|
|
81
|
-
* the ACP rule that covers all three chunk kinds (`ContentChunk`, `types.gen.d.ts:3456`).
|
|
82
|
-
* `user_message_chunk` now passes through here untouched, and `ExternalAgentEventApplier`'s
|
|
83
|
-
* `onBackfillDone` is what closes the last buffered message.
|
|
2
|
+
* "Adoption" — the native agent sessions that already exist on this machine, as an agent lists them, and deleting them.
|
|
3
|
+
* Short-lived connections only (`connectAgent`); both ACP methods are **optional on the agent** and gated by
|
|
4
|
+
* `AcpConnection.capabilities`: {@link listNativeSessions} calls `session/list` (gated on `capabilities.list`),
|
|
5
|
+
* {@link deleteNativeSessions} calls `session/delete` (gated on `capabilities.delete`). Both degrade to "nothing to show" /
|
|
6
|
+
* `unsupported` rather than an error when the method is not advertised — carried forward verbatim from the deleted
|
|
7
|
+
* `bridges/types.ts`'s `AgentBridge.listNativeSessions` contract ("the directory hides the grouping, not an error").
|
|
8
|
+
*
|
|
9
|
+
* Replaying an adopted session's history (`session/load`) is the processor's (`processor/updates.ts`, the history window
|
|
10
|
+
* the daemon opens with `agent_session_open{mode:'load'}`); the replay helpers this file used to carry were deleted with the
|
|
11
|
+
* old live path (Task 29).
|
|
84
12
|
*/
|
|
85
|
-
import { randomUUID } from 'node:crypto';
|
|
86
13
|
import { methods } from '@agentclientprotocol/sdk';
|
|
87
|
-
import { validateWireEvent } from './validate.js';
|
|
88
|
-
import { SessionMapperScopes, scopeKey } from './sessionScope.js';
|
|
89
|
-
import { mapSessionUpdate } from './sessionUpdateMapper.js';
|
|
90
|
-
import { newSessionMetaFor } from './activityReporting.js';
|
|
91
|
-
import { buildSessionMcpServers } from './delegationMcp.js';
|
|
92
14
|
/** `NativeSessionInfo.title` is truncated to this many characters — mirrors the deleted `claudeCode/adoption.ts`'s display-safety guard (`TITLE_MAX_LENGTH`). */
|
|
93
15
|
const TITLE_MAX_LENGTH = 80;
|
|
94
16
|
/** `SessionInfo.updatedAt` is an ISO 8601 string or `null`/absent; missing or unparseable sorts as oldest (0) rather than throwing. */
|
|
@@ -144,311 +66,3 @@ export async function listNativeSessions(connection, query) {
|
|
|
144
66
|
.sort((a, b) => b.updatedAtEpochMs - a.updatedAtEpochMs);
|
|
145
67
|
return response.nextCursor != null ? { sessions, nextCursor: response.nextCursor } : { sessions };
|
|
146
68
|
}
|
|
147
|
-
export function createReplayCapture() {
|
|
148
|
-
const armed = new Set();
|
|
149
|
-
const buffered = new Map();
|
|
150
|
-
const listeners = new Map();
|
|
151
|
-
const takeScoped = (sessionId) => {
|
|
152
|
-
armed.delete(sessionId);
|
|
153
|
-
const list = buffered.get(sessionId) ?? [];
|
|
154
|
-
buffered.delete(sessionId);
|
|
155
|
-
return list;
|
|
156
|
-
};
|
|
157
|
-
const capture = (sessionId, notification) => {
|
|
158
|
-
if (!armed.has(sessionId))
|
|
159
|
-
return false;
|
|
160
|
-
const list = buffered.get(sessionId) ?? [];
|
|
161
|
-
list.push(notification);
|
|
162
|
-
buffered.set(sessionId, list);
|
|
163
|
-
return true;
|
|
164
|
-
};
|
|
165
|
-
return {
|
|
166
|
-
capture,
|
|
167
|
-
takeScoped,
|
|
168
|
-
register(app) {
|
|
169
|
-
app.onNotification(methods.client.session.update, (ctx) => {
|
|
170
|
-
// Armed wins over listening: while a session/load replay is in flight, its history must land
|
|
171
|
-
// in the buffer rather than in a live session's read loop — see `listen`'s doc comment.
|
|
172
|
-
if (capture(ctx.params.sessionId, ctx.params))
|
|
173
|
-
return;
|
|
174
|
-
// Neither armed nor listened to: dropped, so this handler cannot accumulate every session
|
|
175
|
-
// the connection has ever seen an update for (see the file doc comment).
|
|
176
|
-
listeners.get(ctx.params.sessionId)?.(ctx.params.update);
|
|
177
|
-
});
|
|
178
|
-
},
|
|
179
|
-
arm(sessionId) {
|
|
180
|
-
armed.add(sessionId);
|
|
181
|
-
},
|
|
182
|
-
take(sessionId) {
|
|
183
|
-
return takeScoped(sessionId).map(({ update }) => update);
|
|
184
|
-
},
|
|
185
|
-
count(sessionId) {
|
|
186
|
-
return buffered.get(sessionId)?.length ?? 0;
|
|
187
|
-
},
|
|
188
|
-
listen(sessionId, sink) {
|
|
189
|
-
listeners.set(sessionId, sink);
|
|
190
|
-
return () => {
|
|
191
|
-
// Only clear our own entry: a later listen() for the same sessionId may already have
|
|
192
|
-
// replaced it, and unsubscribing then would silence the session that is actually running.
|
|
193
|
-
if (listeners.get(sessionId) === sink)
|
|
194
|
-
listeners.delete(sessionId);
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
/** `payload[key]` narrowed to `string`, or `null` — same helper `acpSession.ts` uses for the identical reason (`payload` is `Record<string, unknown>` by construction). */
|
|
200
|
-
function stringField(payload, key) {
|
|
201
|
-
const value = payload[key];
|
|
202
|
-
return typeof value === 'string' ? value : null;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Map a completed list of replayed `SessionUpdate`s to wire events, applying the same
|
|
206
|
-
* message-completion synthesis {@link AcpSession} applies to the live stream — see the file doc
|
|
207
|
-
* comment for why this is a reimplementation, not a shared call, and `acpSession.ts`'s
|
|
208
|
-
* `trackMessageBoundary`/`flushMessageCompleted` for the rule being mirrored:
|
|
209
|
-
*
|
|
210
|
-
* - `agent_message` is synthesized whenever the streaming `agent_message_chunk`/`agent_thought_chunk`
|
|
211
|
-
* `messageId` changes, and once more at the very end of the list (there is no live "turn end"
|
|
212
|
-
* signal to hang this off during replay — a whole adopted session's history has no `PromptResponse`
|
|
213
|
-
* in it at all — so "end of the replayed list" is this function's equivalent of `AcpSession.finishTurn`'s
|
|
214
|
-
* unconditional turn-end flush).
|
|
215
|
-
* - A `user_message_chunk` still forces that synthesis: a user turn beginning means whatever the
|
|
216
|
-
* assistant was streaming has necessarily already ended. The chunk itself is emitted verbatim —
|
|
217
|
-
* folding it is the server's job now (see the file doc comment).
|
|
218
|
-
*
|
|
219
|
-
* Exported directly (not just through {@link replayNativeSession}) so its synthesis behavior can be
|
|
220
|
-
* pinned by unit tests with no process spawning involved — the process-spawning tests are reserved
|
|
221
|
-
* for what only a real subprocess can prove (capability gating, the actual RPC shape, ordering
|
|
222
|
-
* against the real `session/load` response).
|
|
223
|
-
*/
|
|
224
|
-
export function foldReplayEvents(updates) {
|
|
225
|
-
return foldScopedReplayEvents(updates.map((update) => ({ sessionId: 'root', update })), 'root');
|
|
226
|
-
}
|
|
227
|
-
/** Fold each origin independently, without reordering interleaved native notifications. */
|
|
228
|
-
export function foldScopedReplayEvents(updates, rootNativeSessionId) {
|
|
229
|
-
const events = [];
|
|
230
|
-
const scopes = new SessionMapperScopes();
|
|
231
|
-
const rootRuntimeId = randomUUID();
|
|
232
|
-
const messages = new Map();
|
|
233
|
-
const emit = (event, nativeSessionId) => {
|
|
234
|
-
events.push(nativeSessionId === rootNativeSessionId ? event : { ...event, acpSessionId: nativeSessionId });
|
|
235
|
-
};
|
|
236
|
-
const flush = (key) => {
|
|
237
|
-
const message = messages.get(key);
|
|
238
|
-
if (!message)
|
|
239
|
-
return;
|
|
240
|
-
emit({ eventName: 'agent_message', payload: { messageId: message.messageId ?? 'replay' } }, message.nativeSessionId);
|
|
241
|
-
messages.delete(key);
|
|
242
|
-
};
|
|
243
|
-
for (const { sessionId: nativeSessionId, update } of updates) {
|
|
244
|
-
const origin = { rootRuntimeId, nativeSessionId };
|
|
245
|
-
const key = scopeKey(origin);
|
|
246
|
-
for (const event of mapSessionUpdate(update, scopes.get(origin))) {
|
|
247
|
-
if (event.eventName === 'user_message_chunk')
|
|
248
|
-
flush(key);
|
|
249
|
-
if (event.eventName === 'agent_message_chunk' || event.eventName === 'agent_thought_chunk') {
|
|
250
|
-
const messageId = stringField(event.payload, 'messageId');
|
|
251
|
-
const previous = messages.get(key);
|
|
252
|
-
if (previous && messageId !== null && previous.messageId !== null && messageId !== previous.messageId)
|
|
253
|
-
flush(key);
|
|
254
|
-
messages.set(key, { nativeSessionId, messageId: messageId ?? messages.get(key)?.messageId ?? null });
|
|
255
|
-
}
|
|
256
|
-
emit(event, nativeSessionId);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
for (const key of messages.keys())
|
|
260
|
-
flush(key);
|
|
261
|
-
scopes.clear();
|
|
262
|
-
return events;
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Wait until the `session/update` notifications an agent sent *before* answering a request have
|
|
266
|
-
* actually been **dispatched**, which is **not** implied by that request having responded.
|
|
267
|
-
*
|
|
268
|
-
* The SDK handles the two message kinds on different schedules (`jsonrpc.js`'s
|
|
269
|
-
* `receiveWireMessage`): a response is applied **synchronously** via `handleResponse` the moment the
|
|
270
|
-
* reader pulls it off the stream, while a notification is handed to `processIncomingMessage`, which
|
|
271
|
-
* is *not* awaited by the read loop and which awaits each registered handler in turn. So even though
|
|
272
|
-
* the agent writes every `session/update` before its response — and the reader therefore reads them
|
|
273
|
-
* first — the response's `await` can resume before a single one of our handlers has run. Measured
|
|
274
|
-
* directly on both request paths that have this shape:
|
|
275
|
-
*
|
|
276
|
-
* - `session/load` ({@link replayNativeSession}): the order observed was `response`, then the
|
|
277
|
-
* updates, so `take()` on the spot returned a partial history, or none at all.
|
|
278
|
-
* - `session/prompt` (`acpSession.ts`'s `AcpSession.attach`): the turn's terminal `stop` message was
|
|
279
|
-
* enqueued **ahead of** the tail of the same turn's deltas — `agent_message` and the
|
|
280
|
-
* `TurnResult` landing mid-stream, with the remaining text arriving as orphans after the item the
|
|
281
|
-
* server had already closed.
|
|
282
|
-
*
|
|
283
|
-
* The difference between "works" and "reorders" is nothing but how many handlers happen to be
|
|
284
|
-
* registered on the connection — which is why the same code looked correct until the manager wired
|
|
285
|
-
* permission handlers onto the same `ClientApp`.
|
|
286
|
-
*
|
|
287
|
-
* The one guarantee that survives is ordering *into* the dispatcher: by the time the response is
|
|
288
|
-
* applied, every preceding notification's dispatch has been **started**. This waits for a macrotask
|
|
289
|
-
* boundary, which drains a chain of synchronous handlers' microtask continuations, and keeps waiting
|
|
290
|
-
* while `count()` is still growing.
|
|
291
|
-
*
|
|
292
|
-
* ## This is a heuristic. It is bounded **and** it truncates — three ways, all measured
|
|
293
|
-
*
|
|
294
|
-
* Nothing enforces the premise it rests on, so read these as live failure modes rather than as
|
|
295
|
-
* theoretical corners:
|
|
296
|
-
*
|
|
297
|
-
* | Condition | Measured outcome |
|
|
298
|
-
* |---|---|
|
|
299
|
-
* | Any handler on the chain awaits a **macrotask** (even `setTimeout(…, 0)`) | 6 updates → **0** replayed |
|
|
300
|
-
* | The agent answers **before** it has finished streaming | 6 updates → **4** replayed |
|
|
301
|
-
* | The stream outlasts `maxRounds` | 3000 updates → **2086** replayed |
|
|
302
|
-
*
|
|
303
|
-
* The first is why "every handler on this chain is synchronous" is a **prerequisite**, and why it is
|
|
304
|
-
* written down at the place that actually establishes it — `acpAgents.ts`'s `AcpAgents.open`
|
|
305
|
-
* `configure` hook, the one site where handlers get registered — rather than only here. A function
|
|
306
|
-
* cannot enforce a property of a registration it never sees.
|
|
307
|
-
*
|
|
308
|
-
* The second is outside this function's reach entirely: an agent that responds mid-stream has broken
|
|
309
|
-
* the ordering guarantee this is built on, and no amount of waiting recovers it.
|
|
310
|
-
*
|
|
311
|
-
* The third is `maxRounds`, the backstop against an agent that keeps streaming forever — a wait with
|
|
312
|
-
* no exit would hang the whole open. It is reachable in practice, so hitting it is **logged** rather
|
|
313
|
-
* than returned quietly: that diagnostic is the whole line between "bounded degradation" and
|
|
314
|
-
* "invisible data loss", and it is the only one of the three this function can see happening to
|
|
315
|
-
* itself.
|
|
316
|
-
*
|
|
317
|
-
* `count` is supplied by the caller rather than read off a `ReplayCapture` because the two callers
|
|
318
|
-
* count different things: the replay path counts buffered updates for one armed `sessionId`, the
|
|
319
|
-
* live path counts updates delivered into an attached session's queue.
|
|
320
|
-
*/
|
|
321
|
-
export async function settleUpdates(sessionId, count, maxRounds = 10) {
|
|
322
|
-
let previous = -1;
|
|
323
|
-
for (let round = 0; round < maxRounds; round += 1) {
|
|
324
|
-
const before = count();
|
|
325
|
-
// setImmediate, not setTimeout(0): it runs in the check phase, after the poll phase has drained
|
|
326
|
-
// whatever the transport already had readable, so one turn covers both the microtasks and any
|
|
327
|
-
// further messages sitting in the socket buffer.
|
|
328
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
329
|
-
const after = count();
|
|
330
|
-
if (after === before && after === previous)
|
|
331
|
-
return;
|
|
332
|
-
previous = before;
|
|
333
|
-
}
|
|
334
|
-
// Falling out of the loop is not "settled" — it is "gave up while the agent was still streaming".
|
|
335
|
-
// Whatever arrives from here on may be reordered against the turn/replay boundary this call was
|
|
336
|
-
// holding, or dropped outright. Never silent: a session that quietly loses the tail of its history
|
|
337
|
-
// is indistinguishable from an agent that had no history to give.
|
|
338
|
-
console.error(`[acp] session/update stream for '${sessionId}' was still growing after ${maxRounds} settle rounds ` +
|
|
339
|
-
`(${count()} updates so far) — continuing anyway; later updates may be reordered or lost.`);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Replay one native session's history as wire events via `session/load` — see the file doc comment
|
|
343
|
-
* for why `session/load`, not `session/resume`.
|
|
344
|
-
*
|
|
345
|
-
* When the agent doesn't advertise `agentCapabilities.loadSession` (`connection.capabilities.loadSession`),
|
|
346
|
-
* this yields nothing **without ever calling `session/load`** — same "hidden grouping, not an error"
|
|
347
|
-
* contract as {@link listNativeSessions}, applied to the capability that actually gates replay.
|
|
348
|
-
*
|
|
349
|
-
* Every yielded event has already gone through {@link foldReplayEvents} (same mapping the live path
|
|
350
|
-
* uses, plus the same `agent_message` synthesis) and `validateWireEvent` (same outbound check the live path's
|
|
351
|
-
* `permission_request` events go through) — an event that fails validation is dropped with a
|
|
352
|
-
* diagnostic, never thrown, matching `validate.ts`'s own "drop + diagnostic, never an
|
|
353
|
-
* exception" design. `mapSessionUpdate` never produces `final_result` (Task 4's contract, unchanged
|
|
354
|
-
* here since this function adds no new event-producing path of its own) and `foldReplayEvents` adds
|
|
355
|
-
* no new event names beyond what `mapSessionUpdate` and this file's own `agent_message` synthesis
|
|
356
|
-
* already produce — both already appear in the server's understood vocabulary
|
|
357
|
-
* (`sessionUpdateMapper.test.ts`'s `SERVER_UNDERSTOOD_EVENT_NAMES`), so nothing here needs to extend
|
|
358
|
-
* it.
|
|
359
|
-
*
|
|
360
|
-
* ## `session/load` also activates the session, not just replays it — and Task 8 relies on that
|
|
361
|
-
*
|
|
362
|
-
* The `session/load` request below sends the current MCP list. That is not incidental — `session/load`
|
|
363
|
-
* is not a read-only "give me the history" call: per its own spec, the agent restores this
|
|
364
|
-
* `sessionId` as its **current** session, the same way `session/new` would, just with history
|
|
365
|
-
* attached instead of starting empty.
|
|
366
|
-
*
|
|
367
|
-
* Task 8 resolved the fork this once handed forward: **Axom keeps prompting on
|
|
368
|
-
* `params.nativeSessionId`, the session `session/load` just activated**, rather than opening a fresh
|
|
369
|
-
* `session/new` and treating the replay as read-only history. `agentSessions.ts` receives a
|
|
370
|
-
* `nativeSessionId` on reconnect as well as on adoption (the server persists it and hands it back),
|
|
371
|
-
* so "open a new session and replay read-only" would silently drop the agent's own context on every
|
|
372
|
-
* single reconnect — a regression against the deleted Claude Code bridge, which really did resume.
|
|
373
|
-
* `AcpSession.attach` (`acpSession.ts`) is what that costs: the SDK's `ActiveSession` has a private
|
|
374
|
-
* constructor reachable only through `SessionBuilder.start()`, i.e. only through `session/new`, so
|
|
375
|
-
* driving an already-loaded `sessionId` means hand-building the same small `DrivenSession` surface
|
|
376
|
-
* around raw `session/prompt` / `session/cancel` requests and {@link ReplayCapture.listen}.
|
|
377
|
-
*
|
|
378
|
-
* Both paths use `buildSessionMcpServers`, so an adopted or reconnected session receives the same
|
|
379
|
-
* user servers and a freshly issued delegation lease when managed delegation is enabled.
|
|
380
|
-
*
|
|
381
|
-
* ## Also opts claude into activity reporting, the same as `session/new` — closing a real gap, not a cosmetic one
|
|
382
|
-
*
|
|
383
|
-
* `AcpSession.open`'s `session/new` request carries `_meta.claudeCode.emitRawSDKMessages`
|
|
384
|
-
* (`newSessionMetaFor`, `activityReporting.ts`) so claude's collector pushes
|
|
385
|
-
* `extNotification("_claude/sdkMessage", …)` back on the connection. `session/load` used to send no
|
|
386
|
-
* `_meta` at all, so every session reached through this function — every adoption, and every
|
|
387
|
-
* reconnect after a daemon restart, since a fresh child process's session table is always empty and
|
|
388
|
-
* therefore always takes the `createSession` branch of `getOrCreateSession` — permanently lost
|
|
389
|
-
* activity reporting the moment it was (re)loaded, while `agent_opened.reportsActivity` kept
|
|
390
|
-
* claiming `true`. Verified directly against the installed `claude-agent-acp@0.79.0` adapter
|
|
391
|
-
* (`dist/acp-agent.js`): `loadSession` → `getOrCreateSession` → (no matching in-process session, the
|
|
392
|
-
* case that always applies to a freshly spawned agent) → `createSession({ …, _meta: params._meta },
|
|
393
|
-
* { resume: params.sessionId })`, and `createSession` reads
|
|
394
|
-
* `sessionMeta?.claudeCode?.emitRawSDKMessages` off that same `_meta` exactly as `session/new` does.
|
|
395
|
-
* `newSessionMetaFor` is reused verbatim rather than re-implementing the same agent-name check here,
|
|
396
|
-
* so there is exactly one place that decides which agents opt in.
|
|
397
|
-
*/
|
|
398
|
-
export async function* replayNativeSession(connection, capture, params) {
|
|
399
|
-
if (!connection.capabilities.loadSession)
|
|
400
|
-
return;
|
|
401
|
-
// Exclusive ownership must precede every mutation of the shared capture/known-session state.
|
|
402
|
-
// A rejected concurrent load must not consume or disarm the active owner's buffer.
|
|
403
|
-
const releaseCapture = connection.captureSessionUpdates((notification) => capture.capture(params.nativeSessionId, notification));
|
|
404
|
-
// Arm before requesting — the agent may start sending session/update notifications for this
|
|
405
|
-
// sessionId as soon as it receives the request, and the capture's handler drops anything for a
|
|
406
|
-
// sessionId that isn't armed yet (see ReplayCapture.arm's doc comment).
|
|
407
|
-
capture.arm(params.nativeSessionId);
|
|
408
|
-
// The transport diverts updates for a session it does not know is handled here, because the SDK's
|
|
409
|
-
// router would otherwise drop them on the floor. During a replay this capture *is* the handler,
|
|
410
|
-
// so the claim has to be made explicitly — and withdrawn in the same `finally` that disarms.
|
|
411
|
-
const unmarkSession = connection.markSessionKnown(params.nativeSessionId);
|
|
412
|
-
// Same opt-in `session/new` sends, on the request that actually reaches the agent for every
|
|
413
|
-
// adoption/reconnect — see the doc comment above. `undefined` for every non-claude agent, in which
|
|
414
|
-
// case the spread below adds nothing, so a non-claude `session/load` is byte-for-byte the request
|
|
415
|
-
// it always sent.
|
|
416
|
-
const meta = newSessionMetaFor(connection.agentInfo?.name);
|
|
417
|
-
let updates;
|
|
418
|
-
try {
|
|
419
|
-
const mcpServers = buildSessionMcpServers(params.mcpServers, params.delegationMcp);
|
|
420
|
-
if (params.delegationMcp)
|
|
421
|
-
connection.redactStderrSecret(Object.values(params.delegationMcp.headers)[0]);
|
|
422
|
-
const response = await connection.connection.agent.request(methods.agent.session.load, {
|
|
423
|
-
sessionId: params.nativeSessionId,
|
|
424
|
-
cwd: params.cwd,
|
|
425
|
-
mcpServers,
|
|
426
|
-
...(meta ? { _meta: meta } : {}),
|
|
427
|
-
});
|
|
428
|
-
params.onLoaded?.(response);
|
|
429
|
-
// The response resolving is NOT the end of the replay — see {@link settleUpdates} for the SDK
|
|
430
|
-
// scheduling that makes `take()` on the spot return a partial history.
|
|
431
|
-
await settleUpdates(params.nativeSessionId, () => capture.count(params.nativeSessionId));
|
|
432
|
-
}
|
|
433
|
-
finally {
|
|
434
|
-
releaseCapture();
|
|
435
|
-
unmarkSession();
|
|
436
|
-
// In a `finally` because `arm()` has no other exit. A rejected `session/load` — a stale id the
|
|
437
|
-
// agent has since deleted, the case `AcpAgents.startSession` catches and falls back from — used
|
|
438
|
-
// to throw straight past `take()`, leaving the id armed and whatever had already been buffered
|
|
439
|
-
// for it held until the connection died. That contradicts this file's own claim that "the
|
|
440
|
-
// buffer's upper bound is whatever is currently being replayed": the fallback opens a *different*
|
|
441
|
-
// session id, so nothing would ever come back to take this one. `take()` is both halves at once
|
|
442
|
-
// (release the buffer, disarm), which is why the failure path calls the same function the
|
|
443
|
-
// success path does rather than a disarm of its own.
|
|
444
|
-
updates = capture.takeScoped(params.nativeSessionId);
|
|
445
|
-
}
|
|
446
|
-
for (const event of foldScopedReplayEvents(updates, params.nativeSessionId)) {
|
|
447
|
-
const rejection = validateWireEvent(event);
|
|
448
|
-
if (rejection) {
|
|
449
|
-
console.error(`[acp] dropping a replayed event that fails validateWireEvent: ${rejection.reason} (session ${params.nativeSessionId})`);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
yield event;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
@@ -29,13 +29,14 @@
|
|
|
29
29
|
* but the result says so via `checksumVerified: false`. Nothing here may report an unverified
|
|
30
30
|
* download as checked; that is precisely the distinction {@link InstallOutcome} exists to preserve.
|
|
31
31
|
*
|
|
32
|
-
* ## Layout: several versions can coexist
|
|
32
|
+
* ## Layout: several versions can coexist, and each appears whole or not at all
|
|
33
33
|
*
|
|
34
|
-
* Installs land at `<root
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Installs land at `<root>/bin/<registryAgent>/<version>/` (spec 2026-10-11 §6.2), one directory per
|
|
35
|
+
* version. The archive is unpacked into `<root>/.staging/<uuid>/` and only a complete tree is
|
|
36
|
+
* `rename(2)`d into place, so a directory under `bin/` always holds a whole install — an unpack that
|
|
37
|
+
* is killed half-way leaves only a staging leftover, never a half-written version that something
|
|
38
|
+
* would take for installed. Which version an agent *launches* is no longer chosen here: it is the
|
|
39
|
+
* agent's `current` build (`builds/store.ts`). {@link installedBinaryVersion} only enumerates.
|
|
39
40
|
*
|
|
40
41
|
* ## Unpacking without a new dependency
|
|
41
42
|
*
|
|
@@ -65,12 +66,14 @@
|
|
|
65
66
|
* `<installDir>/<basename of the archive URL>` and marked executable — that filename is exactly
|
|
66
67
|
* what `BinaryTarget.cmd` refers to. See {@link isArchiveFile}.
|
|
67
68
|
*/
|
|
68
|
-
import { createHash } from 'node:crypto';
|
|
69
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
69
70
|
import { createWriteStream, promises as fs, readdirSync } from 'node:fs';
|
|
70
71
|
import { tmpdir } from 'node:os';
|
|
71
72
|
import path from 'node:path';
|
|
72
73
|
import { Readable } from 'node:stream';
|
|
73
74
|
import { pipeline } from 'node:stream/promises';
|
|
75
|
+
import { binaryBuildDir, inside } from './builds/model.js';
|
|
76
|
+
import { publishStaged } from './builds/store.js';
|
|
74
77
|
import { pickInstalledVersion } from './installProbe.js';
|
|
75
78
|
import { runTimedCommand } from './timedCommand.js';
|
|
76
79
|
const PLATFORM_SEGMENT = {
|
|
@@ -122,12 +125,13 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
122
125
|
export function installedBinaryVersion(registryAgent, root, pinned) {
|
|
123
126
|
let entries;
|
|
124
127
|
try {
|
|
125
|
-
entries = readdirSync(path.join(root, registryAgent), { withFileTypes: true });
|
|
128
|
+
entries = readdirSync(path.join(root, 'bin', registryAgent), { withFileTypes: true });
|
|
126
129
|
}
|
|
127
130
|
catch {
|
|
128
131
|
return undefined;
|
|
129
132
|
}
|
|
130
|
-
|
|
133
|
+
// Dot-directories (`.staging`, `.trash`, editor droppings) are never versions.
|
|
134
|
+
const versions = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')).map((entry) => entry.name);
|
|
131
135
|
return pickInstalledVersion(versions, pinned);
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
@@ -203,9 +207,40 @@ const ARCHIVE_DOWNLOAD_TIMEOUT_MS = 300_000;
|
|
|
203
207
|
* longer than the download step right above it is already allowed to take.
|
|
204
208
|
*/
|
|
205
209
|
const ARCHIVE_EXTRACT_TIMEOUT_MS = 300_000;
|
|
210
|
+
/**
|
|
211
|
+
* Attempts made on the *request* phase of one archive download. Registry archives are
|
|
212
|
+
* third-party-hosted, and the failure this retries — the initial `fetch` throwing — is the
|
|
213
|
+
* transient kind: an edge node that answers this minute and not the next, a resolver rotating
|
|
214
|
+
* between a real and a dead address. Deterministic failures (redirect policy, loops, HTTP codes)
|
|
215
|
+
* come back from {@link fetchArchive} as structured results and are never retried. Both attempts
|
|
216
|
+
* share one {@link ARCHIVE_DOWNLOAD_TIMEOUT_MS} signal, so the whole-request budget never doubles;
|
|
217
|
+
* undici's own ~10s connect bound keeps the added worst case small.
|
|
218
|
+
*/
|
|
219
|
+
const ARCHIVE_FETCH_ATTEMPTS = 2;
|
|
220
|
+
/** Pause between the two fetch attempts — long enough for a flipped resolver answer, short next to a 300s budget. */
|
|
221
|
+
const ARCHIVE_FETCH_RETRY_PAUSE_MS = 2_000;
|
|
206
222
|
function errorMessage(err) {
|
|
207
223
|
return err instanceof Error ? err.message : String(err);
|
|
208
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* `fetch`'s failures arrive as a bare `fetch failed` with the real reason (a connect timeout, a
|
|
227
|
+
* reset mid-handshake, a TLS refusal — classes that lead the diagnosis, not decorate it) one or
|
|
228
|
+
* two `cause` links down, and undici names them with stable machine codes (`UND_ERR_CONNECT_TIMEOUT`,
|
|
229
|
+
* `ECONNRESET`). This walks the chain and joins it onto the message, so a daemon log and an error
|
|
230
|
+
* card say what actually happened instead of leaving a one-word verdict. Observed 2026-10-14: a
|
|
231
|
+
* machine whose route to `dl.google.com` had died logged only "fetch failed" and cost a DNS/CDN
|
|
232
|
+
* investigation that `UND_ERR_CONNECT_TIMEOUT` would have named on the spot.
|
|
233
|
+
*/
|
|
234
|
+
function describeFetchFailure(err) {
|
|
235
|
+
const parts = [errorMessage(err)];
|
|
236
|
+
let cause = err?.cause;
|
|
237
|
+
for (let depth = 0; depth < 5 && cause !== undefined && cause !== null; depth += 1) {
|
|
238
|
+
const named = cause;
|
|
239
|
+
parts.push(typeof named.code === 'string' ? named.code : typeof named.message === 'string' ? named.message : String(cause));
|
|
240
|
+
cause = cause.cause;
|
|
241
|
+
}
|
|
242
|
+
return [...new Set(parts)].join(' — ');
|
|
243
|
+
}
|
|
209
244
|
function hasEntries(dir) {
|
|
210
245
|
try {
|
|
211
246
|
return readdirSync(dir).length > 0;
|
|
@@ -308,7 +343,7 @@ async function extractArchive(archivePath, destDir, exec, timeoutMs) {
|
|
|
308
343
|
}
|
|
309
344
|
/**
|
|
310
345
|
* Fetch, verify and unpack `agent`'s binary adapter for this machine's platform triple. Never
|
|
311
|
-
* starts the agent — the counterpart to `
|
|
346
|
+
* starts the agent — the counterpart to `ensurePristine`/`installUvBuild`'s "download, don't run"
|
|
312
347
|
* contract.
|
|
313
348
|
*
|
|
314
349
|
* ## The three outcomes the brief for this task requires, and why each looks the way it does
|
|
@@ -340,7 +375,13 @@ export async function installBinary(agent, opts) {
|
|
|
340
375
|
return { ok: false, error: `Agent '${agent.id}' publishes no archive for ${triple ?? `${platform}-${arch}`}.` };
|
|
341
376
|
}
|
|
342
377
|
const version = agent.version;
|
|
343
|
-
|
|
378
|
+
let installDir;
|
|
379
|
+
try {
|
|
380
|
+
installDir = inside(opts.root, binaryBuildDir(agent.id, version));
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
return { ok: false, error: errorMessage(err) };
|
|
384
|
+
}
|
|
344
385
|
if (hasEntries(installDir)) {
|
|
345
386
|
return { ok: true, checksumVerified: false, alreadyInstalled: true, version, installDir };
|
|
346
387
|
}
|
|
@@ -358,8 +399,24 @@ export async function installBinary(agent, opts) {
|
|
|
358
399
|
// whereas a `BinaryTarget.archive` is an arbitrary third-party URL verified by an optional
|
|
359
400
|
// SHA-256 the registry supplies. Different trust model, so the policy is stated here: one
|
|
360
401
|
// whole-chain budget, a capped number of hops, and HTTPS at every one. See `fetchArchive` for
|
|
361
|
-
// why following redirects is required rather than merely permitted.
|
|
362
|
-
|
|
402
|
+
// why following redirects is required rather than merely permitted. The one retry here covers
|
|
403
|
+
// only the transient request-phase throw ({@link ARCHIVE_FETCH_ATTEMPTS}).
|
|
404
|
+
const signal = AbortSignal.timeout(ARCHIVE_DOWNLOAD_TIMEOUT_MS);
|
|
405
|
+
let fetched;
|
|
406
|
+
for (let attempt = 1;; attempt += 1) {
|
|
407
|
+
try {
|
|
408
|
+
fetched = await fetchArchive(target.archive, fetchImpl, signal);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
if (attempt >= ARCHIVE_FETCH_ATTEMPTS || signal.aborted) {
|
|
413
|
+
throw new Error(describeFetchFailure(err));
|
|
414
|
+
}
|
|
415
|
+
console.warn(`[acp] archive fetch attempt ${attempt} for '${agent.id}' failed ` +
|
|
416
|
+
`(${describeFetchFailure(err)}); retrying once.`);
|
|
417
|
+
await new Promise((resolve) => setTimeout(resolve, ARCHIVE_FETCH_RETRY_PAUSE_MS));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
363
420
|
if (!fetched.ok) {
|
|
364
421
|
return { ok: false, error: `Failed to download adapter archive for '${agent.id}': ${fetched.error}` };
|
|
365
422
|
}
|
|
@@ -374,7 +431,14 @@ export async function installBinary(agent, opts) {
|
|
|
374
431
|
const hash = createHash('sha256');
|
|
375
432
|
const source = Readable.fromWeb(response.body);
|
|
376
433
|
source.on('data', (chunk) => hash.update(chunk));
|
|
377
|
-
|
|
434
|
+
// A body that dies mid-stream (a reset connection, undici's "terminated") is a download
|
|
435
|
+
// failure like any other — reported, not thrown past the caller that owns the retry UX.
|
|
436
|
+
try {
|
|
437
|
+
await pipeline(source, createWriteStream(archivePath, { mode: 0o600 }));
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return { ok: false, error: `Failed to download adapter archive for '${agent.id}': ${describeFetchFailure(err)}` };
|
|
441
|
+
}
|
|
378
442
|
if (target.sha256) {
|
|
379
443
|
const digest = hash.digest('hex');
|
|
380
444
|
if (digest.toLowerCase() !== target.sha256.toLowerCase()) {
|
|
@@ -385,23 +449,32 @@ export async function installBinary(agent, opts) {
|
|
|
385
449
|
};
|
|
386
450
|
}
|
|
387
451
|
}
|
|
388
|
-
|
|
452
|
+
// Unpacked into a staging tree on the same filesystem, then renamed into place whole: nothing
|
|
453
|
+
// under `bin/` is ever half-written (see the module doc's layout section).
|
|
454
|
+
const staged = path.join(opts.root, '.staging', randomUUID());
|
|
389
455
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
456
|
+
await fs.mkdir(staged, { recursive: true });
|
|
457
|
+
try {
|
|
458
|
+
if (await isArchiveFile(archivePath, target.archive)) {
|
|
459
|
+
await extractArchive(archivePath, staged, opts.exec ?? runCommand, ARCHIVE_EXTRACT_TIMEOUT_MS);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
// Not an archive at all — the download *is* the program (e.g. `sigit`'s Linux/Windows
|
|
463
|
+
// targets, see the module doc). Place it under the same name `BinaryTarget.cmd` refers to
|
|
464
|
+
// and mark it executable; `chmod` is a no-op on Windows but harmless there.
|
|
465
|
+
const destPath = path.join(staged, archiveFileName(target.archive));
|
|
466
|
+
await fs.copyFile(archivePath, destPath);
|
|
467
|
+
await fs.chmod(destPath, 0o755);
|
|
468
|
+
}
|
|
392
469
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// targets, see the module doc). Place it under the same name `BinaryTarget.cmd` refers to
|
|
396
|
-
// and mark it executable; `chmod` is a no-op on Windows but harmless there.
|
|
397
|
-
const destPath = path.join(installDir, archiveFileName(target.archive));
|
|
398
|
-
await fs.copyFile(archivePath, destPath);
|
|
399
|
-
await fs.chmod(destPath, 0o755);
|
|
470
|
+
catch (err) {
|
|
471
|
+
return { ok: false, error: `Failed to unpack adapter archive for '${agent.id}': ${errorMessage(err)}` };
|
|
400
472
|
}
|
|
473
|
+
// A concurrent install of the same version may have published first; its tree is equally whole.
|
|
474
|
+
await publishStaged(staged, installDir);
|
|
401
475
|
}
|
|
402
|
-
|
|
403
|
-
await fs.rm(
|
|
404
|
-
return { ok: false, error: `Failed to unpack adapter archive for '${agent.id}': ${errorMessage(err)}` };
|
|
476
|
+
finally {
|
|
477
|
+
await fs.rm(staged, { recursive: true, force: true }).catch(() => undefined);
|
|
405
478
|
}
|
|
406
479
|
return { ok: true, checksumVerified: Boolean(target.sha256), alreadyInstalled: false, version, installDir };
|
|
407
480
|
}
|