@gajae-code/coding-agent 0.16.3 → 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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -1,10 +1,11 @@
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";
6
7
  import type { NativeExactFileIdentity, NativeExactUnlinkResult } from "@gajae-code/natives";
7
- import { getAgentDir, isEnoent } from "@gajae-code/utils";
8
+ import { $credentialEnv, getAgentDir, isEnoent } from "@gajae-code/utils";
8
9
  import * as z from "zod/v4";
9
10
  import { splitSelectorThinkingSuffix } from "../thinking";
10
11
  import { withFileLock } from "./file-lock";
@@ -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 {
@@ -1444,10 +1537,12 @@ function effectiveTrustedKeys(
1444
1537
  return getModelPresetRegistryTestTrustedKeys(agentDir) ?? MODEL_PRESET_REGISTRY_TRUSTED_KEYS;
1445
1538
  }
1446
1539
  function effectiveManifestUrl(dependencies: ModelPresetRegistryDependencies): string {
1447
- return dependencies.manifestUrl ?? process.env.GJC_MODEL_PRESET_REGISTRY_URL ?? DEFAULT_MODEL_PRESET_REGISTRY_URL;
1540
+ return (
1541
+ dependencies.manifestUrl ?? $credentialEnv("GJC_MODEL_PRESET_REGISTRY_URL") ?? DEFAULT_MODEL_PRESET_REGISTRY_URL
1542
+ );
1448
1543
  }
1449
1544
  function environmentDisabled(): boolean {
1450
- return /^(?:1|true|yes|on)$/i.test(process.env.GJC_MODEL_PRESET_REGISTRY_DISABLED ?? "");
1545
+ return /^(?:1|true|yes|on)$/i.test($credentialEnv("GJC_MODEL_PRESET_REGISTRY_DISABLED") ?? "");
1451
1546
  }
1452
1547
  function assertHttpsUrl(raw: string, description: string): URL {
1453
1548
  let url: URL;
@@ -1574,9 +1669,10 @@ function cloneAcceptedModelPresetRegistry(value: AcceptedModelPresetRegistry): A
1574
1669
 
1575
1670
  function registryFileFingerprint(file: string): string {
1576
1671
  try {
1577
- return sha256(fsSync.readFileSync(file));
1578
- } catch {
1579
- return "absent";
1672
+ const bytes = readRegistryBytesSync(file);
1673
+ return bytes === undefined ? "absent" : sha256(bytes);
1674
+ } catch (error) {
1675
+ return isEnoent(error) ? "absent" : "rejected";
1580
1676
  }
1581
1677
  }
1582
1678
 
@@ -1593,9 +1689,10 @@ function acceptedRegistryFingerprint(
1593
1689
 
1594
1690
  async function registryFileFingerprintAsync(file: string): Promise<string> {
1595
1691
  try {
1596
- return sha256(new Uint8Array(await Bun.file(file).arrayBuffer()));
1597
- } catch {
1598
- return "absent";
1692
+ const bytes = await readRegistryBytesBun(file);
1693
+ return bytes === undefined ? "absent" : sha256(bytes);
1694
+ } catch (error) {
1695
+ return isEnoent(error) ? "absent" : "rejected";
1599
1696
  }
1600
1697
  }
1601
1698
 
@@ -1622,6 +1719,8 @@ export function loadAcceptedModelPresetRegistry(
1622
1719
  if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1623
1720
  return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1624
1721
  const fingerprint = acceptedRegistryFingerprint(agentDir, dependencies);
1722
+ if (fingerprint.split("|").includes("rejected"))
1723
+ return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1625
1724
  const cached = acceptedRegistryReadCache.get(agentDir);
1626
1725
  if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1627
1726
  const value = loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
@@ -1698,6 +1797,8 @@ export async function loadAcceptedModelPresetRegistryAsync(
1698
1797
  if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1699
1798
  return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1700
1799
  const fingerprint = await acceptedRegistryFingerprintAsync(agentDir, dependencies);
1800
+ if (fingerprint.split("|").includes("rejected"))
1801
+ return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1701
1802
  const cached = acceptedRegistryReadCache.get(agentDir);
1702
1803
  if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1703
1804
  const value = await loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
@@ -100,6 +100,27 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
100
100
  critic: "openai-codex/gpt-5.6-luna:max",
101
101
  architect: "openai-codex/gpt-5.6-luna:max",
102
102
  }),
103
+ profile("astra-lite", ["openai-codex"], {
104
+ default: "openai-codex/gpt-6-astra:medium",
105
+ executor: "openai-codex/gpt-5.6-luna:xhigh",
106
+ planner: "openai-codex/gpt-6-astra:xhigh",
107
+ critic: "openai-codex/gpt-5.6-sol:high",
108
+ architect: "openai-codex/gpt-5.6-terra:xhigh",
109
+ }),
110
+ profile("astra-default", ["openai-codex"], {
111
+ default: "openai-codex/gpt-6-astra:medium",
112
+ executor: "openai-codex/gpt-5.6-luna:max",
113
+ planner: "openai-codex/gpt-6-astra:xhigh",
114
+ critic: "openai-codex/gpt-5.6-sol:xhigh",
115
+ architect: "openai-codex/gpt-5.6-terra:xhigh",
116
+ }),
117
+ profile("astra-heavy", ["openai-codex"], {
118
+ default: "openai-codex/gpt-6-astra:medium",
119
+ executor: "openai-codex/gpt-5.6-sol:max",
120
+ planner: "openai-codex/gpt-5.6-sol:max",
121
+ critic: "openai-codex/gpt-6-astra:max",
122
+ architect: "openai-codex/gpt-6-astra:max",
123
+ }),
103
124
  profile("macos-omlx-fast", ["omlx"], {
104
125
  default: "omlx/Qwen3.6-35B-A3B-4bit:low",
105
126
  executor: "omlx/Qwen3.6-35B-A3B-4bit:low",
@@ -488,6 +509,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
488
509
  critic: "anthropic/claude-opus-5:high",
489
510
  architect: "openai-codex/gpt-5.6-sol:xhigh",
490
511
  }),
512
+ profile("astra-fable", ["openai-codex", "anthropic"], {
513
+ default: "openai-codex/gpt-6-astra:medium",
514
+ executor: "openai-codex/gpt-5.6-luna:max",
515
+ planner: "openai-codex/gpt-6-astra:xhigh",
516
+ critic: "anthropic/claude-fable-5-1:xhigh",
517
+ architect: "anthropic/claude-fable-5-1:xhigh",
518
+ }),
519
+ profile("astra-fable-opus", ["openai-codex", "anthropic"], {
520
+ default: "openai-codex/gpt-6-astra:medium",
521
+ executor: "openai-codex/gpt-5.6-luna:max",
522
+ planner: "anthropic/claude-opus-5:medium",
523
+ critic: "anthropic/claude-fable-5-1:xhigh",
524
+ architect: "anthropic/claude-fable-5-1:xhigh",
525
+ }),
491
526
  ];
492
527
 
493
528
  /**
@@ -521,6 +556,9 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
521
556
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
522
557
  "codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
523
558
  lunamaxxing: { displayName: "LunaMaxxing", providerGroup: "CODEX" },
559
+ "astra-lite": { displayName: "ASTRA-Lite", providerGroup: "CODEX" },
560
+ "astra-default": { displayName: "ASTRA-Default", providerGroup: "CODEX" },
561
+ "astra-heavy": { displayName: "ASTRA-Heavy", providerGroup: "CODEX" },
524
562
  opencodego: { displayName: "OpenCodeGo", providerGroup: "OPENCODEGO" },
525
563
  "commandcode-goat": { displayName: "Command Code GOAT", providerGroup: "COMMAND CODE GOAT" },
526
564
  "open-weights-glm": { displayName: "GLM", providerGroup: "OPEN WEIGHT MODELS (PROVIDER AGNOSTIC)" },
@@ -594,6 +632,8 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
594
632
  "opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
595
633
  "codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
596
634
  "fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
635
+ "astra-fable": { displayName: "ASTRA + Fable (5.1)", providerGroup: "COMBOS" },
636
+ "astra-fable-opus": { displayName: "ASTRA + Fable (5.1) + Opus", providerGroup: "COMBOS" },
597
637
  "macos-omlx-fast": { displayName: "4-bit Fast (MoE measured 93.5 tok/s)", providerGroup: "macOS Local (oMLX)" },
598
638
  "macos-omlx-balanced": {
599
639
  displayName: "8-bit Balanced (MoE measured 71.1 tok/s)",
@@ -40,6 +40,7 @@ import {
40
40
  unregisterCustomApis,
41
41
  } from "@gajae-code/ai/core";
42
42
  import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-compat";
43
+ import { isDirectXaiReasoningEffortModel } from "@gajae-code/ai/providers/openai-completions-compat";
43
44
  import {
44
45
  detectDiscoveredApiFamily,
45
46
  resolveLoopbackOpenAIBaseUrl,
@@ -279,11 +280,7 @@ export const GJC_MODEL_ASSIGNMENT_TARGETS: Record<GjcModelAssignmentTargetId, Gj
279
280
 
280
281
  export function requiresExplicitThinkingChoice(model: Model, role: GjcModelAssignmentTargetId | null): boolean {
281
282
  if (!modelSupportsReasoningControl(model)) return false;
282
- if (
283
- model.provider === "openai" ||
284
- model.provider === "openai-codex" ||
285
- (model.provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6"))
286
- )
283
+ if (model.provider === "openai" || model.provider === "openai-codex" || isDirectXaiReasoningEffortModel(model))
287
284
  return true;
288
285
  if (role === null) return false;
289
286
  if (role === "default") return true;
@@ -1835,6 +1832,25 @@ export class ModelRegistry {
1835
1832
  /**
1836
1833
  * Reload models from disk (embedded + accepted registry + custom from models.yml).
1837
1834
  */
1835
+ async refreshStatic(): Promise<void> {
1836
+ if (this.#disposed) return;
1837
+ await this.#enqueueCatalogMutation(async () => {
1838
+ if (this.#disposed) return;
1839
+ this.#catalogRefreshGeneration++;
1840
+ this.#suspendRebuild();
1841
+ try {
1842
+ this.#reloadStaticModels();
1843
+ this.#suppressedSelectors.clear();
1844
+ this.#modelBindingsApplier.apply();
1845
+ } finally {
1846
+ this.#resumeRebuild();
1847
+ }
1848
+ });
1849
+ }
1850
+
1851
+ /**
1852
+ * Reload models from disk and refresh runtime provider discovery state.
1853
+ */
1838
1854
  async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
1839
1855
  if (this.#disposed) return;
1840
1856
  await this.#enqueueCatalogMutation(async () => {
@@ -1882,7 +1898,8 @@ export class ModelRegistry {
1882
1898
  const refreshGeneration = this.#catalogRefreshGeneration;
1883
1899
  this.#suspendRebuild();
1884
1900
  try {
1885
- this.#reloadStaticModels();
1901
+ // A provider refresh only updates discovery for this provider.
1902
+ // Static catalog changes are serialized through refresh().
1886
1903
  for (const selector of this.#suppressedSelectors.keys()) {
1887
1904
  if (selector.startsWith(`${providerId}/`)) {
1888
1905
  this.#suppressedSelectors.delete(selector);
@@ -3026,6 +3043,7 @@ export class ModelRegistry {
3026
3043
  refreshGeneration = this.#catalogRefreshGeneration,
3027
3044
  providerRefresh?: ProviderRefreshFence,
3028
3045
  ): Promise<void> {
3046
+ const profileAvailabilityEvidenceBefore = this.#profileAvailabilityEvidenceFingerprint();
3029
3047
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
3030
3048
  const selectedDiscoverableProviders = (
3031
3049
  providerFilter
@@ -3232,7 +3250,10 @@ export class ModelRegistry {
3232
3250
  });
3233
3251
  }
3234
3252
  if (discovered.length === 0) {
3235
- if (clearPublishedModelIds.size > 0) {
3253
+ if (
3254
+ clearPublishedModelIds.size > 0 ||
3255
+ profileAvailabilityEvidenceBefore !== this.#profileAvailabilityEvidenceFingerprint()
3256
+ ) {
3236
3257
  this.#rebuildCanonicalIndex();
3237
3258
  }
3238
3259
  return;
@@ -3240,6 +3261,23 @@ export class ModelRegistry {
3240
3261
  this.#mergeDiscoveredModels(discovered);
3241
3262
  }
3242
3263
 
3264
+ #profileAvailabilityEvidenceFingerprint(): string {
3265
+ return JSON.stringify(
3266
+ [...this.#descriptorDiscoveryEvidence.entries()]
3267
+ .sort(([left], [right]) => left.localeCompare(right))
3268
+ .map(([provider, evidence]) => [
3269
+ provider,
3270
+ evidence.fresh,
3271
+ evidence.profileFresh,
3272
+ evidence.authGeneration,
3273
+ evidence.endpoint,
3274
+ evidence.profileEndpoint,
3275
+ [...evidence.modelIds].sort(),
3276
+ evidence.profileModelIds === undefined ? undefined : [...evidence.profileModelIds].sort(),
3277
+ ]),
3278
+ );
3279
+ }
3280
+
3243
3281
  #mergeDiscoveredModels(discovered: readonly Model<Api>[]): void {
3244
3282
  const discoveredModels = this.#applyHardcodedModelPolicies(
3245
3283
  discovered.map(model =>
@@ -3537,8 +3575,9 @@ export class ModelRegistry {
3537
3575
  // through discovery.type.
3538
3576
  if (this.#configuredProviderIds.has("vllm")) configuredDiscoveryProviders.add("vllm");
3539
3577
  if (this.#configuredProviderIds.has("sglang")) configuredDiscoveryProviders.add("sglang");
3540
- const managerOptions = (await this.#collectBuiltInModelManagerOptions(configuredDiscoveryProviders)).filter(
3541
- entry => (providerFilter ? providerFilter.has(entry.options.providerId) : true),
3578
+ const managerOptions = await this.#collectBuiltInModelManagerOptions(
3579
+ configuredDiscoveryProviders,
3580
+ providerFilter,
3542
3581
  );
3543
3582
  if (managerOptions.length === 0) {
3544
3583
  return [];
@@ -3551,6 +3590,7 @@ export class ModelRegistry {
3551
3590
 
3552
3591
  async #collectBuiltInModelManagerOptions(
3553
3592
  excludedProviderIds: ReadonlySet<string> = new Set(),
3593
+ providerFilter?: ReadonlySet<string>,
3554
3594
  ): Promise<ModelManagerDiscoveryOptions[]> {
3555
3595
  const specialProviderDescriptors: Array<{
3556
3596
  providerId: string;
@@ -3594,10 +3634,16 @@ export class ModelRegistry {
3594
3634
  ];
3595
3635
  const disabledProviders = getDisabledProviderIdsFromSettings(this.#settings);
3596
3636
  const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(
3597
- descriptor => !disabledProviders.has(descriptor.providerId) && !excludedProviderIds.has(descriptor.providerId),
3637
+ descriptor =>
3638
+ !disabledProviders.has(descriptor.providerId) &&
3639
+ !excludedProviderIds.has(descriptor.providerId) &&
3640
+ (providerFilter === undefined || providerFilter.has(descriptor.providerId)),
3598
3641
  );
3599
3642
  const enabledSpecialProviderDescriptors = specialProviderDescriptors.filter(
3600
- descriptor => !disabledProviders.has(descriptor.providerId) && !excludedProviderIds.has(descriptor.providerId),
3643
+ descriptor =>
3644
+ !disabledProviders.has(descriptor.providerId) &&
3645
+ !excludedProviderIds.has(descriptor.providerId) &&
3646
+ (providerFilter === undefined || providerFilter.has(descriptor.providerId)),
3601
3647
  );
3602
3648
  for (const descriptor of standardProviderDescriptors) {
3603
3649
  if (!descriptor.allowUnauthenticated) continue;
@@ -4514,10 +4560,10 @@ export class ModelRegistry {
4514
4560
  .get(model.provider.toLowerCase())
4515
4561
  ?.get(model.id.toLowerCase())?.thinking;
4516
4562
  if (overrideThinking !== undefined) return { ...model, thinking: overrideThinking as ThinkingConfig };
4517
- const overlay = [...this.#runtimeModelOverlays, ...this.#customModelOverlays].find(
4518
- candidate =>
4519
- candidate.provider === model.provider && candidate.id === model.id && candidate.thinking !== undefined,
4520
- );
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);
4521
4567
  return overlay?.thinking === undefined ? model : { ...model, thinking: overlay.thinking };
4522
4568
  }
4523
4569
  /**
@@ -5169,15 +5215,20 @@ export class ModelRegistry {
5169
5215
  * usable until live catalog evidence exists so offline startup is unchanged.
5170
5216
  */
5171
5217
  getAvailableForProfileActivation(): Model<Api>[] {
5218
+ const bundledIdsByProvider = new Map<string, Set<string>>();
5172
5219
  return this.getAvailable().filter(model => {
5173
5220
  const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
5174
5221
  if (!evidence?.profileFresh || evidence.profileModelIds === undefined) return true;
5175
5222
  if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
5176
- const bundledModelIds = new Set(
5177
- (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5178
- candidate => candidate.id,
5179
- ),
5180
- );
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
+ }
5181
5232
  if (!bundledModelIds.has(model.id)) return true;
5182
5233
  if (
5183
5234
  evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
@@ -5190,9 +5241,8 @@ export class ModelRegistry {
5190
5241
  }
5191
5242
 
5192
5243
  #hasCustomModelOverlay(provider: string, id: string): boolean {
5193
- return [...this.#customModelOverlays, ...this.#runtimeModelOverlays].some(
5194
- overlay => overlay.provider === provider && overlay.id === id,
5195
- );
5244
+ const matches = (overlay: CustomModelOverlay) => overlay.provider === provider && overlay.id === id;
5245
+ return this.#customModelOverlays.some(matches) || this.#runtimeModelOverlays.some(matches);
5196
5246
  }
5197
5247
 
5198
5248
  #hasFreshOrStaticModelEvidence(model: Model<Api>): boolean {
@@ -62,6 +62,54 @@ export function parseModelString(
62
62
  : { provider, id: modelStr.slice(slashIdx + 1) };
63
63
  }
64
64
 
65
+ export function resolveStartupModelRefreshSelectors(
66
+ options: {
67
+ model?: string;
68
+ provider?: string;
69
+ credential?: string;
70
+ continue?: boolean;
71
+ resume?: string | boolean;
72
+ hasStartupProfile: boolean;
73
+ },
74
+ settings: ModelRoleSettings,
75
+ ): ModelSelectorValue | undefined {
76
+ if (options.credential) return undefined;
77
+ if (options.model) {
78
+ return options.provider && !options.model.toLowerCase().startsWith(`${options.provider.toLowerCase()}/`)
79
+ ? `${options.provider}/${options.model}`
80
+ : options.model;
81
+ }
82
+ if (options.hasStartupProfile) return undefined;
83
+ if (options.continue || options.resume) return undefined;
84
+ return resolveConfiguredModelPatterns(settings.getModelRole("default"), settings);
85
+ }
86
+
87
+ export async function refreshMissingQualifiedModelProviders(
88
+ selectors: ModelSelectorValue | undefined,
89
+ modelRegistry: Pick<ModelRegistry, "getAvailable" | "getDiscoverableProviders" | "refreshProvider">,
90
+ ): Promise<boolean> {
91
+ const refreshedProviders = new Set<string>();
92
+ for (const selector of normalizeModelSelectorValue(selectors)) {
93
+ const parsedSelector = parseModelString(selector);
94
+ if (!parsedSelector) continue;
95
+ const provider = modelRegistry
96
+ .getDiscoverableProviders()
97
+ .find(candidate => candidate.toLowerCase() === parsedSelector.provider.toLowerCase());
98
+ if (!provider || refreshedProviders.has(provider)) continue;
99
+ const availableModels = modelRegistry.getAvailable();
100
+ const rawModelId = selector.slice(selector.indexOf("/") + 1);
101
+ if (
102
+ resolveProviderModelReference(provider, rawModelId, availableModels) ||
103
+ resolveProviderModelReference(provider, parsedSelector.id, availableModels)
104
+ )
105
+ continue;
106
+
107
+ await modelRegistry.refreshProvider(provider, "online-if-uncached");
108
+ refreshedProviders.add(provider);
109
+ }
110
+ return refreshedProviders.size > 0;
111
+ }
112
+
65
113
  /**
66
114
  * Format a model as "provider/modelId" string.
67
115
  */
@@ -111,7 +111,8 @@ export type StatusLineSegmentId =
111
111
  | "cache_read"
112
112
  | "cache_write"
113
113
  | "session_name"
114
- | "usage";
114
+ | "usage"
115
+ | "command";
115
116
 
116
117
  /** Submenu choice metadata. */
117
118
  export type SubmenuOption<V extends string = string> = {
@@ -1242,6 +1243,26 @@ export const SETTINGS_SCHEMA = {
1242
1243
  },
1243
1244
  },
1244
1245
 
1246
+ reasoningLanguage: {
1247
+ type: "enum",
1248
+ values: ["off", "english"] as const,
1249
+ default: "off",
1250
+ ui: {
1251
+ tab: "model",
1252
+ label: "Reasoning Language",
1253
+ description:
1254
+ "Optionally reason through development work in English while preserving the user's response language",
1255
+ options: [
1256
+ { value: "off", label: "Off", description: "Do not add reasoning-language guidance" },
1257
+ {
1258
+ value: "english",
1259
+ label: "English",
1260
+ description: "Reason through technical work in English and answer in the user's language",
1261
+ },
1262
+ ],
1263
+ },
1264
+ },
1265
+
1245
1266
  hideThinkingBlock: {
1246
1267
  type: "boolean",
1247
1268
  default: false,
@@ -1749,6 +1770,17 @@ export const SETTINGS_SCHEMA = {
1749
1770
  },
1750
1771
  },
1751
1772
 
1773
+ "paseo.autoImport": {
1774
+ type: "boolean",
1775
+ default: false,
1776
+ ui: {
1777
+ tab: "interaction",
1778
+ label: "Announce Sessions to Paseo",
1779
+ description:
1780
+ "Global-only operator opt-in. When enabled, and only when a GJC ACP provider is already registered in Paseo and its daemon is already running, import each interactive session into Paseo so it can be driven from there. Project settings cannot enable it. Never starts the Paseo daemon or removes imported entries; stale entries must be pruned in Paseo. Never runs when Paseo is absent.",
1781
+ },
1782
+ },
1783
+
1752
1784
  "crashReport.nudge": {
1753
1785
  type: "boolean",
1754
1786
  default: true,
@@ -333,6 +333,7 @@ const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disab
333
333
  const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
334
334
  "crashReport.upstream",
335
335
  "crashReport.upstreamDsn",
336
+ "paseo.autoImport",
336
337
  "ui.language",
337
338
  "telemetry.enabled",
338
339
  ]);
@@ -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 (