@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
@@ -1,5 +1,6 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fsSync from "node:fs";
3
+ import type { FileHandle } from "node:fs/promises";
3
4
  import * as fs from "node:fs/promises";
4
5
  import * as path from "node:path";
5
6
  import type { Api, Model } from "@gajae-code/ai/core";
@@ -939,16 +940,80 @@ function validateGeneration(
939
940
  return generation;
940
941
  }
941
942
 
942
- function readJsonSync(file: string): unknown | undefined {
943
+ type RegistryFileStat = {
944
+ dev: number | bigint;
945
+ ino: number | bigint;
946
+ size: number | bigint;
947
+ isFile(): boolean;
948
+ isSymbolicLink(): boolean;
949
+ };
950
+
951
+ function registryFileOpenFlags(): number {
952
+ return (
953
+ fsSync.constants.O_RDONLY |
954
+ (process.platform === "win32" ? 0 : (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0))
955
+ );
956
+ }
957
+
958
+ function sameRegistryFileIdentity(left: RegistryFileStat, right: RegistryFileStat): boolean {
959
+ return (
960
+ left.isFile() &&
961
+ !left.isSymbolicLink() &&
962
+ right.isFile() &&
963
+ !right.isSymbolicLink() &&
964
+ left.dev === right.dev &&
965
+ left.ino === right.ino
966
+ );
967
+ }
968
+
969
+ function readBoundedRegistryFdSync(fd: number): Buffer {
970
+ const chunks: Buffer[] = [];
971
+ const buffer = Buffer.allocUnsafe(64 * 1024);
972
+ let total = 0;
973
+ for (;;) {
974
+ const length = Math.min(buffer.length, MODEL_PRESET_REGISTRY_MAX_STATE_BYTES + 1 - total);
975
+ const bytesRead = fsSync.readSync(fd, buffer, 0, length, null);
976
+ if (bytesRead === 0) return Buffer.concat(chunks, total);
977
+ total += bytesRead;
978
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
979
+ if (total > MODEL_PRESET_REGISTRY_MAX_STATE_BYTES) throw new Error("Registry cache is oversized.");
980
+ }
981
+ }
982
+
983
+ function readRegistryBytesSync(file: string): Buffer | undefined {
984
+ let before: fsSync.Stats;
943
985
  try {
944
- const stat = fsSync.lstatSync(file);
945
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Registry cache path is not a regular file.");
946
- if (stat.size > MODEL_PRESET_REGISTRY_MAX_STATE_BYTES) throw new Error("Registry cache is oversized.");
947
- return JSON.parse(fsSync.readFileSync(file, "utf8"));
986
+ before = fsSync.lstatSync(file);
948
987
  } catch (error) {
949
988
  if (isEnoent(error)) return undefined;
950
989
  throw error;
951
990
  }
991
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Registry cache path is not a regular file.");
992
+ if (before.size > MODEL_PRESET_REGISTRY_MAX_STATE_BYTES) throw new Error("Registry cache is oversized.");
993
+ let fd: number | undefined;
994
+ try {
995
+ fd = fsSync.openSync(file, registryFileOpenFlags());
996
+ const opened = fsSync.fstatSync(fd);
997
+ if (!sameRegistryFileIdentity(before, opened)) throw new Error("Registry cache path changed while opening.");
998
+ const bytes = readBoundedRegistryFdSync(fd);
999
+ const finalOpened = fsSync.fstatSync(fd);
1000
+ let after: fsSync.Stats;
1001
+ try {
1002
+ after = fsSync.lstatSync(file);
1003
+ } catch (error) {
1004
+ throw new Error("Registry cache path changed while reading.", { cause: error });
1005
+ }
1006
+ if (!sameRegistryFileIdentity(opened, finalOpened) || !sameRegistryFileIdentity(opened, after))
1007
+ throw new Error("Registry cache path changed while reading.");
1008
+ return bytes;
1009
+ } finally {
1010
+ if (fd !== undefined) fsSync.closeSync(fd);
1011
+ }
1012
+ }
1013
+
1014
+ function readJsonSync(file: string): unknown | undefined {
1015
+ const bytes = readRegistryBytesSync(file);
1016
+ return bytes === undefined ? undefined : JSON.parse(bytes.toString("utf8"));
952
1017
  }
953
1018
 
954
1019
  /**
@@ -956,16 +1021,44 @@ function readJsonSync(file: string): unknown | undefined {
956
1021
  * Keep the synchronous reader above for legacy startup paths until their
957
1022
  * constructor-wide migration can be completed without changing public APIs.
958
1023
  */
959
- async function readJsonBun(file: string): Promise<unknown | undefined> {
1024
+ async function readRegistryBytesBun(file: string): Promise<Buffer | undefined> {
1025
+ let before: fsSync.BigIntStats;
960
1026
  try {
961
- const stat = await fs.lstat(file, { bigint: true });
962
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Registry cache path is not a regular file.");
963
- if (stat.size > BigInt(MODEL_PRESET_REGISTRY_MAX_STATE_BYTES)) throw new Error("Registry cache is oversized.");
964
- return JSON.parse(await Bun.file(file).text());
1027
+ before = await fs.lstat(file, { bigint: true });
965
1028
  } catch (error) {
966
1029
  if (isEnoent(error)) return undefined;
967
1030
  throw error;
968
1031
  }
1032
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Registry cache path is not a regular file.");
1033
+ if (before.size > BigInt(MODEL_PRESET_REGISTRY_MAX_STATE_BYTES)) throw new Error("Registry cache is oversized.");
1034
+ let handle: FileHandle | undefined;
1035
+ try {
1036
+ handle = await fs.open(file, registryFileOpenFlags());
1037
+ const opened = await handle.stat({ bigint: true });
1038
+ if (!sameRegistryFileIdentity(before, opened)) throw new Error("Registry cache path changed while opening.");
1039
+ const bunFileFromDescriptor = (Bun.file as unknown as (descriptor: number) => Bun.BunFile)(handle.fd);
1040
+ const bytes = Buffer.from(
1041
+ await bunFileFromDescriptor.slice(0, MODEL_PRESET_REGISTRY_MAX_STATE_BYTES + 1).arrayBuffer(),
1042
+ );
1043
+ if (bytes.length > MODEL_PRESET_REGISTRY_MAX_STATE_BYTES) throw new Error("Registry cache is oversized.");
1044
+ const finalOpened = await handle.stat({ bigint: true });
1045
+ let after: fsSync.BigIntStats;
1046
+ try {
1047
+ after = await fs.lstat(file, { bigint: true });
1048
+ } catch (error) {
1049
+ throw new Error("Registry cache path changed while reading.", { cause: error });
1050
+ }
1051
+ if (!sameRegistryFileIdentity(opened, finalOpened) || !sameRegistryFileIdentity(opened, after))
1052
+ throw new Error("Registry cache path changed while reading.");
1053
+ return bytes;
1054
+ } finally {
1055
+ await handle?.close();
1056
+ }
1057
+ }
1058
+
1059
+ async function readJsonBun(file: string): Promise<unknown | undefined> {
1060
+ const bytes = await readRegistryBytesBun(file);
1061
+ return bytes === undefined ? undefined : JSON.parse(bytes.toString("utf8"));
969
1062
  }
970
1063
 
971
1064
  function parseState(value: unknown): RegistryState {
@@ -1576,9 +1669,10 @@ function cloneAcceptedModelPresetRegistry(value: AcceptedModelPresetRegistry): A
1576
1669
 
1577
1670
  function registryFileFingerprint(file: string): string {
1578
1671
  try {
1579
- return sha256(fsSync.readFileSync(file));
1580
- } catch {
1581
- return "absent";
1672
+ const bytes = readRegistryBytesSync(file);
1673
+ return bytes === undefined ? "absent" : sha256(bytes);
1674
+ } catch (error) {
1675
+ return isEnoent(error) ? "absent" : "rejected";
1582
1676
  }
1583
1677
  }
1584
1678
 
@@ -1595,9 +1689,10 @@ function acceptedRegistryFingerprint(
1595
1689
 
1596
1690
  async function registryFileFingerprintAsync(file: string): Promise<string> {
1597
1691
  try {
1598
- return sha256(new Uint8Array(await Bun.file(file).arrayBuffer()));
1599
- } catch {
1600
- return "absent";
1692
+ const bytes = await readRegistryBytesBun(file);
1693
+ return bytes === undefined ? "absent" : sha256(bytes);
1694
+ } catch (error) {
1695
+ return isEnoent(error) ? "absent" : "rejected";
1601
1696
  }
1602
1697
  }
1603
1698
 
@@ -1624,6 +1719,8 @@ export function loadAcceptedModelPresetRegistry(
1624
1719
  if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1625
1720
  return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1626
1721
  const fingerprint = acceptedRegistryFingerprint(agentDir, dependencies);
1722
+ if (fingerprint.split("|").includes("rejected"))
1723
+ return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1627
1724
  const cached = acceptedRegistryReadCache.get(agentDir);
1628
1725
  if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1629
1726
  const value = loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
@@ -1700,6 +1797,8 @@ export async function loadAcceptedModelPresetRegistryAsync(
1700
1797
  if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1701
1798
  return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1702
1799
  const fingerprint = await acceptedRegistryFingerprintAsync(agentDir, dependencies);
1800
+ if (fingerprint.split("|").includes("rejected"))
1801
+ return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1703
1802
  const cached = acceptedRegistryReadCache.get(agentDir);
1704
1803
  if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1705
1804
  const value = await loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
@@ -4560,10 +4560,10 @@ export class ModelRegistry {
4560
4560
  .get(model.provider.toLowerCase())
4561
4561
  ?.get(model.id.toLowerCase())?.thinking;
4562
4562
  if (overrideThinking !== undefined) return { ...model, thinking: overrideThinking as ThinkingConfig };
4563
- const overlay = [...this.#runtimeModelOverlays, ...this.#customModelOverlays].find(
4564
- candidate =>
4565
- candidate.provider === model.provider && candidate.id === model.id && candidate.thinking !== undefined,
4566
- );
4563
+ const hasDeclaredThinking = (candidate: CustomModelOverlay) =>
4564
+ candidate.provider === model.provider && candidate.id === model.id && candidate.thinking !== undefined;
4565
+ const overlay =
4566
+ this.#runtimeModelOverlays.find(hasDeclaredThinking) ?? this.#customModelOverlays.find(hasDeclaredThinking);
4567
4567
  return overlay?.thinking === undefined ? model : { ...model, thinking: overlay.thinking };
4568
4568
  }
4569
4569
  /**
@@ -5215,15 +5215,20 @@ export class ModelRegistry {
5215
5215
  * usable until live catalog evidence exists so offline startup is unchanged.
5216
5216
  */
5217
5217
  getAvailableForProfileActivation(): Model<Api>[] {
5218
+ const bundledIdsByProvider = new Map<string, Set<string>>();
5218
5219
  return this.getAvailable().filter(model => {
5219
5220
  const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
5220
5221
  if (!evidence?.profileFresh || evidence.profileModelIds === undefined) return true;
5221
5222
  if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
5222
- const bundledModelIds = new Set(
5223
- (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5224
- candidate => candidate.id,
5225
- ),
5226
- );
5223
+ let bundledModelIds = bundledIdsByProvider.get(model.provider);
5224
+ if (!bundledModelIds) {
5225
+ bundledModelIds = new Set(
5226
+ (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5227
+ candidate => candidate.id,
5228
+ ),
5229
+ );
5230
+ bundledIdsByProvider.set(model.provider, bundledModelIds);
5231
+ }
5227
5232
  if (!bundledModelIds.has(model.id)) return true;
5228
5233
  if (
5229
5234
  evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
@@ -5236,9 +5241,8 @@ export class ModelRegistry {
5236
5241
  }
5237
5242
 
5238
5243
  #hasCustomModelOverlay(provider: string, id: string): boolean {
5239
- return [...this.#customModelOverlays, ...this.#runtimeModelOverlays].some(
5240
- overlay => overlay.provider === provider && overlay.id === id,
5241
- );
5244
+ const matches = (overlay: CustomModelOverlay) => overlay.provider === provider && overlay.id === id;
5245
+ return this.#customModelOverlays.some(matches) || this.#runtimeModelOverlays.some(matches);
5242
5246
  }
5243
5247
 
5244
5248
  #hasFreshOrStaticModelEvidence(model: Model<Api>): boolean {
@@ -163,6 +163,8 @@ export interface PromptRequestV1 {
163
163
  phase: "claimed" | "remote_started" | "accepted" | "linked" | "terminal" | "completed" | "uncertain";
164
164
  runtime_receipt?: { accepted: true; command_id: string; turn_id: string };
165
165
  coordinator_turn_id?: string;
166
+ /** Retain prompt and source turn until the delegate's outer response is durable. */
167
+ delegate_response_pending?: boolean;
166
168
  safe_response?: Record<string, unknown>;
167
169
  error_code?: PublicReason | "idempotency_conflict";
168
170
  created_at: string;
@@ -244,7 +246,12 @@ export interface CoordinatorSessionTransactionV1 {
244
246
  scheduler_applied_revision?: number;
245
247
  scheduler_digest?: string;
246
248
  };
247
- recovery: { prompt_watermark_at: string | null; last_repaired_at: string | null };
249
+ recovery: {
250
+ prompt_watermark_at: string | null;
251
+ last_repaired_at: string | null;
252
+ /** One-shot authority for the initial delegate prompt, consumed atomically with its claim. */
253
+ initial_delegate_request?: { key_digest: string; request_digest: string };
254
+ };
248
255
  }
249
256
  interface CoordinatorSessionHistoryV1 {
250
257
  schema_version: 1;
@@ -815,6 +822,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
815
822
  typeof request.canonical_prompt.text !== "string" ||
816
823
  !safeId(request.sdk_idempotency_key) ||
817
824
  !promptPhases.has(String(request.phase)) ||
825
+ (request.delegate_response_pending !== undefined && typeof request.delegate_response_pending !== "boolean") ||
818
826
  !isTime(request.created_at) ||
819
827
  !isTime(request.updated_at) ||
820
828
  (request.coordinator_turn_id !== undefined &&
@@ -1194,6 +1202,12 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
1194
1202
  (typeof projection.scheduler_digest !== "string" || projection.scheduler_digest.length === 0)) ||
1195
1203
  (recovery.prompt_watermark_at !== null && !isTime(recovery.prompt_watermark_at)) ||
1196
1204
  (recovery.last_repaired_at !== null && !isTime(recovery.last_repaired_at)) ||
1205
+ (recovery.initial_delegate_request !== undefined &&
1206
+ (!isRecord(recovery.initial_delegate_request) ||
1207
+ typeof recovery.initial_delegate_request.key_digest !== "string" ||
1208
+ !/^[a-f0-9]{64}$/.test(recovery.initial_delegate_request.key_digest) ||
1209
+ typeof recovery.initial_delegate_request.request_digest !== "string" ||
1210
+ !/^[a-f0-9]{64}$/.test(recovery.initial_delegate_request.request_digest))) ||
1197
1211
  (queue.selected_promotion !== null &&
1198
1212
  (!isRecord(queue.selected_promotion) ||
1199
1213
  !safeId(queue.selected_promotion.from_turn_id) ||
@@ -1837,7 +1851,15 @@ export async function commitCreationWal(
1837
1851
  scheduler_applied_revision: 0,
1838
1852
  scheduler_digest: digest(JSON.stringify({ session_id: session.session_id, revision: 1 })),
1839
1853
  },
1840
- recovery: { prompt_watermark_at: null, last_repaired_at: null },
1854
+ recovery: {
1855
+ prompt_watermark_at: null,
1856
+ last_repaired_at: null,
1857
+ // Only a newly created WAL may grant pre-dispatch authority. An
1858
+ // existing WAL above must never replenish a consumed/compacted claim.
1859
+ ...(intent.kind === "delegate"
1860
+ ? { initial_delegate_request: { key_digest: keyDigest, request_digest: request.request_digest } }
1861
+ : {}),
1862
+ },
1841
1863
  };
1842
1864
  assertTransaction(transaction, session.namespace_id, session.session_id);
1843
1865
  await writeAtomic(transactionPath(paths, session.session_id), transaction);
@@ -2320,6 +2342,13 @@ export async function enumeratePublicDeliveries(
2320
2342
  export function compactTransaction(transaction: CoordinatorSessionTransactionV1, now = Date.now()): void {
2321
2343
  normalizeOutbox(transaction);
2322
2344
  const old = (time: string): boolean => Date.parse(time) + RETENTION_MS < now;
2345
+ // Prompt acceptance/canonical finalization precedes the outer delegate receipt.
2346
+ // Neither retention nor pressure compaction may erase that recovery authority.
2347
+ const pinnedPrompts = Object.values(transaction.requests.prompts).filter(
2348
+ request => request.delegate_response_pending === true,
2349
+ );
2350
+ const pinnedRequests = new Set<object>(pinnedPrompts);
2351
+ const pinnedTurns = new Set(pinnedPrompts.map(request => request.coordinator_turn_id));
2323
2352
  for (const [id, event] of Object.entries(transaction.outbox))
2324
2353
  if (
2325
2354
  event.emitted &&
@@ -2332,6 +2361,7 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
2332
2361
  for (const [id, request] of Object.entries(group))
2333
2362
  if (
2334
2363
  request.phase === "completed" &&
2364
+ !pinnedRequests.has(request) &&
2335
2365
  old(request.updated_at) &&
2336
2366
  JSON.stringify(transaction.canonical).includes(id) === false
2337
2367
  )
@@ -2347,6 +2377,7 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
2347
2377
  for (const turnId of terminalTurnIds) {
2348
2378
  const turn = transaction.canonical.turns[turnId];
2349
2379
  if (!turn) continue;
2380
+ if (pinnedTurns.has(turnId)) continue;
2350
2381
  // Both promotion endpoints stay addressable for later projection repair;
2351
2382
  // deleting either would corrupt the WAL instead of completing at capacity.
2352
2383
  if (