@platless/axom-cli 0.3.13 → 0.5.9

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