@gajae-code/coding-agent 0.18.5 → 0.18.7

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 (133) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli-commands.d.ts +10 -0
  4. package/dist/types/cli-ordinary.d.ts +13 -0
  5. package/dist/types/cli.d.ts +12 -3
  6. package/dist/types/commands/contribution-prep.d.ts +0 -3
  7. package/dist/types/commands/sdk.d.ts +3 -0
  8. package/dist/types/commands/update.d.ts +1 -0
  9. package/dist/types/config/model-profile-activation.d.ts +19 -0
  10. package/dist/types/config/settings.d.ts +4 -0
  11. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  12. package/dist/types/discovery/helpers.d.ts +2 -2
  13. package/dist/types/edit/modes/replace.d.ts +3 -3
  14. package/dist/types/edit/streaming.d.ts +2 -2
  15. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  16. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  17. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  18. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  20. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  21. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  22. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  23. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  24. package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
  25. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  26. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  27. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  28. package/dist/types/sdk/host/host.d.ts +6 -0
  29. package/dist/types/sdk/index.d.ts +1 -0
  30. package/dist/types/sdk/router/session-router.d.ts +9 -0
  31. package/dist/types/sdk/session.d.ts +13 -0
  32. package/dist/types/session/agent-session.d.ts +5 -1
  33. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  34. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  35. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  36. package/dist/types/session/session-retirement.d.ts +47 -0
  37. package/dist/types/session/session-storage.d.ts +0 -46
  38. package/dist/types/session/session-transcript-header.d.ts +2 -0
  39. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  40. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  41. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  42. package/dist/types/tools/ask-contract.d.ts +75 -254
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/package.json +7 -7
  45. package/scripts/build-sdk-package-smoke.ts +5 -0
  46. package/scripts/resident-memory-bench.ts +174 -20
  47. package/src/capability/index.ts +1 -0
  48. package/src/capability/types.ts +4 -0
  49. package/src/cli/config-cli.ts +8 -18
  50. package/src/cli/public-command-entry.ts +5 -0
  51. package/src/cli/ssh-cli.ts +6 -3
  52. package/src/cli/web-search-cli.ts +9 -3
  53. package/src/cli-commands.ts +14 -0
  54. package/src/cli-main.ts +7 -8
  55. package/src/cli-ordinary.ts +197 -0
  56. package/src/cli.ts +25 -182
  57. package/src/commands/contribution-prep.ts +1 -3
  58. package/src/commands/sdk.ts +171 -21
  59. package/src/commands/update.ts +15 -1
  60. package/src/config/file-lock.ts +91 -2
  61. package/src/config/model-profile-activation.ts +70 -4
  62. package/src/config/model-profile-contract.ts +4 -1
  63. package/src/config/model-profiles.ts +23 -31
  64. package/src/config/model-resolver.ts +9 -1
  65. package/src/config/settings.ts +44 -4
  66. package/src/coordinator-mcp/event-webhook.ts +19 -14
  67. package/src/coordinator-mcp/policy.ts +14 -4
  68. package/src/coordinator-mcp/server.ts +142 -7
  69. package/src/coordinator-mcp/session-reaper.ts +9 -1
  70. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  71. package/src/discovery/helpers.ts +9 -3
  72. package/src/discovery/ssh.ts +8 -3
  73. package/src/edit/modes/replace.ts +6 -6
  74. package/src/edit/streaming.ts +15 -15
  75. package/src/exec/bash-shell-supervisor.ts +60 -3
  76. package/src/gjc-runtime/gc-runtime.ts +2 -1
  77. package/src/hindsight/config.ts +9 -3
  78. package/src/internal-urls/docs-index.generated.ts +11 -10
  79. package/src/memories/index.ts +8 -2
  80. package/src/modes/acp/acp-agent.ts +724 -370
  81. package/src/modes/components/tool-execution.ts +1 -1
  82. package/src/prompts/tools/ask.md +2 -0
  83. package/src/runtime-mcp/client.ts +13 -4
  84. package/src/runtime-mcp/elicitation.ts +142 -0
  85. package/src/sdk/broker/broker.ts +54 -1
  86. package/src/sdk/broker/daemon-entry.ts +67 -6
  87. package/src/sdk/broker/discovery.ts +8 -0
  88. package/src/sdk/broker/ensure.ts +156 -20
  89. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  90. package/src/sdk/broker/lifecycle.ts +334 -348
  91. package/src/sdk/broker/runtime.ts +5 -1
  92. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  93. package/src/sdk/broker/transport.ts +5 -1
  94. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  95. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  96. package/src/sdk/bus/index.ts +3 -3
  97. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  98. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  99. package/src/sdk/bus/telegram-daemon.ts +38 -3
  100. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  101. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  102. package/src/sdk/host/control/dispatch.ts +52 -4
  103. package/src/sdk/host/control/operations.ts +1 -1
  104. package/src/sdk/host/host.ts +6 -0
  105. package/src/sdk/host/session-runtime.test.ts +83 -4
  106. package/src/sdk/host/session-runtime.ts +15 -7
  107. package/src/sdk/index.ts +1 -0
  108. package/src/sdk/lifecycle-session.ts +1 -0
  109. package/src/sdk/mcp/server.ts +15 -3
  110. package/src/sdk/protocol/operation-registry.ts +1 -1
  111. package/src/sdk/router/session-router.ts +26 -3
  112. package/src/sdk/session.ts +51 -8
  113. package/src/session/agent-session.ts +169 -43
  114. package/src/session/blob-store.ts +78 -33
  115. package/src/session/internal/managed-session-storage.ts +203 -4
  116. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  117. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  118. package/src/session/session-retirement.ts +155 -0
  119. package/src/session/session-storage.ts +1 -160
  120. package/src/session/session-transcript-header.ts +17 -0
  121. package/src/session/task-artifact-owner-codec.ts +578 -0
  122. package/src/session/task-artifact-owner-retirement.ts +475 -0
  123. package/src/session/task-artifact-owner.ts +289 -0
  124. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  125. package/src/setup/hermes-setup.ts +4 -1
  126. package/src/slash-commands/builtin-registry.ts +8 -5
  127. package/src/tools/ask-contract.test.ts +76 -1
  128. package/src/tools/ask-contract.ts +57 -6
  129. package/src/tools/ask.ts +16 -7
  130. package/src/tools/bash.ts +38 -29
  131. package/src/tools/descriptors.test.ts +29 -0
  132. package/src/tools/tool-catalog.generated.ts +7 -4
  133. package/src/web/search/index.ts +2 -2
@@ -14,6 +14,7 @@ import {
14
14
  readBrokerDiscovery,
15
15
  readBrokerRestartIntent,
16
16
  } from "./discovery";
17
+ import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
17
18
  import {
18
19
  isSdkInternalRuntimeImagePresent,
19
20
  resolveSdkInternalSpawnCommand,
@@ -369,6 +370,7 @@ interface BrokerOwner {
369
370
  stop(): Promise<void>;
370
371
  canReuse(discovery: BrokerDiscovery | null): boolean;
371
372
  markReady(discovery: BrokerDiscovery): boolean;
373
+ setTarget(pid: number, incarnation: string): void;
372
374
  }
373
375
  type EnsureInitiator = "discovery" | "fixture-lease";
374
376
  type EnsureOutcome =
@@ -453,25 +455,60 @@ async function reapSpawnedBroker(child: ChildProcess, timing: ReapTiming = DEFAU
453
455
  throw new Error(`Detached SDK broker (pid ${child.pid}) did not exit after SIGKILL during reap.`);
454
456
  }
455
457
 
458
+ async function reapSpawnedBrokerIdentity(
459
+ pid: number,
460
+ incarnation: string,
461
+ timing: ReapTiming = DEFAULT_REAP_TIMING,
462
+ ): Promise<void> {
463
+ const matches = (): boolean => {
464
+ const observed = observeProcessIncarnation(pid);
465
+ return observed.status === "present" && observed.incarnation === incarnation;
466
+ };
467
+ const gone = (): boolean => observeProcessIncarnation(pid).status === "absent";
468
+ const signal = (sig: NodeJS.Signals): void => {
469
+ if (!matches()) return;
470
+ try {
471
+ process.kill(pid, sig);
472
+ } catch {
473
+ // already exited between identity check and signal delivery
474
+ }
475
+ };
476
+ if (gone() || !matches()) return;
477
+ // A reported trampoline has already detached this broker from the retained
478
+ // child handle, so graceful shutdown cannot be observed or reaped reliably.
479
+ // Force termination keeps discovery-timeout cleanup within its caller's
480
+ // bounded deadline while the identity check still prevents PID reuse.
481
+ signal("SIGKILL");
482
+ await Promise.race([waitForProcessAbsence(pid), sleep(timing.killVerifyMs)]);
483
+ if (!gone()) throw new Error(`Detached SDK broker (pid ${pid}) did not exit after SIGKILL during reap.`);
484
+ }
485
+
486
+ async function waitForProcessAbsence(pid: number): Promise<void> {
487
+ while (observeProcessIncarnation(pid).status !== "absent") await Bun.sleep(10);
488
+ }
489
+
456
490
  function registerBrokerOwner(
457
491
  agentDir: string,
458
492
  child: ChildProcess,
459
493
  timing: ReapTiming = DEFAULT_REAP_TIMING,
460
494
  ): BrokerOwner {
461
- const incarnation = child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
495
+ let targetPid = child.pid;
496
+ let targetIncarnation = child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
462
497
  let state: "starting" | "ready" | "cleanup-unverified" = "starting";
463
498
  const matches = (discovery: BrokerDiscovery | null): boolean =>
464
499
  Boolean(
465
500
  discovery &&
466
- child.pid !== undefined &&
467
- incarnation &&
468
- discovery.pid === child.pid &&
469
- discovery.incarnation === incarnation,
501
+ targetPid !== undefined &&
502
+ targetIncarnation !== undefined &&
503
+ discovery.pid === targetPid &&
504
+ discovery.incarnation === targetIncarnation,
470
505
  );
471
506
  const owner: BrokerOwner = {
472
507
  async stop(): Promise<void> {
473
508
  try {
474
- await reapSpawnedBroker(child, timing);
509
+ if (targetPid !== undefined && targetIncarnation !== undefined && targetPid !== child.pid)
510
+ await reapSpawnedBrokerIdentity(targetPid, targetIncarnation, timing);
511
+ else await reapSpawnedBroker(child, timing);
475
512
  } catch (error) {
476
513
  state = "cleanup-unverified";
477
514
  throw error;
@@ -486,6 +523,10 @@ function registerBrokerOwner(
486
523
  state = "ready";
487
524
  return true;
488
525
  },
526
+ setTarget(pid, incarnation): void {
527
+ targetPid = pid;
528
+ targetIncarnation = incarnation;
529
+ },
489
530
  };
490
531
  owners.set(agentDir, owner);
491
532
  return owner;
@@ -688,7 +729,11 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
688
729
  // that finished between our first read and the lock acquisition.
689
730
  const discoveredUnderLock = await reconcileBrokerGenerationForStartup(settings, deadline);
690
731
  if (discoveredUnderLock) return { kind: "external-discovery", discovery: discoveredUnderLock };
691
- const command = resolveSdkInternalSpawnCommand("broker-internal");
732
+ const command = resolveSdkInternalSpawnCommand(
733
+ process.platform === "win32" || initiator === "fixture-lease"
734
+ ? "broker-internal"
735
+ : "broker-trampoline-internal",
736
+ );
692
737
  spawnLog = await openBrokerSpawnLog(settings.agentDir);
693
738
  // A stale marker must never be misattributed to this spawn; clear it first.
694
739
  await clearBrokerStartupExitRecord(settings.agentDir);
@@ -696,23 +741,83 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
696
741
  const childSpawnedAt = Date.now();
697
742
  const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
698
743
  detached: true,
699
- stdio: ["ignore", "ignore", spawnLog ? spawnLog.handle.fd : "ignore"],
744
+ stdio: ["ignore", process.platform === "win32" ? "ignore" : "pipe", spawnLog ? spawnLog.handle.fd : "ignore"],
700
745
  env: brokerSpawnEnvironment(command, settings.env),
701
746
  ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
702
747
  });
748
+ const isTrampoline = process.platform !== "win32" && initiator !== "fixture-lease";
703
749
  let spawnError: Error | undefined;
750
+ let childExited = false;
751
+ let childExitCode: number | null = null;
752
+ let childSignalCode: NodeJS.Signals | null = null;
753
+ let brokerDeathObserved = false;
704
754
  child.once("error", error => {
705
755
  spawnError = error;
706
756
  });
707
- const childIncarnation = child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
757
+ child.once("close", (code, signal) => {
758
+ childExited = true;
759
+ childExitCode = code;
760
+ childSignalCode = signal;
761
+ });
762
+ let trampolineReported = !isTrampoline;
763
+ let spawnedBrokerPid = isTrampoline ? undefined : child.pid;
764
+ let spawnedBrokerIncarnation =
765
+ isTrampoline || child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
708
766
  const owner = registerBrokerOwner(settings.agentDir, child);
767
+ if (isTrampoline && child.stdout) {
768
+ let output = "";
769
+ child.stdout.on("data", chunk => {
770
+ output += Buffer.from(chunk).toString("utf8");
771
+ const newline = output.indexOf("\n");
772
+ if (newline < 0) return;
773
+ const line = output.slice(0, newline).trim();
774
+ const [rawPid, rawIncarnation] = line?.split("\t") ?? [];
775
+ const pid = rawPid ? Number(rawPid) : NaN;
776
+ if (!Number.isSafeInteger(pid) || pid <= 0) return;
777
+ if (isProcessIncarnation(rawIncarnation)) {
778
+ const incarnation = rawIncarnation;
779
+ trampolineReported = true;
780
+ spawnedBrokerPid = pid;
781
+ spawnedBrokerIncarnation = incarnation;
782
+ owner.setTarget(pid, incarnation);
783
+ }
784
+ });
785
+ }
709
786
  child.unref();
710
787
  // The child holds its own duplicate of the descriptor. Failure to close the
711
788
  // parent's diagnostic handle must not discard exact ownership of a live child.
712
789
  await spawnLog?.handle.close().catch(() => undefined);
713
790
  let discoveryError: unknown;
714
791
  while (ensureBrokerTiming.now() < deadline) {
715
- if (spawnError || child.exitCode !== null || child.signalCode !== null) break;
792
+ if (
793
+ spawnError ||
794
+ (isTrampoline && childExited && (!trampolineReported || childExitCode !== 0 || childSignalCode !== null)) ||
795
+ (!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null))
796
+ )
797
+ break;
798
+ if (
799
+ isTrampoline &&
800
+ trampolineReported &&
801
+ spawnedBrokerPid !== undefined &&
802
+ spawnedBrokerIncarnation !== undefined
803
+ ) {
804
+ const observed = observeProcessIncarnation(spawnedBrokerPid);
805
+ if (observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation) {
806
+ brokerDeathObserved = true;
807
+ break;
808
+ }
809
+ if (observed.status === "absent") {
810
+ brokerDeathObserved = true;
811
+ break;
812
+ }
813
+ }
814
+ // The trampoline's stdout line identifies the real broker. Do not read
815
+ // discovery until that line has been parsed, or a marker from the
816
+ // trampoline could be trusted as the broker's own startup evidence.
817
+ if (isTrampoline && !trampolineReported) {
818
+ await ensureBrokerTiming.sleep(50);
819
+ continue;
820
+ }
716
821
  try {
717
822
  const discovered = await readBrokerDiscoveryBeforeDeadline(
718
823
  settings.agentDir,
@@ -724,6 +829,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
724
829
  await ensureBrokerTiming.sleep(50);
725
830
  continue;
726
831
  }
832
+ if (isTrampoline && !childExited) {
833
+ await ensureBrokerTiming.sleep(50);
834
+ continue;
835
+ }
836
+ if (isTrampoline && spawnedBrokerPid !== undefined && spawnedBrokerIncarnation !== undefined) {
837
+ const observed = observeProcessIncarnation(spawnedBrokerPid);
838
+ if (
839
+ observed.status === "absent" ||
840
+ (observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation)
841
+ ) {
842
+ brokerDeathObserved = true;
843
+ break;
844
+ }
845
+ }
727
846
  if (owner.markReady(discovered)) {
728
847
  return initiator === "fixture-lease"
729
848
  ? { kind: "local-started-fixture", discovery: discovered, owner, child }
@@ -737,8 +856,13 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
737
856
  }
738
857
  await ensureBrokerTiming.sleep(50);
739
858
  }
740
- const exitedBeforeDiscovery = child.exitCode !== null || child.signalCode !== null;
741
- if (exitedBeforeDiscovery && child.exitCode === 0) {
859
+ const exitedBeforeDiscovery =
860
+ (isTrampoline &&
861
+ (!trampolineReported ||
862
+ (childExited && (childExitCode !== 0 || childSignalCode !== null)) ||
863
+ brokerDeathObserved)) ||
864
+ (!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null));
865
+ if (!isTrampoline && exitedBeforeDiscovery && childExitCode === 0) {
742
866
  // A clean exit means another broker won the ownership lock (two ACP
743
867
  // processes racing a cold broker state, e.g. a provider probe and an
744
868
  // agent launch). The winner may publish its discovery right after our
@@ -765,16 +889,16 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
765
889
  const startupExitRecord = await readBrokerStartupExitRecord(settings.agentDir);
766
890
  const trustedMarker =
767
891
  marker &&
768
- child.pid !== undefined &&
769
- childIncarnation !== undefined &&
770
- marker.pid === child.pid &&
771
- marker.incarnation === childIncarnation
892
+ spawnedBrokerPid !== undefined &&
893
+ spawnedBrokerIncarnation !== undefined &&
894
+ marker.pid === spawnedBrokerPid &&
895
+ marker.incarnation === spawnedBrokerIncarnation
772
896
  ? marker
773
897
  : undefined;
774
898
  const trustedStartupExitRecord =
775
899
  startupExitRecord &&
776
- child.pid !== undefined &&
777
- startupExitRecord.pid === child.pid &&
900
+ spawnedBrokerPid !== undefined &&
901
+ startupExitRecord.pid === spawnedBrokerPid &&
778
902
  startupExitRecord.writtenAt >= childSpawnedAt
779
903
  ? startupExitRecord
780
904
  : undefined;
@@ -828,8 +952,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
828
952
  ? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
829
953
  : exitedBeforeDiscovery
830
954
  ? new BrokerStartupError({
831
- exitCode: child.exitCode ?? trustedStartupExitRecord?.exitCode ?? null,
832
- signal: child.signalCode ?? trustedStartupExitRecord?.signal ?? null,
955
+ exitCode: isTrampoline
956
+ ? (trustedStartupExitRecord?.exitCode ??
957
+ trustedMarker?.exitCode ??
958
+ childExitCode ??
959
+ child.exitCode ??
960
+ null)
961
+ : (childExitCode ?? child.exitCode ?? trustedStartupExitRecord?.exitCode ?? null),
962
+ signal: isTrampoline
963
+ ? (trustedStartupExitRecord?.signal ??
964
+ trustedMarker?.signal ??
965
+ childSignalCode ??
966
+ child.signalCode ??
967
+ null)
968
+ : (childSignalCode ?? child.signalCode ?? trustedStartupExitRecord?.signal ?? null),
833
969
  reason:
834
970
  brokerStartupExitReason(trustedStartupExitRecord) ?? brokerStartupFailureReason(trustedMarker),
835
971
  stderrExcerpt: spawnLogTail.length > 0 ? spawnLogTail : undefined,
@@ -74,6 +74,8 @@ export interface LifecycleDurableEffectsReceipt {
74
74
  export interface LifecycleLedgerEntry {
75
75
  operationKey?: string;
76
76
  fingerprint?: string;
77
+ /** True only when a session.close request supplied immutable generation authority. */
78
+ closeAuthorityBound?: boolean;
77
79
  version: typeof SDK_STATE_VERSION;
78
80
  identity: string;
79
81
  requestHash: string;
@@ -213,6 +215,20 @@ function pendingCleanupSessionId(response: unknown): string | undefined {
213
215
  return canonicalCleanupSessionId(cleanup?.sessionId) ? cleanup.sessionId : undefined;
214
216
  }
215
217
 
218
+ function retirementCreateIdentity(response: unknown): string | undefined {
219
+ if (!response || typeof response !== "object") return undefined;
220
+ const error = (response as { error?: unknown }).error;
221
+ if (!error || typeof error !== "object") return undefined;
222
+ const cleanup = (error as { cleanup?: unknown }).cleanup;
223
+ if (!cleanup || typeof cleanup !== "object") return undefined;
224
+ const receipt = (cleanup as { uncertainRetirement?: unknown }).uncertainRetirement;
225
+ if (!receipt || typeof receipt !== "object") return undefined;
226
+ const identity = (receipt as { identity?: unknown }).identity;
227
+ if (!identity || typeof identity !== "object") return undefined;
228
+ const createIdentity = (identity as { createIdentity?: unknown }).createIdentity;
229
+ return typeof createIdentity === "string" && createIdentity.length > 0 ? createIdentity : undefined;
230
+ }
231
+
216
232
  function hasValidTerminalDigests(entry: LifecycleLedgerEntry): boolean {
217
233
  const response = entry.response as { ok?: unknown; error?: { code?: unknown; cleanup?: unknown } } | undefined;
218
234
  const cleanupPendingResponse =
@@ -646,23 +662,91 @@ export class LifecycleLedger {
646
662
  await directory.close();
647
663
  }
648
664
  }
649
- async #compact(replacement?: LifecycleLedgerEntry): Promise<boolean> {
665
+ async #compact(
666
+ replacement?: LifecycleLedgerEntry,
667
+ incoming?: { readonly bytes: number; readonly rows: number },
668
+ ): Promise<boolean> {
650
669
  const anchors = new Map<string, LifecycleLedgerEntry>();
651
670
  for (const entry of this.#entries) {
652
671
  const latest = replacement?.identity === entry.identity ? replacement : this.#byIdentity.get(entry.identity);
653
672
  if (entry.state === "accepted" && !anchors.has(entry.identity) && entry.requestHash === latest?.requestHash)
654
673
  anchors.set(entry.identity, entry);
655
674
  }
656
- const snapshot: LifecycleLedgerEntry[] = [];
657
675
  const compacted = new Map(this.#byIdentity);
658
676
  if (replacement) compacted.set(replacement.identity, replacement);
659
- for (const [identity, latest] of compacted) {
660
- const anchor = anchors.get(identity);
661
- if (!anchor) throw new Error("Lifecycle ledger compaction requires an accepted identity anchor.");
662
- snapshot.push(replacement?.identity === identity && latest.state === "accepted" ? latest : anchor);
663
- if (latest.state !== "accepted") snapshot.push(latest);
677
+ const buildSnapshot = (): LifecycleLedgerEntry[] => {
678
+ const next: LifecycleLedgerEntry[] = [];
679
+ for (const [identity, latest] of compacted) {
680
+ const anchor = anchors.get(identity);
681
+ if (!anchor) throw new Error("Lifecycle ledger compaction requires an accepted identity anchor.");
682
+ next.push(replacement?.identity === identity && latest.state === "accepted" ? latest : anchor);
683
+ if (latest.state !== "accepted") next.push(latest);
684
+ }
685
+ return next;
686
+ };
687
+ let snapshot = buildSnapshot();
688
+ let contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
689
+ const needsEviction = incoming
690
+ ? snapshot.length + incoming.rows > this.#limits.maxRows ||
691
+ contents.length + incoming.bytes > this.#limits.maxBytes
692
+ : replacement === undefined
693
+ ? snapshot.length >= this.#limits.maxRows || contents.length >= this.#limits.maxBytes
694
+ : snapshot.length > this.#limits.maxRows || contents.length > this.#limits.maxBytes;
695
+ if (needsEviction) {
696
+ const protectedCreateIdentities = new Set<string>();
697
+ for (const entry of compacted.values()) {
698
+ const createIdentity =
699
+ retirementCreateIdentity(entry.response) ?? retirementCreateIdentity(entry.unresolvedCleanupResponse);
700
+ if (createIdentity !== undefined) protectedCreateIdentities.add(createIdentity);
701
+ }
702
+ const evictable = [...compacted.values()]
703
+ .filter(
704
+ latest =>
705
+ latest.identity !== replacement?.identity &&
706
+ terminal(latest.state) &&
707
+ latest.operationKey !== undefined &&
708
+ latest.fingerprint !== undefined &&
709
+ (!latest.operationKey.startsWith("session.close\u0000") || latest.closeAuthorityBound === true) &&
710
+ !protectedCreateIdentities.has(latest.identity) &&
711
+ pendingCleanupSessionId(latest.response) === undefined &&
712
+ pendingCleanupSessionId(latest.unresolvedCleanupResponse) === undefined,
713
+ )
714
+ .sort((left, right) => left.ts - right.ts);
715
+ const rowsByIdentity = new Map<string, number>();
716
+ const bytesByIdentity = new Map<string, number>();
717
+ for (const entry of snapshot) {
718
+ rowsByIdentity.set(entry.identity, (rowsByIdentity.get(entry.identity) ?? 0) + 1);
719
+ bytesByIdentity.set(
720
+ entry.identity,
721
+ (bytesByIdentity.get(entry.identity) ?? 0) + Buffer.byteLength(`${JSON.stringify(entry)}\n`),
722
+ );
723
+ }
724
+ let remainingRows = snapshot.length;
725
+ let remainingBytes = contents.length;
726
+ const evicted = new Set<string>();
727
+ for (const latest of evictable) {
728
+ if (
729
+ remainingRows <= this.#limits.maxRows / 2 &&
730
+ remainingBytes <= this.#limits.maxBytes / 2 &&
731
+ remainingRows + (incoming?.rows ?? 0) <= this.#limits.maxRows &&
732
+ remainingBytes + (incoming?.bytes ?? 0) <= this.#limits.maxBytes
733
+ )
734
+ break;
735
+ evicted.add(latest.identity);
736
+ remainingRows -= rowsByIdentity.get(latest.identity) ?? 0;
737
+ remainingBytes -= bytesByIdentity.get(latest.identity) ?? 0;
738
+ }
739
+ for (const identity of evicted) compacted.delete(identity);
740
+ snapshot = buildSnapshot();
741
+ contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
742
+ // Replay is guaranteed only for identities retained in the compacted ledger.
664
743
  }
665
- const contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
744
+ if (
745
+ incoming &&
746
+ (snapshot.length + incoming.rows > this.#limits.maxRows ||
747
+ contents.length + incoming.bytes > this.#limits.maxBytes)
748
+ )
749
+ throw new Error("Lifecycle ledger append exceeds configured bounds.");
666
750
  if (
667
751
  snapshot.length > this.#limits.maxRows ||
668
752
  contents.length > this.#limits.maxBytes ||
@@ -735,7 +819,7 @@ export class LifecycleLedger {
735
819
  async migrateIdentity(
736
820
  from: string,
737
821
  to: string,
738
- metadata: { operationKey: string; fingerprint: string },
822
+ metadata: { operationKey: string; fingerprint: string; closeAuthorityBound?: boolean },
739
823
  ): Promise<LifecycleLedgerEntry | undefined> {
740
824
  return this.#mutate(async () => {
741
825
  const existing = this.#byIdentity.get(to);
@@ -776,9 +860,12 @@ export class LifecycleLedger {
776
860
  );
777
861
  if (line.length - 1 > this.#limits.maxLineBytes)
778
862
  throw new Error("Lifecycle ledger row exceeds the maximum byte length.");
863
+ if (line.length > this.#limits.maxBytes) throw new Error("Lifecycle ledger append exceeds configured bounds.");
779
864
  let replacementCompacted = false;
780
865
  if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
781
- replacementCompacted = await this.#compact(this.#byIdentity.has(entry.identity) ? entry : undefined);
866
+ replacementCompacted = this.#byIdentity.has(entry.identity)
867
+ ? await this.#compact(entry)
868
+ : await this.#compact(undefined, { bytes: line.length, rows: 1 });
782
869
  if (replacementCompacted) return entry;
783
870
  if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
784
871
  throw new Error("Lifecycle ledger append exceeds configured bounds.");
@@ -798,7 +885,12 @@ export class LifecycleLedger {
798
885
  async begin(
799
886
  identity: string,
800
887
  requestHash: string,
801
- metadata: { operationKey?: string; fingerprint?: string; intendedSessionId?: string } = {},
888
+ metadata: {
889
+ operationKey?: string;
890
+ fingerprint?: string;
891
+ intendedSessionId?: string;
892
+ closeAuthorityBound?: boolean;
893
+ } = {},
802
894
  ): Promise<BeginResult> {
803
895
  return this.#mutate(async () => this.#begin(identity, requestHash, metadata));
804
896
  }
@@ -806,7 +898,12 @@ export class LifecycleLedger {
806
898
  async #begin(
807
899
  identity: string,
808
900
  requestHash: string,
809
- metadata: { operationKey?: string; fingerprint?: string; intendedSessionId?: string },
901
+ metadata: {
902
+ operationKey?: string;
903
+ fingerprint?: string;
904
+ intendedSessionId?: string;
905
+ closeAuthorityBound?: boolean;
906
+ },
810
907
  ): Promise<BeginResult> {
811
908
  const prior = this.#byIdentity.get(identity);
812
909
  if (!prior)
@@ -819,6 +916,7 @@ export class LifecycleLedger {
819
916
  operationKey: metadata.operationKey,
820
917
  fingerprint: metadata.fingerprint,
821
918
  intendedSessionId: metadata.intendedSessionId,
919
+ closeAuthorityBound: metadata.closeAuthorityBound,
822
920
  state: "accepted",
823
921
  ts: Date.now(),
824
922
  }),