@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.
Files changed (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /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
- /** The agent `initialize` named (`AcpConnection.agentInfo`), if it is ready. */
141
- function agentInfo(state) {
142
- return state.initialize.status === 'ready' ? state.initialize.facts.agentInfo : undefined;
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
- /** The terminal repair, once a `qoder_terminal_repair` note enabled it. */
145
- export function repairFold(state) {
146
- return state.boundaries.repair ? new TerminalRepairFold(state.boundaries.repair) : undefined;
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
- /** `this.subworkBoundary?.map(event) ?? [event]`. */
149
- function throughSubwork(state, event) {
150
- const root = state.rootSessionId;
151
- if (root === null || !supportsSubworkMeta(agentInfo(state)))
152
- return [event];
153
- return new SubworkBoundary(root, state.boundaries.subwork).map(event);
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
- /** `this.foregroundOutput?.endTurn()` — no turn remains. */
156
- export function endForegroundTurn(state) {
157
- state.boundaries.foreground.pending = {};
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 root = state.rootSessionId;
162
- if (certifiedRuntime(agentInfo(state)) && !new ForegroundOutputBoundary(root, state.boundaries.foreground).accept(event))
163
- return;
164
- repairFold(state)?.observe(event);
165
- const subwork = supportsSubworkMeta(agentInfo(state));
166
- for (const mapped of throughSubwork(state, event)) {
167
- observe(state, mapped);
168
- observeBackgroundWriter(state, mapped);
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
- repairFold(state)?.observe(event);
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
- for (const mapped of throughSubwork(state, normalized)) {
187
- observe(state, mapped);
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 event of events) {
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 { sideDemand } from './processor/sideChannel.js';
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
- if (seed && seed.imports.length > 0) {
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 = seed.imports.map((entry, k) => ({ seq: entry.seq, src: { o: at, i: DAEMON_FRAME_INDEX + k }, frame: { ...entry.frame } }));
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.boundaries.sideChunk === null && state.history === null &&
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 + (sideDemand(state)?.active() ? 1 : 0);
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
- const repair = request.mode === 'load' ? request.repair : undefined;
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)