@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
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
// // before `.connect()`, since it only works if the
|
|
73
73
|
// // handler exists in time to answer this call
|
|
74
74
|
// pingResultSink?: string,
|
|
75
|
-
// requestPermissionDuringPrompt?: { toolCall: ToolCallUpdate, options: PermissionOption[] },
|
|
75
|
+
// requestPermissionDuringPrompt?: { sessionId?: string, toolCall: ToolCallUpdate, options: PermissionOption[] },
|
|
76
76
|
// // if set, session/prompt calls the client's
|
|
77
77
|
// // session/request_permission with this payload
|
|
78
78
|
// // before returning, and writes the raw response to
|
|
@@ -85,6 +85,24 @@
|
|
|
85
85
|
// // before returning, and writes the raw response to
|
|
86
86
|
// // createElicitationResultSink.
|
|
87
87
|
// createElicitationResultSink?: string,
|
|
88
|
+
// createElicitationsDuringPrompt?: object[],
|
|
89
|
+
// // the plural of the above: session/prompt calls the
|
|
90
|
+
// // client's elicitation/create once per element, IN
|
|
91
|
+
// // ORDER and ONE AT A TIME — each call is awaited
|
|
92
|
+
// // before the next is sent, so exactly one card is
|
|
93
|
+
// // ever open. That sequencing is the point: it is how
|
|
94
|
+
// // a real agent asks a follow-up question, and it is
|
|
95
|
+
// // the only way to observe one property kind's answer
|
|
96
|
+
// // without the others' cards confusing the timeline.
|
|
97
|
+
// // Each raw response is appended to the JSON array at
|
|
98
|
+
// // createElicitationResultsSink, which is REWRITTEN
|
|
99
|
+
// // after every answer rather than once at the end —
|
|
100
|
+
// // a driver (a test, or a human clicking through a
|
|
101
|
+
// // browser) has to be able to read the answer to card
|
|
102
|
+
// // N while card N+1 is still open, and a single
|
|
103
|
+
// // write at the end of the turn would only be
|
|
104
|
+
// // readable after the last card was answered.
|
|
105
|
+
// createElicitationResultsSink?: string,
|
|
88
106
|
// authenticateSink?: string, // if set, a file this process writes (the raw JSON
|
|
89
107
|
// // `authenticate` params, i.e. `{ methodId }`) the
|
|
90
108
|
// // moment it receives that request — lets a test
|
|
@@ -124,6 +142,8 @@
|
|
|
124
142
|
// // stderr-only login shape (antigravity), and it
|
|
125
143
|
// // starts at spawn on purpose, which is exactly the
|
|
126
144
|
// // window `ConnectAgentOptions.present` warns about.
|
|
145
|
+
// newSessionDelayMs?: number, // delays the session/new response
|
|
146
|
+
// hangOnSessionLoad?: boolean, // session/load never resolves, for startup cleanup tests
|
|
127
147
|
// hangOnSessionNew?: boolean, // if true, session/new never resolves — the agent
|
|
128
148
|
// // answered initialize and then went quiet on the
|
|
129
149
|
// // very next request. Distinct from hangOnInitialize:
|
|
@@ -145,14 +165,41 @@
|
|
|
145
165
|
// // 这么干的:setTimeout 0,见 acp-agent.js:756)。
|
|
146
166
|
// // 用数组的数组是因为「推几次」本身就是被测行为:
|
|
147
167
|
// // claude 会推两次(70 → 68),codex 只推一次。
|
|
168
|
+
// rejectLaterSessionNew?: string, // p3-fix7c 3: a marker path; the first session/new (across
|
|
169
|
+
// // processes) succeeds and creates it, every later one throws —
|
|
170
|
+
// // a probe retry that fails.
|
|
171
|
+
// commandsBeforeLaterSessionReply?: { commands: AvailableCommand[], markerPath: string }, // p3-fix7b M-4:
|
|
172
|
+
// // like commandsBeforeNewSessionReply, but only from the second
|
|
173
|
+
// // session/new on (counted across processes through markerPath) —
|
|
174
|
+
// // a first probe that saw nothing, then a retry the table races.
|
|
175
|
+
// commandsBeforeNewSessionReply?: AvailableCommand[], // p3-fix7 N9: one available_commands_update
|
|
176
|
+
// // sent INSIDE the session/new handler, before its reply —
|
|
177
|
+
// // the order a fast Claude adapter produced in run 5, which
|
|
178
|
+
// // reached the client before it knew its own session id.
|
|
148
179
|
// newSessionCommandsDelayMs?: number, // 每次推送之间的间隔,默认 0。让测试能把一条晚到的
|
|
149
180
|
// // 推送放在 settle 窗口的里面或外面。
|
|
181
|
+
// newSessionSink?: string, // if set, a file this process writes (the raw JSON
|
|
182
|
+
// // session/new params, including `_meta`) the moment
|
|
183
|
+
// // it receives that request — Task 5's proof that
|
|
184
|
+
// // `AcpSession.open` actually sent
|
|
185
|
+
// // `_meta.claudeCode.emitRawSDKMessages` (or did not,
|
|
186
|
+
// // for an agent that is not claude)
|
|
187
|
+
// extNotifications?: { method: string, params: unknown }[], // sent, in order, as raw
|
|
188
|
+
// // extNotification calls after session/new answers —
|
|
189
|
+
// // same "after the reply" timing as
|
|
190
|
+
// // `newSessionCommands` above. Task 5 uses this to
|
|
191
|
+
// // model claude-agent-acp's
|
|
192
|
+
// // `extNotification("_claude/sdkMessage", …)`.
|
|
193
|
+
// extNotificationDelayMs?: number, // delay between each of the above; default 0.
|
|
150
194
|
// sessions?: SessionInfo[], // session/list's response `sessions` array; default []
|
|
151
195
|
// nextCursor?: string, // session/list's response `nextCursor`
|
|
152
196
|
// sessionListSink?: string, // if set, a file this process writes (the raw JSON
|
|
153
197
|
// // session/list params, i.e. `{cwd, cursor}`) the
|
|
154
198
|
// // moment it receives that request — Task 7's proof
|
|
155
199
|
// // that listNativeSessions really sent cwd/cursor
|
|
200
|
+
// deleteSessionSink?: string, // if set, a file each session/delete request's params
|
|
201
|
+
// // are appended to as a JSON line
|
|
202
|
+
// deleteSessionFailures?: string[], // session ids whose session/delete throws internalError
|
|
156
203
|
// loadUpdates?: SessionUpdate[], // Task 7 (adoption.ts): sent as session/update
|
|
157
204
|
// // notifications during session/load, before it
|
|
158
205
|
// // returns — this is the replay mechanism ACP v1
|
|
@@ -172,6 +219,9 @@
|
|
|
172
219
|
// // the shape credentials expiring mid-conversation
|
|
173
220
|
// // actually takes, and the only failure the login
|
|
174
221
|
// // card may be drawn for
|
|
222
|
+
// exitOnPrompt?: boolean, // simulate transport loss after accepting a prompt
|
|
223
|
+
// promptRpcError?: { code: number, message: string }, // explicit remote JSON-RPC failure
|
|
224
|
+
// promptCallsSink?: string, // append-only request log for no-replay assertions
|
|
175
225
|
// promptSink?: string, // if set, a file this process overwrites with the raw
|
|
176
226
|
// // JSON session/prompt params on every prompt — the
|
|
177
227
|
// // only way to observe WHICH sessionId a prompt was
|
|
@@ -179,12 +229,18 @@
|
|
|
179
229
|
// // to a loaded session rather than opening a new one
|
|
180
230
|
// loadSessionResponse?: LoadSessionResponse, // session/load's response; default {}
|
|
181
231
|
// loadSessionSink?: string, // if set, a file this process writes (the raw JSON
|
|
182
|
-
// // session/load params) the
|
|
183
|
-
// // request — lets a test
|
|
184
|
-
// // sessionId/cwd were sent,
|
|
185
|
-
// // this and asserting the
|
|
186
|
-
// // adoption.ts never calls
|
|
187
|
-
// // capability isn't advertised
|
|
232
|
+
// // session/load params, including `_meta`) the
|
|
233
|
+
// // moment it receives that request — lets a test
|
|
234
|
+
// // prove the right sessionId/cwd/`_meta` were sent,
|
|
235
|
+
// // and (by NOT configuring this and asserting the
|
|
236
|
+
// // file never appears) that adoption.ts never calls
|
|
237
|
+
// // session/load when the capability isn't advertised
|
|
238
|
+
// loadExtNotifications?: { method: string, params: unknown }[], // Task 9's equivalent of
|
|
239
|
+
// // `extNotifications` above, but pushed after
|
|
240
|
+
// // session/load answers instead of session/new — the
|
|
241
|
+
// // shape claude-agent-acp's activity push actually
|
|
242
|
+
// // takes on the adoption/reconnect path, where no
|
|
243
|
+
// // session/new is ever sent
|
|
188
244
|
// resumeSessionResponse?: ResumeSessionResponse, // session/resume's response; default {}
|
|
189
245
|
// resumeSessionSink?: string, // same idea as loadSessionSink, for session/resume
|
|
190
246
|
// // — Task 7's proof that adoption.ts NEVER calls
|
|
@@ -206,6 +262,16 @@
|
|
|
206
262
|
// // `promptRequired`, so seeing it back means the
|
|
207
263
|
// // agent ignored the opt-in we always send
|
|
208
264
|
// steerRejectsWith?: string, // if set, the handler throws with this message
|
|
265
|
+
// asyncTaskStopSink?: string, // if set, a file this process writes (the raw JSON
|
|
266
|
+
// // `_session/async_task/stop` params) the moment it
|
|
267
|
+
// // receives that request. Also an EXTENSION, not ACP
|
|
268
|
+
// // (`claude-agent-acp` `acp-agent.js@0.79.0:165`),
|
|
269
|
+
// // routed by its literal method name for the same
|
|
270
|
+
// // reason `_session/steering` is
|
|
271
|
+
// asyncTaskStopStopped?: boolean, // what `{ stopped }` the handler answers with;
|
|
272
|
+
// // default true. `false` is what the adapter answers
|
|
273
|
+
// // for an id it has no live task under
|
|
274
|
+
// // (`acp-agent.js@0.79.0:1788`)
|
|
209
275
|
// newSessionConfigOptions?: SessionConfigOption[], // returned verbatim on session/new (types.gen.d.ts:2546)
|
|
210
276
|
// newSessionModes?: SessionModeState, // returned verbatim on session/new (:2542)
|
|
211
277
|
// setConfigOptionSink?: string, // file receiving the JSON params of each session/set_config_option
|
|
@@ -225,7 +291,7 @@
|
|
|
225
291
|
// fixture that can prove "honour the advertised capability, not the router" end to end, not just
|
|
226
292
|
// assert a boolean.
|
|
227
293
|
async function main() {
|
|
228
|
-
const { writeFileSync } = require('node:fs');
|
|
294
|
+
const { writeFileSync, appendFileSync, existsSync } = require('node:fs');
|
|
229
295
|
// Parsed and wired up before the (slow, cold-start) dynamic import below, so a caller that kills
|
|
230
296
|
// this process almost immediately after spawning it (exercising connectAgent's initialize
|
|
231
297
|
// timeout with a short deadline) still gets a SIGTERM handler in place. Registering this only
|
|
@@ -307,16 +373,56 @@ async function main() {
|
|
|
307
373
|
return {};
|
|
308
374
|
})
|
|
309
375
|
.onRequest(methods.agent.session.new, async (ctx) => {
|
|
376
|
+
if (config.echoMcpAuthorizationToStderr) {
|
|
377
|
+
const authorization = ctx.params.mcpServers.find((server) => server.name === 'axom-delegation')?.headers?.[0]?.value;
|
|
378
|
+
process.stderr.write('adapter diagnostic: started\n');
|
|
379
|
+
process.stderr.write(`configured Authorization: ${authorization?.slice(0, 9) ?? ''}`);
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
381
|
+
process.stderr.write(`${authorization?.slice(9) ?? ''}\n`);
|
|
382
|
+
process.stderr.write(`configured token: ${authorization?.slice(7) ?? ''}\n`);
|
|
383
|
+
process.stderr.write(`Open https://auth.example.test/device?token=${authorization?.slice(7) ?? ''} to continue\n`);
|
|
384
|
+
process.stderr.write('adapter diagnostic: finished\n');
|
|
385
|
+
}
|
|
310
386
|
if (config.newSessionRejectsWith)
|
|
311
387
|
throw new Error(config.newSessionRejectsWith);
|
|
312
388
|
// Checked after the rejection so the two flags stay independently exercisable; a promise that
|
|
313
389
|
// never settles is the only faithful way to model an agent that simply stops answering.
|
|
314
390
|
if (config.hangOnSessionNew)
|
|
315
391
|
return new Promise(() => { });
|
|
392
|
+
if (config.newSessionDelayMs)
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, config.newSessionDelayMs));
|
|
394
|
+
if (config.newSessionSink)
|
|
395
|
+
writeFileSync(config.newSessionSink, JSON.stringify(ctx.params));
|
|
396
|
+
for (const { method, params } of config.beforeSessionReplyNotifications ?? []) {
|
|
397
|
+
await ctx.client.notify(method, params);
|
|
398
|
+
}
|
|
316
399
|
// Pushed AFTER this handler returns, not before: the real adapter schedules its first
|
|
317
400
|
// `available_commands_update` on a `setTimeout(…, 0)` that runs once `session/new` has been
|
|
318
401
|
// answered (`acp-agent.js:752-757`). Sending them inline would let a client that only reads
|
|
319
402
|
// updates *before* the reply still pass, which is the bug this fixture exists to catch.
|
|
403
|
+
if (config.rejectLaterSessionNew) {
|
|
404
|
+
if (existsSync(config.rejectLaterSessionNew))
|
|
405
|
+
throw new Error('second session refused');
|
|
406
|
+
writeFileSync(config.rejectLaterSessionNew, 'seen');
|
|
407
|
+
}
|
|
408
|
+
if (config.commandsBeforeLaterSessionReply) {
|
|
409
|
+
const { commands, markerPath } = config.commandsBeforeLaterSessionReply;
|
|
410
|
+
if (existsSync(markerPath)) {
|
|
411
|
+
await ctx.client.notify(methods.client.session.update, {
|
|
412
|
+
sessionId,
|
|
413
|
+
update: { sessionUpdate: 'available_commands_update', availableCommands: commands },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
writeFileSync(markerPath, 'seen');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (config.commandsBeforeNewSessionReply) {
|
|
421
|
+
await ctx.client.notify(methods.client.session.update, {
|
|
422
|
+
sessionId,
|
|
423
|
+
update: { sessionUpdate: 'available_commands_update', availableCommands: config.commandsBeforeNewSessionReply },
|
|
424
|
+
});
|
|
425
|
+
}
|
|
320
426
|
const pushes = config.newSessionCommands ?? [];
|
|
321
427
|
if (pushes.length > 0) {
|
|
322
428
|
void (async () => {
|
|
@@ -329,6 +435,18 @@ async function main() {
|
|
|
329
435
|
}
|
|
330
436
|
})();
|
|
331
437
|
}
|
|
438
|
+
// Same "after the reply, not before" reasoning as `newSessionCommands` above — Task 5
|
|
439
|
+
// (agent-reported activity) uses this to model claude-agent-acp's
|
|
440
|
+
// `extNotification("_claude/sdkMessage", …)`, which only ever arrives once a session exists.
|
|
441
|
+
const extNotifications = config.extNotifications ?? [];
|
|
442
|
+
if (extNotifications.length > 0) {
|
|
443
|
+
void (async () => {
|
|
444
|
+
for (const { method, params } of extNotifications) {
|
|
445
|
+
await new Promise((r) => setTimeout(r, config.extNotificationDelayMs ?? 0));
|
|
446
|
+
await ctx.client.notify(method, params);
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
}
|
|
332
450
|
return {
|
|
333
451
|
sessionId,
|
|
334
452
|
...(config.newSessionConfigOptions ? { configOptions: config.newSessionConfigOptions } : {}),
|
|
@@ -339,20 +457,49 @@ async function main() {
|
|
|
339
457
|
if (config.sessionListSink)
|
|
340
458
|
writeFileSync(config.sessionListSink, JSON.stringify(ctx.params));
|
|
341
459
|
return { sessions: config.sessions ?? [], ...(config.nextCursor ? { nextCursor: config.nextCursor } : {}) };
|
|
460
|
+
})
|
|
461
|
+
.onRequest(methods.agent.session.delete, (ctx) => {
|
|
462
|
+
if (config.deleteSessionSink)
|
|
463
|
+
appendFileSync(config.deleteSessionSink, JSON.stringify(ctx.params) + '\n');
|
|
464
|
+
if (config.deleteSessionFailures?.includes(ctx.params.sessionId))
|
|
465
|
+
throw RequestError.internalError(undefined, 'boom');
|
|
466
|
+
return {};
|
|
342
467
|
})
|
|
343
468
|
.onRequest(methods.agent.session.load, async (ctx) => {
|
|
344
469
|
if (config.loadSessionSink)
|
|
345
470
|
writeFileSync(config.loadSessionSink, JSON.stringify(ctx.params));
|
|
346
471
|
if (config.loadSessionRejectsWith)
|
|
347
472
|
throw new Error(config.loadSessionRejectsWith);
|
|
473
|
+
if (config.hangOnSessionLoad)
|
|
474
|
+
return new Promise(() => { });
|
|
475
|
+
for (const { method, params } of config.beforeSessionReplyNotifications ?? []) {
|
|
476
|
+
await ctx.client.notify(method, params);
|
|
477
|
+
}
|
|
478
|
+
for (const notification of config.scopedLoadUpdates ?? []) {
|
|
479
|
+
await ctx.client.notify(methods.client.session.update, notification);
|
|
480
|
+
}
|
|
348
481
|
for (const update of config.loadUpdates ?? []) {
|
|
349
482
|
await ctx.client.notify(methods.client.session.update, { sessionId: ctx.params.sessionId, update });
|
|
350
483
|
}
|
|
484
|
+
// Same "after the reply, not before" reasoning as `extNotifications` on session/new above —
|
|
485
|
+
// Task 9 uses this to model claude-agent-acp's `extNotification("_claude/sdkMessage", …)`
|
|
486
|
+
// arriving on the session/load (adoption/reconnect) path, where no session/new is ever sent.
|
|
487
|
+
const loadExtNotifications = config.loadExtNotifications ?? [];
|
|
488
|
+
if (loadExtNotifications.length > 0) {
|
|
489
|
+
void (async () => {
|
|
490
|
+
for (const { method, params } of loadExtNotifications) {
|
|
491
|
+
await new Promise((r) => setTimeout(r, config.extNotificationDelayMs ?? 0));
|
|
492
|
+
await ctx.client.notify(method, params);
|
|
493
|
+
}
|
|
494
|
+
})();
|
|
495
|
+
}
|
|
351
496
|
return config.loadSessionResponse ?? {};
|
|
352
497
|
})
|
|
353
498
|
.onRequest(methods.agent.session.resume, async (ctx) => {
|
|
354
499
|
if (config.resumeSessionSink)
|
|
355
500
|
writeFileSync(config.resumeSessionSink, JSON.stringify(ctx.params));
|
|
501
|
+
if (config.resumeSessionRejectsWith)
|
|
502
|
+
throw new Error(config.resumeSessionRejectsWith);
|
|
356
503
|
return config.resumeSessionResponse ?? {};
|
|
357
504
|
})
|
|
358
505
|
.onRequest(methods.agent.session.setConfigOption, (ctx) => {
|
|
@@ -384,10 +531,21 @@ async function main() {
|
|
|
384
531
|
throw new Error(config.steerRejectsWith);
|
|
385
532
|
const outcome = config.steerOutcome ?? 'injected';
|
|
386
533
|
return outcome === 'promptRequired' ? { outcome, reason: 'noRunningTurn' } : { outcome };
|
|
534
|
+
})
|
|
535
|
+
.onRequest('_session/async_task/stop', (params) => params, (ctx) => {
|
|
536
|
+
if (config.asyncTaskStopSink)
|
|
537
|
+
writeFileSync(config.asyncTaskStopSink, JSON.stringify(ctx.params));
|
|
538
|
+
return { stopped: config.asyncTaskStopStopped ?? true };
|
|
387
539
|
})
|
|
388
540
|
.onRequest(methods.agent.session.prompt, async (ctx) => {
|
|
541
|
+
if (config.promptCallsSink)
|
|
542
|
+
appendFileSync(config.promptCallsSink, JSON.stringify(ctx.params) + '\n');
|
|
389
543
|
if (config.promptSink)
|
|
390
544
|
writeFileSync(config.promptSink, JSON.stringify(ctx.params));
|
|
545
|
+
if (config.exitOnPrompt)
|
|
546
|
+
process.exit(1);
|
|
547
|
+
if (config.promptRpcError)
|
|
548
|
+
throw new RequestError(config.promptRpcError.code, config.promptRpcError.message);
|
|
391
549
|
// A plain Error is masked by the SDK as JSON-RPC -32603; `RequestError.authRequired()` is
|
|
392
550
|
// the real thing an agent whose credentials expired answers with (-32000). The two shapes are
|
|
393
551
|
// what the login card has to tell apart, and only a real agent process on a real wire proves
|
|
@@ -396,6 +554,11 @@ async function main() {
|
|
|
396
554
|
throw RequestError.authRequired();
|
|
397
555
|
if (config.promptRejectsWith)
|
|
398
556
|
throw new Error(config.promptRejectsWith);
|
|
557
|
+
if (config.foregroundWriteSink && ctx.params.prompt.some(block => block.type === 'text' && block.text === 'partial-write')) {
|
|
558
|
+
appendFileSync(config.foregroundWriteSink, 'partial\n');
|
|
559
|
+
await cancelled;
|
|
560
|
+
return { stopReason: 'cancelled' };
|
|
561
|
+
}
|
|
399
562
|
for (const update of updates) {
|
|
400
563
|
await ctx.client.notify(methods.client.session.update, { sessionId: ctx.params.sessionId, update });
|
|
401
564
|
}
|
|
@@ -405,7 +568,7 @@ async function main() {
|
|
|
405
568
|
// OS process speaking real ACP, not just callable in-process.
|
|
406
569
|
if (config.requestPermissionDuringPrompt) {
|
|
407
570
|
const result = await ctx.client.request(methods.client.session.requestPermission, {
|
|
408
|
-
sessionId: ctx.params.sessionId,
|
|
571
|
+
sessionId: config.requestPermissionDuringPrompt.sessionId ?? ctx.params.sessionId,
|
|
409
572
|
toolCall: config.requestPermissionDuringPrompt.toolCall,
|
|
410
573
|
options: config.requestPermissionDuringPrompt.options,
|
|
411
574
|
});
|
|
@@ -416,6 +579,19 @@ async function main() {
|
|
|
416
579
|
const result = await ctx.client.request(methods.client.elicitation.create, config.createElicitationDuringPrompt);
|
|
417
580
|
writeFileSync(config.createElicitationResultSink, JSON.stringify(result));
|
|
418
581
|
}
|
|
582
|
+
// The plural form. `await` inside the loop is load-bearing, not incidental: the whole point is
|
|
583
|
+
// that card N is answered before card N+1 is raised. The sink is rewritten on every iteration
|
|
584
|
+
// so the answer to card N is readable while card N+1 is still open.
|
|
585
|
+
if (Array.isArray(config.createElicitationsDuringPrompt)) {
|
|
586
|
+
const results = [];
|
|
587
|
+
for (const payload of config.createElicitationsDuringPrompt) {
|
|
588
|
+
const result = await ctx.client.request(methods.client.elicitation.create, payload);
|
|
589
|
+
results.push(result);
|
|
590
|
+
if (config.createElicitationResultsSink) {
|
|
591
|
+
writeFileSync(config.createElicitationResultsSink, JSON.stringify(results));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
419
595
|
if (config.hangUntilCancel) {
|
|
420
596
|
await cancelled;
|
|
421
597
|
return { stopReason: config.stopReasonAfterCancel ?? 'end_turn' };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared world of the keeper-acp end-to-end tests (Task 21's adoption tests, Task 23's fault-injection gate): a temp config dir
|
|
3
|
+
* with a real keeper-acp (the built bundle in `dist/keepers`), a real adapter process (`scriptAgent.ts`, driven by marker files), and
|
|
4
|
+
* the server's half played by {@link FakeServer} (frames applied once per seq — the server's de-duplication — plus the answers it
|
|
5
|
+
* recorded). A daemon life is either in-process ({@link KeeperWorld.life}: its own keeper connection, AcpAgents, manager and
|
|
6
|
+
* TaskRegistry; "killing" it drops the connection without any goodbye) or a real process ({@link KeeperWorld.daemonProcess}:
|
|
7
|
+
* `daemonLife.ts` under vite-node, killed with SIGKILL). {@link KeeperWorld.dispose} kills every keeper, adapter and daemon process it
|
|
8
|
+
* started, by pid.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { rm } from 'node:fs/promises';
|
|
13
|
+
import path, { join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { expect, vi } from 'vitest';
|
|
16
|
+
import { killKeepersUnder, tempDir } from '../../../test/keeperKit.js';
|
|
17
|
+
import { AcpKeeperClient } from '../../keepers/acp/client.js';
|
|
18
|
+
import { computeBuildVersion } from '../../keepers/kit/buildVersion.js';
|
|
19
|
+
import { keeperGen } from '../../keepers/kit/identity.js';
|
|
20
|
+
import { KeeperLauncher } from '../../keepers/kit/launcher.js';
|
|
21
|
+
import { AgentSessionManager } from '../../agentSessions.js';
|
|
22
|
+
import { AGENT_WIRE_VERSION } from '../../protocol/agent.js';
|
|
23
|
+
import { TaskRegistry } from '../../update/taskRegistry.js';
|
|
24
|
+
import { AcpAgents } from '../acpAgents.js';
|
|
25
|
+
import { keeperGenerations } from '../processor/transport/generations.js';
|
|
26
|
+
import { KeeperTransport } from '../processor/transport/keeper.js';
|
|
27
|
+
import { LocalTransport } from '../processor/transport/local.js';
|
|
28
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
export const packageRoot = path.join(here, '..', '..', '..', '..');
|
|
30
|
+
export const realDist = path.join(packageRoot, 'dist', 'keepers');
|
|
31
|
+
export const SCRIPT_AGENT = join(here, 'scriptAgent.ts');
|
|
32
|
+
export const DAEMON_LIFE = join(here, 'daemonLife.ts');
|
|
33
|
+
export const BOOTSTRAP = "eval(require('node:fs').readFileSync(process.argv[1], 'utf8'))";
|
|
34
|
+
export const touch = (dir, name) => writeFileSync(join(dir, name), '');
|
|
35
|
+
export const agentPid = (dir) => Number(readFileSync(join(dir, 'pid'), 'utf8'));
|
|
36
|
+
export const stdinLines = (dir) => existsSync(join(dir, 'stdin.jsonl')) ? readFileSync(join(dir, 'stdin.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
37
|
+
export const downMethods = (records) => records.filter((r) => r.kind === 'down').map((r) => JSON.parse(r.line).method);
|
|
38
|
+
/** The server's half: applies each seq once, in order (bridge_seq), per ref; remembers the answers it gave (spec §5.5.4). */
|
|
39
|
+
export class FakeServer {
|
|
40
|
+
applied = new Map();
|
|
41
|
+
lastSeen = new Map();
|
|
42
|
+
received = [];
|
|
43
|
+
/** ref → requestId → outcome: what the user answered (the card's `answer`). */
|
|
44
|
+
answers = new Map();
|
|
45
|
+
apply = (frame) => {
|
|
46
|
+
this.received.push(frame);
|
|
47
|
+
if ((frame.t === 'agent_event' || frame.t === 'agent_turn_done') && typeof frame.seq === 'number') {
|
|
48
|
+
const last = this.lastSeen.get(frame.sessionRef) ?? 0;
|
|
49
|
+
if (frame.seq <= last)
|
|
50
|
+
return; // the server's de-duplication
|
|
51
|
+
this.lastSeen.set(frame.sessionRef, frame.seq);
|
|
52
|
+
const list = this.applied.get(frame.sessionRef) ?? [];
|
|
53
|
+
list.push(frame);
|
|
54
|
+
this.applied.set(frame.sessionRef, list);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
timeline(ref) { return this.applied.get(ref) ?? []; }
|
|
58
|
+
seqs(ref) { return this.timeline(ref).map((frame) => frame.seq); }
|
|
59
|
+
turnDones(ref) { return this.timeline(ref).filter((frame) => frame.t === 'agent_turn_done'); }
|
|
60
|
+
texts(ref) {
|
|
61
|
+
return this.timeline(ref).filter((f) => f.t === 'agent_event' && f.eventName === 'agent_message_chunk')
|
|
62
|
+
.map((f) => (f.payload.content?.text ?? ''));
|
|
63
|
+
}
|
|
64
|
+
cards(ref) {
|
|
65
|
+
return this.timeline(ref).filter((f) => f.eventName === 'permission_request').map((f) => f.payload.requestId);
|
|
66
|
+
}
|
|
67
|
+
/** The user answers a card: recorded first (the server's item), then sent if a daemon is there. */
|
|
68
|
+
answer(ref, requestId, outcome) {
|
|
69
|
+
const answered = this.answers.get(ref) ?? new Map();
|
|
70
|
+
answered.set(requestId, outcome);
|
|
71
|
+
this.answers.set(ref, answered);
|
|
72
|
+
return { t: 'agent_answer', sessionRef: ref, requestId, outcome };
|
|
73
|
+
}
|
|
74
|
+
/** Task 22's resend: the answers this side recorded for cards a durable reopen still reports pending. */
|
|
75
|
+
resends(ref, opened) {
|
|
76
|
+
const answered = this.answers.get(ref);
|
|
77
|
+
return (opened.pendingRequestIds ?? []).filter((id) => answered?.has(id))
|
|
78
|
+
.map((id) => ({ t: 'agent_answer', sessionRef: ref, requestId: id, outcome: answered.get(id) }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** What the server's timeline says, without the ref: compared between two runs. */
|
|
82
|
+
export const comparable = (frames) => frames.map(({ sessionRef: _ref, ...rest }) => rest);
|
|
83
|
+
/** Seqs 1..n, no duplicate, no gap. */
|
|
84
|
+
export function expectContiguous(seqs) {
|
|
85
|
+
expect(seqs).toEqual(Array.from({ length: seqs.length }, (_, k) => k + 1));
|
|
86
|
+
}
|
|
87
|
+
export class KeeperWorld {
|
|
88
|
+
configDir;
|
|
89
|
+
agentDir;
|
|
90
|
+
launcher;
|
|
91
|
+
lives = [];
|
|
92
|
+
processes = [];
|
|
93
|
+
extraDists = [];
|
|
94
|
+
agentDirs = [];
|
|
95
|
+
constructor(prefix) {
|
|
96
|
+
this.configDir = tempDir(prefix);
|
|
97
|
+
this.agentDir = this.newAgentDir(`${prefix}agent-`);
|
|
98
|
+
this.launcher = this.launcherFor(realDist);
|
|
99
|
+
}
|
|
100
|
+
launcherFor(distDir) {
|
|
101
|
+
return new KeeperLauncher({ configDir: this.configDir, workstationId: 'ws-t21', server: 'http://test.invalid', distDir, useSystemd: false,
|
|
102
|
+
startTimeoutMs: 20_000, umask: 0o022 });
|
|
103
|
+
}
|
|
104
|
+
/** Another adapter's marker directory (its pid is killed on dispose). */
|
|
105
|
+
newAgentDir(prefix = 'axom-t23-agent-') {
|
|
106
|
+
const dir = tempDir(prefix);
|
|
107
|
+
this.agentDirs.push(dir);
|
|
108
|
+
return dir;
|
|
109
|
+
}
|
|
110
|
+
scriptArgs(config, dir = this.agentDir) {
|
|
111
|
+
return ['-e', BOOTSTRAP, SCRIPT_AGENT, JSON.stringify({ dir, ...config })];
|
|
112
|
+
}
|
|
113
|
+
/** One daemon life: its own keeper-acp connection, AcpAgents (with the generations), manager and TaskRegistry, bound to `server`. */
|
|
114
|
+
life(server, over = {}) {
|
|
115
|
+
const use = over.launcher ?? this.launcher;
|
|
116
|
+
const real = new KeeperTransport(new AcpKeeperClient({ launcher: use }));
|
|
117
|
+
const keeper = (over.wrapKeeper ? over.wrapKeeper(real) : real);
|
|
118
|
+
const generations = keeperGenerations({ launcher: use, current: keeper, log: () => { } });
|
|
119
|
+
const local = new LocalTransport({ configDir: this.configDir });
|
|
120
|
+
const acp = new AcpAgents({ cacheDir: this.configDir, registry: { load: async () => [] }, transports: { keeper, local }, keeperGenerations: generations,
|
|
121
|
+
sessionStartTimeoutMs: 15_000, ...over.acp });
|
|
122
|
+
const tasks = new TaskRegistry();
|
|
123
|
+
const sent = [];
|
|
124
|
+
const manager = new AgentSessionManager(acp, null, join(this.configDir, 'agents'), tasks);
|
|
125
|
+
manager.bindSend((frame) => { sent.push(frame); server.apply(frame); }, AGENT_WIRE_VERSION);
|
|
126
|
+
let killed = false;
|
|
127
|
+
const result = {
|
|
128
|
+
keeper, generations, acp, tasks, manager, sent,
|
|
129
|
+
// kill -9 as keeper-acp sees it: the connection drops, nothing is detached, nothing is said.
|
|
130
|
+
kill() { if (killed)
|
|
131
|
+
return; killed = true; keeper.close(); generations.close(); void local.dispose(0); },
|
|
132
|
+
};
|
|
133
|
+
this.lives.push(result);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
openFrame(ref, config, over = {}) {
|
|
137
|
+
return {
|
|
138
|
+
t: 'agent_open', id: `open-${ref}-${Math.random().toString(36).slice(2, 8)}`, sessionRef: ref, agentKey: 'script', bridge: 'script-acp',
|
|
139
|
+
cwd: this.configDir, shell: '/bin/sh', command: process.execPath, args: this.scriptArgs(config), ...over,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Opens `ref` on life `l` and waits for the reply. */
|
|
143
|
+
async openOn(l, ref, config, over = {}) {
|
|
144
|
+
const frame = this.openFrame(ref, config, over);
|
|
145
|
+
await l.manager.handleFrame(frame);
|
|
146
|
+
const reply = opened(l, frame.id)[0];
|
|
147
|
+
expect(reply, `no agent_opened for ${ref}`).toBeDefined();
|
|
148
|
+
expect(reply.ok, reply.error ?? '').toBe(true);
|
|
149
|
+
return reply;
|
|
150
|
+
}
|
|
151
|
+
/** The server reopens `ref` on life `l` after a reconnect: `agent_open` with its watermark, then Task 22's answer resend. */
|
|
152
|
+
async reopenOn(l, server, ref, config, over = {}) {
|
|
153
|
+
const reply = await this.openOn(l, ref, config, { resumeFromSeq: server.lastSeen.get(ref) ?? 0, ...over });
|
|
154
|
+
for (const frame of server.resends(ref, reply))
|
|
155
|
+
await l.manager.handleFrame(frame);
|
|
156
|
+
return reply;
|
|
157
|
+
}
|
|
158
|
+
/** Every raw record of `ref`, read over a connection of its own. */
|
|
159
|
+
async rawOf(ref, launcher = this.launcher) {
|
|
160
|
+
const reader = new KeeperTransport(new AcpKeeperClient({ launcher }));
|
|
161
|
+
try {
|
|
162
|
+
await reader.attach(ref);
|
|
163
|
+
const records = [];
|
|
164
|
+
const summary = (await reader.list()).find((s) => s.sessionKey === ref);
|
|
165
|
+
for (let at = summary.raw.first;;) {
|
|
166
|
+
const page = await reader.readRaw(ref, at);
|
|
167
|
+
if (page.length === 0)
|
|
168
|
+
break;
|
|
169
|
+
records.push(...page);
|
|
170
|
+
at = page[page.length - 1].o + 1;
|
|
171
|
+
}
|
|
172
|
+
return records;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
reader.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** keeper-acp's listing of `ref`, over a connection of its own. */
|
|
179
|
+
async summaryOf(ref) {
|
|
180
|
+
const reader = new KeeperTransport(new AcpKeeperClient({ launcher: this.launcher }));
|
|
181
|
+
try {
|
|
182
|
+
return (await reader.list()).find((s) => s.sessionKey === ref);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
reader.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A daemon life in a process of its own (`daemonLife.ts` under vite-node): it adopts what keeper-acp holds, then handles the frames
|
|
190
|
+
* it is given on stdin (one JSON per line; `{cmd:'forceBegin'}` runs a force update's `TaskRegistry.stopAll()`, `{cmd:'forceStop'}`
|
|
191
|
+
* also the restart path's detach, and exits). Every
|
|
192
|
+
* frame it sends lands in a file; its stderr is kept in `<configDir>/daemon-<n>.stderr` (the sentinel grep reads it).
|
|
193
|
+
*/
|
|
194
|
+
daemonProcess(faults = {}) {
|
|
195
|
+
const n = this.processes.length + 1;
|
|
196
|
+
const framesOut = join(this.configDir, `frames-daemon-${n}.jsonl`);
|
|
197
|
+
const stderrFile = join(this.configDir, `daemon-${n}.stderr`);
|
|
198
|
+
const child = spawn(join(packageRoot, 'node_modules', '.bin', 'vite-node'), [DAEMON_LIFE,
|
|
199
|
+
JSON.stringify({ configDir: this.configDir, distDir: realDist, framesOut, faults })], { cwd: packageRoot, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
200
|
+
let stderr = '';
|
|
201
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); appendFileSync(stderrFile, chunk); });
|
|
202
|
+
const exited = new Promise((resolve) => child.once('exit', () => resolve()));
|
|
203
|
+
const result = {
|
|
204
|
+
child,
|
|
205
|
+
frames: () => existsSync(framesOut) ? readFileSync(framesOut, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line)) : [],
|
|
206
|
+
send: (frame) => { child.stdin.write(`${JSON.stringify(frame)}\n`); },
|
|
207
|
+
stderr: () => stderr,
|
|
208
|
+
kill: async () => { if (child.exitCode === null && child.signalCode === null)
|
|
209
|
+
child.kill('SIGKILL'); await exited; },
|
|
210
|
+
exited,
|
|
211
|
+
};
|
|
212
|
+
this.processes.push(result);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
/** Opens `ref` in a daemon process and waits for its `agent_opened{ok}` (the server never prompts before it). */
|
|
216
|
+
async openIn(proc, frame) {
|
|
217
|
+
proc.send(frame);
|
|
218
|
+
let reply;
|
|
219
|
+
await vi.waitFor(() => {
|
|
220
|
+
expect(proc.child.exitCode, proc.stderr()).toBeNull();
|
|
221
|
+
reply = proc.frames().find((f) => f.t === 'agent_opened' && f.id === frame.id);
|
|
222
|
+
expect(reply).toBeDefined();
|
|
223
|
+
}, { timeout: 45_000, interval: 100 });
|
|
224
|
+
expect(reply.ok, reply.error ?? '').toBe(true);
|
|
225
|
+
return reply;
|
|
226
|
+
}
|
|
227
|
+
/** An "older build" of keeper-acp: the same bundle with one byte more (its own buildVersion, so its own generation). */
|
|
228
|
+
olderGeneration() {
|
|
229
|
+
const oldDist = tempDir('axom-t21-old-dist-');
|
|
230
|
+
this.extraDists.push(oldDist);
|
|
231
|
+
mkdirSync(join(oldDist, 'acp'), { recursive: true });
|
|
232
|
+
writeFileSync(join(oldDist, 'acp', 'keeper.mjs'), `${readFileSync(join(realDist, 'acp', 'keeper.mjs'), 'utf8')}\n// an older generation\n`);
|
|
233
|
+
const manifest = JSON.parse(readFileSync(join(realDist, 'acp', 'manifest.json'), 'utf8'));
|
|
234
|
+
const oldBuild = computeBuildVersion(join(oldDist, 'acp'));
|
|
235
|
+
writeFileSync(join(oldDist, 'acp', 'manifest.json'), JSON.stringify({ ...manifest, buildVersion: oldBuild }));
|
|
236
|
+
return { oldLauncher: this.launcherFor(oldDist), oldGen: keeperGen(oldBuild) };
|
|
237
|
+
}
|
|
238
|
+
/** Kills every daemon process, life, keeper-acp generation and adapter this world started (by pid); the directories stay. */
|
|
239
|
+
async stop() {
|
|
240
|
+
for (const proc of this.processes.splice(0))
|
|
241
|
+
await proc.kill().catch(() => { });
|
|
242
|
+
for (const life of this.lives.splice(0))
|
|
243
|
+
life.kill();
|
|
244
|
+
for (const gen of await this.launcher.liveGenerations('acp').catch(() => []))
|
|
245
|
+
await this.launcher.stop('acp', gen, 'force').catch(() => { });
|
|
246
|
+
for (const dir of this.agentDirs) {
|
|
247
|
+
const pidFile = join(dir, 'pid');
|
|
248
|
+
if (existsSync(pidFile)) {
|
|
249
|
+
try {
|
|
250
|
+
process.kill(Number(readFileSync(pidFile, 'utf8')), 'SIGKILL');
|
|
251
|
+
}
|
|
252
|
+
catch { /* gone */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** {@link stop}, then removes every directory (killing any keeper whose pid manifest is still under the config dir). */
|
|
257
|
+
async dispose() {
|
|
258
|
+
await this.stop();
|
|
259
|
+
for (const dist of this.extraDists.splice(0))
|
|
260
|
+
await rm(dist, { recursive: true, force: true });
|
|
261
|
+
for (const dir of this.agentDirs.splice(0))
|
|
262
|
+
await rm(dir, { recursive: true, force: true });
|
|
263
|
+
killKeepersUnder(this.configDir);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export const opened = (l, id) => l.sent.filter((frame) => frame.t === 'agent_opened' && (id === undefined || frame.id === id));
|
|
267
|
+
export const liveSessionOf = (l, ref) => l.manager.sessions.get(ref)?.session;
|