@gajae-code/coding-agent 0.16.4 → 0.16.6

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 (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -382,6 +382,11 @@ export class MCPManager {
382
382
  pending?.delete(error);
383
383
  if (pending?.size === 0) this.#pendingAcquireCleanups.delete(name);
384
384
  }
385
+ #pruneSettledPendingAcquireCleanups(name: string): void {
386
+ for (const error of this.#pendingAcquireCleanups.get(name) ?? []) {
387
+ if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
388
+ }
389
+ }
385
390
 
386
391
  #retainPendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
387
392
  if (error.cleanupGeneration === undefined || !error.cleanup || !this.#pool.hasPendingAcquireCleanup(error))
@@ -393,7 +398,11 @@ export class MCPManager {
393
398
  }
394
399
  if (pending.has(error)) return;
395
400
  pending.add(error);
396
- void error.cleanup.catch(() => {});
401
+ void error.cleanup
402
+ .finally(() => {
403
+ if (!this.#pool.hasPendingAcquireCleanup(error)) this.#removePendingAcquireCleanup(name, error);
404
+ })
405
+ .catch(() => {});
397
406
  }
398
407
 
399
408
  #retainConnectionCleanupFailure(name: string, error: unknown): MCPConnectionCleanupFailure | undefined {
@@ -518,6 +527,7 @@ export class MCPManager {
518
527
  onRequest: (method, params) => this.#handleServerRequest(method, params),
519
528
  onCleanupPending: error => this.#retainPendingAcquireCleanup(name, error),
520
529
  });
530
+ this.#pruneSettledPendingAcquireCleanups(name);
521
531
  if (trackManagerLease) this.#leaseByConnection.set(lease.connection, lease);
522
532
  if (!this.#toolsOnly) lease.updateRoots(this.#getRoots().roots);
523
533
  return lease;
@@ -1040,6 +1050,10 @@ export class MCPManager {
1040
1050
  const connectionTasks: ConnectionTask[] = [];
1041
1051
 
1042
1052
  for (const [name, config] of Object.entries(configs)) {
1053
+ // Another manager sharing the pool may have retried and completed an
1054
+ // abandoned-acquire cleanup. Retire this manager's observation before
1055
+ // applying its reconnect fence; pool ownership remains authoritative.
1056
+ this.#pruneSettledPendingAcquireCleanups(name);
1043
1057
  const pendingAcquire = this.#pendingAcquireCleanups.get(name);
1044
1058
  const pendingCleanup = this.#pendingConnectionCleanups.get(name);
1045
1059
  if (pendingCleanup || pendingAcquire) {
@@ -1781,7 +1795,11 @@ export class MCPManager {
1781
1795
 
1782
1796
  let closeError: unknown;
1783
1797
  try {
1784
- await this.#closePendingConnectionCleanup(name);
1798
+ // A transport may ignore its abort signal while the initial connection
1799
+ // is still opening. The pool owns that abandoned flight and closes any
1800
+ // late connection, so disconnect must not wait for the uncooperative
1801
+ // opener. Already-materialized cleanup failures are still retried here.
1802
+ await this.#closePendingConnectionCleanup(name, false);
1785
1803
  } catch (error) {
1786
1804
  closeError = error;
1787
1805
  }
@@ -462,7 +462,7 @@ describe("MCPConnectionPool", () => {
462
462
  expect(recoveryTransports).toHaveLength(2);
463
463
  });
464
464
 
465
- test("manager disconnect joins and retries pool-owned late cleanup without spawning", async () => {
465
+ test("manager disconnect preserves late cleanup failure until pool retry clears every manager fence", async () => {
466
466
  const firstFlight = Promise.withResolvers<MCPServerConnection>();
467
467
  const firstStarted = Promise.withResolvers<void>();
468
468
  const cleanupTransport = new FakeTransport();
@@ -481,7 +481,7 @@ describe("MCPConnectionPool", () => {
481
481
  firstStarted.resolve();
482
482
  return firstFlight.promise;
483
483
  }
484
- return connection(name, cfg, new FakeTransport());
484
+ return connection(name, cfg, new ToolListTransport());
485
485
  },
486
486
  });
487
487
  const manager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
@@ -492,16 +492,19 @@ describe("MCPConnectionPool", () => {
492
492
  });
493
493
  await firstStarted.promise;
494
494
  controller.abort(new Error("abort before deferred cleanup failure"));
495
- await expect(operation).rejects.toBeInstanceOf(MCPPoolAcquireAbortError);
495
+ const abortedAcquire = await operation.catch(error => error);
496
+ expect(abortedAcquire).toBeInstanceOf(MCPPoolAcquireAbortError);
497
+ if (!(abortedAcquire instanceof MCPPoolAcquireAbortError)) throw new Error("Expected aborted acquisition");
496
498
  expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
497
499
 
498
- const disconnect = manager.disconnectServer("server");
500
+ await manager.disconnectServer("server");
499
501
  const fenced = await manager.connectServers({ server: serverConfig }, {});
500
502
  expect(fenced.errors.has("server")).toBe(true);
501
503
  expect(opens).toBe(1);
502
504
  const primaryFailure = new Error("late connection cleanup failed");
503
505
  firstFlight.reject(new MCPConnectionCleanupFailure(primaryFailure, cleanupTransport));
504
- const disconnectError = await disconnect.catch(error => error);
506
+ await expect(abortedAcquire.cleanup).rejects.toBeInstanceOf(MCPConnectionCleanupFailure);
507
+ const disconnectError = await manager.disconnectServer("server").catch(error => error);
505
508
  expect(disconnectError).toBeInstanceOf(MCPConnectionCleanupFailure);
506
509
  if (!(disconnectError instanceof MCPConnectionCleanupFailure)) {
507
510
  throw new Error("Expected retained disconnect cleanup failure");
@@ -513,14 +516,20 @@ describe("MCPConnectionPool", () => {
513
516
  expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
514
517
  expect(pool.pendingAcquireCleanupCountForTests).toBe(1);
515
518
 
516
- await manager.disconnectServer("server");
517
- expect(cleanupAttempts).toBe(2);
518
- expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
519
- expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
520
- await manager.withPreparedLease("server", serverConfig, async lease => {
519
+ const recoveryManager = new MCPManager(".", null, { pool, sessionId: "disconnect-cleanup" });
520
+ await recoveryManager.withPreparedLease("server", serverConfig, async lease => {
521
521
  expect(lease.serverName).toBe("server");
522
522
  });
523
- expect(opens).toBe(2);
523
+ expect(cleanupAttempts).toBe(2);
524
+ expect(pool.pendingAcquireCleanupCountForTests).toBe(0);
525
+ expect(manager.pendingConnectionCleanupCountForTests).toBe(1);
526
+
527
+ const recovered = await manager.connectServers({ server: serverConfig }, {});
528
+ expect(recovered.errors.size).toBe(0);
529
+ expect(recovered.connectedServers).toEqual(["server"]);
530
+ expect(manager.pendingConnectionCleanupCountForTests).toBe(0);
531
+ expect(opens).toBe(3);
532
+ await recoveryManager.disconnectAll();
524
533
  await manager.disconnectAll();
525
534
  });
526
535
 
@@ -41,7 +41,9 @@ import {
41
41
  executeLifecycle,
42
42
  isCanonicalSessionId,
43
43
  prepareSpawnChildHostLaunch,
44
+ readSessionLifecycleFailure,
44
45
  validateBrokerModelPresetSync,
46
+ writeEffectMarker,
45
47
  } from "./lifecycle";
46
48
  import {
47
49
  type LifecycleDurableEffectsReceipt,
@@ -724,6 +726,7 @@ function credentialFreeLifecycleResponse(value: unknown): unknown {
724
726
  type LifecycleReplayEndpoint = {
725
727
  endpoint: Record<string, unknown>;
726
728
  endpointGeneration: number;
729
+ endpointIncarnation: string;
727
730
  pid: number;
728
731
  endpointMtimeMs: number;
729
732
  };
@@ -1588,12 +1591,34 @@ export class Broker {
1588
1591
  ).claim;
1589
1592
  return spawnFailureError(failure);
1590
1593
  }
1594
+ const { pid, processIncarnation: incarnation } = launched.proof;
1595
+ if (pid === undefined || incarnation === undefined) {
1596
+ const failure: SpawnSubstrateFailure = {
1597
+ substrateKind: launched.proof.substrateKind,
1598
+ code: "substrate_proof_failed",
1599
+ message: "session.spawn substrate lacks lifecycle process authority",
1600
+ };
1601
+ await this.#releaseUnownedSubstrate(provider, launchedProof);
1602
+ launchedProof = undefined;
1603
+ current = (
1604
+ await store.persistTransition(lifecycleIdentity, {
1605
+ claimId: current.claimId,
1606
+ from: "substrate_starting",
1607
+ to: "pre_send_rejected",
1608
+ failure,
1609
+ })
1610
+ ).claim;
1611
+ return spawnFailureError(failure);
1612
+ }
1613
+ const marker = { pid, incarnation, effectMarker: prep.effectMarker };
1614
+ await writeEffectMarker(prep.stateRoot, prep.childId, marker);
1591
1615
  const registration = await this.#spawnPromptLayer.awaitRegistration({
1592
1616
  childId: prep.childId,
1593
1617
  cwd: prep.cwd,
1594
1618
  stateRoot: prep.stateRoot,
1595
1619
  });
1596
1620
  if (!registration.ok) {
1621
+ const startupFailure = await readSessionLifecycleFailure(prep.stateRoot, prep.childId, marker);
1597
1622
  await this.#releaseUnownedSubstrate(provider, launchedProof);
1598
1623
  launchedProof = undefined;
1599
1624
  current = (
@@ -1603,7 +1628,11 @@ export class Broker {
1603
1628
  to: "uncertain",
1604
1629
  })
1605
1630
  ).claim;
1606
- return error("terminal_uncertain", "session.spawn child registration is uncertain");
1631
+ return error(
1632
+ "terminal_uncertain",
1633
+ "session.spawn child registration is uncertain" +
1634
+ (startupFailure ? ` (${startupFailure.phase}/${startupFailure.reason})` : ""),
1635
+ );
1607
1636
  }
1608
1637
  pinnedRegistration = registration.registration;
1609
1638
  const now = Date.now();
@@ -2404,26 +2433,35 @@ export class Broker {
2404
2433
  )
2405
2434
  return;
2406
2435
 
2407
- // Snapshot validation and the deterministic instance suffix protect successor locks.
2408
- const tombstone = path.join(
2409
- path.dirname(this.#lock),
2410
- `.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}`,
2411
- );
2412
- try {
2413
- await fs.rename(this.#lock, tombstone);
2414
- } catch (e) {
2415
- const code = (e as NodeJS.ErrnoException).code;
2416
- if (["ENOENT", "EEXIST", "ENOTEMPTY", "EISDIR", "ENOTDIR"].includes(code ?? "")) return;
2417
- if (code === "EPERM") {
2418
- try {
2419
- await fs.lstat(tombstone);
2420
- return;
2421
- } catch (statError) {
2422
- if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
2436
+ // Give every reclaimed generation a fresh quarantine name. Older brokers
2437
+ // used only the lock inode hash; if that tombstone survived a crash, the
2438
+ // deterministic name collided forever and left the dead canonical lock in
2439
+ // place. The random suffix preserves no-replace rename semantics without
2440
+ // letting retained cleanup debris block takeover.
2441
+ for (let attempt = 0; attempt < 8; attempt++) {
2442
+ const tombstone = path.join(
2443
+ path.dirname(this.#lock),
2444
+ `.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}-${randomBytes(8).toString("hex")}`,
2445
+ );
2446
+ try {
2447
+ await fs.rename(this.#lock, tombstone);
2448
+ return;
2449
+ } catch (e) {
2450
+ const code = (e as NodeJS.ErrnoException).code;
2451
+ if (code === "EEXIST" || code === "ENOTEMPTY") continue;
2452
+ if (["ENOENT", "EISDIR", "ENOTDIR"].includes(code ?? "")) return;
2453
+ if (code === "EPERM") {
2454
+ try {
2455
+ await fs.lstat(tombstone);
2456
+ continue;
2457
+ } catch (statError) {
2458
+ if ((statError as NodeJS.ErrnoException).code !== "ENOENT") throw statError;
2459
+ }
2423
2460
  }
2461
+ throw e;
2424
2462
  }
2425
- throw e;
2426
2463
  }
2464
+ throw new Error(`Broker lock quarantine namespace is saturated for ${this.#lock}`);
2427
2465
  }
2428
2466
  async #releaseOwnedLock(): Promise<void> {
2429
2467
  try {
@@ -2549,6 +2587,11 @@ export class Broker {
2549
2587
  startedAt: now,
2550
2588
  heartbeatAt: now,
2551
2589
  };
2590
+ // Readiness must not be externally visible until the initial session
2591
+ // checkpoint settles. The bootstrap watchdog owns this pre-publication
2592
+ // interval; publishing first allowed it to kill an endpoint already handed
2593
+ // to callers when a legitimate index-lock wait outlived the fence.
2594
+ await this.#checkpointSessionHeartbeats();
2552
2595
  this.#publication = await publishBrokerDiscovery(this.settings.agentDir, this.discovery);
2553
2596
  this.#publicationState = "healthy-owned";
2554
2597
  this.#publishedAt = process.hrtime.bigint();
@@ -2560,7 +2603,6 @@ export class Broker {
2560
2603
  void this.#watchPublication();
2561
2604
  void this.#reapSpawnOrphans();
2562
2605
  }, cadenceMs);
2563
- await this.#checkpointSessionHeartbeats();
2564
2606
  return this.discovery;
2565
2607
  } catch (error) {
2566
2608
  await this.#transport?.stop();
@@ -2862,6 +2904,8 @@ export class Broker {
2862
2904
  endpointMtimeMs <= 0
2863
2905
  )
2864
2906
  return error("endpoint_stale", "session endpoint authority is incomplete");
2907
+ const currentIncarnation = endpointIncarnation(record, sessionId);
2908
+ if (!currentIncarnation) return error("endpoint_stale", "session endpoint incarnation is unavailable");
2865
2909
  const endpoint = await this.#readEndpoint(record, {});
2866
2910
  if (!endpoint.ok) return endpoint;
2867
2911
  if (endpoint.result === null || typeof endpoint.result !== "object" || Array.isArray(endpoint.result))
@@ -2869,6 +2913,7 @@ export class Broker {
2869
2913
  return {
2870
2914
  endpoint: endpoint.result as Record<string, unknown>,
2871
2915
  endpointGeneration: record.endpointGeneration,
2916
+ endpointIncarnation: currentIncarnation,
2872
2917
  pid: record.pid,
2873
2918
  endpointMtimeMs,
2874
2919
  };
@@ -3041,16 +3086,21 @@ export class Broker {
3041
3086
  if (this.#chains.get(target) === chain) this.#chains.delete(target);
3042
3087
  }
3043
3088
  }
3044
- #storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string | BrokerResponse {
3089
+ #storeSessionListCursor(cursor: SessionListCursor, replacingToken?: string): string {
3045
3090
  const now = Date.now();
3046
3091
  for (const [token, stored] of this.#sessionListCursors) {
3047
3092
  if (stored.expiresAt <= now) this.#sessionListCursors.delete(token);
3048
3093
  }
3049
- const replacing = replacingToken !== undefined && this.#sessionListCursors.has(replacingToken);
3050
- if (!replacing && this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS)
3051
- return error("invalid_input", "session.list cursor capacity is exhausted");
3052
- const token = randomBytes(24).toString("base64url");
3053
3094
  if (replacingToken !== undefined) this.#sessionListCursors.delete(replacingToken);
3095
+ // Pagination cursors are a paging convenience, not durable state (#5370).
3096
+ // Evict the oldest cursor when the budget is full so abandoned or partial
3097
+ // paginations degrade gracefully instead of failing unrelated session ops.
3098
+ while (this.#sessionListCursors.size >= SESSION_LIST_MAX_CURSORS) {
3099
+ const oldest = this.#sessionListCursors.keys().next();
3100
+ if (oldest.done) break;
3101
+ this.#sessionListCursors.delete(oldest.value);
3102
+ }
3103
+ const token = randomBytes(24).toString("base64url");
3054
3104
  this.#sessionListCursors.set(token, cursor);
3055
3105
  return token;
3056
3106
  }
@@ -3121,7 +3171,6 @@ export class Broker {
3121
3171
  { ...snapshot, offset, expiresAt: Date.now() + SESSION_LIST_CURSOR_TTL_MS },
3122
3172
  typeof cursor === "string" ? cursor : undefined,
3123
3173
  );
3124
- if (isBrokerResponse(continuationCursor)) return continuationCursor;
3125
3174
  return {
3126
3175
  ok: true,
3127
3176
  result: {
@@ -3326,15 +3375,32 @@ export class Broker {
3326
3375
  (operation === "session.create" || operation === "session.fork" || operation === "session.resume") &&
3327
3376
  typeof (replay.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
3328
3377
  ) {
3329
- const refreshed = await this.#readLifecycleReplayEndpoint(
3330
- (replay.result as { sessionId: string }).sessionId,
3331
- );
3378
+ const replayResult = objectRecord(replay.result);
3379
+ const replaySessionId = (replay.result as { sessionId: string }).sessionId;
3380
+ const replayIncarnation =
3381
+ typeof replayResult?.endpointIncarnation === "string" &&
3382
+ /^[a-f0-9]{64}$/.test(replayResult.endpointIncarnation)
3383
+ ? replayResult.endpointIncarnation
3384
+ : endpointIncarnation(
3385
+ {
3386
+ endpointGeneration: replayResult?.endpointGeneration as number,
3387
+ endpointMtimeMs: replayResult?.endpointMtimeMs as number,
3388
+ pid: replayResult?.pid as number,
3389
+ },
3390
+ replaySessionId,
3391
+ );
3392
+ if (!replayIncarnation)
3393
+ return error("endpoint_stale", "lifecycle replay lacks original endpoint authority");
3394
+ const refreshed = await this.#readLifecycleReplayEndpoint(replaySessionId);
3332
3395
  if (isBrokerResponse(refreshed)) return refreshed;
3396
+ if (refreshed.endpointIncarnation !== replayIncarnation)
3397
+ return error("endpoint_stale", "lifecycle replay target was replaced");
3333
3398
  return {
3334
3399
  ok: true,
3335
3400
  result: {
3336
3401
  ...(replay.result as Record<string, unknown>),
3337
3402
  endpointGeneration: refreshed.endpointGeneration,
3403
+ endpointIncarnation: refreshed.endpointIncarnation,
3338
3404
  pid: refreshed.pid,
3339
3405
  endpointMtimeMs: refreshed.endpointMtimeMs,
3340
3406
  endpoint: refreshed.endpoint,
@@ -3347,6 +3413,12 @@ export class Broker {
3347
3413
  const response = outcome.response;
3348
3414
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3349
3415
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3416
+ ...(operation === "session.delete" &&
3417
+ typeof input.sessionId === "string" &&
3418
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3419
+ !pendingCleanupSessionId(response)
3420
+ ? { intendedSessionId: input.sessionId }
3421
+ : {}),
3350
3422
  response: storedResponse,
3351
3423
  responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
3352
3424
  ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
@@ -3366,6 +3438,12 @@ export class Broker {
3366
3438
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3367
3439
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3368
3440
  ...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
3441
+ ...(operation === "session.delete" &&
3442
+ typeof input.sessionId === "string" &&
3443
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3444
+ !pendingCleanupSessionId(response)
3445
+ ? { intendedSessionId: input.sessionId }
3446
+ : {}),
3369
3447
  response: storedResponse,
3370
3448
  responseDigest: createHash("sha256").update(canonicalJson(storedResponse)).digest("hex"),
3371
3449
  ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
@@ -3377,8 +3455,21 @@ export class Broker {
3377
3455
  const outcome = await executeLifecycle(this, operation, input, identity);
3378
3456
  const response = outcome.response;
3379
3457
  const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
3458
+ // Record the refusal's own target session so the fence it may leave
3459
+ // is scoped to that session. Without this, a session.delete refusal
3460
+ // for X persists as an unbound terminal_uncertain row and fences
3461
+ // every later delete for unrelated sessions (#5364). A refusal that
3462
+ // truly cannot name its target fences nothing (see
3463
+ // hasUncertainCleanupForSession).
3464
+ const refusalSessionId =
3465
+ operation === "session.delete" && typeof input.sessionId === "string" ? input.sessionId : undefined;
3380
3466
  await this.ledger.transition(identity, lifecycleResponseState(response), {
3381
3467
  ...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
3468
+ ...(refusalSessionId !== undefined &&
3469
+ lifecycleResponseState(response) === "terminal_uncertain" &&
3470
+ !pendingCleanupSessionId(response)
3471
+ ? { intendedSessionId: refusalSessionId }
3472
+ : {}),
3382
3473
  resultSessionId:
3383
3474
  response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
3384
3475
  ? (response.result as { sessionId: string }).sessionId