@platless/axom-cli 0.6.36 → 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 (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. package/dist/native/darwin-arm64/axom-quic +0 -0
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "kind": "terminal",
3
- "protocolVersion": 1,
4
- "buildVersion": "c0f319e5906ddf108ac7f65222a35db8423b49702609c2ea74aca69937ad1a05",
3
+ "protocolVersion": 2,
4
+ "buildVersion": "80542c28e2e2ce561c3acc8dab28422a0df482b778665e982272e923261c775c",
5
5
  "daemonProtocol": [
6
- 1,
7
- 1
6
+ 2,
7
+ 2
8
8
  ]
9
9
  }
Binary file
@@ -510,6 +510,7 @@ export class AcpAgents {
510
510
  /** {@link OPEN_INSTALL_BUDGET_MS}, overridable for tests. */
511
511
  openInstallBudgetMs;
512
512
  cacheDir;
513
+ ownership;
513
514
  transports;
514
515
  generations;
515
516
  /** The once-per-daemon adoption's discovery ({@link adoptKeeperSessions}). */
@@ -583,9 +584,10 @@ export class AcpAgents {
583
584
  this.patchAssetRoot = options.patchAssetRoot;
584
585
  this.commandRetryFirstBroadcastMs = options.commandRetryFirstBroadcastMs ?? COMMAND_RETRY_FIRST_BROADCAST_MS;
585
586
  this.openInstallBudgetMs = options.openInstallBudgetMs ?? OPEN_INSTALL_BUDGET_MS;
586
- this.adapterRoot = path.join(options.cacheDir, 'acp-adapters');
587
+ this.adapterRoot = path.join(options.adapterCacheDirectory ?? options.cacheDir, 'acp-adapters');
587
588
  this.builds = new BuildStore(this.adapterRoot);
588
589
  this.cacheDir = options.cacheDir;
590
+ this.ownership = options.ownership;
589
591
  this.transports = { ...options.transports };
590
592
  this.generations = options.keeperGenerations;
591
593
  this.initializeTimeoutMs = options.initializeTimeoutMs ?? DEFAULT_SESSION_INITIALIZE_TIMEOUT_MS;
@@ -1843,6 +1845,8 @@ export class AcpAgents {
1843
1845
  * be listed. One at a time; never rejects (failures are logged).
1844
1846
  */
1845
1847
  collectBuilds(options = {}) {
1848
+ if (this.ownership)
1849
+ return Promise.resolve({ removed: [], discarded: [], kept: [], legacyRemoved: [], skipped: 'Device-wide build references are incomplete' });
1846
1850
  const previous = this.collecting;
1847
1851
  let run;
1848
1852
  run = (async () => {
@@ -2065,15 +2069,18 @@ export class AcpAgents {
2065
2069
  }
2066
2070
  /** The fallback transport (spec §5.7), made on first use. */
2067
2071
  localTransport() {
2068
- this.transports.local ??= new LocalTransport({ configDir: this.cacheDir });
2072
+ this.transports.local ??= new LocalTransport({ configDir: this.cacheDir, ownership: this.ownership });
2069
2073
  return this.transports.local;
2070
2074
  }
2071
2075
  /** Daemon shutdown: keeper-acp's connection is dropped (its adapters keep running); local adapters are stopped. */
2072
- closeTransports() {
2076
+ async closeTransports() {
2073
2077
  this.clearQuotaReaders();
2074
2078
  this.transports.keeper?.close();
2075
2079
  this.generations?.close();
2076
- this.transports.local?.close();
2080
+ if (this.transports.local instanceof LocalTransport)
2081
+ await this.transports.local.dispose();
2082
+ else
2083
+ this.transports.local?.close();
2077
2084
  }
2078
2085
  clearQuotaReaders(agentId) {
2079
2086
  for (const [key, reader] of this.quotaReaders)
@@ -22,7 +22,7 @@
22
22
  * ## Residual (documented, not fixed)
23
23
  *
24
24
  * While the producing window is full — a server parked on an approval with ≥ 256 root frames held, a connection that is down, an
25
- * applier that never acknowledges — the record loop folds nothing: a steer or config RPC waits for the processor's reply and can
25
+ * applier that never acknowledges — the record loop folds nothing: an already-admitted steer or config RPC waits for the processor's reply and can
26
26
  * time out on the server; an `agent_interrupt` note is folded (and the `session/cancel` written) only once the server unparks.
27
27
  */
28
28
  /** Unacknowledged events per session (spec §5.6, `W_EVT`). `ACK_EVERY_EVENTS(32) < W_EVT ≤ HOLD_OFF_CAPACITY(512)`. */
@@ -278,16 +278,22 @@ export class SessionOutbox {
278
278
  }
279
279
  /**
280
280
  * A new connection: everything recorded so far — sent on an earlier connection or produced while none was bound — is held until
281
- * the server reopens the ref and says how far it got ({@link SENT_EARLIER}); what is recorded from now on is sent at once. (The
282
- * journal's semantics: frames produced while unbound were replayed by the reopen, never sent on their own.)
281
+ * the server reopens the ref and says how far it got ({@link SENT_EARLIER}). Newly recorded frames wait behind those older
282
+ * entries so a higher seq cannot advance the server's watermark past retained output.
283
283
  */
284
284
  connectionChanged() {
285
285
  for (const item of this.items)
286
286
  item.sentOn = SENT_EARLIER;
287
287
  }
288
- /** The first entry never sent (the sender's next candidate), in seq order. */
288
+ /** The first entry never sent, without overtaking an older entry held for reopen. */
289
289
  nextUnsent() {
290
- return this.items.find((item) => item.sentOn === null);
290
+ for (const item of this.items) {
291
+ if (item.sentOn === SENT_EARLIER)
292
+ return undefined;
293
+ if (item.sentOn === null)
294
+ return item;
295
+ }
296
+ return undefined;
291
297
  }
292
298
  /** Resolves when this life's unacknowledged backlog is below the window (immediately once {@link stopWaiting} ran). */
293
299
  room() {
@@ -295,6 +301,7 @@ export class SessionOutbox {
295
301
  return undefined;
296
302
  return new Promise((resolve) => { this.roomWaiters.add(resolve); });
297
303
  }
304
+ /** Whether another raw record can be folded now, without registering a waiter. */
298
305
  hasRoom() {
299
306
  if (this.stopped)
300
307
  return true;
@@ -174,7 +174,8 @@ export function buildPermissionRequestedEvent(input) {
174
174
  * forced into a selection the user never made.
175
175
  */
176
176
  export async function handleRequestPermission(input, sink) {
177
- if (input.options.length === 0) {
177
+ const optionIds = new Set(input.options.map((option) => option.optionId));
178
+ if (input.options.length === 0 || optionIds.size !== input.options.length) {
178
179
  return { outcome: 'rejected' };
179
180
  }
180
181
  if (input.signal?.aborted) {
@@ -192,7 +193,6 @@ export async function handleRequestPermission(input, sink) {
192
193
  console.error(`[acp] refusing to raise a permission card that fails validateWireEvent: ${rejection.reason}`);
193
194
  return { outcome: 'rejected' };
194
195
  }
195
- const optionIds = new Set(input.options.map((option) => option.optionId));
196
196
  return new Promise((resolve) => {
197
197
  let settled = false;
198
198
  const settle = (decision) => {
@@ -8,9 +8,10 @@ import { initialState, PROCESSOR_STATE_SCHEMA } from './state.js';
8
8
  * 6 — child status precedes process results; ended calls release data;
9
9
  * 7 — install outcomes become step events;
10
10
  * 8 — loaded-session levels fold live; output gaps become steps;
11
- * 9 — adapter compatibility folds moved out and retired optional state removed.
11
+ * 9 — adapter compatibility folds moved out and retired optional state removed;
12
+ * 10 — ended replay scopes cannot leak late level snapshots into the live journal.
12
13
  */
13
- export const PROCESSOR_VERSION = 9;
14
+ export const PROCESSOR_VERSION = 10;
14
15
  export const CHECKPOINT_SCHEMA = 1;
15
16
  export class CheckpointUnreadableError extends Error {
16
17
  }
@@ -64,7 +65,7 @@ export function decodeCheckpoint(blob) {
64
65
  || !('activity' in state)) {
65
66
  throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
66
67
  }
67
- return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone({ ...(state.schema === 4 ? upgradeStateFromSchema4(state) : state), ...(!Object.hasOwn(state, 'draftSubagents') ? { draftSubagents: { children: {}, overflow: false } } : {}) }) };
68
+ return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone({ ...(state.schema === 4 ? upgradeStateFromSchema4(state) : state), ...(!Object.hasOwn(state, 'draftSubagents') ? { draftSubagents: { children: {}, overflow: false } } : {}), ...(!Object.hasOwn(state, 'closedReplayScopes') ? { closedReplayScopes: {} } : {}) }) };
68
69
  }
69
70
  /** Read only the current neutral keys; retired adapter state is deliberately not copied. */
70
71
  function upgradeStateFromSchema4(state) {
@@ -3,7 +3,7 @@ import { newSessionMetaFor } from '../activityReporting.js';
3
3
  import { flatChoicesOf, modesAlreadyRepresented, modelsAlreadyRepresented, normalizeConfigOptions, SYNTHESIZED_MODES_CONFIG_ID, SYNTHESIZED_MODEL_CONFIG_ID } from '../configOptions.js';
4
4
  import { ASYNC_TASK_STOP_METHOD, mapSteerOutcome, STEER_METHOD } from '../steering.js';
5
5
  import { emitEvent, isRecord } from './emit.js';
6
- import { settlementErrorCode } from './jsonrpc.js';
6
+ import { jsonRpcErrorText, settlementErrorCode } from './jsonrpc.js';
7
7
  import { sameCause } from './record.js';
8
8
  import { onLaunchNote } from './runtime.js';
9
9
  import { bindRootSession, closeErrorText, connected } from './session.js';
@@ -58,7 +58,7 @@ export function settleOpen(state, entry, settlement, out) {
58
58
  const { mode } = ctx;
59
59
  if (settlement.outcome !== 'result') {
60
60
  const code = settlementErrorCode(settlement);
61
- const error = settlement.outcome === 'error' ? settlement.error.message : closeErrorText(state);
61
+ const error = settlement.outcome === 'error' ? jsonRpcErrorText(settlement.error) : closeErrorText(state);
62
62
  state.open = { status: 'failed', mode, error, ...(code !== undefined ? { code } : {}) };
63
63
  endHistory(state, out, false);
64
64
  // A failed open closes the attached session, and its repair with it (`AcpSession.close`).
@@ -132,6 +132,14 @@ export function settlementOf(response) {
132
132
  const { code, message, data } = response.error;
133
133
  return { outcome: 'error', error: { code, message, data } };
134
134
  }
135
+ /**
136
+ * The text an agent's error is shown as. The ACP SDK's `RequestError.internalError(details)` puts the
137
+ * real cause only in `data.details` behind a generic "Internal error", so a string detail is kept.
138
+ */
139
+ export function jsonRpcErrorText(error) {
140
+ const details = isRecord(error.data) && typeof error.data.details === 'string' ? error.data.details.trim().slice(0, 2000) : '';
141
+ return details && !error.message.includes(details) ? `${error.message}: ${details}` : error.message;
142
+ }
135
143
  /** `jsonRpcErrorCode(err)` for a settlement: the code an agent answered with, if a frame may carry it. */
136
144
  export function settlementErrorCode(settlement) {
137
145
  return settlement.outcome === 'error' ? jsonRpcErrorCodeOf(settlement.error.code) : undefined;
@@ -2,7 +2,7 @@
2
2
  import { buildElicitationRequestedEvent, buildPermissionRequestedEvent, elicitationFieldKeys, isBlank, permissionRequestId, } from '../permission.js';
3
3
  import { validateWireEvent } from '../validate.js';
4
4
  import { isRecord, nextSeq } from './emit.js';
5
- import { inboundKey, settlementErrorCode } from './jsonrpc.js';
5
+ import { inboundKey, jsonRpcErrorText, settlementErrorCode } from './jsonrpc.js';
6
6
  import { sameCause } from './record.js';
7
7
  import { publishCallbacks, resetRuntime } from './runtime.js';
8
8
  import { getOwn, initialCallbacks, initialMessage, PENDING_PERMISSION_LIMIT, SETTLED_PERMISSION_LIMIT, } from './state.js';
@@ -51,6 +51,7 @@ export function resetForSpawn(state) {
51
51
  state.steered = [];
52
52
  state.asyncTasks = {};
53
53
  state.draftSubagents = { children: {}, overflow: false };
54
+ state.closedReplayScopes = {};
54
55
  resetRuntime(state);
55
56
  state.turns = [];
56
57
  state.exactExecutionUnconfirmed = false;
@@ -246,7 +247,7 @@ export function settlePrompt(state, entry, settlement, out) {
246
247
  // The one exit where the agent itself answered (`TurnResult.agentRefused`).
247
248
  const errorCode = settlementErrorCode(settlement);
248
249
  finishTurn(state, out, index, 'failed', {
249
- error: settlement.error.message, agentRefused: true, errorCode, executionEnded: errorCode !== undefined,
250
+ error: jsonRpcErrorText(settlement.error), agentRefused: true, errorCode, executionEnded: errorCode !== undefined,
250
251
  });
251
252
  return;
252
253
  }
@@ -43,6 +43,7 @@ export function initialState() {
43
43
  open: { status: 'idle' },
44
44
  configOptions: null,
45
45
  history: null,
46
+ closedReplayScopes: {},
46
47
  mapperScopes: {},
47
48
  message: initialMessage(),
48
49
  steers: {},
@@ -19,6 +19,7 @@ import { AcpSession } from '../../../keepers/acp/session.js';
19
19
  import { KeeperMethodError } from '../../../keepers/kit/protocol.js';
20
20
  import { StreamLog } from '../../../keepers/kit/streamLog.js';
21
21
  import { DAEMON_FRAME_INDEX } from '../../outbox.js';
22
+ import { assertOwnedDirectory } from '../../../runtime/ownership.js';
22
23
  import { RawTruncatedError, checkpointBlob, rawRecordOf, } from './types.js';
23
24
  const DEFAULT_WINDOW = 32;
24
25
  /** Longest directory name used as is (file systems cap a name at 255 bytes); longer keys get a hashed name. */
@@ -43,12 +44,15 @@ export class LocalTransport {
43
44
  logLine;
44
45
  constructor(options) {
45
46
  this.options = options;
46
- this.root = path.join(options.configDir, 'agents', 'local');
47
+ this.root = options.outboxDirectory ?? path.join(options.configDir, 'agents', 'local');
47
48
  this.now = options.now ?? Date.now;
48
49
  this.logLine = options.log ?? (() => { });
49
50
  }
50
51
  open(sessionKey, label, baseOffset, orphan) {
51
52
  const dir = localSessionDir(this.root, sessionKey);
53
+ assertOwnedDirectory(dir, this.options.ownership, 'local-outbox');
54
+ if (this.options.ownership)
55
+ label = { ...label, registrationId: this.options.ownership.registrationId };
52
56
  if (path.basename(dir).startsWith('~')) {
53
57
  mkdirSync(dir, { recursive: true, mode: 0o700 });
54
58
  writeFileSync(path.join(dir, KEY_FILE), sessionKey, { mode: 0o600 });
@@ -71,12 +75,15 @@ export class LocalTransport {
71
75
  async importOutbox(key, records) {
72
76
  const sessionKey = sessionKeyOf(key);
73
77
  const dir = localSessionDir(this.root, sessionKey);
74
- if (this.entries.has(sessionKey) || existsSync(dir))
78
+ if (this.entries.has(sessionKey) || existsSync(dir)) {
79
+ assertOwnedDirectory(dir, this.options.ownership, 'local-outbox');
75
80
  return false;
81
+ }
76
82
  mkdirSync(this.root, { recursive: true, mode: 0o700 });
77
83
  const staging = path.join(this.root, `.staging-${randomUUID()}`);
78
84
  try {
79
85
  mkdirSync(staging, { mode: 0o700 });
86
+ assertOwnedDirectory(staging, this.options.ownership);
80
87
  if (path.basename(dir).startsWith('~'))
81
88
  writeFileSync(path.join(staging, KEY_FILE), sessionKey, { mode: 0o600 });
82
89
  const log = StreamLog.open({ dir: staging, now: this.now, immediateKinds: [] });
@@ -127,6 +134,13 @@ export class LocalTransport {
127
134
  catch {
128
135
  continue;
129
136
  }
137
+ try {
138
+ assertOwnedDirectory(path.join(this.root, name), this.options.ownership, 'local-outbox');
139
+ }
140
+ catch {
141
+ this.logLine('ACP local state skipped: registration owner is unknown or mismatched');
142
+ continue;
143
+ }
130
144
  if (!this.entries.has(key))
131
145
  keys.push(key);
132
146
  }
@@ -135,8 +149,10 @@ export class LocalTransport {
135
149
  entry(key) {
136
150
  const sessionKey = sessionKeyOf(key);
137
151
  const existing = this.entries.get(sessionKey);
138
- if (existing)
152
+ if (existing) {
153
+ assertOwnedDirectory(existing.dir, this.options.ownership, 'local-outbox');
139
154
  return existing;
155
+ }
140
156
  if (this.orphans().includes(sessionKey))
141
157
  return this.open(sessionKey, {}, undefined, true);
142
158
  throw new KeeperMethodError(AcpErrorCode.notFound, 'ACP session not found');
@@ -17,6 +17,10 @@ const DRAFT_NAMES = new Set(DRAFT_SESSION_UPDATE_NAMES);
17
17
  const LEVEL_UPDATES = new Set([
18
18
  'available_commands_update', 'current_mode_update', 'config_option_update', 'usage_update', 'session_info_update',
19
19
  ]);
20
+ /** Only configuration snapshots: terminal usage, titles and actual output remain observable. */
21
+ const CLOSED_REPLAY_LEVELS = new Set([
22
+ 'available_commands_update', 'current_mode_update', 'config_option_update',
23
+ ]);
20
24
  const SESSION_NOTIFICATION = INBOUND_NOTIFICATION_SCHEMAS['session/update'];
21
25
  function stringField(payload, key) {
22
26
  const value = payload[key];
@@ -49,6 +53,15 @@ export function onSessionUpdate(state, params, out, t) {
49
53
  state.diagnostics.droppedUpdates += 1;
50
54
  return;
51
55
  }
56
+ // A load may replay and end a child that the host did not request as backfill.
57
+ // Its later configuration snapshot is still historical, not a live child event.
58
+ // Keep this narrow: unknown scopes and actual content/tool/lifecycle events retain
59
+ // their normal routing and ownership checks. A live spawn removes the tombstone.
60
+ if (sessionId !== root && getOwn(state.closedReplayScopes, sessionId) && CLOSED_REPLAY_LEVELS.has(String(update.sessionUpdate))) {
61
+ state.diagnostics.droppedUpdates += 1;
62
+ out.unjournaled({ t: 'processor.closed_replay_level', sessionId, sessionUpdate: update.sessionUpdate });
63
+ return;
64
+ }
52
65
  const variant = update.sessionUpdate;
53
66
  const draft = typeof variant === 'string' && DRAFT_NAMES.has(variant);
54
67
  if (!draft && sessionId === root) {
@@ -188,6 +201,11 @@ export function emitSessionEvent(state, out, event) {
188
201
  function observe(state, event) {
189
202
  if (event.eventName === 'subagent_spawned') {
190
203
  observeSubagentSpawned(state, event.acpSessionId ?? state.rootSessionId, event.payload.subagentSessionId);
204
+ const child = event.payload.subagentSessionId;
205
+ if (typeof child === 'string' && getOwn(state.permissionOrigins, child) &&
206
+ getOwn(state.permissionParents, child) === (event.acpSessionId ?? state.rootSessionId)) {
207
+ delete state.closedReplayScopes[child];
208
+ }
191
209
  }
192
210
  trackAsyncTask(state, event);
193
211
  trackDraftSubagent(state, event);
@@ -361,6 +379,13 @@ function emitHistory(state, history, out, event, sessionId) {
361
379
  observeSubagentSpawned(state, scoped.acpSessionId ?? history.rootSessionId, scoped.payload.subagentSessionId, history.rootSessionId);
362
380
  }
363
381
  trackDraftSubagent(state, scoped);
382
+ if (scoped.eventName === 'subagent_state_update' && DRAFT_TERMINALS.has(String(scoped.payload.state))) {
383
+ const child = scoped.payload.subagentSessionId;
384
+ // Share the lifecycle tracker's bounded, validated identity set.
385
+ if (typeof child === 'string' && getOwn(state.draftSubagents.children, child) === false) {
386
+ putOwn(state.closedReplayScopes, child, true);
387
+ }
388
+ }
364
389
  observeBackgroundWriter(state, scoped);
365
390
  out.unjournaled({
366
391
  t: 'processor.history_event',
@@ -13,6 +13,7 @@ import { pendingRequestIds } from './processor/session.js';
13
13
  import { steeredIds } from './processor/control.js';
14
14
  import { RawTruncatedError } from './processor/transport/types.js';
15
15
  import { AcpKeeperUnavailableError } from '../keepers/acp/client.js';
16
+ import { RegistrationOwnershipError } from '../runtime/ownership.js';
16
17
  import { AcpErrorCode } from '../keepers/acp/protocol.js';
17
18
  import { KeeperClosedError, KeeperConnectionLostError, KeeperNotConnectedError, KeeperRequestError, KeeperRequestTimeoutError, } from '../keepers/kit/client.js';
18
19
  /** What the executor reports when a prompt's blocks are no longer in this daemon's memory (interface-contracts). */
@@ -157,7 +158,7 @@ export class ProcessorSession {
157
158
  }
158
159
  catch (error) {
159
160
  // keeper-acp unreachable is not a launch failure: the caller falls back to the local transport.
160
- if (error instanceof AcpKeeperUnavailableError)
161
+ if (error instanceof AcpKeeperUnavailableError || error instanceof RegistrationOwnershipError)
161
162
  throw error;
162
163
  throw spawnFailure(options.spawn.command, error);
163
164
  }
@@ -575,6 +576,9 @@ export class ProcessorSession {
575
576
  this.history.push(event);
576
577
  }
577
578
  continue;
579
+ case 'processor.closed_replay_level':
580
+ this.log(`[acp] session ${this.key}: ignored ${String(frame.sessionUpdate)} for ended replay scope ${JSON.stringify(frame.sessionId)}`);
581
+ continue;
578
582
  case 'processor.history_end': {
579
583
  const droppedCount = typeof frame.droppedCount === 'number' ? frame.droppedCount : 0;
580
584
  this.historyEnd = { ok: frame.ok === true, droppedCount };
@@ -1015,6 +1019,13 @@ export class ProcessorSession {
1015
1019
  async steer(blocks, priority, id) {
1016
1020
  if (this.pendingReplies.has(`agent_steered:${id}`))
1017
1021
  throw new Error(`A steer with id '${id}' is already in flight`);
1022
+ // A full producing window stops the raw loop, including control notes. Appending here
1023
+ // would leave a steer that can execute long after the server's RPC timed out. Refuse
1024
+ // before recording anything: promptRequired proves this attempt cannot inject later.
1025
+ if (!this.outbox.hasRoom()) {
1026
+ this.log(`[acp] session ${this.key}: steer ${id} not admitted while awaiting event acknowledgements`);
1027
+ return 'promptRequired';
1028
+ }
1018
1029
  const answered = this.reply('agent_steered', id);
1019
1030
  await this.send({ type: 'agent_steer', id, blocksRef: this.holdBlocks(blocks), ...(priority !== undefined ? { priority } : {}) })
1020
1031
  .catch((error) => { this.pendingReplies.delete(`agent_steered:${id}`); throw error; });
@@ -86,7 +86,7 @@ export const AXOM_AGENT_CATALOG = [
86
86
  defaultName: 'claude-agent',
87
87
  defaultLabel: 'Claude Agent',
88
88
  detect: { commands: ['claude'] },
89
- // 钉版本的唯一理由是补丁:claude-account-limits 的配方只核验过 0.97.0(runtime.json 的
89
+ // 钉版本的唯一理由是补丁:账号配额与 replay lifecycle 配方只核验过 0.97.0(runtime.json 的
90
90
  // package_patches)。不钉的话 registry 一发新版,Install/Update 就装上一个配方不覆盖的包,
91
91
  // 账号配额扩展静默消失——2026-10-14 的 antigravity(registry 已到 1.4.0、native extension
92
92
  // 只认 1.3.0)就是这个缺口的实例。钉住后 scan 的 Update 比对(installedVersion vs 本清单的
@@ -204,19 +204,19 @@ export const AXOM_AGENT_CATALOG = [
204
204
  // 2026-10-13 真机验证(ZCode 桌面 3.14.5 + 0.69.3):initialize → session/new → 首轮 prompt 通过,
205
205
  // 日志 "serving the coding-plan API key (config.json or store)"。0.63/0.64 那轮不兼容(模型只剩
206
206
  // 无授权的 builtin:bigmodel-coding-plan\GLM-5.3,首轮 CONFIGURATION_ERROR "Model creation failed",
207
- // 2026-10-06 两台机器复现)自 0.68.1 起修复。2026-10-27 nightly 升到 0.76.3:subagent 探针在
208
- // 0.76.3 补丁副本上通过、pristine 复现缺失(上游仍无 subagent/async 变体、无配额能力、无模型
209
- // 兜底);全部锚点存活,配方纯重哈希。
207
+ // 2026-10-06 两台机器复现)自 0.68.1 起修复。2026-10-29 升到 0.80.0:迁移配额、模型兜底、
208
+ // subagent/async 补丁;env 账号包装复用上游 bridge-accounts(不写桌面配置)。
209
+ // ZCODE_PROVIDER + ZCODE_API_KEY 支持独立个人 Coding Plan,无需复用桌面 OAuth。
210
210
  supplement: {
211
211
  name: 'ZCode',
212
- version: '0.76.3',
212
+ version: '0.80.0',
213
213
  description: 'Community bridge that drives the real ZCode app-server (GLM) over ACP.',
214
214
  repository: 'https://github.com/william0wang/zcode-acp',
215
215
  website: 'https://github.com/william0wang/zcode-acp',
216
216
  authors: ['William Wang'],
217
217
  license: 'Apache-2.0',
218
218
  icon: 'https://raw.githubusercontent.com/william0wang/zcode-acp/main/registry/zcode-acp/icon.svg',
219
- distribution: { npx: { package: 'zcode-acp-server@0.76.3' } },
219
+ distribution: { npx: { package: 'zcode-acp-server@0.80.0' } },
220
220
  },
221
221
  prerequisite: {
222
222
  label: 'ZCode',
@@ -144,12 +144,13 @@ export class KeeperWorld {
144
144
  const real = new KeeperTransport(new AcpKeeperClient({ launcher: use }));
145
145
  const keeper = (over.wrapKeeper ? over.wrapKeeper(real) : real);
146
146
  const generations = keeperGenerations({ launcher: use, current: keeper, log: () => { } });
147
- const local = new LocalTransport({ configDir: this.configDir });
147
+ const journalRoot = use.ownership ? join(this.configDir, 'registrations', use.ownership.registrationId) : this.configDir;
148
+ const local = new LocalTransport({ configDir: journalRoot, ownership: use.ownership });
148
149
  const acp = new AcpAgents({ cacheDir: this.configDir, registry: { load: async () => [] }, transports: { keeper, local }, keeperGenerations: generations,
149
150
  sessionStartTimeoutMs: 15_000, ...over.acp });
150
151
  const tasks = new TaskRegistry();
151
152
  const sent = [];
152
- const manager = new AgentSessionManager(acp, null, join(this.configDir, 'agents'), tasks);
153
+ const manager = new AgentSessionManager(acp, null, join(journalRoot, 'agents'), tasks, () => true, use.ownership);
153
154
  manager.bindSend((frame) => { sent.push(frame); server.apply(frame); }, AGENT_WIRE_VERSION);
154
155
  let killed = false;
155
156
  const result = {
@@ -223,7 +224,7 @@ export class KeeperWorld {
223
224
  const n = this.processes.length + 1;
224
225
  const framesOut = join(this.configDir, `frames-daemon-${n}.jsonl`);
225
226
  const stderrFile = join(this.configDir, `daemon-${n}.stderr`);
226
- const child = spawn(join(packageRoot, 'node_modules', '.bin', 'vite-node'), [DAEMON_LIFE,
227
+ const child = spawn(join(packageRoot, 'node_modules', '.bin', 'vite-node'), ['--config', join(packageRoot, 'src/test/fixtures/nodeRuntime.vite.config.ts'), DAEMON_LIFE,
227
228
  JSON.stringify({ configDir: this.configDir, distDir: realDist, framesOut, faults })], { cwd: packageRoot, stdio: ['pipe', 'ignore', 'pipe'] });
228
229
  let stderr = '';
229
230
  child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); appendFileSync(stderrFile, chunk); });