@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -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/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -1,21 +1,14 @@
1
1
  import path from 'node:path';
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { AGENT_WIRE_AUTH_URL_VERSION, AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION, AGENT_WIRE_SCOPED_ORIGIN_VERSION, AGENT_WIRE_ORPHAN_OFFER_VERSION, AGENT_WIRE_SESSION_AUTH_METHODS_VERSION, AGENT_WIRE_SESSION_LOST_VERSION, AGENT_WIRE_VERSION, AGENT_WIRE_DELEGATION_MCP_VERSION, AGENT_WIRE_DELEGATION_RENEW_VERSION, AGENT_WIRE_PROVIDER_TASK_VERSION, AGENT_WIRE_REOPEN_REQUEST_VERSION, AGENT_WIRE_RELATION_ACK_VERSION, outboundAgentFrameRejection, outboundFailureReply, } from './protocol/agent.js';
2
+ import { AGENT_WIRE_AUTH_URL_VERSION, AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION, AGENT_WIRE_SCOPED_ORIGIN_VERSION, AGENT_WIRE_ORPHAN_OFFER_VERSION, AGENT_WIRE_SESSION_AUTH_METHODS_VERSION, AGENT_WIRE_SESSION_LOST_VERSION, AGENT_WIRE_VERSION, AGENT_WIRE_PROVIDER_TASK_VERSION, AGENT_WIRE_REOPEN_REQUEST_VERSION, AGENT_WIRE_RELATION_ACK_VERSION, outboundAgentFrameRejection, outboundFailureReply, } from './protocol/agent.js';
4
3
  import { listLegacyJournalRefs, readLegacyJournal, removeLegacyJournal } from './journal.js';
5
4
  import { SESSION_WINDOW_BYTES, SESSION_WINDOW_EVENTS } from './acp/outbox.js';
6
5
  import { SocketGate } from './agentFlow.js';
7
6
  import { AGENT_PROBE_TIMEOUT_MS, AgentRefusedSessionError, LOCAL_SESSION_UNMOVABLE } from './acp/acpAgents.js';
8
- import { AdapterStillInstallingError } from './acp/builds/model.js';
9
7
  import { projectFrame } from './acp/processor/projection.js';
8
+ import { TaskRegistry } from './update/taskRegistry.js';
10
9
  import { reportsActivity, supportsAsyncTaskStop } from './acp/activityReporting.js';
11
10
  import { activityIsBusy } from './acp/activityBusy.js';
12
- import { buildSessionMcpServers } from './acp/delegationMcp.js';
13
11
  import { ADAPTER_LOST_REOPEN_ATTEMPTS, plainSwitchBlocker, REOPEN_REQUEST_RETRY_MS, switchBlockers, SWITCH_QUIET_MS } from './acp/switching.js';
14
- function delegationLeaseId(lease) {
15
- return createHash('sha256').update(JSON.stringify([
16
- lease.url, lease.headers, lease.bindingId,
17
- ])).digest('hex');
18
- }
19
12
  /**
20
13
  * 从线上收来的验证预算的绝对上限。
21
14
  *
@@ -129,10 +122,6 @@ function openSpecOf(frame) {
129
122
  openRequestId: frame.id,
130
123
  sessionRef: frame.sessionRef,
131
124
  agentKey: frame.agentKey,
132
- ...(frame.delegationMcp ? { delegation: {
133
- leaseId: delegationLeaseId(frame.delegationMcp),
134
- ...(frame.delegationMcp.bindingId ? { bindingId: frame.delegationMcp.bindingId, instanceId: randomUUID() } : {}),
135
- } } : {}),
136
125
  // `frame.bridge` is the registry agent id now — the wire field keeps its name because Plan A
137
126
  // renames nothing (Global Constraint 3); ruling R2 replaced what it *means*.
138
127
  agentId: frame.bridge,
@@ -143,7 +132,6 @@ function openSpecOf(frame) {
143
132
  args: frame.args ?? [],
144
133
  ...(frame.nativeSessionId ? { nativeSessionId: frame.nativeSessionId } : {}),
145
134
  ...(frame.requireSessionLoad ? { requireSessionLoad: true } : {}),
146
- ...(frame.delegationMcp ? { delegationMcp: frame.delegationMcp } : {}),
147
135
  // Retaining a whole adopted history costs memory for the session's lifetime, so it is only
148
136
  // kept when this open is the one that will stream it back.
149
137
  backfill: frame.backfill === true,
@@ -163,13 +151,6 @@ function exitWords(exit) {
163
151
  function errorMessage(err) {
164
152
  return err instanceof Error ? err.message : String(err);
165
153
  }
166
- /**
167
- * The reply text of an open that failed. A managed (delegation) open hides the cause behind one fixed sentence — except when
168
- * the adapter is still being installed: that one is actionable ("try again shortly") and carries nothing about the lease (15c).
169
- */
170
- function openFailureText(frame, err) {
171
- return frame.delegationMcp && !(err instanceof AdapterStillInstallingError) ? 'Managed delegation session could not open' : errorMessage(err);
172
- }
173
154
  /**
174
155
  * Batch size for `agent_backfill_events`. Kept modest so a session with a long history does not
175
156
  * arrive as one giant frame; the last batch of a stream is whatever remains below this size.
@@ -250,7 +231,11 @@ export class AgentSessionManager {
250
231
  agents;
251
232
  journalDir;
252
233
  tasks;
234
+ admission;
235
+ ownership;
253
236
  sessions = new Map();
237
+ /** A failed recorded-build respawn keeps this binding even after no LiveSession remains. */
238
+ retainedRespawnBuilds = new Map();
254
239
  /** In-flight opens, keyed by sessionRef, so two racing agent_open frames cannot spawn two agents. */
255
240
  opening = new Map();
256
241
  /**
@@ -356,10 +341,12 @@ export class AgentSessionManager {
356
341
  /** `<configDir>/agents`: the legacy journals (`sessions/<ref>.jsonl`) an older CLI left are migrated from here, once. */
357
342
  journalDir,
358
343
  /** Shared update admission and force-cancellation authority, when this daemon supports updates. */
359
- tasks) {
344
+ tasks, admission = () => true, ownership) {
360
345
  this.agents = agents;
361
346
  this.journalDir = journalDir;
362
347
  this.tasks = tasks;
348
+ this.admission = admission;
349
+ this.ownership = ownership;
363
350
  this.send = send;
364
351
  }
365
352
  /** Where an older CLI's journals live: `<journalDir>/sessions/<sessionRef>.jsonl` (read-only legacy, `journal.ts`). */
@@ -387,7 +374,7 @@ export class AgentSessionManager {
387
374
  }
388
375
  for (const sessionRef of refs) {
389
376
  try {
390
- const entries = await readLegacyJournal(this.sessionsDir, sessionRef);
377
+ const entries = await readLegacyJournal(this.sessionsDir, sessionRef, this.ownership);
391
378
  const imported = await importOutbox(sessionRef, entries.map((entry) => ({
392
379
  seq: entry.seq,
393
380
  ...(entry.src ? { src: entry.src } : {}),
@@ -578,7 +565,7 @@ export class AgentSessionManager {
578
565
  this.send = send;
579
566
  this.serverAgentWire = serverAgentWire;
580
567
  // A new connection: its own window and socket. What was sent on the previous one waits for the server's reopen to say how far
581
- // it got; what was never sent goes out now.
568
+ // it got. An already-exited adapter's recovered turn is replayed in full to finish it before reopen.
582
569
  this.epoch += 1;
583
570
  for (const live of this.sessions.values())
584
571
  live.session.outbox.connectionChanged();
@@ -586,6 +573,9 @@ export class AgentSessionManager {
586
573
  // Before any inbound frame on this connection can be answered: a sweep notice that arrived here
587
574
  // after the server had already re-opened its ref would invalidate a healthy generation.
588
575
  this.flushSwept();
576
+ for (const live of this.sessions.values())
577
+ if (live.replayDeadTail)
578
+ live.session.outbox.resendAll();
589
579
  this.pump();
590
580
  // Account observations are outside the session journal. Replay the cached state on reconnect
591
581
  // so a server restart does not depend on the provider sending another quota notification.
@@ -756,12 +746,17 @@ export class AgentSessionManager {
756
746
  highestScopedJournalSeq = Math.max(highestScopedJournalSeq, item.seq);
757
747
  }
758
748
  const live = {
759
- sessionRef: candidate.sessionRef, adopted: true, agentId: candidate.agentId, agentKey: candidate.agentKey, session,
749
+ sessionRef: candidate.sessionRef, adopted: true, replayDeadTail: candidate.agentAlive === false, agentId: candidate.agentId, agentKey: candidate.agentKey, session,
760
750
  nativeSessionId: session.nativeSessionId ?? null,
761
751
  scopedReplayBlocked: false, highestScopedJournalSeq, unwatch: () => { }, replays: Promise.resolve(), closed: false,
762
752
  lastActivityAt: Date.now(),
763
753
  };
764
- this.register(live);
754
+ this.register(live, this.adoptionOrigin);
755
+ // An exited adapter is adopted only to finish its recovered turn. Release its entire retained
756
+ // prefix in order so that tail can settle the server's turn before a reopen respawns the adapter.
757
+ // Live adapters retain the ordinary reopen/watermark gate.
758
+ if (live.replayDeadTail)
759
+ live.session.outbox.resendAll();
765
760
  console.info(`[acp:${live.agentId} agent:${live.agentKey}] adopted session ${live.sessionRef} from keeper-acp`);
766
761
  }
767
762
  /** Waits (bounded) for the start-time adoption; what is still rebuilding afterwards is reported as such. */
@@ -796,6 +791,23 @@ export class AgentSessionManager {
796
791
  clearTimeout(timer);
797
792
  }
798
793
  async handleFrame(frame) {
794
+ const live = 'sessionRef' in frame && typeof frame.sessionRef === 'string' ? this.liveSession(frame.sessionRef) : undefined;
795
+ const openContinuation = frame.t === 'agent_open' && ((live && !this.needsRespawn(live)) || this.opening.has(frame.sessionRef) || this.adopting.has(frame.sessionRef));
796
+ const newWork = !openContinuation && ['agent_open', 'agent_prompt', 'agent_list_native', 'agent_delete_native', 'agent_account_read', 'agent_verify', 'agent_authenticate', 'agent_install', 'agent_switch_idle'].includes(frame.t);
797
+ const registry = this.tasks ?? this.acceptedTasks;
798
+ const ticket = newWork && this.admission() ? registry.beginAcceptedAgentWork(frame.t, frame, frame.t === 'agent_prompt' ? this.adoptionOrigin : undefined) : null;
799
+ const admitted = (!newWork && (frame.t !== 'agent_open' || this.admission())) || ticket !== null;
800
+ try {
801
+ await this.handleAcceptedFrame(frame, ticket, admitted, registry);
802
+ }
803
+ finally {
804
+ if (ticket)
805
+ registry.finishAcceptedAgentWork(ticket);
806
+ }
807
+ }
808
+ acceptedTasks = new TaskRegistry();
809
+ adoptionOrigin = {};
810
+ async handleAcceptedFrame(frame, ticket, admitted, registry) {
799
811
  // A frame for a ref keeper-acp still runs from a previous daemon waits until that session is rebuilt (Task 21): answering
800
812
  // it "unknown session" would end a turn that is going on.
801
813
  const frameRef = 'sessionRef' in frame && typeof frame.sessionRef === 'string' ? frame.sessionRef : undefined;
@@ -809,40 +821,8 @@ export class AgentSessionManager {
809
821
  for (let parked = frameRef !== undefined ? this.switching.get(frameRef) : undefined; parked; parked = this.switching.get(frameRef))
810
822
  await parked;
811
823
  switch (frame.t) {
812
- case 'agent_delegation_renew': {
813
- if (this.serverAgentWire < AGENT_WIRE_DELEGATION_RENEW_VERSION)
814
- return;
815
- const live = this.liveSession(frame.sessionRef);
816
- const now = Date.now();
817
- // Delegation's non-secret fields are processor state (Task 19): the checks read it, an accepted renewal is a note.
818
- const delegation = live?.session.delegation ?? undefined;
819
- if (!live || !delegation || live.closed || !live.session.liveness.agentAlive || live.session.liveness.turnRunning ||
820
- activityIsBusy(live.session.activityRaw) || delegation.refreshPending ||
821
- live.nativeSessionId !== frame.nativeSessionId || delegation.instanceId !== frame.instanceId ||
822
- delegation.bindingId !== frame.bindingId || !frame.bindingId ||
823
- !Number.isSafeInteger(frame.renewalRevision) || frame.renewalRevision < 1 ||
824
- !Number.isSafeInteger(frame.expiresAtEpochMs) || frame.expiresAtEpochMs <= now ||
825
- frame.expiresAtEpochMs > now + 330_000 || frame.renewalRevision < (delegation.renewalRevision ?? 0) ||
826
- frame.expiresAtEpochMs < delegation.expiresAtEpochMs ||
827
- (frame.renewalRevision === delegation.renewalRevision && frame.expiresAtEpochMs !== delegation.expiresAtEpochMs)) {
828
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: false, error: 'The original managed instance cannot renew' });
829
- return;
830
- }
831
- try {
832
- await live.session.note({ type: 'agent_delegation_renewed', expiresAtEpochMs: frame.expiresAtEpochMs, renewalRevision: frame.renewalRevision });
833
- }
834
- catch (err) {
835
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: false, error: errorMessage(err) });
836
- return;
837
- }
838
- const renewed = live.session.delegation ?? delegation;
839
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: true,
840
- nativeSessionId: live.nativeSessionId, instanceId: renewed.instanceId, bindingId: renewed.bindingId,
841
- renewalRevision: renewed.renewalRevision, expiresAtEpochMs: renewed.expiresAtEpochMs });
842
- return;
843
- }
844
824
  case 'agent_open': {
845
- const admission = this.reserveOpen(frame.id, frame.sessionRef);
825
+ const admission = this.reserveOpen(frame, ticket, admitted, registry);
846
826
  if (admission === 'rejected')
847
827
  return;
848
828
  const ref = frame.sessionRef;
@@ -852,7 +832,7 @@ export class AgentSessionManager {
852
832
  }
853
833
  finally {
854
834
  if (admission === 'reserved')
855
- this.tasks?.agentOpenFinished(ref);
835
+ registry.agentOpenFinished(ref);
856
836
  const left = (this.openHandling.get(ref) ?? 1) - 1;
857
837
  if (left > 0)
858
838
  this.openHandling.set(ref, left);
@@ -868,7 +848,7 @@ export class AgentSessionManager {
868
848
  const live = this.routableSession(frame.sessionRef, 'prompt');
869
849
  if (!live)
870
850
  return;
871
- if (this.tasks && !this.tasks.tryAdmitTurn(frame.sessionRef)) {
851
+ if (!admitted) {
872
852
  // The refusal is the daemon's decision, so it is a note (`agent_prompt{admitted:false}`) and the processor ends the
873
853
  // turn `failed` with the same words (`ADMISSION_REFUSED`), numbered in the session's own sequence (spec §5.4). No
874
854
  // `authMethods`: the processor's short frame carries no `agentRefused`, so the projection adds none — the prompt
@@ -876,6 +856,14 @@ export class AgentSessionManager {
876
856
  live.session.refusePrompt(frame.platformTurnId);
877
857
  return;
878
858
  }
859
+ if (ticket)
860
+ try {
861
+ registry.consumeAcceptedAgentWork(ticket, frame.t, frame);
862
+ }
863
+ catch {
864
+ live.session.refusePrompt(frame.platformTurnId);
865
+ return;
866
+ }
879
867
  live.session.prompt(promptBlocks(frame), frame.platformTurnId);
880
868
  return;
881
869
  }
@@ -1013,6 +1001,10 @@ export class AgentSessionManager {
1013
1001
  * keep holding that row or to deliver it the ordinary way. A silent drop parks it forever.
1014
1002
  */
1015
1003
  case 'agent_steer': {
1004
+ if (!this.admission()) {
1005
+ this.emit({ t: 'agent_steered', id: frame.id, ok: false, error: 'Registration admission is paused' });
1006
+ return;
1007
+ }
1016
1008
  const live = this.routableSession(frame.sessionRef, 'steer');
1017
1009
  if (!live) {
1018
1010
  this.emit({ t: 'agent_steered', id: frame.id, ok: false, error: `no live session '${frame.sessionRef}'` });
@@ -1054,6 +1046,10 @@ export class AgentSessionManager {
1054
1046
  * a request that never resolves.
1055
1047
  */
1056
1048
  case 'agent_set_config_option': {
1049
+ if (!this.admission()) {
1050
+ this.emit({ t: 'agent_config_option_set', id: frame.id, ok: false, error: 'Registration admission is paused' });
1051
+ return;
1052
+ }
1057
1053
  const live = this.routableSession(frame.sessionRef, 'set_config_option');
1058
1054
  if (!live) {
1059
1055
  this.emit({ t: 'agent_config_option_set', id: frame.id, ok: false, error: `no live session '${frame.sessionRef}'` });
@@ -1089,14 +1085,14 @@ export class AgentSessionManager {
1089
1085
  return;
1090
1086
  }
1091
1087
  case 'agent_list_native':
1092
- if (this.tasks && !this.tasks.accepting) {
1088
+ if (!admitted) {
1093
1089
  this.emit({ t: 'agent_native_sessions', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1094
1090
  return;
1095
1091
  }
1096
1092
  await this.onListNative(frame);
1097
1093
  return;
1098
1094
  case 'agent_delete_native':
1099
- if (this.tasks && !this.tasks.accepting) {
1095
+ if (!admitted) {
1100
1096
  this.emit({ t: 'agent_native_deleted', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1101
1097
  return;
1102
1098
  }
@@ -1105,7 +1101,7 @@ export class AgentSessionManager {
1105
1101
  // Both of these spawn an agent subprocess, exactly as `agent_list_native` does, so they answer
1106
1102
  // a draining workstation the same way rather than starting work it is trying to finish.
1107
1103
  case 'agent_account_read': {
1108
- if (this.tasks && !this.tasks.accepting) {
1104
+ if (!admitted) {
1109
1105
  this.emit({ t: 'agent_account_read_result', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1110
1106
  return;
1111
1107
  }
@@ -1127,14 +1123,14 @@ export class AgentSessionManager {
1127
1123
  return;
1128
1124
  }
1129
1125
  case 'agent_verify':
1130
- if (this.tasks && !this.tasks.accepting) {
1126
+ if (!admitted) {
1131
1127
  this.emit({ t: 'agent_verified', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1132
1128
  return;
1133
1129
  }
1134
1130
  await this.onVerify(frame);
1135
1131
  return;
1136
1132
  case 'agent_authenticate':
1137
- if (this.tasks && !this.tasks.accepting) {
1133
+ if (!admitted) {
1138
1134
  this.emit({ t: 'agent_authenticated', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1139
1135
  return;
1140
1136
  }
@@ -1143,7 +1139,7 @@ export class AgentSessionManager {
1143
1139
  // Downloads a package; starts nothing. Still refused while draining, because a workstation on
1144
1140
  // its way down should not begin a multi-megabyte fetch it will be killed in the middle of.
1145
1141
  case 'agent_install':
1146
- if (this.tasks && !this.tasks.accepting) {
1142
+ if (!admitted) {
1147
1143
  this.emit({ t: 'agent_installed', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1148
1144
  return;
1149
1145
  }
@@ -1151,7 +1147,7 @@ export class AgentSessionManager {
1151
1147
  return;
1152
1148
  // "Move idle sessions" (Task 25): a move spawns adapters, so a draining workstation refuses it like an install.
1153
1149
  case 'agent_switch_idle':
1154
- if (this.tasks && !this.tasks.accepting) {
1150
+ if (!admitted) {
1155
1151
  this.emit({ t: 'agent_switch_idle_result', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
1156
1152
  return;
1157
1153
  }
@@ -1199,24 +1195,31 @@ export class AgentSessionManager {
1199
1195
  * stop leaves attached. A ref absent from both maps is genuinely new work and still needs
1200
1196
  * the ordinary TaskRegistry admission reservation.
1201
1197
  */
1202
- reserveOpen(openId, sessionRef) {
1203
- if (!this.tasks)
1204
- return 'continuation';
1198
+ reserveOpen(frame, ticket, admitted, registry) {
1199
+ const { id: openId, sessionRef } = frame;
1200
+ if (!admitted) {
1201
+ this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
1202
+ return 'rejected';
1203
+ }
1205
1204
  const live = this.liveSession(sessionRef);
1206
1205
  // A keeper-hosted session whose adapter is gone is respawned by this open: new work, not a continuation — it takes a reservation,
1207
1206
  // and a draining workstation refuses it like any open (spec §8.2; fix round 1, minor 9).
1208
1207
  const respawn = live !== undefined && this.needsRespawn(live);
1209
- if (this.tasks.isForceStopping()) {
1208
+ if (registry.isForceStopping()) {
1210
1209
  // A keeper-hosted session a force update left attached (TaskRegistry.stopAll) is still reopened as a continuation — a reconnect
1211
1210
  // during the download must not cut it off; anything else is refused.
1212
- if (live && !respawn && this.tasks.isDurableSession(sessionRef))
1211
+ if (live && !respawn && registry.isDurableSession(sessionRef))
1213
1212
  return 'continuation';
1214
1213
  this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
1215
1214
  return 'rejected';
1216
1215
  }
1217
1216
  if ((live && !respawn) || this.opening.has(sessionRef) || this.adopting.has(sessionRef))
1218
1217
  return 'continuation';
1219
- if (this.tasks.tryReserveAgentOpen(sessionRef))
1218
+ if (ticket) {
1219
+ registry.reserveAcceptedAgentOpen(ticket, frame);
1220
+ return 'reserved';
1221
+ }
1222
+ if (registry.tryReserveAgentOpen(sessionRef))
1220
1223
  return 'reserved';
1221
1224
  this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
1222
1225
  return 'rejected';
@@ -1490,20 +1493,6 @@ export class AgentSessionManager {
1490
1493
  return idle.map((live) => live.sessionRef);
1491
1494
  }
1492
1495
  async onOpen(frame) {
1493
- if (frame.delegationMcp && (this.serverAgentWire < AGENT_WIRE_DELEGATION_MCP_VERSION ||
1494
- (frame.delegationMcp.bindingId && this.serverAgentWire < AGENT_WIRE_DELEGATION_RENEW_VERSION))) {
1495
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation requires a newer server' });
1496
- return;
1497
- }
1498
- if (frame.delegationMcp) {
1499
- try {
1500
- buildSessionMcpServers([], frame.delegationMcp);
1501
- }
1502
- catch {
1503
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Delegation MCP configuration is unavailable' });
1504
- return;
1505
- }
1506
- }
1507
1496
  // Rotation is server-driven. A busy reconnect resends the original process's outbox.
1508
1497
  let existing = this.liveSession(frame.sessionRef);
1509
1498
  // An open that would spawn the ref's adapter anew after it kept exiting on its own waits out the backoff (no crash loop).
@@ -1532,69 +1521,33 @@ export class AgentSessionManager {
1532
1521
  retireForRefresh = existing;
1533
1522
  existing = undefined;
1534
1523
  }
1524
+ // A keeper session's label records the build that created its native session. Preserve that
1525
+ // binding across an adapter exit: the agent's current pointer may have advanced meanwhile,
1526
+ // but a cached reconnect must retry the recorded package before any explicit switch.
1527
+ const recordedBuildId = retireForRefresh
1528
+ ? this.agents.placementOf?.(retireForRefresh.session)?.buildId
1529
+ : this.retainedRespawnBuilds.get(frame.sessionRef);
1535
1530
  if (existing) {
1536
- const delegation = existing.session.delegation ?? undefined;
1537
- if (delegation && !frame.delegationMcp) {
1538
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation lease is required' });
1531
+ // Spec §6.3: a session with a switch pending (a persistent intent: another adapter build, or off an older keeper-acp
1532
+ // generation) moves at this open — the only moment its spawn secrets are at hand — when it is strictly idle.
1533
+ if (await this.switchAtOpen(existing, frame))
1539
1534
  return;
1540
- }
1541
- if (frame.delegationMcp && frame.nativeSessionId && frame.nativeSessionId !== existing.nativeSessionId) {
1542
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation cannot reload the original session' });
1535
+ // Reusing a live session: neither the process nor the native session moved.
1536
+ if (!await this.resumeAndReply(existing, frame, false))
1543
1537
  return;
1544
- }
1545
- const refresh = !!frame.delegationMcp &&
1546
- (delegation?.leaseId !== delegationLeaseId(frame.delegationMcp) || delegation?.refreshPending === true);
1547
- if (refresh) {
1548
- if (!existing.nativeSessionId || (frame.nativeSessionId && frame.nativeSessionId !== existing.nativeSessionId)) {
1549
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation cannot reload the original session' });
1550
- return;
1551
- }
1552
- if (existing.session.liveness.turnRunning || activityIsBusy(existing.session.activityRaw)) {
1553
- if (!delegation?.refreshPending) {
1554
- try {
1555
- await existing.session.note({ type: 'agent_delegation_refresh_pending' });
1556
- }
1557
- catch (err) {
1558
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: errorMessage(err) });
1559
- return;
1560
- }
1561
- }
1562
- if (!await this.resumeAndReply(existing, frame, false))
1563
- return;
1564
- this.replyNoReplayNeeded(frame);
1565
- return;
1566
- }
1567
- openFrame = { ...frame, nativeSessionId: existing.nativeSessionId, requireSessionLoad: true };
1568
- retireForRefresh = existing;
1569
- }
1570
- else {
1571
- // Spec §6.3: a session with a switch pending (a persistent intent: another adapter build, or off an older keeper-acp
1572
- // generation) moves at this open — the only moment its spawn secrets are at hand — when it is strictly idle.
1573
- if (await this.switchAtOpen(existing, frame))
1574
- return;
1575
- // Reusing a live session: neither the process nor the native session moved.
1576
- if (!await this.resumeAndReply(existing, frame, false))
1577
- return;
1578
- // The session already exists, so any history it has was already streamed under whichever
1579
- // open first established it (or never will be, if that open didn't ask for it) — replaying it
1580
- // again here would duplicate what the platform already holds. But if THIS frame asked for
1581
- // backfill, the server is waiting on exactly one `agent_backfill_done` for THIS open's id:
1582
- // silence is not "no", it is a wait that never resolves. Answer it without replaying.
1583
- this.replyNoReplayNeeded(frame);
1584
- return;
1585
- }
1538
+ // The session already exists, so any history it has was already streamed under whichever
1539
+ // open first established it (or never will be, if that open didn't ask for it) — replaying it
1540
+ // again here would duplicate what the platform already holds. But if THIS frame asked for
1541
+ // backfill, the server is waiting on exactly one `agent_backfill_done` for THIS open's id:
1542
+ // silence is not "no", it is a wait that never resolves. Answer it without replaying.
1543
+ this.replyNoReplayNeeded(frame);
1544
+ return;
1586
1545
  }
1587
1546
  const inFlight = this.opening.get(frame.sessionRef);
1588
1547
  if (inFlight) {
1589
1548
  // Piggyback on the attempt already running, and report its real failure — not a generic one.
1590
1549
  const { live, error, agentRefused } = await inFlight;
1591
1550
  if (live) {
1592
- const delegation = live.session.delegation ?? undefined;
1593
- if (!!frame.delegationMcp !== !!delegation ||
1594
- (frame.delegationMcp && delegation?.leaseId !== delegationLeaseId(frame.delegationMcp))) {
1595
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation lease does not match the live session' });
1596
- return;
1597
- }
1598
1551
  // Piggybacking on the attempt already running: a non-null `live` means that attempt was a
1599
1552
  // real open (`session/load` or `session/new`), which this frame is reporting secondhand.
1600
1553
  if (!await this.resumeAndReply(live, frame, true))
@@ -1623,9 +1576,9 @@ export class AgentSessionManager {
1623
1576
  const attempt = (async () => {
1624
1577
  if (retireForRefresh)
1625
1578
  await this.retire(retireForRefresh, 'keep');
1626
- return this.openSession(openFrame);
1579
+ return this.openSession(openFrame, recordedBuildId);
1627
1580
  })()
1628
- .catch((err) => ({ live: null, error: openFailureText(openFrame, err) }))
1581
+ .catch((err) => ({ live: null, error: errorMessage(err) }))
1629
1582
  .finally(() => releaseHold?.());
1630
1583
  this.opening.set(frame.sessionRef, attempt);
1631
1584
  let outcome;
@@ -1648,6 +1601,7 @@ export class AgentSessionManager {
1648
1601
  // tombstone without anything ever closing it. Nothing but microtasks separates `attempt`
1649
1602
  // resolving from the finally, and microtasks always drain before the next inbound frame.
1650
1603
  if (outcome.live) {
1604
+ this.retainedRespawnBuilds.delete(frame.sessionRef);
1651
1605
  // openSession returned successfully, having just done session/load or session/new.
1652
1606
  if (!await this.resumeAndReply(outcome.live, frame, true))
1653
1607
  return;
@@ -1658,6 +1612,8 @@ export class AgentSessionManager {
1658
1612
  await this.streamBackfill(frame.id, outcome.live.session);
1659
1613
  }
1660
1614
  else {
1615
+ if (recordedBuildId !== undefined)
1616
+ this.retainedRespawnBuilds.set(frame.sessionRef, recordedBuildId);
1661
1617
  this.emit({
1662
1618
  t: 'agent_opened', id: frame.id, ok: false, error: outcome.error,
1663
1619
  // Only when the agent is the one that said no, and then it is the one refusal that can be
@@ -1689,7 +1645,7 @@ export class AgentSessionManager {
1689
1645
  */
1690
1646
  async switchAtOpen(live, frame) {
1691
1647
  const agents = this.agents;
1692
- if (!agents.switchSession || !agents.pendingSwitchFor || frame.delegationMcp)
1648
+ if (!agents.switchSession || !agents.pendingSwitchFor)
1693
1649
  return false;
1694
1650
  // A switch spawns an adapter: new work, refused while the workstation drains (spec §8.2).
1695
1651
  if (this.tasks && !this.tasks.accepting)
@@ -1847,7 +1803,7 @@ export class AgentSessionManager {
1847
1803
  * "Move idle sessions" (Task 25's `agent_switch_idle`, spec §6.4): every held session of `agentId` that runs another build than
1848
1804
  * `toBuild` (else the agent's current build) gets a persistent intent to move there; those already strictly idle get their
1849
1805
  * `agent_reopen_request` at once, the others when they become idle. A session that can never be moved this way
1850
- * ({@link unmovableWhy}: a local-fallback session, a managed delegation, no native session, a target that already refused it) gets
1806
+ * ({@link unmovableWhy}: a local-fallback session, no native session, a target that already refused it) gets
1851
1807
  * no intent — it would only be asked to reopen again and again — and is listed in `unmovable` with the reason. A `toBuild` that is
1852
1808
  * not installed for the agent is refused (throws). Returns the refs registered, the ones a request went out for, and the unmovable.
1853
1809
  */
@@ -1889,7 +1845,7 @@ export class AgentSessionManager {
1889
1845
  }
1890
1846
  /**
1891
1847
  * Why `live` can never be moved to `target` by a switch (spec §6.3 "当前限制"), or `undefined`: a session keeper-acp does not host
1892
- * (the local fallback), a managed delegation, a session with no native session to load, or one a switch to the same build already
1848
+ * (the local fallback), a session with no native session to load, or one a switch to the same build already
1893
1849
  * found unmovable. `plain` is what the scan card and the result frame say (Task 25); `detail` is the switch's own words, for the
1894
1850
  * log, when there are any.
1895
1851
  */
@@ -1897,8 +1853,6 @@ export class AgentSessionManager {
1897
1853
  const placement = this.agents.placementOf?.call(this.agents, live.session);
1898
1854
  if (!placement?.keeper)
1899
1855
  return { plain: LOCAL_SESSION_UNMOVABLE };
1900
- if (live.session.delegation)
1901
- return { plain: 'it is a delegated session, which can\'t be moved to another build yet' };
1902
1856
  if (!live.nativeSessionId)
1903
1857
  return { plain: 'it has no saved conversation the new build could reload' };
1904
1858
  const refused = this.agents.unmovableFor?.call(this.agents, live.sessionRef);
@@ -2128,7 +2082,6 @@ export class AgentSessionManager {
2128
2082
  this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'session was closed while opening' });
2129
2083
  return false;
2130
2084
  }
2131
- const delegation = live.session.delegation ?? undefined;
2132
2085
  const activityRaw = live.session.activityRaw;
2133
2086
  this.emit({
2134
2087
  t: 'agent_opened',
@@ -2140,13 +2093,6 @@ export class AgentSessionManager {
2140
2093
  // Emitted only when true, the same `supportsSteering` convention as below: absent is the
2141
2094
  // field's only "no", and it is also what an older peer sends.
2142
2095
  ...(reloaded ? { reloaded: true } : {}),
2143
- // Delegation's non-secret fields, from processor state (Task 19: `agent_opened` is assembled from state).
2144
- ...(delegation ? { delegationExpiresAtEpochMs: delegation.expiresAtEpochMs } : {}),
2145
- ...(delegation?.refreshPending ? { delegationRefreshPending: true } : {}),
2146
- ...(this.serverAgentWire >= AGENT_WIRE_DELEGATION_RENEW_VERSION && delegation?.instanceId ? {
2147
- delegationInstanceId: delegation.instanceId, delegationBindingId: delegation.bindingId,
2148
- delegationRenewalRevision: delegation.renewalRevision,
2149
- } : {}),
2150
2096
  // The server learns this here and nowhere else. It used to be published only on the roster in
2151
2097
  // the `agent_hello` handshake, which is a snapshot taken *before* any agent was connected to —
2152
2098
  // and the capability can only be observed on a live `initialize`, whose record
@@ -2223,7 +2169,7 @@ export class AgentSessionManager {
2223
2169
  });
2224
2170
  return true;
2225
2171
  }
2226
- async openSession(frame) {
2172
+ async openSession(frame, recordedBuildId) {
2227
2173
  // A previous incarnation of this ref may still be closing: its adapter stopped and its transport session (raw, outbox) kept or
2228
2174
  // released — the new spawn goes under the same key (see {@link retiring}). This is the first statement on purpose — before the
2229
2175
  // agent spawns, before anything touches the transport. The one-time legacy journal migration lands before any open too.
@@ -2272,13 +2218,14 @@ export class AgentSessionManager {
2272
2218
  });
2273
2219
  };
2274
2220
  try {
2275
- const result = await this.agents.open({ ...openSpecOf(frame), onInstallOutcome, seqBase: () => resumeFrom });
2221
+ const result = await this.agents.open({
2222
+ ...openSpecOf(frame),
2223
+ ...(recordedBuildId !== undefined ? { recordedBuildId } : {}),
2224
+ onInstallOutcome,
2225
+ seqBase: () => resumeFrom,
2226
+ });
2276
2227
  session = result.session;
2277
2228
  nativeSessionId = result.nativeSessionId;
2278
- if (frame.delegationMcp && !nativeSessionId) {
2279
- await session.close();
2280
- return { live: null, error: 'Managed delegation requires a native session ID' };
2281
- }
2282
2229
  }
2283
2230
  catch (err) {
2284
2231
  await reportFailedInstall();
@@ -2288,7 +2235,7 @@ export class AgentSessionManager {
2288
2235
  // on `session/new` arrives as; see {@link OpenOutcome.agentRefused}.
2289
2236
  return {
2290
2237
  live: null,
2291
- error: openFailureText(frame, err),
2238
+ error: errorMessage(err),
2292
2239
  // The code comes off the marker, not off `errorMessage(err)` above: the wrapper captured it
2293
2240
  // from the original rejection at the moment that object still existed.
2294
2241
  ...(err instanceof AgentRefusedSessionError
@@ -2347,7 +2294,7 @@ export class AgentSessionManager {
2347
2294
  return { live };
2348
2295
  }
2349
2296
  /** A session joins: held, registered with the TaskRegistry, its frames and its outbox wired to the sender. */
2350
- register(live) {
2297
+ register(live, adoption) {
2351
2298
  const session = live.session;
2352
2299
  this.detached.delete(live.sessionRef);
2353
2300
  this.sessions.set(live.sessionRef, live);
@@ -2357,7 +2304,7 @@ export class AgentSessionManager {
2357
2304
  () => this.retire(live, durable ? 'detach' : 'destroy'),
2358
2305
  // 两个层级量取或:本进程发出的提示词是否在途(agent 自己也许在续跑一个我们没发起的回合,
2359
2306
  // 那一种由第二项回答),以及 agent 自报的状态。两者都不累积,所以没有可泄漏的状态。
2360
- () => live.session.liveness.turnRunning || activityIsBusy(live.session.activityRaw), { durable });
2307
+ () => live.session.liveness.turnRunning || activityIsBusy(live.session.activityRaw), { durable, adoption });
2361
2308
  // Everything the session produces arrives as processor frames (Task 19): numbered by the processor, projected for the
2362
2309
  // connected server here, recorded in the outbox (the numbered ones) and sent from it (Task 20).
2363
2310
  live.unwatch = session.outbox.onChange(() => this.pump());
@@ -2843,6 +2790,7 @@ export class AgentSessionManager {
2843
2790
  void this.agents.cancelSwitch?.({ sessionRef }).catch(() => false);
2844
2791
  this.reopenRequests.delete(sessionRef);
2845
2792
  this.adapterCrashes.delete(sessionRef);
2793
+ this.retainedRespawnBuilds.delete(sessionRef);
2846
2794
  // Closing a ref that is still spawning: nothing to close yet, so tombstone it — openSession
2847
2795
  // closes the session (and destroys its journal) the moment the bridge hands it over.
2848
2796
  //
@@ -3,6 +3,7 @@ import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
3
3
  import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_RESUME_OFFER_VERSION, AGENT_WIRE_VERSION, decodeAgentFrame, encodeAgentFrame, outboundAgentFrameRejection, } from '../protocol/agent.js';
4
4
  import { bridgesForWire, helloRoster } from '../protocol/common.js';
5
5
  import { workstationChannelUrl } from './url.js';
6
+ import { DeviceEventRouter } from '../runtime/events.js';
6
7
  /** Binds daemon-scoped sessions only to the encrypted agent channel's sender. */
7
8
  export class AgentChannel {
8
9
  options;
@@ -24,7 +25,14 @@ export class AgentChannel {
24
25
  });
25
26
  // Hoisted rather than inlined twice: `closeAll` compares it by identity to decide whether the
26
27
  // binding it is being asked to drop is still this connection's. See AgentSessionManager.closeAll.
27
- const send = (frame) => channel.send(frame);
28
+ const registrationId = this.options.registrationId;
29
+ const events = registrationId ? new DeviceEventRouter((_recipient, event) => channel.send(event.payload), new Set([registrationId])) : undefined;
30
+ const send = (frame) => {
31
+ if (events && registrationId)
32
+ events.publish({ scope: 'registration', registrationId, payload: frame });
33
+ else
34
+ channel.send(frame);
35
+ };
28
36
  // The socket's unsent bytes: the manager's socket gate holds session frames back above 1 MiB so the heartbeat (written by the
29
37
  // channel itself) never queues behind a backlog (spec §5.6).
30
38
  void dispatchAgentChannelFrames(channel.messages, send, this.options.agents, () => channel.close(), () => channel.bufferedAmount);