@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
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session layer of the ACP processor (Task 17a): turns, interrupt, permission and elicitation
|
|
3
|
+
* cards, and the permission-origin graph — `AcpSession` + `permission.ts` + the turn half of
|
|
4
|
+
* `agentSessions.ts`, re-expressed as a synchronous fold over the raw log (spec 2026-10-11 §5.4).
|
|
5
|
+
*
|
|
6
|
+
* ## Turns
|
|
7
|
+
*
|
|
8
|
+
* - An `agent_prompt` note asks for a turn. Admission (`TaskRegistry.tryAdmitTurn`, a daemon-wide
|
|
9
|
+
* decision) arrives **in** the note (`admitted: false`), so a refusal lands on the note's offset and
|
|
10
|
+
* replays identically; so do the processor's own refusals (no connection, an overlapping exact
|
|
11
|
+
* turn). Those are the three daemon-initiated `turn_done`s of `agentSessions.ts`'s `agent_prompt`
|
|
12
|
+
* branch, with the same two frame shapes (see {@link refuseTurn} / {@link finishTurn}).
|
|
13
|
+
* - An admitted prompt becomes one `session/prompt` command. The note carries no blocks — only a
|
|
14
|
+
* `blocksRef` the executor resolves (`DownCommand.fill`), so the blocks appear once in the log, in
|
|
15
|
+
* that `down` record (§5.4). `platformTurnId` rides in the command's annotation and is **read back
|
|
16
|
+
* from the `down` record** when the processor folds it: that record is what started the turn on the
|
|
17
|
+
* agent.
|
|
18
|
+
* - A turn ends exactly on the record that settles its request — the agent's response line, a
|
|
19
|
+
* `proc exit`, or an `agent_prompt_failed` note (the executor could not write the line). There is no
|
|
20
|
+
* `settleUpdates` heuristic: log order is the truth (analysis B A4). One consequence, recorded
|
|
21
|
+
* rather than hidden: an update the agent writes **after** its `session/prompt` response belongs to
|
|
22
|
+
* whatever follows, not to the finished turn (analysis B §1.5).
|
|
23
|
+
* - Turns are matched to their response by request id, not FIFO: two overlapping prompts the agent
|
|
24
|
+
* answers out of order keep their own results (`AcpSession.inFlightTurns` documents the swap the
|
|
25
|
+
* FIFO rule had).
|
|
26
|
+
*
|
|
27
|
+
* ## Cards
|
|
28
|
+
*
|
|
29
|
+
* `session/request_permission` and `'form'` `elicitation/create` follow `permission.ts` step for step
|
|
30
|
+
* (the same builders and `validateWireEvent`), with ids derived from the log: a permission card is
|
|
31
|
+
* `perm:<spawnId>:<JSON agent id>` ({@link permissionRequestId}, shared with the live path), an
|
|
32
|
+
* elicitation card keeps the agent's id. A card the user has not answered stays in state with the
|
|
33
|
+
* JSON-RPC id it came on, so after a restore it is still pending under the same id
|
|
34
|
+
* ({@link pendingRequestIds}, spec §5.5.3/4) and the answer is written back on the original id.
|
|
35
|
+
*
|
|
36
|
+
* ## Task 17b
|
|
37
|
+
*
|
|
38
|
+
* `session/update` mapping and `agent_message` synthesis live in `updates.ts` (a turn's end flushes
|
|
39
|
+
* the streaming message before its own frame, and a turn's start closes a message that began
|
|
40
|
+
* outside any turn); the open session, config, steering and delegation in `control.ts`.
|
|
41
|
+
*
|
|
42
|
+
* ## Task 17c: callbacks and the adapter boundaries
|
|
43
|
+
*
|
|
44
|
+
* Every change to the pending/settled permission cards publishes the callback snapshot
|
|
45
|
+
* (`runtime.ts` `publishCallbacks`), at the points `AcpSession` does: a registration (accepted, or
|
|
46
|
+
* refused for its fields/capacity/a reused id — which also makes the snapshot incomplete), a
|
|
47
|
+
* settlement (`answered` by a note, `canceled` by the agent or for want of a callback context), a
|
|
48
|
+
* drain (interrupt, an interrupted turn, the connection closing — incomplete from then on). A Qoder
|
|
49
|
+
* SDK adapter's root card carries its `callbackContext` (`qoderAdapter/callbackContext.ts`, the
|
|
50
|
+
* function `AcpSession.emitEvent` uses), computed from that snapshot and the runtime profile — log
|
|
51
|
+
* data only; without one the card is settled `canceled` and answered `cancelled`, as today. Cards are
|
|
52
|
+
* emitted through the same boundaries as any other event (`updates.ts` `emitSessionEvent`).
|
|
53
|
+
*/
|
|
54
|
+
import { buildElicitationRequestedEvent, buildPermissionRequestedEvent, elicitationFieldKeys, isBlank, permissionRequestId, } from '../permission.js';
|
|
55
|
+
import { validateWireEvent } from '../validate.js';
|
|
56
|
+
import { isSdkCallbackCard, permissionCallbackContext } from '../../qoderAdapter/callbackContext.js';
|
|
57
|
+
import { isRecord, nextSeq } from './emit.js';
|
|
58
|
+
import { inboundKey, settlementErrorCode } from './jsonrpc.js';
|
|
59
|
+
import { sameCause } from './record.js';
|
|
60
|
+
import { callbackSnapshot, publishCallbacks, resetRuntime } from './runtime.js';
|
|
61
|
+
import { getOwn, initialBoundaries, initialCallbacks, initialMessage, PENDING_PERMISSION_LIMIT, SETTLED_PERMISSION_LIMIT, } from './state.js';
|
|
62
|
+
import { emitSessionEvent, endForegroundTurn, endHistory, flushMessage, flushOutOfTurnMessage } from './updates.js';
|
|
63
|
+
/** `agentSessions.ts`'s admission refusal text, verbatim. */
|
|
64
|
+
export const ADMISSION_REFUSED = 'Workstation is not accepting new work';
|
|
65
|
+
/** `AcpSession.prompt`'s overlap refusal text, verbatim. */
|
|
66
|
+
export const OVERLAPPING_TURN = 'An exact platform turn is still running; overlapping prompts are unavailable';
|
|
67
|
+
/** What the SDK rejects pending requests with when the adapter's stdout ends (`jsonrpc.js` `close()`). */
|
|
68
|
+
export const CONNECTION_CLOSED = 'ACP connection closed';
|
|
69
|
+
/** A prompt for a connection with no bound session (Task 17b binds it from the open response). */
|
|
70
|
+
export const NO_SESSION = 'No ACP session is open on this connection';
|
|
71
|
+
/** `ClientApp` closes the connection on a batch; that reason is kept as the turn's error. */
|
|
72
|
+
export const BATCH_CLOSE_REASON = 'JSON-RPC batches are not supported on this connection';
|
|
73
|
+
/** Pending `'form'` elicitations kept at once (new bound, mirrors permissions' 64). */
|
|
74
|
+
export const PENDING_ELICITATION_LIMIT = 64;
|
|
75
|
+
const PERMISSION_CANCELLED = { outcome: { outcome: 'cancelled' } };
|
|
76
|
+
const ELICITATION_DECLINE = { action: 'decline' };
|
|
77
|
+
const ELICITATION_CANCEL = { action: 'cancel' };
|
|
78
|
+
/** Connected = a running process whose JSON-RPC connection has not closed. */
|
|
79
|
+
export function connected(state) {
|
|
80
|
+
return state.process.status === 'running' && state.closed === undefined;
|
|
81
|
+
}
|
|
82
|
+
/** The one setter of `rootSessionId` — `control.ts` calls it on the `session/new|load|resume` response. */
|
|
83
|
+
export function bindRootSession(state, sessionId) {
|
|
84
|
+
state.rootSessionId = sessionId;
|
|
85
|
+
}
|
|
86
|
+
/** Every unanswered card id — `agent_opened.pendingRequestIds` (spec §5.5.4). */
|
|
87
|
+
export function pendingRequestIds(state) {
|
|
88
|
+
return [...Object.keys(state.permissions), ...Object.keys(state.elicitations)];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A new adapter process is a new connection: today's per-connection, per-`AcpSession` and
|
|
92
|
+
* per-`LiveSession` fields start fresh (a respawn is a new session there too). `seq` is not reset.
|
|
93
|
+
*/
|
|
94
|
+
export function resetForSpawn(state) {
|
|
95
|
+
state.rootSessionId = null;
|
|
96
|
+
state.open = { status: 'idle' };
|
|
97
|
+
state.configOptions = null;
|
|
98
|
+
state.delegation = null;
|
|
99
|
+
state.history = null;
|
|
100
|
+
state.mapperScopes = {};
|
|
101
|
+
state.message = initialMessage();
|
|
102
|
+
state.steers = {};
|
|
103
|
+
state.steered = [];
|
|
104
|
+
state.asyncTasks = {};
|
|
105
|
+
resetRuntime(state);
|
|
106
|
+
state.turns = [];
|
|
107
|
+
state.exactExecutionUnconfirmed = false;
|
|
108
|
+
state.backgroundWriterUnconfirmed = false;
|
|
109
|
+
state.permissionsInterrupted = false;
|
|
110
|
+
state.permissions = {};
|
|
111
|
+
state.elicitations = {};
|
|
112
|
+
state.settledPermissions = [];
|
|
113
|
+
state.callbacks = initialCallbacks();
|
|
114
|
+
state.boundaries = initialBoundaries();
|
|
115
|
+
state.permissionOrigins = {};
|
|
116
|
+
state.permissionParents = {};
|
|
117
|
+
state.unresolvedPermissionOrigins = {};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The connection closed: unanswered cards can no longer be answered (nothing to write them to) — the
|
|
121
|
+
* read loop's `drainPendingApprovals`, so the callback snapshot goes incomplete and is published.
|
|
122
|
+
*/
|
|
123
|
+
export function dropCards(state, out) {
|
|
124
|
+
state.permissions = {};
|
|
125
|
+
state.elicitations = {};
|
|
126
|
+
state.callbacks.complete = false;
|
|
127
|
+
publishCallbacks(state, out);
|
|
128
|
+
}
|
|
129
|
+
export function closeErrorText(state) {
|
|
130
|
+
return state.closed === BATCH_CLOSE_REASON ? BATCH_CLOSE_REASON : CONNECTION_CLOSED;
|
|
131
|
+
}
|
|
132
|
+
// ---- turns ------------------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* The short `agent_turn_done` `agentSessions.ts` builds itself for a prompt that never reached the
|
|
135
|
+
* session (admission refused, `prompt()` threw): no `nativeSessionId`, no `stopReason`.
|
|
136
|
+
*/
|
|
137
|
+
function refuseTurn(state, out, error, platformTurnId) {
|
|
138
|
+
out.frame({
|
|
139
|
+
t: 'agent_turn_done',
|
|
140
|
+
seq: nextSeq(state),
|
|
141
|
+
status: 'failed',
|
|
142
|
+
error,
|
|
143
|
+
...(platformTurnId !== undefined ? { platformTurnId } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const STOP_REASONS = {
|
|
147
|
+
end_turn: 'end_turn',
|
|
148
|
+
max_tokens: 'max_tokens',
|
|
149
|
+
max_turn_requests: 'max_turns',
|
|
150
|
+
refusal: 'refusal',
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* End turn `index` — `AcpSession.finishTurn` + `agentSessions.ts`'s `onAgentTurnDone` frame, in one.
|
|
154
|
+
* The frame keeps that method's key order (`agent_turn_done` minus `sessionRef`); `agentRefused` and
|
|
155
|
+
* `errorCode` are carried for the projection, which turns them into `refusalFields` (Task 17b).
|
|
156
|
+
*/
|
|
157
|
+
function finishTurn(state, out, index, status, finish) {
|
|
158
|
+
const [turn] = state.turns.splice(index, 1);
|
|
159
|
+
if (!turn)
|
|
160
|
+
return;
|
|
161
|
+
if (state.turns.length === 0)
|
|
162
|
+
endForegroundTurn(state);
|
|
163
|
+
// Before the turn's own frame, on every path that ends a turn (`flushMessageCompleted`).
|
|
164
|
+
flushMessage(state, out);
|
|
165
|
+
const resolved = turn.interrupted ? 'interrupted' : status;
|
|
166
|
+
if (resolved === 'interrupted') {
|
|
167
|
+
state.permissionsInterrupted = true;
|
|
168
|
+
drainCards(state, out);
|
|
169
|
+
}
|
|
170
|
+
const frame = { t: 'agent_turn_done', seq: nextSeq(state), status: resolved };
|
|
171
|
+
if (turn.platformTurnId !== undefined)
|
|
172
|
+
frame.platformTurnId = turn.platformTurnId;
|
|
173
|
+
if (finish.executionEnded && state.turns.length === 0 && !state.backgroundWriterUnconfirmed) {
|
|
174
|
+
frame.executionEnded = true;
|
|
175
|
+
state.exactExecutionUnconfirmed = false;
|
|
176
|
+
}
|
|
177
|
+
const stopReason = typeof finish.stopReason === 'string' ? STOP_REASONS[finish.stopReason] : undefined;
|
|
178
|
+
if (resolved === 'completed' && stopReason !== undefined)
|
|
179
|
+
frame.stopReason = stopReason;
|
|
180
|
+
frame.nativeSessionId = state.rootSessionId !== null && state.rootSessionId.length > 0 ? state.rootSessionId : null;
|
|
181
|
+
frame.error = resolved === 'failed' && finish.error ? finish.error : null;
|
|
182
|
+
if (resolved === 'failed' && finish.agentRefused) {
|
|
183
|
+
frame.agentRefused = true;
|
|
184
|
+
if (finish.errorCode !== undefined)
|
|
185
|
+
frame.errorCode = finish.errorCode;
|
|
186
|
+
}
|
|
187
|
+
out.frame(frame);
|
|
188
|
+
}
|
|
189
|
+
/** `agent_prompt{blocksRef, platformTurnId?, admitted?}`. */
|
|
190
|
+
export function onPromptNote(state, note, out) {
|
|
191
|
+
const { blocksRef, platformTurnId, admitted } = note;
|
|
192
|
+
if (typeof blocksRef !== 'string' || blocksRef === '' || (platformTurnId !== undefined && typeof platformTurnId !== 'string')
|
|
193
|
+
|| (admitted !== undefined && typeof admitted !== 'boolean')) {
|
|
194
|
+
state.diagnostics.droppedNotes += 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (admitted === false) {
|
|
198
|
+
refuseTurn(state, out, ADMISSION_REFUSED, platformTurnId);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!connected(state) || state.rootSessionId === null) {
|
|
202
|
+
// `AcpSession.prompt`'s `readLoopDead` branch: registered as a turn and finished on the spot, so
|
|
203
|
+
// "one prompt, one terminal result" holds here too.
|
|
204
|
+
state.turns.push({ requestId: -1, cause: { o: out.o, i: -1 }, interrupted: false, sent: false,
|
|
205
|
+
...(platformTurnId !== undefined ? { platformTurnId } : {}) });
|
|
206
|
+
finishTurn(state, out, state.turns.length - 1, 'failed', { error: connected(state) ? NO_SESSION : closeErrorText(state) });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (state.exactExecutionUnconfirmed || (state.turns.length > 0 && (platformTurnId !== undefined
|
|
210
|
+
|| state.turns.some((turn) => turn.platformTurnId !== undefined)))) {
|
|
211
|
+
refuseTurn(state, out, OVERLAPPING_TURN, platformTurnId);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
state.permissionsInterrupted = false;
|
|
215
|
+
// A turn ends any load history still open: its output is live, never history (T19 writes
|
|
216
|
+
// `historyEnd` before admitting a prompt; this is the backstop).
|
|
217
|
+
endHistory(state, out, true);
|
|
218
|
+
flushOutOfTurnMessage(state, out);
|
|
219
|
+
const { id, cause } = out.request('session/prompt', { sessionId: state.rootSessionId, prompt: [] }, {
|
|
220
|
+
fill: { blocksRef },
|
|
221
|
+
...(platformTurnId !== undefined ? { annotation: { platformTurnId } } : {}),
|
|
222
|
+
});
|
|
223
|
+
const pending = { id, method: 'session/prompt', kind: 'prompt', cause };
|
|
224
|
+
state.outbound[String(id)] = pending;
|
|
225
|
+
state.turns.push({ requestId: id, cause, interrupted: false, sent: false, ...(platformTurnId !== undefined ? { platformTurnId } : {}) });
|
|
226
|
+
if (platformTurnId !== undefined)
|
|
227
|
+
state.exactExecutionUnconfirmed = true;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A `down` record: if it is a queued prompt's line, the turn is now on the agent, and its
|
|
231
|
+
* `platformTurnId` is the one the record's annotation carries.
|
|
232
|
+
*/
|
|
233
|
+
export function onDown(state, record) {
|
|
234
|
+
const annotation = record.annotation;
|
|
235
|
+
if (!annotation)
|
|
236
|
+
return;
|
|
237
|
+
const turn = state.turns.find((candidate) => sameCause(annotation.cause, candidate.cause));
|
|
238
|
+
if (!turn || turn.sent)
|
|
239
|
+
return;
|
|
240
|
+
turn.sent = true;
|
|
241
|
+
if (typeof annotation.platformTurnId === 'string')
|
|
242
|
+
turn.platformTurnId = annotation.platformTurnId;
|
|
243
|
+
else
|
|
244
|
+
delete turn.platformTurnId;
|
|
245
|
+
}
|
|
246
|
+
/** A `session/prompt` request settled: its turn ends on this record. */
|
|
247
|
+
export function settlePrompt(state, entry, settlement, out) {
|
|
248
|
+
const index = state.turns.findIndex((turn) => turn.requestId === entry.id);
|
|
249
|
+
if (index < 0)
|
|
250
|
+
return;
|
|
251
|
+
switch (settlement.outcome) {
|
|
252
|
+
case 'result': {
|
|
253
|
+
const stopReason = isRecord(settlement.result) ? settlement.result.stopReason : undefined;
|
|
254
|
+
// ACP's own `cancelled` forces `interrupted` even when no interrupt was sent (a backstop).
|
|
255
|
+
finishTurn(state, out, index, stopReason === 'cancelled' ? 'interrupted' : 'completed', { stopReason, executionEnded: true });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
case 'error': {
|
|
259
|
+
// The one exit where the agent itself answered (`TurnResult.agentRefused`).
|
|
260
|
+
const errorCode = settlementErrorCode(settlement);
|
|
261
|
+
finishTurn(state, out, index, 'failed', {
|
|
262
|
+
error: settlement.error.message, agentRefused: true, errorCode, executionEnded: errorCode !== undefined,
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
case 'closed':
|
|
267
|
+
finishTurn(state, out, index, 'failed', { error: closeErrorText(state) });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `agent_prompt_failed{requestId, error}` — the executor could not write a prompt's line (its blocks
|
|
273
|
+
* were gone after a restart, or the write failed). Ignored once the line is in raw: then the agent
|
|
274
|
+
* has it and its response ends the turn. The prompt never reached the agent, which is proof its
|
|
275
|
+
* execution ended (`TurnResult.executionEnded`).
|
|
276
|
+
*/
|
|
277
|
+
export function onPromptFailedNote(state, note, out) {
|
|
278
|
+
const { requestId, error } = note;
|
|
279
|
+
const entry = typeof requestId === 'number' ? state.outbound[String(requestId)] : undefined;
|
|
280
|
+
const index = entry ? state.turns.findIndex((turn) => turn.requestId === entry.id) : -1;
|
|
281
|
+
if (!entry || entry.kind !== 'prompt' || index < 0 || state.turns[index].sent || typeof error !== 'string') {
|
|
282
|
+
state.diagnostics.droppedNotes += 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
delete state.outbound[String(entry.id)];
|
|
286
|
+
finishTurn(state, out, index, 'failed', { error, executionEnded: true });
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* `agent_interrupt{}` — `AcpSession.interrupt`: every turn is marked interrupted (its result will say
|
|
290
|
+
* so whatever the agent answers), later permission requests are refused until the next prompt, every
|
|
291
|
+
* pending card is answered "cancelled", then `session/cancel` goes out.
|
|
292
|
+
*/
|
|
293
|
+
export function onInterruptNote(state, out) {
|
|
294
|
+
for (const turn of state.turns)
|
|
295
|
+
turn.interrupted = true;
|
|
296
|
+
state.permissionsInterrupted = true;
|
|
297
|
+
drainCards(state, out);
|
|
298
|
+
if (connected(state) && state.rootSessionId !== null)
|
|
299
|
+
out.notify('session/cancel', { sessionId: state.rootSessionId });
|
|
300
|
+
}
|
|
301
|
+
/** The connection is gone: every turn left ends `failed` (`finishAllTurns`). */
|
|
302
|
+
export function finishAllTurns(state, out) {
|
|
303
|
+
while (state.turns.length > 0)
|
|
304
|
+
finishTurn(state, out, 0, 'failed', { error: closeErrorText(state) });
|
|
305
|
+
}
|
|
306
|
+
// ---- cards ------------------------------------------------------------------------------------
|
|
307
|
+
function respond(state, out, rpcKey, result) {
|
|
308
|
+
const inbound = state.inbound[rpcKey];
|
|
309
|
+
if (!inbound)
|
|
310
|
+
return;
|
|
311
|
+
delete state.inbound[rpcKey];
|
|
312
|
+
if (connected(state))
|
|
313
|
+
out.result(inbound.id, result);
|
|
314
|
+
}
|
|
315
|
+
/** `AcpSession.settleApproval`'s bookkeeping: remembered with its reason, or — past the bound — the snapshot goes incomplete. */
|
|
316
|
+
function rememberSettledPermission(state, requestId, reason) {
|
|
317
|
+
if (state.settledPermissions.length < SETTLED_PERMISSION_LIMIT)
|
|
318
|
+
state.settledPermissions.push({ requestId, reason });
|
|
319
|
+
else
|
|
320
|
+
state.callbacks.complete = false;
|
|
321
|
+
}
|
|
322
|
+
/** Settle one card and answer the agent; `publish: false` inside a drain, which publishes once for all. */
|
|
323
|
+
function settlePermission(state, out, requestId, result, reason, publish = true) {
|
|
324
|
+
const entry = state.permissions[requestId];
|
|
325
|
+
if (!entry)
|
|
326
|
+
return;
|
|
327
|
+
delete state.permissions[requestId];
|
|
328
|
+
rememberSettledPermission(state, requestId, reason);
|
|
329
|
+
if (publish)
|
|
330
|
+
publishCallbacks(state, out);
|
|
331
|
+
respond(state, out, entry.rpcKey, result);
|
|
332
|
+
}
|
|
333
|
+
function settleElicitation(state, out, requestId, result) {
|
|
334
|
+
const entry = state.elicitations[requestId];
|
|
335
|
+
if (!entry)
|
|
336
|
+
return;
|
|
337
|
+
delete state.elicitations[requestId];
|
|
338
|
+
respond(state, out, entry.rpcKey, result);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* `drainPendingApprovals` + `drainPendingElicitations`: nobody will answer these any more. The
|
|
342
|
+
* callback snapshot goes incomplete and is published once (`drainPendingApprovals` publishes before
|
|
343
|
+
* it resolves the cards).
|
|
344
|
+
*/
|
|
345
|
+
function drainCards(state, out) {
|
|
346
|
+
state.callbacks.complete = false;
|
|
347
|
+
publishCallbacks(state, out);
|
|
348
|
+
for (const requestId of Object.keys(state.permissions))
|
|
349
|
+
settlePermission(state, out, requestId, PERMISSION_CANCELLED, 'canceled', false);
|
|
350
|
+
for (const requestId of Object.keys(state.elicitations))
|
|
351
|
+
settleElicitation(state, out, requestId, ELICITATION_CANCEL);
|
|
352
|
+
}
|
|
353
|
+
/** A request answered without a card. */
|
|
354
|
+
function refuseRequest(state, out, id, result) {
|
|
355
|
+
state.diagnostics.refusedRequests += 1;
|
|
356
|
+
out.result(id, result);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
/** `AcpSession.registerPendingApproval`'s field rule. */
|
|
360
|
+
function badCardField(value) {
|
|
361
|
+
// `Buffer.byteLength` without `Buffer`: UTF-8 length of a JS string.
|
|
362
|
+
let bytes = 0;
|
|
363
|
+
for (const char of value) {
|
|
364
|
+
const code = char.codePointAt(0);
|
|
365
|
+
bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
366
|
+
}
|
|
367
|
+
return !value.trim() || bytes > 256 || /[\x00-\x1f\x7f]/.test(value);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `session/request_permission` (params already through the SDK's zod schema) — `registerPermissionHandlers`
|
|
371
|
+
* → `handleRequestPermission` → `registerPendingApproval` → `emitEvent`, in their order. Returns the
|
|
372
|
+
* card id if a card now waits for the user (the request stays held), `undefined` if it was answered.
|
|
373
|
+
*/
|
|
374
|
+
export function onPermissionRequest(state, id, params, out) {
|
|
375
|
+
const sessionId = params.sessionId;
|
|
376
|
+
if (typeof sessionId !== 'string' || !sessionId.trim())
|
|
377
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
378
|
+
const options = Array.isArray(params.options) ? params.options : [];
|
|
379
|
+
if (options.length === 0)
|
|
380
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
381
|
+
if (state.process.status !== 'running')
|
|
382
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
383
|
+
const requestId = permissionRequestId(state.process.spawn.spawnId, id);
|
|
384
|
+
const rpcRequestId = String(id);
|
|
385
|
+
const event = buildPermissionRequestedEvent({
|
|
386
|
+
requestId, rpcRequestId, sessionId, toolCall: params.toolCall, options: options,
|
|
387
|
+
});
|
|
388
|
+
if (validateWireEvent(event))
|
|
389
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
390
|
+
const origin = sessionId;
|
|
391
|
+
// Inside a load's history window the root is not bound yet, but the session being loaded is it
|
|
392
|
+
// (today's attached session already knows its id).
|
|
393
|
+
const root = state.rootSessionId ?? state.history?.rootSessionId ?? null;
|
|
394
|
+
if (origin !== root && !getOwn(state.permissionOrigins, origin))
|
|
395
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
396
|
+
if (state.permissionsInterrupted)
|
|
397
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
398
|
+
if ([requestId, origin, rpcRequestId].some(badCardField) || Object.keys(state.permissions).length >= PENDING_PERMISSION_LIMIT
|
|
399
|
+
|| state.permissions[requestId] !== undefined || state.settledPermissions.some((settled) => settled.requestId === requestId)) {
|
|
400
|
+
// `registerPendingApproval`'s refusal: the snapshot can no longer vouch for every card.
|
|
401
|
+
state.callbacks.complete = false;
|
|
402
|
+
publishCallbacks(state, out);
|
|
403
|
+
return refuseRequest(state, out, id, PERMISSION_CANCELLED);
|
|
404
|
+
}
|
|
405
|
+
const rpcKey = inboundKey(id);
|
|
406
|
+
state.permissions[requestId] = { requestId, rpcKey, origin, rpcRequestId, optionIds: options.map((option) => option.optionId) };
|
|
407
|
+
publishCallbacks(state, out);
|
|
408
|
+
let payload = event.payload;
|
|
409
|
+
const toolCall = payload.toolCall;
|
|
410
|
+
if (isSdkCallbackCard(toolCall)) {
|
|
411
|
+
// `AcpSession.emitEvent`: an SDK callback card is shown only with the evidence the server needs
|
|
412
|
+
// to route its answer back (`callbackContext`); without it, it is cancelled.
|
|
413
|
+
const spawnId = state.process.spawn.spawnId;
|
|
414
|
+
const callbackContext = root === null ? undefined : permissionCallbackContext({
|
|
415
|
+
rootSessionId: root, origin, requestId, toolCall, pendingRpcRequestId: rpcRequestId,
|
|
416
|
+
diagnostics: { generation: spawnId, available: state.runtime.available, profile: state.runtime.profile, callbacks: callbackSnapshot(state) },
|
|
417
|
+
});
|
|
418
|
+
if (!callbackContext) {
|
|
419
|
+
settlePermission(state, out, requestId, PERMISSION_CANCELLED, 'canceled');
|
|
420
|
+
state.diagnostics.refusedRequests += 1;
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
payload = { ...payload, callbackContext };
|
|
424
|
+
}
|
|
425
|
+
emitSessionEvent(state, out, { eventName: event.eventName, payload, acpSessionId: origin });
|
|
426
|
+
return requestId;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* `elicitation/create` — `handleCreateElicitation` on a session connection (no `present`, so `'url'`
|
|
430
|
+
* declines) → `handleFormElicitation` → `registerPendingElicitation`. Returns the card id, as
|
|
431
|
+
* {@link onPermissionRequest} does.
|
|
432
|
+
*/
|
|
433
|
+
export function onElicitationRequest(state, id, params, out) {
|
|
434
|
+
if (params.mode !== 'form')
|
|
435
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
436
|
+
const requestId = String(id);
|
|
437
|
+
if (!requestId)
|
|
438
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
439
|
+
const request = params;
|
|
440
|
+
if (elicitationFieldKeys(request.requestedSchema).length === 0)
|
|
441
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
442
|
+
const event = buildElicitationRequestedEvent(request, requestId);
|
|
443
|
+
if (validateWireEvent(event))
|
|
444
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
445
|
+
if (typeof request.message === 'string' && isBlank(request.message)) {
|
|
446
|
+
// Legible, just not collectible: raise it, then decline (`handleFormElicitation`).
|
|
447
|
+
emitSessionEvent(state, out, event);
|
|
448
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
449
|
+
}
|
|
450
|
+
if (state.elicitations[requestId] === undefined && Object.keys(state.elicitations).length >= PENDING_ELICITATION_LIMIT) {
|
|
451
|
+
return refuseRequest(state, out, id, ELICITATION_DECLINE);
|
|
452
|
+
}
|
|
453
|
+
// Same id twice replaces the earlier card, as `pendingElicitations.set` does.
|
|
454
|
+
state.elicitations[requestId] = { requestId, rpcKey: inboundKey(id) };
|
|
455
|
+
emitSessionEvent(state, out, event);
|
|
456
|
+
return requestId;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* The agent sent `$/cancel_request` for one of its requests: a card waiting on it is settled the way
|
|
460
|
+
* the handler's abort listener settles it (permission → cancelled, elicitation → `cancel`). The card
|
|
461
|
+
* the server shows is not retracted, as today.
|
|
462
|
+
*/
|
|
463
|
+
export function onInboundCancelled(state, rpcKey, out) {
|
|
464
|
+
const card = state.inbound[rpcKey]?.card;
|
|
465
|
+
if (card === undefined)
|
|
466
|
+
return;
|
|
467
|
+
if (state.permissions[card]?.rpcKey === rpcKey)
|
|
468
|
+
settlePermission(state, out, card, PERMISSION_CANCELLED, 'canceled');
|
|
469
|
+
else if (state.elicitations[card]?.rpcKey === rpcKey)
|
|
470
|
+
settleElicitation(state, out, card, ELICITATION_CANCEL);
|
|
471
|
+
}
|
|
472
|
+
/** A low-level `rpc.respond` answered a carded request: the card goes with it (settled as answered). */
|
|
473
|
+
export function forgetCard(state, rpcKey, card, out) {
|
|
474
|
+
if (card === undefined)
|
|
475
|
+
return;
|
|
476
|
+
if (state.permissions[card]?.rpcKey === rpcKey) {
|
|
477
|
+
delete state.permissions[card];
|
|
478
|
+
rememberSettledPermission(state, card, 'answered');
|
|
479
|
+
publishCallbacks(state, out);
|
|
480
|
+
}
|
|
481
|
+
else if (state.elicitations[card]?.rpcKey === rpcKey) {
|
|
482
|
+
delete state.elicitations[card];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** `agent_answer{requestId, outcome}` — `AcpSession.answer`; an unknown id is ignored (counted). */
|
|
486
|
+
export function onAnswerNote(state, note, out) {
|
|
487
|
+
const { requestId, outcome } = note;
|
|
488
|
+
const entry = typeof requestId === 'string' ? state.permissions[requestId] : undefined;
|
|
489
|
+
if (!entry || typeof outcome !== 'string' || !connected(state)) {
|
|
490
|
+
state.diagnostics.droppedNotes += 1;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
settlePermission(state, out, entry.requestId, entry.optionIds.includes(outcome)
|
|
494
|
+
? { outcome: { outcome: 'selected', optionId: outcome } }
|
|
495
|
+
: PERMISSION_CANCELLED, 'answered');
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* `agent_answer_undeliverable{requestId}` — a permission card the server cannot be shown: the send-time
|
|
499
|
+
* projection dropped it (a child session's card for a peer older than `AGENT_WIRE_SCOPED_ORIGIN_VERSION`),
|
|
500
|
+
* so the daemon logs this note and the agent is answered "cancelled", as `onAgentEvent` answers it
|
|
501
|
+
* today. Driven by a note, so a replay answers it again from the log, never from the connection.
|
|
502
|
+
*/
|
|
503
|
+
export function onUndeliverableNote(state, note, out) {
|
|
504
|
+
const { requestId } = note;
|
|
505
|
+
const entry = typeof requestId === 'string' ? getOwn(state.permissions, requestId) : undefined;
|
|
506
|
+
if (!entry || !connected(state)) {
|
|
507
|
+
state.diagnostics.droppedNotes += 1;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
settlePermission(state, out, entry.requestId, PERMISSION_CANCELLED, 'answered');
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* `agent_elicitation_answer{requestId, action, content?}` — `agentSessions.ts`'s narrowing (an action
|
|
514
|
+
* other than accept/decline/cancel declines) + `AcpSession.answerElicitation`.
|
|
515
|
+
*/
|
|
516
|
+
export function onElicitationAnswerNote(state, note, out) {
|
|
517
|
+
const { requestId, action, content } = note;
|
|
518
|
+
const entry = typeof requestId === 'string' ? state.elicitations[requestId] : undefined;
|
|
519
|
+
if (!entry || typeof action !== 'string' || !connected(state) || (content !== undefined && !isRecord(content))) {
|
|
520
|
+
state.diagnostics.droppedNotes += 1;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const known = action === 'accept' || action === 'decline' || action === 'cancel';
|
|
524
|
+
settleElicitation(state, out, entry.requestId, known ? { action, ...(content ? { content } : {}) } : ELICITATION_DECLINE);
|
|
525
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side channels as notes (Task 17c, spec 2026-10-11 §5.4: "一切输入都进日志" — Qoder side-file events
|
|
3
|
+
* included).
|
|
4
|
+
*
|
|
5
|
+
* Qoder's certified background terminal repair reads the agent's transcript files. The processor
|
|
6
|
+
* never does: a file can change between the live run and a replay. The daemon reads them
|
|
7
|
+
* (`qoderAdapter/backgroundTerminal.ts` `BackgroundSideNotes`, run by T19) and appends what it
|
|
8
|
+
* derived as notes; the processor folds a fact only when it reads its note, with the pure fold
|
|
9
|
+
* (`qoderAdapter/backgroundTerminalFold.ts`) over checkpointed state. A replay therefore gives the same
|
|
10
|
+
* frames whatever the files say by then, or if they are gone.
|
|
11
|
+
*
|
|
12
|
+
* - `qoder_terminal_repair{nativeSessionId, restoreCalls?}` — the daemon enabled the repair where
|
|
13
|
+
* today's path calls `enableBackgroundTerminalRepair`: after the open note for a load (so the
|
|
14
|
+
* history is observed), after the open result for new/resume (`restoreCalls: true` on resume).
|
|
15
|
+
* Refused (`droppedNotes`) unless connected, the runtime is the certified one, the id is the root
|
|
16
|
+
* (or the session being loaded) and no repair is on yet. `includeProcess` follows the subwork
|
|
17
|
+
* boundary (`supportsSubworkMeta`), as `AcpSession` decides it.
|
|
18
|
+
* - `qoder_side{fact}` / `qoder_side{chunk}` — one derived fact (or the consecutive chunks of one too
|
|
19
|
+
* large for a note). Its events — a child transcript row's, or the results a poll's end publishes —
|
|
20
|
+
* are emitted the way `AcpSession.emitEvent` emits the repair's (`updates.ts` `emitSessionEvent`).
|
|
21
|
+
* Facts are idempotent by their own fields (ids, row uuids), so a reader that restarted from the
|
|
22
|
+
* start of a file repeats nothing. Dropped while no repair is on or the connection is closed.
|
|
23
|
+
*/
|
|
24
|
+
import { certifiedRuntime } from '../../qoderAdapter/foregroundOutput.js';
|
|
25
|
+
import { supportsSubworkMeta } from '../../qoderAdapter/subwork.js';
|
|
26
|
+
import { newTerminalRepairData, readSideNote, TerminalRepairFold } from '../../qoderAdapter/backgroundTerminalFold.js';
|
|
27
|
+
import { connected } from './session.js';
|
|
28
|
+
import { emitSessionEvent } from './updates.js';
|
|
29
|
+
function agentInfo(state) {
|
|
30
|
+
return state.initialize.status === 'ready' ? state.initialize.facts.agentInfo : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** `qoder_terminal_repair{nativeSessionId, restoreCalls?}`. */
|
|
33
|
+
export function onRepairNote(state, note) {
|
|
34
|
+
const { nativeSessionId, restoreCalls } = note;
|
|
35
|
+
const root = state.rootSessionId ?? state.history?.rootSessionId ?? null;
|
|
36
|
+
if (!connected(state) || state.boundaries.repair !== null || !certifiedRuntime(agentInfo(state)) || typeof nativeSessionId !== 'string'
|
|
37
|
+
|| nativeSessionId !== root || (restoreCalls !== undefined && typeof restoreCalls !== 'boolean')) {
|
|
38
|
+
state.diagnostics.droppedNotes += 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
state.boundaries.repair = newTerminalRepairData({
|
|
42
|
+
nativeSessionId, restoreCalls: restoreCalls === true, includeProcess: supportsSubworkMeta(agentInfo(state)),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** `qoder_side{fact | chunk}`. */
|
|
46
|
+
export function onSideNote(state, note, out) {
|
|
47
|
+
const repair = state.boundaries.repair;
|
|
48
|
+
if (repair === null || !connected(state)) {
|
|
49
|
+
state.diagnostics.droppedNotes += 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const read = readSideNote(state.boundaries.sideChunk, note);
|
|
53
|
+
state.boundaries.sideChunk = read.buffer;
|
|
54
|
+
if (read.malformed) {
|
|
55
|
+
state.diagnostics.droppedNotes += 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!read.fact)
|
|
59
|
+
return;
|
|
60
|
+
for (const event of new TerminalRepairFold(repair).apply(read.fact))
|
|
61
|
+
emitSessionEvent(state, out, event);
|
|
62
|
+
}
|
|
63
|
+
/** The connection or the session is gone: the repair stops (`BackgroundTerminalRepair.close`). */
|
|
64
|
+
export function closeRepair(state) {
|
|
65
|
+
state.boundaries.repair = null;
|
|
66
|
+
state.boundaries.sideChunk = null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What the daemon's reader asks before it reads (`BackgroundSideNotes.poll(demand)`), answered from the
|
|
70
|
+
* processor's **current** state, or `undefined` while no repair is on (stop the reader). The answers
|
|
71
|
+
* only steer which files are read; what was read reaches the processor through the notes alone.
|
|
72
|
+
*/
|
|
73
|
+
export function sideDemand(state) {
|
|
74
|
+
if (state.boundaries.repair === null)
|
|
75
|
+
return undefined;
|
|
76
|
+
return {
|
|
77
|
+
active: () => state.boundaries.repair !== null && new TerminalRepairFold(state.boundaries.repair).active(),
|
|
78
|
+
wantsProcess: (toolUseId) => state.boundaries.repair !== null && new TerminalRepairFold(state.boundaries.repair).wantsProcess(toolUseId),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2: Task 17b's open-session layer. 3: Task 17c (`boundaries`, `callbacks`, settled permissions with
|
|
3
|
+
* their reason, `rpcRequestId` on a pending permission). 4: the repair's `polledProcesses`. An older
|
|
4
|
+
* checkpoint is refused, never half-read.
|
|
5
|
+
*/
|
|
6
|
+
export const PROCESSOR_STATE_SCHEMA = 4;
|
|
7
|
+
export const SETTLED_HISTORY_LIMIT = 16;
|
|
8
|
+
export const SETTLED_PERMISSION_LIMIT = 64;
|
|
9
|
+
export const PENDING_PERMISSION_LIMIT = 64;
|
|
10
|
+
export const UNRESOLVED_ORIGIN_LIMIT = 1024;
|
|
11
|
+
export const PENDING_PROFILE_LIMIT = 8;
|
|
12
|
+
export const STEERED_HISTORY_LIMIT = 64;
|
|
13
|
+
/** Unfinished async tasks tracked per connection, all sessions together. */
|
|
14
|
+
export const ASYNC_TASK_LIMIT = 1024;
|
|
15
|
+
export function initialRuntime() {
|
|
16
|
+
return { revision: 0, available: true, adapterRevision: 0, pendingProfiles: {} };
|
|
17
|
+
}
|
|
18
|
+
export function initialBoundaries() {
|
|
19
|
+
return { foreground: { pending: {} }, subwork: { calls: {}, count: 0 }, repair: null, sideChunk: null };
|
|
20
|
+
}
|
|
21
|
+
export function initialCallbacks() {
|
|
22
|
+
return { complete: true, rootSessionId: null };
|
|
23
|
+
}
|
|
24
|
+
export function initialMessage() {
|
|
25
|
+
return { open: false, messageId: null, outOfTurn: false };
|
|
26
|
+
}
|
|
27
|
+
export function initialState() {
|
|
28
|
+
return {
|
|
29
|
+
schema: PROCESSOR_STATE_SCHEMA,
|
|
30
|
+
offset: -1,
|
|
31
|
+
t: 0,
|
|
32
|
+
process: { status: 'none' },
|
|
33
|
+
initialize: { status: 'idle' },
|
|
34
|
+
outbound: {},
|
|
35
|
+
inbound: {},
|
|
36
|
+
rootSessionId: null,
|
|
37
|
+
seq: 0,
|
|
38
|
+
turns: [],
|
|
39
|
+
exactExecutionUnconfirmed: false,
|
|
40
|
+
backgroundWriterUnconfirmed: false,
|
|
41
|
+
permissionsInterrupted: false,
|
|
42
|
+
permissions: {},
|
|
43
|
+
elicitations: {},
|
|
44
|
+
settledPermissions: [],
|
|
45
|
+
callbacks: initialCallbacks(),
|
|
46
|
+
permissionOrigins: {},
|
|
47
|
+
permissionParents: {},
|
|
48
|
+
unresolvedPermissionOrigins: {},
|
|
49
|
+
open: { status: 'idle' },
|
|
50
|
+
configOptions: null,
|
|
51
|
+
delegation: null,
|
|
52
|
+
history: null,
|
|
53
|
+
mapperScopes: {},
|
|
54
|
+
message: initialMessage(),
|
|
55
|
+
steers: {},
|
|
56
|
+
steered: [],
|
|
57
|
+
asyncTasks: {},
|
|
58
|
+
activity: null,
|
|
59
|
+
runtime: initialRuntime(),
|
|
60
|
+
boundaries: initialBoundaries(),
|
|
61
|
+
diagnostics: {
|
|
62
|
+
orphanResponses: 0, protocolErrors: 0, ignoredLines: 0, unknownNotes: 0, droppedNotes: 0, refusedRequests: 0,
|
|
63
|
+
rejectedRelations: 0, droppedUpdates: 0, invalidEvents: 0, untrackedAsyncTasks: 0, settled: [],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Set `record[key]` as an own property: agent-chosen keys (`__proto__`) stay plain keys. */
|
|
68
|
+
export function putOwn(record, key, value) {
|
|
69
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
70
|
+
}
|
|
71
|
+
/** `record[key]` if it is an own property. */
|
|
72
|
+
export function getOwn(record, key) {
|
|
73
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
74
|
+
}
|