@platless/axom-cli 0.3.13 → 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/dist/axom-patch/cli/README.md +30 -8
- 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-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- 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 +33 -6
- 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/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 +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +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/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 +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- 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 +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/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/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- 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/{fileOps.js → files/text.js} +117 -38
- 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/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ## Subagent relations bypass the window (fix round 1, spec §5.6)
|
|
3
|
+
*
|
|
4
|
+
* The server holds back its applied watermark — so its acks — at the first child frame whose subagent relation (`subagent_spawned`,
|
|
5
|
+
* the server's `TimelineEvent.SubagentSpawned`) it has not received yet (`noteApplied` capped at `nativePendingBefore - 1`,
|
|
6
|
+
* `ExternalAgentEventApplier.kt`). Every later frame then counts against the window; if the relation comes 256 or more frames after
|
|
7
|
+
* that first orphan child frame, a window that held it back would stall the session for good (the server waits 30 s, poisons, the
|
|
8
|
+
* reopen repeats it). So:
|
|
9
|
+
*
|
|
10
|
+
* - **producing**: while a child frame whose relation is not recorded yet is unacknowledged ({@link orphanOutstanding}), the record
|
|
11
|
+
* loop may run past the window, so the relation can be folded at all. Whatever the branch, this life never holds more than
|
|
12
|
+
* {@link RELATION_WAIT_EVENTS} / {@link RELATION_WAIT_BYTES} (1024 frames / 32 MiB) unacknowledged — pinned entries included — plus
|
|
13
|
+
* the frames of the one record being folded: that is the daemon's memory bound per session;
|
|
14
|
+
* - **sending**: once a relation that resolves such a child (a *resolving* relation) is recorded, it and every entry before it stop
|
|
15
|
+
* counting against either window ({@link pinnedThrough}): the sender takes them past the window, in seq order (sending the
|
|
16
|
+
* relation first would make the server drop the lower seqs as duplicates); the socket gate still applies. They must keep not
|
|
17
|
+
* counting after they are sent: the server applied them but wasted its acks while capped (each ack attempt resets its 32-event
|
|
18
|
+
* count), so its next ack needs up to 31 more applied frames — a window still full of them would deadlock there. They leave the
|
|
19
|
+
* outbox with that ack. A relation that resolves nothing recorded is not what the server waits for and keeps to the window
|
|
20
|
+
* (sending past it could overflow a parked server's 512-frame hold-off).
|
|
21
|
+
*
|
|
22
|
+
* ## Residual (documented, not fixed)
|
|
23
|
+
*
|
|
24
|
+
* While the producing window is full — a server parked on an approval with ≥ 256 root frames held, a connection that is down, an
|
|
25
|
+
* applier that never acknowledges — the record loop folds nothing: a steer or config RPC waits for the processor's reply and can
|
|
26
|
+
* time out on the server; an `agent_interrupt` note is folded (and the `session/cancel` written) only once the server unparks.
|
|
27
|
+
*/
|
|
28
|
+
/** Unacknowledged events per session (spec §5.6, `W_EVT`). `ACK_EVERY_EVENTS(32) < W_EVT ≤ HOLD_OFF_CAPACITY(512)`. */
|
|
29
|
+
export const SESSION_WINDOW_EVENTS = 256;
|
|
30
|
+
/** Unacknowledged bytes per session (spec §5.6): a few huge frames cannot hog the window. */
|
|
31
|
+
export const SESSION_WINDOW_BYTES = 8 * 1024 * 1024;
|
|
32
|
+
/** How far production may run past the window while a recorded child frame still waits for its relation (bounds memory). */
|
|
33
|
+
export const RELATION_WAIT_EVENTS = 4 * SESSION_WINDOW_EVENTS;
|
|
34
|
+
export const RELATION_WAIT_BYTES = 4 * SESSION_WINDOW_BYTES;
|
|
35
|
+
/** A frame carrying a subagent relation (the server's `SubagentSpawned`): it bypasses the session window. */
|
|
36
|
+
export function isRelationFrame(frame) {
|
|
37
|
+
return frame.t === 'agent_event' && frame.eventName === 'subagent_spawned';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `src.i` of an outbox entry no record produced: a frame this daemon numbered itself (a degraded session's failed `turn_done`,
|
|
41
|
+
* `src.o` = the last record folded before it) or one imported from a legacy journal (`src.o` = 0). A replay never reproduces it
|
|
42
|
+
* (T21 skips it when comparing). Far above any per-record frame index.
|
|
43
|
+
*/
|
|
44
|
+
export const DAEMON_FRAME_INDEX = 1 << 24;
|
|
45
|
+
/**
|
|
46
|
+
* Connection epoch of an entry that may have reached the server on an earlier connection (or in an earlier daemon life): it is not
|
|
47
|
+
* sent again until the server reopens the ref and says how far it got ({@link SessionOutbox.resendAll} after an `ack` of the open's
|
|
48
|
+
* `resumeFromSeq`) — resending a frame the server already applied could, for a `turn_done`, end a newer turn (analysis B §3.1).
|
|
49
|
+
*/
|
|
50
|
+
export const SENT_EARLIER = -1;
|
|
51
|
+
const frameBytes = (frame) => Buffer.byteLength(JSON.stringify(frame));
|
|
52
|
+
export function isOutboxTail(value) {
|
|
53
|
+
if (typeof value !== 'object' || value === null)
|
|
54
|
+
return false;
|
|
55
|
+
const { seq, src } = value;
|
|
56
|
+
return Number.isSafeInteger(seq) && typeof src === 'object' && src !== null &&
|
|
57
|
+
Number.isSafeInteger(src.o) && Number.isSafeInteger(src.i);
|
|
58
|
+
}
|
|
59
|
+
export class SessionOutbox {
|
|
60
|
+
store;
|
|
61
|
+
items = [];
|
|
62
|
+
committed;
|
|
63
|
+
highestSeq = 0;
|
|
64
|
+
ownEvents = 0;
|
|
65
|
+
ownBytes = 0;
|
|
66
|
+
chain = Promise.resolve();
|
|
67
|
+
roomWaiters = new Set();
|
|
68
|
+
listeners = new Set();
|
|
69
|
+
stopped = false;
|
|
70
|
+
/** Children whose relation frame this outbox recorded. */
|
|
71
|
+
related = new Set();
|
|
72
|
+
/** Seq of the latest resolving relation: entries at or below it count against neither window (file comment). */
|
|
73
|
+
pinned = 0;
|
|
74
|
+
isRelated;
|
|
75
|
+
/**
|
|
76
|
+
* `isRelated`: whether the session already knows a child's relation from elsewhere (the processor's relation graph — a relation
|
|
77
|
+
* recorded by an earlier life, or replayed by a load). Without it only relations this outbox recorded count.
|
|
78
|
+
*/
|
|
79
|
+
constructor(store, init = {}) {
|
|
80
|
+
this.store = store;
|
|
81
|
+
this.isRelated = init.isRelated ?? (() => false);
|
|
82
|
+
this.committed = isOutboxTail(init.committedTail) ? { seq: init.committedTail.seq, src: { ...init.committedTail.src } } : null;
|
|
83
|
+
this.highestSeq = this.committed?.seq ?? 0;
|
|
84
|
+
for (const record of init.leftovers ?? []) {
|
|
85
|
+
// Read back past the watermark only; a record at or below it is acknowledged (truncation is by whole segments).
|
|
86
|
+
if (this.committed && record.seq <= this.committed.seq)
|
|
87
|
+
continue;
|
|
88
|
+
this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes: frameBytes(record.frame), leftover: true,
|
|
89
|
+
...this.classify(record.seq, record.frame), sentOn: SENT_EARLIER });
|
|
90
|
+
this.highestSeq = Math.max(this.highestSeq, record.seq);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolving relation / orphan frame, in record order (a relation relates its child for every later frame). An orphan is a frame
|
|
95
|
+
* the server retains until a relation it has not received: a child frame (`acpSessionId`) whose child is unrelated, and — Task
|
|
96
|
+
* 22 — a `subagent_state_update` (any origin) naming an unrelated child (`payload.subagentSessionId`): the server's
|
|
97
|
+
* `receiveNativeEventLocked` holds it, and with it its acks, until that child's `subagent_spawned` exactly as it holds a child
|
|
98
|
+
* frame. The state update's own child is the key: a root-origin one would otherwise not be an orphan at all, its relation would
|
|
99
|
+
* keep to the window, and the session would stall where Task 20 fixed it for child frames.
|
|
100
|
+
*/
|
|
101
|
+
classify(seq, frame) {
|
|
102
|
+
const unrelated = (child) => child !== undefined && !this.related.has(child) && !this.isRelated(child) ? child : undefined;
|
|
103
|
+
const origin = frame.t === 'agent_event' && typeof frame.acpSessionId === 'string' ? frame.acpSessionId : undefined;
|
|
104
|
+
const stateChild = frame.t === 'agent_event' && frame.eventName === 'subagent_state_update'
|
|
105
|
+
? frame.payload?.subagentSessionId
|
|
106
|
+
: undefined;
|
|
107
|
+
const orphanOf = unrelated(typeof stateChild === 'string' ? stateChild : undefined) ?? unrelated(origin);
|
|
108
|
+
let resolving = false;
|
|
109
|
+
if (isRelationFrame(frame)) {
|
|
110
|
+
const child = frame.payload?.subagentSessionId;
|
|
111
|
+
if (typeof child === 'string') {
|
|
112
|
+
resolving = this.items.some((item) => item.orphanOf === child);
|
|
113
|
+
this.related.add(child);
|
|
114
|
+
if (resolving)
|
|
115
|
+
this.pinned = Math.max(this.pinned, seq);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { resolving, ...(orphanOf !== undefined ? { orphanOf } : {}) };
|
|
119
|
+
}
|
|
120
|
+
/** Entries at or below this seq count against neither window (the latest resolving relation; 0: none). */
|
|
121
|
+
get pinnedThrough() { return this.pinned; }
|
|
122
|
+
/** An unacknowledged child frame whose relation is still not recorded: the server is (likely) holding its acks for it. */
|
|
123
|
+
orphanOutstanding() {
|
|
124
|
+
return this.items.some((item) => item.orphanOf !== undefined && !this.related.has(item.orphanOf) && !this.isRelated(item.orphanOf));
|
|
125
|
+
}
|
|
126
|
+
/** Highest seq this outbox ever held (entries, or the acknowledged watermark): a new life numbers after it (§5.5). */
|
|
127
|
+
get lastSeq() { return this.highestSeq; }
|
|
128
|
+
/** The acknowledged watermark (`committedTail`), `null` before the first truncating ack. */
|
|
129
|
+
get committedTail() { return this.committed; }
|
|
130
|
+
/** Unacknowledged entries, in seq order. */
|
|
131
|
+
get pending() { return this.items; }
|
|
132
|
+
/** Unacknowledged entries appended by this life (the producing window), past {@link pinnedThrough}. */
|
|
133
|
+
get backlog() {
|
|
134
|
+
if (this.pinned === 0)
|
|
135
|
+
return { events: this.ownEvents, bytes: this.ownBytes };
|
|
136
|
+
let events = 0;
|
|
137
|
+
let bytes = 0;
|
|
138
|
+
for (const item of this.items)
|
|
139
|
+
if (!item.leftover && item.seq > this.pinned) {
|
|
140
|
+
events += 1;
|
|
141
|
+
bytes += item.bytes;
|
|
142
|
+
}
|
|
143
|
+
return { events, bytes };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Entries sent on connection `epoch` and not acknowledged (the sending window). Entries at or below {@link pinnedThrough} are
|
|
147
|
+
* sent past the window either way (the sender bypasses it for them); once sent they count only when `pinnedCountsWhenSent` —
|
|
148
|
+
* a server that acknowledges as soon as the relation releases its cap (`AGENT_WIRE_RELATION_ACK_VERSION`, Task 22). An older
|
|
149
|
+
* server wastes its acks while capped, so its first ack after the relation can need up to 31 more applied frames: counting the
|
|
150
|
+
* pinned ones there would deadlock right after the relation (fix round 1 of Task 20).
|
|
151
|
+
*/
|
|
152
|
+
inflight(epoch, pinnedCountsWhenSent = false) {
|
|
153
|
+
let events = 0;
|
|
154
|
+
let bytes = 0;
|
|
155
|
+
for (const item of this.items) {
|
|
156
|
+
if (item.sentOn === epoch && (pinnedCountsWhenSent || item.seq > this.pinned)) {
|
|
157
|
+
events += 1;
|
|
158
|
+
bytes += item.bytes;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { events, bytes };
|
|
162
|
+
}
|
|
163
|
+
/** Called whenever there may be something new to send (an append, an ack opening the window, a resend). */
|
|
164
|
+
onChange(listener) {
|
|
165
|
+
this.listeners.add(listener);
|
|
166
|
+
return () => { this.listeners.delete(listener); };
|
|
167
|
+
}
|
|
168
|
+
changed() { for (const listener of [...this.listeners])
|
|
169
|
+
listener(); }
|
|
170
|
+
run(op) {
|
|
171
|
+
const result = this.chain.then(op);
|
|
172
|
+
this.chain = result.then(() => undefined, () => undefined);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/** Waits for every stream operation queued so far. */
|
|
176
|
+
flush() { return this.chain; }
|
|
177
|
+
/**
|
|
178
|
+
* Record frames before they are sent. Resolves once they are durable; rejects when the stream refused them (the caller degrades
|
|
179
|
+
* the session — a frame that could not be recorded is not sent either).
|
|
180
|
+
*/
|
|
181
|
+
append(frames) {
|
|
182
|
+
if (frames.length === 0)
|
|
183
|
+
return this.chain;
|
|
184
|
+
return this.run(async () => {
|
|
185
|
+
const entries = frames.map((f) => ({ seq: f.seq, src: { o: f.src.o, i: f.src.i }, frame: { ...f.frame } }));
|
|
186
|
+
const { first } = await this.store.append(entries);
|
|
187
|
+
entries.forEach((entry, index) => {
|
|
188
|
+
const bytes = frameBytes(entry.frame);
|
|
189
|
+
this.items.push({ o: first + index, seq: entry.seq, src: entry.src, frame: entry.frame, bytes, leftover: false, ...this.classify(entry.seq, entry.frame), sentOn: null });
|
|
190
|
+
this.highestSeq = Math.max(this.highestSeq, entry.seq);
|
|
191
|
+
this.ownEvents += 1;
|
|
192
|
+
this.ownBytes += bytes;
|
|
193
|
+
});
|
|
194
|
+
this.wakeRoom(); // a resolving relation unpins the producing window
|
|
195
|
+
this.changed();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The server applied everything through `throughSeq` (`agent_event_ack`, or an open's `resumeFromSeq`): forget those entries and
|
|
200
|
+
* truncate the stream through the last of them, persisting `{seq, src}` of that entry as the watermark. Nothing held at or below
|
|
201
|
+
* it: nothing changes. A failed commit is reported (`onError`) — the entries are acknowledged either way; the stream keeps them
|
|
202
|
+
* until a later commit passes them.
|
|
203
|
+
*/
|
|
204
|
+
ack(throughSeq, onError) {
|
|
205
|
+
return this.run(async () => {
|
|
206
|
+
let k = -1;
|
|
207
|
+
for (let i = 0; i < this.items.length && this.items[i].seq <= throughSeq; i++)
|
|
208
|
+
k = i;
|
|
209
|
+
if (k < 0)
|
|
210
|
+
return;
|
|
211
|
+
const last = this.items[k];
|
|
212
|
+
const removed = this.items.splice(0, k + 1);
|
|
213
|
+
for (const item of removed)
|
|
214
|
+
if (!item.leftover) {
|
|
215
|
+
this.ownEvents -= 1;
|
|
216
|
+
this.ownBytes -= item.bytes;
|
|
217
|
+
}
|
|
218
|
+
this.committed = { seq: last.seq, src: { ...last.src } };
|
|
219
|
+
this.wakeRoom();
|
|
220
|
+
this.changed();
|
|
221
|
+
try {
|
|
222
|
+
await this.store.commit({ outbox: { through: last.o, tail: { seq: last.seq, src: { ...last.src } } } });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
onError?.(error);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A processor checkpoint at raw `offset`, and raw truncation through it (the keeper refuses anything past it) unless `truncate` is
|
|
231
|
+
* false (a marker checkpoint, Task 21).
|
|
232
|
+
*/
|
|
233
|
+
commitCheckpoint(offset, blob, truncate = true) {
|
|
234
|
+
return this.run(async () => {
|
|
235
|
+
await this.store.commit({ checkpoint: { offset, blob }, ...(truncate ? { rawThrough: offset } : {}) });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The session re-attached to keeper-acp after its connection was lost (Task 21): the stream is the truth again. An append whose
|
|
240
|
+
* reply was lost may have landed (it is in `leftovers` now) or not (it is gone, and the record loop folds its record again). The
|
|
241
|
+
* entries are replaced by what the stream holds past its watermark; an entry this object already had keeps whether (and on which
|
|
242
|
+
* connection) it was sent and whether an earlier life left it; one it never saw was never sent. Call it with no stream operation
|
|
243
|
+
* in flight ({@link flush} first). Listeners and room waiters are kept.
|
|
244
|
+
*/
|
|
245
|
+
rebase(init) {
|
|
246
|
+
const previous = new Map(this.items.map((item) => [item.seq, item]));
|
|
247
|
+
this.items.length = 0;
|
|
248
|
+
this.related.clear();
|
|
249
|
+
this.pinned = 0;
|
|
250
|
+
this.ownEvents = 0;
|
|
251
|
+
this.ownBytes = 0;
|
|
252
|
+
if (isOutboxTail(init.committedTail) && (this.committed === null || init.committedTail.seq >= this.committed.seq)) {
|
|
253
|
+
this.committed = { seq: init.committedTail.seq, src: { ...init.committedTail.src } };
|
|
254
|
+
}
|
|
255
|
+
this.highestSeq = Math.max(this.highestSeq, this.committed?.seq ?? 0);
|
|
256
|
+
for (const record of init.leftovers) {
|
|
257
|
+
if (this.committed && record.seq <= this.committed.seq)
|
|
258
|
+
continue;
|
|
259
|
+
const before = previous.get(record.seq);
|
|
260
|
+
const leftover = before?.leftover ?? false;
|
|
261
|
+
const bytes = frameBytes(record.frame);
|
|
262
|
+
this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes, leftover,
|
|
263
|
+
...this.classify(record.seq, record.frame), sentOn: before ? before.sentOn : null });
|
|
264
|
+
if (!leftover) {
|
|
265
|
+
this.ownEvents += 1;
|
|
266
|
+
this.ownBytes += bytes;
|
|
267
|
+
}
|
|
268
|
+
this.highestSeq = Math.max(this.highestSeq, record.seq);
|
|
269
|
+
}
|
|
270
|
+
this.wakeRoom();
|
|
271
|
+
this.changed();
|
|
272
|
+
}
|
|
273
|
+
/** Mark every unacknowledged entry unsent: the sender resends them in order (a reopen's replay, after the ack of its watermark). */
|
|
274
|
+
resendAll() {
|
|
275
|
+
for (const item of this.items)
|
|
276
|
+
item.sentOn = null;
|
|
277
|
+
this.changed();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A new connection: everything recorded so far — sent on an earlier connection or produced while none was bound — is held until
|
|
281
|
+
* the server reopens the ref and says how far it got ({@link SENT_EARLIER}); what is recorded from now on is sent at once. (The
|
|
282
|
+
* journal's semantics: frames produced while unbound were replayed by the reopen, never sent on their own.)
|
|
283
|
+
*/
|
|
284
|
+
connectionChanged() {
|
|
285
|
+
for (const item of this.items)
|
|
286
|
+
item.sentOn = SENT_EARLIER;
|
|
287
|
+
}
|
|
288
|
+
/** The first entry never sent (the sender's next candidate), in seq order. */
|
|
289
|
+
nextUnsent() {
|
|
290
|
+
return this.items.find((item) => item.sentOn === null);
|
|
291
|
+
}
|
|
292
|
+
/** Resolves when this life's unacknowledged backlog is below the window (immediately once {@link stopWaiting} ran). */
|
|
293
|
+
room() {
|
|
294
|
+
if (this.hasRoom())
|
|
295
|
+
return undefined;
|
|
296
|
+
return new Promise((resolve) => { this.roomWaiters.add(resolve); });
|
|
297
|
+
}
|
|
298
|
+
hasRoom() {
|
|
299
|
+
if (this.stopped)
|
|
300
|
+
return true;
|
|
301
|
+
// A hard bound on everything this life holds unacknowledged, pinned or not: chained resolving relations each reset `backlog`,
|
|
302
|
+
// but pinned entries leave memory only with an ack.
|
|
303
|
+
if (this.ownEvents >= RELATION_WAIT_EVENTS || this.ownBytes >= RELATION_WAIT_BYTES)
|
|
304
|
+
return false;
|
|
305
|
+
const { events, bytes } = this.backlog;
|
|
306
|
+
if (events < SESSION_WINDOW_EVENTS && bytes < SESSION_WINDOW_BYTES)
|
|
307
|
+
return true;
|
|
308
|
+
// Past the window only while a relation is awaited: the server cannot acknowledge before it, so it must be folded.
|
|
309
|
+
return this.orphanOutstanding();
|
|
310
|
+
}
|
|
311
|
+
wakeRoom() {
|
|
312
|
+
if (!this.hasRoom())
|
|
313
|
+
return;
|
|
314
|
+
for (const resolve of [...this.roomWaiters])
|
|
315
|
+
resolve();
|
|
316
|
+
this.roomWaiters.clear();
|
|
317
|
+
}
|
|
318
|
+
/** The session is closing or degraded: nobody waits for room any more. */
|
|
319
|
+
stopWaiting() {
|
|
320
|
+
this.stopped = true;
|
|
321
|
+
this.wakeRoom();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** An outbox store over a transport session (`acp.outbox.append` / `acp.commit`). */
|
|
325
|
+
export function transportOutboxStore(transport, sessionKey) {
|
|
326
|
+
return {
|
|
327
|
+
append: (entries) => transport.appendOutbox(sessionKey, entries),
|
|
328
|
+
commit: (params) => transport.commit(sessionKey, params),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -20,9 +20,10 @@ function validateCondition(id, when, until, whenKey, untilKey) {
|
|
|
20
20
|
export function validateOverride(entry) {
|
|
21
21
|
validateCondition(entry.id, entry.when_registry_version, entry.until_registry_version, 'when_registry_version', 'until_registry_version');
|
|
22
22
|
const until = entry.until_registry_version;
|
|
23
|
-
// A pin
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// A catch-up pin may equal its bound: use it until the registry publishes that release.
|
|
24
|
+
// A pin above the bound would instead let the registry replace it with an older release.
|
|
25
|
+
if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lte(entry.distribution.version, until))) {
|
|
26
|
+
throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must not sort above ${until}`);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
export function validatePackagePatch(recipe) {
|
|
@@ -61,3 +62,19 @@ export const packagePatches = runtime.package_patches.map((file) => {
|
|
|
61
62
|
validatePackagePatch(recipe);
|
|
62
63
|
return recipe;
|
|
63
64
|
});
|
|
65
|
+
/** The declared native wrappers, for the catalog↔patch consistency guard in `catalog.test.ts`. */
|
|
66
|
+
export const nativeExtensions = runtime.native_extensions ?? [];
|
|
67
|
+
/** The bundled overrides, for the same catalog↔patch consistency guard. */
|
|
68
|
+
export const runtimeOverrides = runtime.overrides;
|
|
69
|
+
/** Native wrappers are declared in the patch inventory, never selected by agent-specific launch branches. */
|
|
70
|
+
export function nativeExtensionFor(agent, version) {
|
|
71
|
+
const extension = runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version));
|
|
72
|
+
if (!extension)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (extension.entry.startsWith('/') || extension.entry.split('/').includes('..'))
|
|
75
|
+
throw new Error('Invalid native extension path');
|
|
76
|
+
return new URL(extension.entry, patchAssetRoot);
|
|
77
|
+
}
|
|
78
|
+
export function nativeAccountLimitsVersionFor(agent, version) {
|
|
79
|
+
return runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version))?.accountLimitsVersion ?? 2;
|
|
80
|
+
}
|
|
@@ -78,7 +78,6 @@
|
|
|
78
78
|
* complete it, so `authenticate` remembers every id it handed out and retracts what is left when
|
|
79
79
|
* the login settles.
|
|
80
80
|
*/
|
|
81
|
-
import { randomUUID } from 'node:crypto';
|
|
82
81
|
import { methods } from '@agentclientprotocol/sdk';
|
|
83
82
|
import { validateWireEvent } from './validate.js';
|
|
84
83
|
/** A value guaranteed not to equal any real `optionId` in `input.options` for a given call — used to purge a pending entry without accidentally supplying a matchable answer. Regenerated per call rather than a fixed literal, for the same anti-collision reasoning as `AcpSession`'s own abandonment sentinel (review finding, task-5 item 6). */
|
|
@@ -123,8 +122,9 @@ function toolCallPayload(toolCall) {
|
|
|
123
122
|
* ACP `RequestPermissionRequest` (`types.gen.d.ts:108`) on Axom's wire: `{toolCall, options}` plus
|
|
124
123
|
* `requestId`.
|
|
125
124
|
*
|
|
126
|
-
* `requestId` is a host-
|
|
127
|
-
*
|
|
125
|
+
* `requestId` is a host-derived resolver token — {@link permissionRequestId}, the same on the live
|
|
126
|
+
* path and in the ACP processor. The raw JSON-RPC ID stays local to its connection; exact `sessionId`
|
|
127
|
+
* travels in the wire event's `acpSessionId` origin field.
|
|
128
128
|
*
|
|
129
129
|
* **`toolCall` is nested, not flattened.** The flattened version lifted the tool's
|
|
130
130
|
* `name`/`title`/`kind`/`arguments` to the top level, which put `name` (the tool's programmatic name,
|
|
@@ -135,7 +135,7 @@ function toolCallPayload(toolCall) {
|
|
|
135
135
|
* `toolCall.title`, and the renderer computes it from the same field now
|
|
136
136
|
* (`WorkTimelineInteractions.foldPermission`) instead of storing a second, drifting copy.
|
|
137
137
|
*/
|
|
138
|
-
function buildPermissionRequestedEvent(input) {
|
|
138
|
+
export function buildPermissionRequestedEvent(input) {
|
|
139
139
|
return {
|
|
140
140
|
eventName: 'permission_request',
|
|
141
141
|
...(input.sessionId !== undefined ? { acpSessionId: input.sessionId } : {}),
|
|
@@ -164,7 +164,7 @@ function buildPermissionRequestedEvent(input) {
|
|
|
164
164
|
* `sink.answer` — the agent gave up on this specific request, so nothing should still expect an
|
|
165
165
|
* answer for it. Session-level abandonment (the session closing, or the connection dying) is instead
|
|
166
166
|
* `AcpSession`'s own job (`registerPendingApproval` resolving immediately when already closed, and
|
|
167
|
-
* `runReadLoop` draining on an aborted connection) — see `acpSession.ts
|
|
167
|
+
* `runReadLoop` draining on an aborted connection) — see the old `acpSession.ts` (deleted in Task 29).
|
|
168
168
|
*
|
|
169
169
|
* The eventual answer is whatever string reaches `sink`'s registered resolver: normally one of
|
|
170
170
|
* `input.options`' own `optionId`s (relayed from the user through `AcpSession.answer`), but both
|
|
@@ -320,7 +320,7 @@ function isFoldedCompanionKey(key, properties) {
|
|
|
320
320
|
* each return `null` from `elicitationRequestFromPayload`, because `MODE_FORM` requires
|
|
321
321
|
* `fields.isNotEmpty()`. Everything else it now decodes (see {@link handleFormElicitation}).
|
|
322
322
|
*/
|
|
323
|
-
function elicitationFieldKeys(requestedSchema) {
|
|
323
|
+
export function elicitationFieldKeys(requestedSchema) {
|
|
324
324
|
const properties = requestedSchema?.properties;
|
|
325
325
|
if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
|
|
326
326
|
return [];
|
|
@@ -336,7 +336,7 @@ function elicitationFieldKeys(requestedSchema) {
|
|
|
336
336
|
* request is dropped server-side, which is why this check survived Task 9's removal of the
|
|
337
337
|
* per-property profile predicate (see the ruling-R24 block below).
|
|
338
338
|
*/
|
|
339
|
-
function isBlank(value) {
|
|
339
|
+
export function isBlank(value) {
|
|
340
340
|
return value.trim().length === 0;
|
|
341
341
|
}
|
|
342
342
|
/**
|
|
@@ -406,7 +406,7 @@ function isBlank(value) {
|
|
|
406
406
|
* event always carries exactly what the agent asked; {@link handleFormElicitation} is what decides,
|
|
407
407
|
* from that same schema, whether to wait for an answer or decline right after raising the card.
|
|
408
408
|
*/
|
|
409
|
-
function buildElicitationRequestedEvent(request, requestId) {
|
|
409
|
+
export function buildElicitationRequestedEvent(request, requestId) {
|
|
410
410
|
return {
|
|
411
411
|
eventName: 'elicitation_request',
|
|
412
412
|
payload: {
|
|
@@ -521,6 +521,35 @@ async function handleFormElicitation(request, ctx, sink) {
|
|
|
521
521
|
export function handleElicitationComplete(notification, deps) {
|
|
522
522
|
deps.dismiss(notification.elicitationId);
|
|
523
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The card id of one `session/request_permission`: `perm:<hostGeneration>:<JSON agent id>` (spec
|
|
526
|
+
* 2026-10-11 §5.4).
|
|
527
|
+
*
|
|
528
|
+
* ## Why derived, not random
|
|
529
|
+
*
|
|
530
|
+
* It used to be `randomUUID()` per invocation. A card id the server is already showing must still
|
|
531
|
+
* resolve after the daemon restarts underneath a live adapter, and a random id cannot: the re-presented
|
|
532
|
+
* request would get a new one, `settleApproval` ignores unknown ids, and the user's answer to the
|
|
533
|
+
* visible card would be lost (analysis A §4). Derived from the request itself, the same request gets
|
|
534
|
+
* the same id in every daemon generation, and the ACP processor — which may not draw randomness at
|
|
535
|
+
* all (`processor/guard.test.ts`) — computes it with this very function.
|
|
536
|
+
*
|
|
537
|
+
* ## Why it is still unique
|
|
538
|
+
*
|
|
539
|
+
* - `hostGeneration` identifies one adapter process: the keeper's `spawnId` on the processor path,
|
|
540
|
+
* `AgentRuntimeDiagnostics.generation` (fresh per `connectAgent`) on today's live path — the same
|
|
541
|
+
* value published as `callbackContext.hostGeneration`. A reconnect is a new process, so a new
|
|
542
|
+
* generation: the agent's ids restart at 0 there without colliding with an earlier card.
|
|
543
|
+
* - Within one process the agent never reuses a pending JSON-RPC id. `JSON.stringify` keeps the
|
|
544
|
+
* number `1` and the string `"1"` apart.
|
|
545
|
+
*
|
|
546
|
+
* Kept short enough for `AcpSession.registerPendingApproval`'s 256-byte rule for every id a real
|
|
547
|
+
* adapter sends; a pathological id longer than ~200 bytes is refused there, as an overlong
|
|
548
|
+
* `rpcRequestId` already was.
|
|
549
|
+
*/
|
|
550
|
+
export function permissionRequestId(hostGeneration, agentRequestId) {
|
|
551
|
+
return `perm:${hostGeneration}:${JSON.stringify(agentRequestId)}`;
|
|
552
|
+
}
|
|
524
553
|
/**
|
|
525
554
|
* Register this module's three handlers onto `app`. Must run inside `connectAgent`'s `configure`
|
|
526
555
|
* hook — see the file doc comment for why a post-connect `.onRequest`/`.onNotification` would compile
|
|
@@ -534,15 +563,18 @@ export function handleElicitationComplete(notification, deps) {
|
|
|
534
563
|
* the call site instead. Its own `present` may still be omitted (see {@link PresentElicitationUrl}) —
|
|
535
564
|
* that is `acpAgents.ts`'s session connection saying, on purpose, that a `url` elicitation raised
|
|
536
565
|
* outside a login has nowhere to go, and it declines rather than accepting into nothing.
|
|
566
|
+
*
|
|
567
|
+
* `connection.generation` is the adapter process's generation — `connectAgent` hands it to its
|
|
568
|
+
* `configure` hook — and scopes the card ids {@link permissionRequestId} derives.
|
|
537
569
|
*/
|
|
538
|
-
export function registerPermissionHandlers(app, sink, elicitationDeps) {
|
|
570
|
+
export function registerPermissionHandlers(app, sink, elicitationDeps, connection) {
|
|
539
571
|
app
|
|
540
572
|
.onRequest(methods.client.session.requestPermission, async (ctx) => {
|
|
541
573
|
if (typeof ctx.params.sessionId !== 'string' || !ctx.params.sessionId.trim()) {
|
|
542
574
|
console.error(JSON.stringify({ component: 'acp', error: 'invalid_permission_origin', rpcRequestId: String(ctx.requestId) }));
|
|
543
575
|
return { outcome: { outcome: 'cancelled' } };
|
|
544
576
|
}
|
|
545
|
-
const decision = await handleRequestPermission({ requestId:
|
|
577
|
+
const decision = await handleRequestPermission({ requestId: permissionRequestId(connection.generation, ctx.requestId), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
|
|
546
578
|
return {
|
|
547
579
|
outcome: decision.outcome === 'answered' ? { outcome: 'selected', optionId: decision.optionId } : { outcome: 'cancelled' },
|
|
548
580
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processor checkpoints (spec 2026-10-11 §5.5): the state after record `offset`, as JSON.
|
|
3
|
+
*
|
|
4
|
+
* ## Two versions, two questions
|
|
5
|
+
*
|
|
6
|
+
* - `schema` answers "can this daemon **read** it?". An unknown schema throws
|
|
7
|
+
* {@link CheckpointUnreadableError}; the caller then replays from the session's start (outbox and
|
|
8
|
+
* watermark cover the frames already sent), or, if the log was truncated, marks the session
|
|
9
|
+
* degraded and switches it at the next open (§5.5.6). Readability of older schemas is a test-guarded
|
|
10
|
+
* promise: never change the meaning of an existing field without bumping `schema` and adding a
|
|
11
|
+
* reader for the old one.
|
|
12
|
+
* - `processorVersion` answers "would **this** code have produced the same frames?". Bump it whenever
|
|
13
|
+
* `step` can emit different output for the same records. A readable checkpoint from another
|
|
14
|
+
* processor version is still restored — that is the whole point of cutting a head checkpoint at
|
|
15
|
+
* upgrade — but the caller must not count replay-vs-outbox differences as divergence then.
|
|
16
|
+
*
|
|
17
|
+
* Checkpoints are cut only on record boundaries (a synchronous `step` leaves no half-processed record)
|
|
18
|
+
* and carry no secrets (`state.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import { PROCESSOR_STATE_SCHEMA } from './state.js';
|
|
21
|
+
/**
|
|
22
|
+
* Bump when `step`'s output for the same records changes. 2: Task 17a (turns, cards, `seq`; prompt
|
|
23
|
+
* methods refused by `rpc.request`). 3: Task 17b (`session/update` mapped, the open session bound
|
|
24
|
+
* from its response, config/steer/levels; events validated before numbering). 4: commands carry
|
|
25
|
+
* their `spawnId`; `processor.history_end` carries `droppedCount`; a prompt closes open history.
|
|
26
|
+
* 5: Task 17c (the callback snapshot in the runtime diagnostics and its revision bumps; Qoder SDK
|
|
27
|
+
* cards carry a `callbackContext`; the Qoder adapter boundaries; side-channel notes). 6: a process call's
|
|
28
|
+
* result waits for its child's first status; ended/delivered calls keep no dead data.
|
|
29
|
+
* 7: Task 20 — the `agent_install_outcome` note becomes a `step` event (it was an unknown note).
|
|
30
|
+
* 8: Task 21 — a level update after a load's response is folded live, not as history; the `agent_output_gap` note is a `step`.
|
|
31
|
+
*/
|
|
32
|
+
export const PROCESSOR_VERSION = 8;
|
|
33
|
+
export const CHECKPOINT_SCHEMA = 1;
|
|
34
|
+
export class CheckpointUnreadableError extends Error {
|
|
35
|
+
}
|
|
36
|
+
function clone(value) {
|
|
37
|
+
return JSON.parse(JSON.stringify(value));
|
|
38
|
+
}
|
|
39
|
+
/** A detached copy of `state`, so later steps cannot reach into a checkpoint already handed out. */
|
|
40
|
+
export function checkpointOf(state) {
|
|
41
|
+
return { schema: CHECKPOINT_SCHEMA, processorVersion: PROCESSOR_VERSION, offset: state.offset, state: clone(state) };
|
|
42
|
+
}
|
|
43
|
+
export function encodeCheckpoint(checkpoint) {
|
|
44
|
+
return JSON.stringify(checkpoint);
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
/** Parse and check a checkpoint blob (string or already-parsed JSON). */
|
|
50
|
+
export function decodeCheckpoint(blob) {
|
|
51
|
+
let value = blob;
|
|
52
|
+
if (typeof blob === 'string') {
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(blob);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new CheckpointUnreadableError('checkpoint is not JSON');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(value))
|
|
61
|
+
throw new CheckpointUnreadableError('checkpoint is not an object');
|
|
62
|
+
if (value.schema !== CHECKPOINT_SCHEMA)
|
|
63
|
+
throw new CheckpointUnreadableError(`unknown checkpoint schema ${JSON.stringify(value.schema)}`);
|
|
64
|
+
const { processorVersion, offset, state } = value;
|
|
65
|
+
if (typeof processorVersion !== 'number' || !Number.isInteger(processorVersion))
|
|
66
|
+
throw new CheckpointUnreadableError('checkpoint has no processorVersion');
|
|
67
|
+
if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < -1)
|
|
68
|
+
throw new CheckpointUnreadableError('checkpoint offset is invalid');
|
|
69
|
+
if (!isRecord(state) || state.schema !== PROCESSOR_STATE_SCHEMA)
|
|
70
|
+
throw new CheckpointUnreadableError('checkpoint state schema is unknown');
|
|
71
|
+
if (state.offset !== offset)
|
|
72
|
+
throw new CheckpointUnreadableError('checkpoint offset disagrees with its state');
|
|
73
|
+
for (const key of ['process', 'initialize', 'outbound', 'inbound', 'diagnostics', 'permissions', 'elicitations',
|
|
74
|
+
'permissionOrigins', 'permissionParents', 'unresolvedPermissionOrigins', 'open', 'mapperScopes', 'message', 'steers',
|
|
75
|
+
'asyncTasks', 'runtime', 'callbacks', 'boundaries']) {
|
|
76
|
+
if (!isRecord(state[key]))
|
|
77
|
+
throw new CheckpointUnreadableError(`checkpoint state has no ${key}`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof state.seq !== 'number' || !Array.isArray(state.turns) || !Array.isArray(state.settledPermissions)) {
|
|
80
|
+
throw new CheckpointUnreadableError('checkpoint state has no session layer (seq/turns)');
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(state.steered) || !('configOptions' in state) || !('delegation' in state) || !('history' in state)
|
|
83
|
+
|| !('activity' in state)) {
|
|
84
|
+
throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
|
|
85
|
+
}
|
|
86
|
+
return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone(state) };
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The line to write for `command`: `fill`ed from `resolve`, or `undefined` if its blocks are gone. */
|
|
2
|
+
export function materializeLine(command, resolve) {
|
|
3
|
+
if (command.fill === undefined)
|
|
4
|
+
return command.line;
|
|
5
|
+
const blocks = resolve?.(command.fill.blocksRef);
|
|
6
|
+
if (blocks === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const message = JSON.parse(command.line);
|
|
9
|
+
// Assigning an existing key keeps its position: the line keeps the SDK's key order.
|
|
10
|
+
message.params.prompt = blocks;
|
|
11
|
+
return JSON.stringify(message);
|
|
12
|
+
}
|
|
13
|
+
export function executeDownCommands(sink, commands, resolve) {
|
|
14
|
+
const written = [];
|
|
15
|
+
const dropped = [];
|
|
16
|
+
const refused = [];
|
|
17
|
+
const unresolved = [];
|
|
18
|
+
for (const command of commands) {
|
|
19
|
+
if (sink.hasDown(command.cause)) {
|
|
20
|
+
dropped.push(command);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const line = materializeLine(command, resolve);
|
|
24
|
+
if (line === undefined) {
|
|
25
|
+
unresolved.push(command);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const { fill: _fill, ...rest } = command;
|
|
29
|
+
if (sink.writeDown({ ...rest, line }) === 'refused')
|
|
30
|
+
refused.push(command);
|
|
31
|
+
else
|
|
32
|
+
written.push(command);
|
|
33
|
+
}
|
|
34
|
+
return { written, dropped, refused, unresolved };
|
|
35
|
+
}
|