@platless/axom-cli 0.5.9 → 0.6.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/axom-patch/cli/README.md +100 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +28 -0
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -1,69 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `session/update` → journaled `agent_event`s, one record at a time, in log order (Task 17b).
|
|
3
|
-
*
|
|
4
|
-
* ## One path, in log order (replaces `draftInterceptor` + the SDK's async chain)
|
|
5
|
-
*
|
|
6
|
-
* Today a draft variant or a child session's update is diverted by `interceptDraftUpdates` and
|
|
7
|
-
* applied **synchronously** while the stream is read, and every other update travels the SDK's
|
|
8
|
-
* **asynchronous** handler chain — so the two orders depend on pipe chunking (analysis B §0.1: the
|
|
9
|
-
* same bytes, written at once or line by line, gave different event orders). Here both kinds are
|
|
10
|
-
* handled on their own record, so the order of events is the order of the log, whatever the
|
|
11
|
-
* chunking. What differs between them is only what `AcpSession` does with each, kept as it is:
|
|
12
|
-
*
|
|
13
|
-
* - **The root session's own ordinary update** is parsed with the SDK's `zSessionNotification` (as the
|
|
14
|
-
* SDK's router does — so unknown keys are stripped and an unknown variant is dropped), mapped with
|
|
15
|
-
* the root scope, and routed like `AcpSession.routeMappedEvent`: background-writer evidence and
|
|
16
|
-
* message-boundary tracking for `agent_message` synthesis.
|
|
17
|
-
* - **A draft variant (any session) or any update on another session** is mapped raw, with that
|
|
18
|
-
* session's own scope, like `AcpSession.applyDivertedUpdate` → `emitEvent`: a child's events carry
|
|
19
|
-
* `acpSessionId`, and none of them touch the root's message boundaries.
|
|
20
|
-
*
|
|
21
|
-
* Both feed the permission-origin graph (`subagent_spawned`) and the unfinished async-task set.
|
|
22
|
-
*
|
|
23
|
-
* ## Binding, and what replaced the pre-bind buffer
|
|
24
|
-
*
|
|
25
|
-
* The root is bound by the open **response** (`control.ts`). An update that arrives before any root
|
|
26
|
-
* is bound — outside a load's history window — has no session to belong to and is dropped
|
|
27
|
-
* (`droppedUpdates`). Today's 32-entry, 5-second pre-bind buffer existed because the SDK processed
|
|
28
|
-
* the `session/new` response *after* an update the agent had written *after* it; in log order that
|
|
29
|
-
* update follows the response and is applied normally (analysis B A3/C1; the raw captures in `tmp/`
|
|
30
|
-
* show Claude's first `available_commands_update` after the response line).
|
|
31
|
-
*
|
|
32
|
-
* ## Load history
|
|
33
|
-
*
|
|
34
|
-
* Between a `session/load` request and the `historyEnd` note, every update (any session) is history — except a level update
|
|
35
|
-
* ({@link LEVEL_UPDATES}) the loaded session sends once its response is in, which is folded live (Task 21):
|
|
36
|
-
* folded with its own scopes and message boundaries exactly as `adoption.ts` `foldScopedReplayEvents`
|
|
37
|
-
* folds a finished list — incrementally here — then validated and emitted **unnumbered** as
|
|
38
|
-
* `processor.history_event` frames for the daemon's backfill, and observed for origins and
|
|
39
|
-
* background writers (`AcpSession.observeReplayEvent`).
|
|
40
|
-
*
|
|
41
|
-
* ## `agent_message` synthesis and out-of-turn chunks
|
|
42
|
-
*
|
|
43
|
-
* `AcpSession`'s rule: an `agent_message` closes the streaming assistant message when its `messageId`
|
|
44
|
-
* changes and when a turn ends (before the turn's `agent_turn_done`). One addition, decided for 17b:
|
|
45
|
-
* chunks that arrive with **no turn running** — after a prompt's response, or an agent working on its
|
|
46
|
-
* own — are emitted as they are (an `agent_event` never carries a turn id; that is what today's path
|
|
47
|
-
* does with them too), and the message they open is closed **before the next turn starts** instead
|
|
48
|
-
* of being merged into that turn's message when neither carries a `messageId`.
|
|
49
|
-
*
|
|
50
|
-
* ## The Qoder adapter boundaries (Task 17c)
|
|
51
|
-
*
|
|
52
|
-
* `AcpSession` passes events through three adapter boundaries, and so does the processor, over its
|
|
53
|
-
* checkpointed data (`state.boundaries`), at the same points:
|
|
54
|
-
* - the root's own update ({@link routeOwnEvent} = `routeMappedEvent`): the foreground-output boundary
|
|
55
|
-
* may hold an unaddressed chunk back (certified qoder-cli), the terminal repair observes it, the
|
|
56
|
-
* subwork boundary maps it (`supportsSubworkMeta`) — and only an unscoped mapped chunk tracks the
|
|
57
|
-
* root's message boundary;
|
|
58
|
-
* - everything `AcpSession.emitEvent` emits ({@link emitSessionEvent}): diverted updates, cards, and the
|
|
59
|
-
* terminal repair's own events (`sideChannel.ts`) — observed by the repair, then subwork-mapped;
|
|
60
|
-
* - load history (`observeReplayEvent`): observed by the repair.
|
|
61
|
-
*/
|
|
1
|
+
/** Session updates map in log order. Root chunks track streaming message boundaries; diverted and child updates retain their session address. Both feed the permission-origin graph and unfinished async-task state. Load history uses separate mapper scopes and emits unnumbered backfill events. */
|
|
62
2
|
import { DRAFT_SESSION_UPDATE_NAMES } from '../draftSubagents.js';
|
|
63
3
|
import { mapSessionUpdate, newMapperScopeData, SessionMapperState } from '../sessionUpdateMapper.js';
|
|
64
|
-
import { certifiedRuntime, ForegroundOutputBoundary } from '../../qoderAdapter/foregroundOutput.js';
|
|
65
|
-
import { SubworkBoundary, supportsSubworkMeta } from '../../qoderAdapter/subwork.js';
|
|
66
|
-
import { TerminalRepairFold } from '../../qoderAdapter/backgroundTerminalFold.js';
|
|
67
4
|
import { validateWireEvent } from '../validate.js';
|
|
68
5
|
import { emitEvent, isRecord } from './emit.js';
|
|
69
6
|
import { observeSubagentSpawned } from './origins.js';
|
|
@@ -137,56 +74,115 @@ export function onSessionUpdate(state, params, out, t) {
|
|
|
137
74
|
for (const event of events)
|
|
138
75
|
emitSessionEvent(state, out, sessionId === root ? event : { ...event, acpSessionId: sessionId });
|
|
139
76
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
77
|
+
const CHUNK_CHURN = new WeakMap();
|
|
78
|
+
const CHUNK_TICKS = new WeakMap();
|
|
79
|
+
const canonical = (value) => { try {
|
|
80
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
143
81
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
82
|
+
catch {
|
|
83
|
+
return 'unserializable';
|
|
84
|
+
} };
|
|
85
|
+
const isVolatileMetaKey = (key) => key === 'timestamp' || key.endsWith('.timestamp') || key.endsWith('_timestamp');
|
|
86
|
+
/** Every event through the live/history entry points ticks once; a tracker whose last chunk is not the immediately
|
|
87
|
+
* preceding event starts fresh. A tool call or any other update between two chunks therefore keeps their ids
|
|
88
|
+
* meaningful — which also matches the server, where an unrelated event already flushes the pending text. */
|
|
89
|
+
function tickEvent(state) {
|
|
90
|
+
const ticks = CHUNK_TICKS.get(state) ?? { tick: 0 };
|
|
91
|
+
ticks.tick += 1;
|
|
92
|
+
CHUNK_TICKS.set(state, ticks);
|
|
93
|
+
return ticks.tick;
|
|
147
94
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
95
|
+
function churnStateOf(state, key, tick) {
|
|
96
|
+
let perState = CHUNK_CHURN.get(state);
|
|
97
|
+
if (!perState)
|
|
98
|
+
CHUNK_CHURN.set(state, perState = new Map());
|
|
99
|
+
const fresh = state.turns.length;
|
|
100
|
+
let churn = perState.get(key);
|
|
101
|
+
if (!churn || churn.seenTurns !== fresh || churn.lastTick !== tick - 1)
|
|
102
|
+
perState.set(key, churn = { prevId: null, anchorId: null, trusted: false, prevMeta: null, anchorMeta: {}, churnyMetaKeys: new Set(), trustedMetaKeys: new Set(), seenTurns: fresh, lastTick: tick });
|
|
103
|
+
churn.lastTick = tick;
|
|
104
|
+
return churn;
|
|
154
105
|
}
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
106
|
+
/** Absorb per-delta id/meta churn on one assistant chunk (see the block comment above); other events pass through. */
|
|
107
|
+
function absorbAssistantChunkChurn(state, tick, event) {
|
|
108
|
+
if (event.eventName !== 'agent_message_chunk' && event.eventName !== 'agent_thought_chunk')
|
|
109
|
+
return event;
|
|
110
|
+
const payload = event.payload;
|
|
111
|
+
const churn = churnStateOf(state, `${event.acpSessionId ?? 'root'}:${event.eventName}`, tick);
|
|
112
|
+
let changed = false;
|
|
113
|
+
const rawId = typeof payload.messageId === 'string' ? payload.messageId : null;
|
|
114
|
+
if (rawId !== null) {
|
|
115
|
+
if (churn.prevId !== null && rawId === churn.prevId)
|
|
116
|
+
churn.trusted = true;
|
|
117
|
+
if (churn.trusted) {
|
|
118
|
+
churn.anchorId = rawId;
|
|
119
|
+
}
|
|
120
|
+
else if (churn.prevId !== null) {
|
|
121
|
+
// A change with no stable pair ever seen: churn — keep the anchor.
|
|
122
|
+
if (churn.anchorId !== null && churn.anchorId !== rawId) {
|
|
123
|
+
payload.messageId = churn.anchorId;
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
churn.anchorId = rawId;
|
|
129
|
+
}
|
|
130
|
+
churn.prevId = rawId;
|
|
131
|
+
}
|
|
132
|
+
const rawMeta = payload._meta;
|
|
133
|
+
if (rawMeta !== null && typeof rawMeta === 'object') {
|
|
134
|
+
const meta = rawMeta;
|
|
135
|
+
const emitted = {};
|
|
136
|
+
let anchored = false;
|
|
137
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
138
|
+
if (!isVolatileMetaKey(key)) {
|
|
139
|
+
emitted[key] = value;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const previous = churn.prevMeta;
|
|
143
|
+
if (previous !== null && key in previous) {
|
|
144
|
+
if (canonical(value) === canonical(previous[key]))
|
|
145
|
+
churn.trustedMetaKeys.add(key);
|
|
146
|
+
else if (!churn.trustedMetaKeys.has(key)) {
|
|
147
|
+
// First sighting of a change on a never-stable key: anchor it to what the previous chunk
|
|
148
|
+
// already emitted, so every chunk of this stream carries the identical map.
|
|
149
|
+
if (!(key in churn.anchorMeta))
|
|
150
|
+
churn.anchorMeta[key] = previous[key];
|
|
151
|
+
churn.churnyMetaKeys.add(key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
emitted[key] = churn.churnyMetaKeys.has(key) && key in churn.anchorMeta ? churn.anchorMeta[key] : value;
|
|
155
|
+
if (emitted[key] !== value)
|
|
156
|
+
anchored = true;
|
|
157
|
+
}
|
|
158
|
+
churn.prevMeta = meta;
|
|
159
|
+
if (anchored) {
|
|
160
|
+
payload._meta = emitted;
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return changed ? { ...event, payload } : event;
|
|
158
165
|
}
|
|
159
|
-
/** `AcpSession.routeMappedEvent`, Qoder boundaries included. */
|
|
160
166
|
function routeOwnEvent(state, out, event) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if ((!subwork || !mapped.acpSessionId || mapped.acpSessionId === root) &&
|
|
170
|
-
(mapped.eventName === 'agent_message_chunk' || mapped.eventName === 'agent_thought_chunk'))
|
|
171
|
-
trackMessageBoundary(state, out, mapped);
|
|
172
|
-
emitEvent(state, out, mapped);
|
|
173
|
-
}
|
|
167
|
+
const tick = tickEvent(state);
|
|
168
|
+
event = absorbAssistantChunkChurn(state, tick, event);
|
|
169
|
+
observe(state, event);
|
|
170
|
+
observeBackgroundWriter(state, event);
|
|
171
|
+
if ((!event.acpSessionId || event.acpSessionId === state.rootSessionId) &&
|
|
172
|
+
(event.eventName === 'agent_message_chunk' || event.eventName === 'agent_thought_chunk'))
|
|
173
|
+
trackMessageBoundary(state, out, event);
|
|
174
|
+
emitEvent(state, out, event);
|
|
174
175
|
}
|
|
175
|
-
/**
|
|
176
|
-
* `AcpSession.emitEvent` (minus a card's own checks, `session.ts`): a diverted update, a card, or a
|
|
177
|
-
* repair event. The repair observes it as given; a root id on `acpSessionId` is dropped; the subwork
|
|
178
|
-
* boundary maps it; each mapped event feeds the origin graph and is numbered.
|
|
179
|
-
*/
|
|
176
|
+
/** Normalize the root session address, observe the event, then emit. */
|
|
180
177
|
export function emitSessionEvent(state, out, event) {
|
|
181
|
-
|
|
178
|
+
const tick = tickEvent(state);
|
|
179
|
+
event = absorbAssistantChunkChurn(state, tick, event);
|
|
182
180
|
// Inside a load's history window the session being loaded is the root (an attached `AcpSession` knows it).
|
|
183
181
|
const normalized = event.acpSessionId === (state.rootSessionId ?? state.history?.rootSessionId)
|
|
184
182
|
? { eventName: event.eventName, payload: event.payload }
|
|
185
183
|
: event;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
emitEvent(state, out, mapped);
|
|
189
|
-
}
|
|
184
|
+
observe(state, normalized);
|
|
185
|
+
emitEvent(state, out, normalized);
|
|
190
186
|
}
|
|
191
187
|
/** What every live event feeds besides the outbox: the origin graph and the async-task set. */
|
|
192
188
|
function observe(state, event) {
|
|
@@ -194,6 +190,7 @@ function observe(state, event) {
|
|
|
194
190
|
observeSubagentSpawned(state, event.acpSessionId ?? state.rootSessionId, event.payload.subagentSessionId);
|
|
195
191
|
}
|
|
196
192
|
trackAsyncTask(state, event);
|
|
193
|
+
trackDraftSubagent(state, event);
|
|
197
194
|
}
|
|
198
195
|
/** `AcpSession.observeBackgroundWriter`: an explicitly detached writer blocks `executionEnded` for good. */
|
|
199
196
|
function observeBackgroundWriter(state, event) {
|
|
@@ -205,6 +202,33 @@ function observeBackgroundWriter(state, event) {
|
|
|
205
202
|
state.backgroundWriterUnconfirmed = true;
|
|
206
203
|
}
|
|
207
204
|
}
|
|
205
|
+
const DRAFT_CHILD_LIMIT = 4096;
|
|
206
|
+
const DRAFT_TERMINALS = new Set(['completed', 'failed', 'cancelled']);
|
|
207
|
+
/** Keep terminal tombstones: duplicated/reordered spawns cannot resurrect completed children.
|
|
208
|
+
* At capacity retain a conservative blocker rather than forget potentially running work. */
|
|
209
|
+
function trackDraftSubagent(state, event) {
|
|
210
|
+
if (event.eventName !== 'subagent_spawned' && event.eventName !== 'subagent_state_update')
|
|
211
|
+
return;
|
|
212
|
+
const id = event.payload.subagentSessionId;
|
|
213
|
+
if (typeof id !== 'string' || !id.length)
|
|
214
|
+
return;
|
|
215
|
+
const data = state.draftSubagents;
|
|
216
|
+
if (id.length > 256) {
|
|
217
|
+
data.overflow = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const terminal = event.eventName === 'subagent_state_update' && DRAFT_TERMINALS.has(String(event.payload.state));
|
|
221
|
+
if (getOwn(data.children, id) === false)
|
|
222
|
+
return;
|
|
223
|
+
if (!Object.hasOwn(data.children, id) && Object.keys(data.children).length >= DRAFT_CHILD_LIMIT) {
|
|
224
|
+
data.overflow = true;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
putOwn(data.children, id, !terminal);
|
|
228
|
+
}
|
|
229
|
+
export function unfinishedDraftSubagentCount(state) {
|
|
230
|
+
return Object.values(state.draftSubagents.children).filter(Boolean).length + Number(state.draftSubagents.overflow);
|
|
231
|
+
}
|
|
208
232
|
const TERMINAL_ASYNC_STATES = new Set(['completed', 'failed', 'stopped']);
|
|
209
233
|
/**
|
|
210
234
|
* The unfinished async tasks (AIR `async_task_*`), per native session: spawned → running; a state
|
|
@@ -296,7 +320,9 @@ function onHistoryUpdate(state, history, sessionId, update, out) {
|
|
|
296
320
|
history.droppedCount += 1;
|
|
297
321
|
return;
|
|
298
322
|
}
|
|
299
|
-
for (const
|
|
323
|
+
for (const rawEvent of events) {
|
|
324
|
+
// Same churn absorption as the live path: a replayed CodeBuddy history fragments identically.
|
|
325
|
+
const event = absorbAssistantChunkChurn(state, tickEvent(state), rawEvent);
|
|
300
326
|
if (event.eventName === 'user_message_chunk')
|
|
301
327
|
flushHistoryMessage(state, history, sessionId, out);
|
|
302
328
|
if (event.eventName === 'agent_message_chunk' || event.eventName === 'agent_thought_chunk') {
|
|
@@ -331,10 +357,10 @@ function emitHistory(state, history, out, event, sessionId) {
|
|
|
331
357
|
return;
|
|
332
358
|
}
|
|
333
359
|
// `AcpSession.observeReplayEvent`: history restores the repair's calls, addresses and background-writer evidence.
|
|
334
|
-
repairFold(state)?.observe(scoped);
|
|
335
360
|
if (scoped.eventName === 'subagent_spawned') {
|
|
336
361
|
observeSubagentSpawned(state, scoped.acpSessionId ?? history.rootSessionId, scoped.payload.subagentSessionId, history.rootSessionId);
|
|
337
362
|
}
|
|
363
|
+
trackDraftSubagent(state, scoped);
|
|
338
364
|
observeBackgroundWriter(state, scoped);
|
|
339
365
|
out.unjournaled({
|
|
340
366
|
t: 'processor.history_event',
|
|
@@ -1,96 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One live agent session driven by the deterministic ACP processor over a transport (Task 19, spec 2026-10-11 §5.3/§5.4/§5.7):
|
|
3
|
-
* the daemon side of keeper-acp — or of the local fallback, which is the same session mechanism in this process.
|
|
4
|
-
*
|
|
5
|
-
* ## The record loop
|
|
6
|
-
*
|
|
7
|
-
* The transport's raw stream (`subscribeRaw`) is the only input. For each record, in order:
|
|
8
|
-
* `step(record)` → every command it produced is executed through the transport (`executeDownCommandsVia`: idempotent by cause,
|
|
9
|
-
* refused unless its spawn runs now, prompt blocks filled from this daemon's memory, `secretRef`s substituted from this daemon's
|
|
10
|
-
* memory) → a prompt whose blocks are gone is failed with an `agent_prompt_failed` note → the record's frames go out.
|
|
11
|
-
* Everything the server or this daemon decides reaches the processor as a note or a write first (`agent_prompt`, `agent_answer`,
|
|
12
|
-
* `agent_interrupt`, …); nothing here changes processor state directly, and nothing here numbers a frame — the processor does —
|
|
13
|
-
* with the one exception the poison contract names (below).
|
|
14
|
-
*
|
|
15
|
-
* ## What leaves
|
|
16
|
-
*
|
|
17
|
-
* Frames the manager sends (`onFrames`), unchanged, with `src`. The ones meant for the daemon itself stay here:
|
|
18
|
-
* `processor.open_result` settles {@link openSession}; `processor.history_event`/`history_end` become the load's backfill
|
|
19
|
-
* (never journaled: a daemon that dies inside the window loads again); an `agent_steered` / `agent_config_option_set` whose
|
|
20
|
-
* request this object made settles that call (one with nobody waiting goes out as a frame).
|
|
21
|
-
*
|
|
22
|
-
* ## Secrets and blocks
|
|
23
|
-
*
|
|
24
|
-
* Prompt and steer blocks are held in memory under a `blocksRef` until their `down` is written; secrets (a delegation bearer)
|
|
25
|
-
* under a `secretRef`, for the life of the session. Notes carry only the refs (spec §13).
|
|
26
|
-
*
|
|
27
|
-
* ## Poison (interface-contracts, "中毒恢复契约")
|
|
28
|
-
*
|
|
29
|
-
* A step that throws poisons the processor. The session is degraded: every turn still open ends with a failed `agent_turn_done`
|
|
30
|
-
* this object numbers itself, after the processor's last seq; the adapter is terminated; `onDegraded` tells the manager, which
|
|
31
|
-
* retires the session so the next `agent_open` starts a new spawn (`session/load`). Never retried from a checkpoint: replay is
|
|
32
|
-
* deterministic and would poison again on the same record. A transport that fails under the loop (keeper gone, raw truncated) is
|
|
33
|
-
* handled the same way: Task 19 has no re-attach (Task 21).
|
|
34
|
-
*
|
|
35
|
-
* ## Qoder side files (17c)
|
|
36
|
-
*
|
|
37
|
-
* When the repair is on, a 1 s timer asks the processor's **current** `sideDemand` — only once the processor has folded every
|
|
38
|
-
* record this object appended — runs `BackgroundSideNotes.poll(demand)` and appends the notes it returns, in order. The reader
|
|
39
|
-
* stops when the demand is `undefined` (no repair any more).
|
|
40
|
-
*
|
|
41
|
-
* ## Outbox, backpressure, checkpoints (Task 20, spec §5.5/§5.6)
|
|
42
|
-
*
|
|
43
|
-
* The manager records every journaled frame in {@link outbox} (the transport's outbox stream) before it sends it; the listener
|
|
44
|
-
* {@link onFrames} registers returns that append, and the loop awaits it before folding the next record — so the outbox holding a
|
|
45
|
-
* frame with `src.o = o` proves record `o`'s commands are in raw, and a checkpoint at `o` never precedes `o`'s frames. Before each
|
|
46
|
-
* record the loop waits for {@link SessionOutbox.room}: a server that does not acknowledge stops the processor, and the backlog
|
|
47
|
-
* stays in the raw log.
|
|
48
|
-
*
|
|
49
|
-
* Checkpoints (`acp.commit{checkpoint, rawThrough}`, raw truncated to the checkpoint): at the end of every turn; within a turn
|
|
50
|
-
* every {@link CHECKPOINT_RAW_BYTES} of folded raw or {@link CHECKPOINT_INTERVAL_MS}; and on a `close('keep')` (graceful exit,
|
|
51
|
-
* detach). Never while `boundaries.sideChunk` holds a half-read Qoder fact or a load's history window is open — deferred to the
|
|
52
|
-
* next record where both are clear. Each carries `processorVersion` (`checkpointOf`).
|
|
53
|
-
*
|
|
54
|
-
* ## A session left under the same key
|
|
55
|
-
*
|
|
56
|
-
* An earlier life of this ref (a swept or degraded session, a previous daemon's, a local outbox) is reused, not released: the new
|
|
57
|
-
* spawn continues the same raw log and outbox. Its unacknowledged frames are read back into {@link outbox} (resent when the server
|
|
58
|
-
* reopens the ref) and the processor numbers after the highest seq the outbox ever held — `max(outbox.maxSeq, committedTail.seq,
|
|
59
|
-
* resumeFromSeq)` (§5.5). An earlier life whose adapter still runs is never stopped here: the daemon adopts it at start
|
|
60
|
-
* ({@link ProcessorSession.adopt}), and keeper-acp refuses a second spawn into a running session.
|
|
61
|
-
*
|
|
62
|
-
* ## Adoption (Task 21, spec §5.5)
|
|
63
|
-
*
|
|
64
|
-
* A keeper-acp session whose adapter outlived the daemon (restart, upgrade, crash) is adopted, not stopped:
|
|
65
|
-
* `acp.attach` → the head checkpoint is restored → raw `(checkpoint.offset, replayEnd]` is replayed **updating state only** —
|
|
66
|
-
* `replayEnd = max(outbox last.src.o, committedTail.src.o)`, everything whose frames were recorded — and the frames the replay
|
|
67
|
-
* produces are compared byte for byte with the outbox entries that survive (daemon-numbered ones, `src.i ≥ DAEMON_FRAME_INDEX`,
|
|
68
|
-
* skipped; differences are only counted when the checkpoint came from this very processor version) → the seq continues after
|
|
69
|
-
* `max(replayed seq, outbox.maxSeq, committedTail.seq)` → the unacknowledged outbox entries are resent on the server's reopen →
|
|
70
|
-
* the records after `replayEnd` are folded live (their commands are idempotent by cause). A session whose open never completed, or
|
|
71
|
-
* that cannot be rebuilt (no readable checkpoint and no spawn record left), is refused ({@link AdoptionRefusedError}); the caller
|
|
72
|
-
* stops its adapter and keeps its outbox. A checkpoint is cut right after the spawn and when the open completes, so every session
|
|
73
|
-
* has one that says whether it is open.
|
|
74
|
-
*
|
|
75
|
-
* ## Detach (Task 21, spec §5.5.5)
|
|
76
|
-
*
|
|
77
|
-
* {@link detach} ends this daemon's hold on a keeper-hosted session without touching the adapter: the loop stops after the
|
|
78
|
-
* record in hand, a head checkpoint is cut when the state is safe (never mid Qoder chunk or load history — then the next daemon
|
|
79
|
-
* replays), the subscription is dropped. No `session/cancel`, no answer to a pending card, no `turn_done(failed)`: a detaching
|
|
80
|
-
* session never degrades. The next daemon adopts it.
|
|
81
|
-
*
|
|
82
|
-
* ## Keeper connection loss (Task 21)
|
|
83
|
-
*
|
|
84
|
-
* keeper-acp unreachable under the loop (a request lost on a dropped connection, a stream that ended) is not a degradation: the
|
|
85
|
-
* session re-attaches — `acp.attach` (which relaunches the current generation's keeper-acp if it died; an older generation is
|
|
86
|
-
* only reconnected) and the same rebuild as an adoption, in place — with bounded retries. A note whose append failed is looked
|
|
87
|
-
* for in raw before it is appended again. Only a keeper that is gone with the session (not found, or unreachable after the
|
|
88
|
-
* retries) degrades it. When keeper-acp itself restarted, the session it recovered ends with `proc exit{reason:'keeper_lost'}`:
|
|
89
|
-
* that tail is folded (a real `turn_done` in it is delivered) and the session reports its adapter dead.
|
|
90
|
-
*/
|
|
1
|
+
/** Daemon-side keeper session: records notes before commands, awaits outbox persistence before advancing, and checkpoints only complete history boundaries. A poisoned processor degrades the session and ends pending turns; it never retries half-updated state. */
|
|
91
2
|
import { PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
|
|
92
3
|
import { ACCOUNT_LIMITS_READ_TIMEOUT_MS } from '../protocol/accountLimits.js';
|
|
93
|
-
import { BackgroundSideNotes, nativeTerminalHistoryPath } from '../qoderAdapter/backgroundTerminal.js';
|
|
94
4
|
import { AgentInitializeTimeoutError, AgentSpawnError, UnsupportedProtocolVersionError } from './acpClient.js';
|
|
95
5
|
import { executeDownCommandsVia } from './processor/transport/types.js';
|
|
96
6
|
import { createProcessor, ProcessorPoisonedError, restoreProcessor } from './processor/step.js';
|
|
@@ -98,12 +8,10 @@ import { checkpointOf, decodeCheckpoint, encodeCheckpoint, PROCESSOR_VERSION } f
|
|
|
98
8
|
import { DAEMON_FRAME_INDEX, isOutboxTail, SessionOutbox, transportOutboxStore } from './outbox.js';
|
|
99
9
|
import { initialState } from './processor/state.js';
|
|
100
10
|
import { activityRaw, runtimeDiagnostics } from './processor/runtime.js';
|
|
101
|
-
import {
|
|
102
|
-
import { unfinishedAsyncTasks } from './processor/updates.js';
|
|
11
|
+
import { unfinishedAsyncTasks, unfinishedDraftSubagentCount } from './processor/updates.js';
|
|
103
12
|
import { pendingRequestIds } from './processor/session.js';
|
|
104
13
|
import { steeredIds } from './processor/control.js';
|
|
105
14
|
import { RawTruncatedError } from './processor/transport/types.js';
|
|
106
|
-
import { REPAIR_NOTE } from '../qoderAdapter/backgroundTerminalFold.js';
|
|
107
15
|
import { AcpKeeperUnavailableError } from '../keepers/acp/client.js';
|
|
108
16
|
import { AcpErrorCode } from '../keepers/acp/protocol.js';
|
|
109
17
|
import { KeeperClosedError, KeeperConnectionLostError, KeeperNotConnectedError, KeeperRequestError, KeeperRequestTimeoutError, } from '../keepers/kit/client.js';
|
|
@@ -111,8 +19,6 @@ import { KeeperClosedError, KeeperConnectionLostError, KeeperNotConnectedError,
|
|
|
111
19
|
export const PROMPT_LOST = 'The prompt was lost before it reached the agent';
|
|
112
20
|
/** The error a turn ends with when the session degrades under it (poison, or a transport that failed under the loop). */
|
|
113
21
|
export const SESSION_DEGRADED = 'The agent session failed and was stopped; reopen it to continue';
|
|
114
|
-
/** How often the Qoder side-file reader polls (the live repair's timer). */
|
|
115
|
-
const SIDE_POLL_MS = 1_000;
|
|
116
22
|
/** stderr kept for a startup failure's message (the `err` records the processor ignores). */
|
|
117
23
|
const STDERR_TAIL_BYTES = 8_192;
|
|
118
24
|
/** Within a turn, a checkpoint every this many bytes of folded raw (spec §5.5, §14). */
|
|
@@ -210,7 +116,6 @@ export class ProcessorSession {
|
|
|
210
116
|
openResult;
|
|
211
117
|
historyEnd;
|
|
212
118
|
history = [];
|
|
213
|
-
side;
|
|
214
119
|
/** The record loop's tail: notes it appends are serialised behind it. */
|
|
215
120
|
chain = Promise.resolve();
|
|
216
121
|
/** The loop is held at a record boundary for a switch ({@link suspend}); `release` lets it go on. */
|
|
@@ -262,11 +167,12 @@ export class ProcessorSession {
|
|
|
262
167
|
// resent when the server reopens the ref, and numbering continues after the highest seq the outbox ever held.
|
|
263
168
|
const attached = await transport.attach(sessionKey);
|
|
264
169
|
let leftovers = await readOutboxTail(transport, sessionKey, attached.outbox);
|
|
265
|
-
|
|
170
|
+
const imports = (seed?.imports ?? options.imports ?? []).filter((entry) => entry.seq > (attached.outbox?.lastSeq ?? 0));
|
|
171
|
+
if (imports.length > 0) {
|
|
266
172
|
// A switch's successor (Task 24): the predecessor's unacknowledged frames land here before anything this session numbers —
|
|
267
173
|
// the predecessor is released only after this append returned (never the other way round: the frames would be nowhere).
|
|
268
174
|
const at = Math.max(spawned.o - 1, 0);
|
|
269
|
-
const entries =
|
|
175
|
+
const entries = imports.map((entry, index) => ({ seq: entry.seq, src: { o: at, i: DAEMON_FRAME_INDEX + index }, frame: { ...entry.frame } }));
|
|
270
176
|
const { first } = await transport.appendOutbox(sessionKey, entries);
|
|
271
177
|
leftovers = [...leftovers, ...entries.map((entry, k) => ({ o: first + k, t: Date.now(), ...entry }))];
|
|
272
178
|
}
|
|
@@ -552,14 +458,9 @@ export class ProcessorSession {
|
|
|
552
458
|
if (this.checkpointDue && this.checkpointSafe())
|
|
553
459
|
this.cutCheckpoint();
|
|
554
460
|
}
|
|
555
|
-
/**
|
|
556
|
-
* A checkpoint must let a replay continue from it alone: not while a chunked Qoder fact is half-read (`sideChunk`, its first
|
|
557
|
-
* chunks would be truncated away) nor inside a load's history window (history is never journaled: a daemon that dies in the
|
|
558
|
-
* window loads again — interface-contracts, T19).
|
|
559
|
-
*/
|
|
560
461
|
checkpointSafe() {
|
|
561
462
|
const state = this.processor.state;
|
|
562
|
-
return state.offset >= 0 && state.offset <= this.recordedThrough && state.
|
|
463
|
+
return state.offset >= 0 && state.offset <= this.recordedThrough && state.history === null &&
|
|
563
464
|
this.processor.poisoned === undefined;
|
|
564
465
|
}
|
|
565
466
|
/** Snapshot now (a record boundary: the loop is between records) and commit it with raw truncated through it (not a marker's). */
|
|
@@ -644,7 +545,7 @@ export class ProcessorSession {
|
|
|
644
545
|
/** Async tasks / background work the adapter still runs for this session (processor state): a switch would kill them. */
|
|
645
546
|
get liveBackgroundWork() {
|
|
646
547
|
const state = this.processor.state;
|
|
647
|
-
return unfinishedAsyncTasks(state).length + (
|
|
548
|
+
return unfinishedAsyncTasks(state).length + unfinishedDraftSubagentCount(state);
|
|
648
549
|
}
|
|
649
550
|
/** The adapter's exit, once it has one: the spawn it ended and why (keeper-acp's `reason`, absent for an exit of its own). */
|
|
650
551
|
get adapterExit() {
|
|
@@ -945,7 +846,6 @@ export class ProcessorSession {
|
|
|
945
846
|
for (const [, reply] of this.pendingReplies)
|
|
946
847
|
reply({ ok: false, error: reason });
|
|
947
848
|
this.pendingReplies.clear();
|
|
948
|
-
this.stopSide();
|
|
949
849
|
void this.transport.terminate(this.key).catch(() => null);
|
|
950
850
|
try {
|
|
951
851
|
this.options.onDegrade?.(this.headSeq);
|
|
@@ -1010,20 +910,8 @@ export class ProcessorSession {
|
|
|
1010
910
|
...(request.sessionId !== undefined ? { sessionId: request.sessionId } : {}),
|
|
1011
911
|
mcpServers: request.mcpServers,
|
|
1012
912
|
...(request.redact && request.redact.length > 0 ? { redact: [...request.redact] } : {}),
|
|
1013
|
-
...(request.delegation ? { delegation: request.delegation } : {}),
|
|
1014
913
|
};
|
|
1015
|
-
|
|
1016
|
-
if (repair) {
|
|
1017
|
-
// The repair note must follow the open note before the loop folds the open note (and so writes the `session/load` line
|
|
1018
|
-
// the history answers): the loop is held while both are appended.
|
|
1019
|
-
await this.holdingLoop(async () => {
|
|
1020
|
-
await this.send(note);
|
|
1021
|
-
await this.enableRepair(request.sessionId, request.cwd, repair.historyHome);
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
await this.send(note);
|
|
1026
|
-
}
|
|
914
|
+
await this.send(note);
|
|
1027
915
|
return this.waitFor(() => this.openResult);
|
|
1028
916
|
}
|
|
1029
917
|
/** Runs `work` (appends only — never a wait on the loop) with the record loop held, so no record is folded meanwhile. */
|
|
@@ -1044,55 +932,6 @@ export class ProcessorSession {
|
|
|
1044
932
|
await this.send({ type: 'historyEnd' });
|
|
1045
933
|
return this.waitFor(() => this.historyEnd);
|
|
1046
934
|
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Turn the Qoder background terminal repair on (17c): the `qoder_terminal_repair` note, then the side-file reader. Nothing for a
|
|
1049
|
-
* runtime the repair does not certify (no history path).
|
|
1050
|
-
*/
|
|
1051
|
-
async enableRepair(nativeSessionId, cwd, historyHome, restoreCalls = false) {
|
|
1052
|
-
const path = nativeTerminalHistoryPath(this.agentInfo, nativeSessionId, cwd, historyHome);
|
|
1053
|
-
if (!path || this.side?.nativeSessionId === nativeSessionId)
|
|
1054
|
-
return;
|
|
1055
|
-
// A load that failed and fell back to `session/new`: the processor closed the load's repair (`settleOpen`); its reader goes
|
|
1056
|
-
// too, and the new session gets its own (the old path closed the attached session and opened a fresh one).
|
|
1057
|
-
this.stopSide();
|
|
1058
|
-
const from = await this.appendNote({ type: REPAIR_NOTE, nativeSessionId, ...(restoreCalls ? { restoreCalls: true } : {}) });
|
|
1059
|
-
const reader = new BackgroundSideNotes({ path, nativeSessionId, cwd, restoreCalls });
|
|
1060
|
-
const timer = setInterval(() => { void this.pollSide(); }, SIDE_POLL_MS);
|
|
1061
|
-
timer.unref();
|
|
1062
|
-
this.side = { reader, timer, from, polling: false, nativeSessionId };
|
|
1063
|
-
}
|
|
1064
|
-
async pollSide() {
|
|
1065
|
-
const side = this.side;
|
|
1066
|
-
if (!side || side.polling || this.degraded !== undefined || this.closing)
|
|
1067
|
-
return;
|
|
1068
|
-
// The demand is the processor's *current* state, so only once it has folded everything this object appended.
|
|
1069
|
-
if (this.processor.state.offset < Math.max(side.from, this.appended))
|
|
1070
|
-
return;
|
|
1071
|
-
const demand = sideDemand(this.processor.state);
|
|
1072
|
-
if (!demand) {
|
|
1073
|
-
this.stopSide();
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
side.polling = true;
|
|
1077
|
-
try {
|
|
1078
|
-
const notes = await side.reader.poll(demand);
|
|
1079
|
-
for (const note of notes)
|
|
1080
|
-
await this.appendNote(note);
|
|
1081
|
-
}
|
|
1082
|
-
catch (error) {
|
|
1083
|
-
this.log(`[acp] session ${this.key}: background terminal read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1084
|
-
}
|
|
1085
|
-
finally {
|
|
1086
|
-
side.polling = false;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
stopSide() {
|
|
1090
|
-
if (!this.side)
|
|
1091
|
-
return;
|
|
1092
|
-
clearInterval(this.side.timer);
|
|
1093
|
-
this.side.reader.close();
|
|
1094
|
-
this.side = undefined;
|
|
1095
|
-
}
|
|
1096
935
|
// ---- AcpAgentSession ----------------------------------------------------------------------------------------------------
|
|
1097
936
|
get facts() {
|
|
1098
937
|
const initialize = this.processor.state.initialize;
|
|
@@ -1108,7 +947,6 @@ export class ProcessorSession {
|
|
|
1108
947
|
get configOptions() { return this.processor.state.configOptions ?? []; }
|
|
1109
948
|
get runtimeDiagnostics() { return runtimeDiagnostics(this.processor.state); }
|
|
1110
949
|
get activityRaw() { return activityRaw(this.processor.state); }
|
|
1111
|
-
get delegation() { return this.processor.state.delegation ?? null; }
|
|
1112
950
|
get nativeSessionId() {
|
|
1113
951
|
const root = this.processor.state.rootSessionId;
|
|
1114
952
|
return root !== null && root.length > 0 ? root : null;
|
|
@@ -1283,7 +1121,6 @@ export class ProcessorSession {
|
|
|
1283
1121
|
this.resume();
|
|
1284
1122
|
this.detached = true;
|
|
1285
1123
|
this.unlisten();
|
|
1286
|
-
this.stopSide();
|
|
1287
1124
|
for (const waiter of [...this.waiters])
|
|
1288
1125
|
waiter.reject(new Error('The agent session was handed to the next daemon'));
|
|
1289
1126
|
// A loop waiting for room stops (it sees `closing`); the head checkpoint is cut at the record boundary after it.
|
|
@@ -1307,7 +1144,6 @@ export class ProcessorSession {
|
|
|
1307
1144
|
this.closing ??= (async () => {
|
|
1308
1145
|
this.resume(); // a held loop drains (every fold sees `closing`)
|
|
1309
1146
|
this.unlisten();
|
|
1310
|
-
this.stopSide();
|
|
1311
1147
|
for (const waiter of [...this.waiters])
|
|
1312
1148
|
waiter.reject(new Error('The agent session is closed'));
|
|
1313
1149
|
for (const [, reply] of this.pendingReplies)
|