@gajae-code/coding-agent 0.18.0 → 0.18.5

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 (86) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/types/cli/stats-cli.d.ts +0 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +29 -2
  4. package/dist/types/config/model-registry.d.ts +16 -1
  5. package/dist/types/config/settings-schema.d.ts +2 -2
  6. package/dist/types/exa/mcp-client.d.ts +1 -5
  7. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  8. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
  9. package/dist/types/hashline/parser.d.ts +28 -5
  10. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  11. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  12. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  13. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  14. package/dist/types/sdk/broker/runtime.d.ts +20 -0
  15. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  16. package/dist/types/sdk/bus/index.d.ts +1 -0
  17. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  18. package/dist/types/sdk/client/client.d.ts +5 -1
  19. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  20. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  21. package/dist/types/session/agent-session.d.ts +2 -0
  22. package/dist/types/task/index.d.ts +0 -1
  23. package/dist/types/tools/bash.d.ts +1 -0
  24. package/dist/types/tools/descriptors.d.ts +5 -0
  25. package/dist/types/tools/eval-backends.d.ts +37 -0
  26. package/dist/types/tools/eval.d.ts +1 -6
  27. package/dist/types/tools/index.d.ts +1 -36
  28. package/dist/types/tools/session-descriptions.d.ts +22 -0
  29. package/package.json +7 -7
  30. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  31. package/src/cli/doctor/managed-link.ts +4 -4
  32. package/src/cli/doctor/repairs.ts +9 -1
  33. package/src/cli/stats-cli.ts +4 -36
  34. package/src/cli-main.ts +4 -8
  35. package/src/cli.ts +4 -8
  36. package/src/commands/commit.ts +2 -2
  37. package/src/commands/sdk.ts +1 -2
  38. package/src/config/autorouting-tier-map.ts +20 -2
  39. package/src/config/model-preset-registry.ts +17 -2
  40. package/src/config/model-profile-activation.ts +3 -1
  41. package/src/config/model-profiles.ts +11 -3
  42. package/src/config/model-registry.ts +45 -17
  43. package/src/config/settings-schema.ts +8 -2
  44. package/src/exa/mcp-client.ts +41 -8
  45. package/src/exec/bash-shell-guardian.ts +214 -48
  46. package/src/gjc-runtime/session-state-sidecar.ts +88 -58
  47. package/src/hashline/execute.ts +51 -3
  48. package/src/hashline/parser.ts +63 -16
  49. package/src/internal-urls/docs-index.generated.ts +25 -25
  50. package/src/modes/acp/acp-agent.ts +136 -20
  51. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  52. package/src/modes/interactive-mode.ts +10 -0
  53. package/src/modes/utils/context-usage.ts +2 -6
  54. package/src/prompts/tools/task.md +8 -27
  55. package/src/sdk/acp/adapter.ts +38 -3
  56. package/src/sdk/broker/doctor-restart.ts +38 -3
  57. package/src/sdk/broker/lifecycle.ts +408 -218
  58. package/src/sdk/broker/recovery-backoff.ts +135 -0
  59. package/src/sdk/broker/runtime.ts +83 -0
  60. package/src/sdk/broker/startup-budget.ts +6 -1
  61. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  62. package/src/sdk/bus/index.ts +308 -22
  63. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  64. package/src/sdk/client/client.ts +53 -35
  65. package/src/sdk/host/session-runtime.ts +99 -3
  66. package/src/sdk/prompt-watchdog.ts +17 -4
  67. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  68. package/src/session/agent-session.ts +79 -17
  69. package/src/session/internal/managed-session-storage.ts +23 -0
  70. package/src/task/index.ts +21 -66
  71. package/src/tools/bash.ts +39 -0
  72. package/src/tools/descriptors.ts +27 -1
  73. package/src/tools/eval-backends.ts +95 -0
  74. package/src/tools/eval.ts +3 -15
  75. package/src/tools/index.ts +9 -88
  76. package/src/tools/search.ts +2 -7
  77. package/src/tools/session-descriptions.ts +135 -0
  78. package/src/tools/tool-catalog.generated.ts +1 -1
  79. package/dist/types/cli/grep-cli.d.ts +0 -17
  80. package/dist/types/cli/shell-cli.d.ts +0 -8
  81. package/dist/types/commands/grep.d.ts +0 -42
  82. package/dist/types/commands/shell.d.ts +0 -21
  83. package/src/cli/grep-cli.ts +0 -160
  84. package/src/cli/shell-cli.ts +0 -176
  85. package/src/commands/grep.ts +0 -48
  86. package/src/commands/shell.ts +0 -29
@@ -0,0 +1,37 @@
1
+ import type { ToolSession } from ".";
2
+ export interface EvalBackendsAllowance {
3
+ python: boolean;
4
+ js: boolean;
5
+ }
6
+ /**
7
+ * Parse the `GJC_PY` multi-value token into per-backend booleans.
8
+ *
9
+ * Tokens (case-insensitive):
10
+ * - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
11
+ * - `1` / `py` → Python only (`{ py: true, js: false }`)
12
+ * - `js` → JavaScript only (`{ py: false, js: true }`)
13
+ * - `mix` / `both` → both backends (`{ py: true, js: true }`)
14
+ *
15
+ * Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
16
+ * token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
17
+ * per-key settings. This matches the documented contract that invalid values
18
+ * are ignored.
19
+ */
20
+ export declare function parseGjcPy(env: Record<string, string | undefined>): {
21
+ py: boolean;
22
+ js: boolean;
23
+ } | null;
24
+ /**
25
+ * Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
26
+ * to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
27
+ * Returns `null` when no env override is set so the caller can defer to settings.
28
+ */
29
+ export declare function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null;
30
+ /** Read per-backend allowance from settings (defaults true). */
31
+ export declare function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance;
32
+ /**
33
+ * Materialize the active eval backend allowance. `GJC_PY` takes precedence
34
+ * over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
35
+ * per-key settings (defaults true). When no env override is set, settings win.
36
+ */
37
+ export declare function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance;
@@ -42,11 +42,7 @@ export type EvalToolResult = {
42
42
  details: EvalToolDetails | undefined;
43
43
  };
44
44
  export type EvalProxyExecutor = (params: EvalToolParams, signal?: AbortSignal) => Promise<EvalToolResult>;
45
- export interface EvalToolDescriptionOptions {
46
- py?: boolean;
47
- js?: boolean;
48
- }
49
- export declare function getEvalToolDescription(options?: EvalToolDescriptionOptions): string;
45
+ export { type EvalToolDescriptionOptions, getEvalToolDescription } from "./session-descriptions";
50
46
  export interface EvalToolOptions {
51
47
  proxyExecutor?: EvalProxyExecutor;
52
48
  }
@@ -105,4 +101,3 @@ export declare const evalToolRenderer: {
105
101
  mergeCallAndResult: boolean;
106
102
  inline: boolean;
107
103
  };
108
- export {};
@@ -405,42 +405,7 @@ export interface BuiltinCapabilityCatalogEntry {
405
405
  }
406
406
  export declare const BUILTIN_CAPABILITY_CATALOG: readonly BuiltinCapabilityCatalogEntry[];
407
407
  export type ToolName = keyof typeof BUILTIN_TOOLS;
408
- export interface EvalBackendsAllowance {
409
- python: boolean;
410
- js: boolean;
411
- }
412
- /**
413
- * Parse the `GJC_PY` multi-value token into per-backend booleans.
414
- *
415
- * Tokens (case-insensitive):
416
- * - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
417
- * - `1` / `py` → Python only (`{ py: true, js: false }`)
418
- * - `js` → JavaScript only (`{ py: false, js: true }`)
419
- * - `mix` / `both` → both backends (`{ py: true, js: true }`)
420
- *
421
- * Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
422
- * token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
423
- * per-key settings. This matches the documented contract that invalid values
424
- * are ignored.
425
- */
426
- export declare function parseGjcPy(env: Record<string, string | undefined>): {
427
- py: boolean;
428
- js: boolean;
429
- } | null;
430
- /**
431
- * Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
432
- * to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
433
- * Returns `null` when no env override is set so the caller can defer to settings.
434
- */
435
- export declare function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null;
436
- /** Read per-backend allowance from settings (defaults true). */
437
- export declare function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance;
438
- /**
439
- * Materialize the active eval backend allowance. `GJC_PY` takes precedence
440
- * over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
441
- * per-key settings (defaults true). When no env override is set, settings win.
442
- */
443
- export declare function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance;
408
+ export { type EvalBackendsAllowance, parseGjcPy, readEvalBackendsAllowance, resolveEvalBackends, resolveEvalBackendsFromEnv, } from "./eval-backends";
444
409
  /**
445
410
  * Create tools from the descriptor registry.
446
411
  */
@@ -0,0 +1,22 @@
1
+ import { type TaskToolSchemaInstance } from "../task/types";
2
+ interface DescriptionSettings {
3
+ get(key: "task.maxConcurrency" | "task.isolation.mode" | "task.simple" | "task.disabledAgents" | "eval.py" | "eval.js" | "readLineNumbers" | "readHashLines" | "edit.mode"): unknown;
4
+ getEffectiveAutorouting(): {
5
+ active: boolean;
6
+ };
7
+ }
8
+ interface DescriptionSession {
9
+ settings: DescriptionSettings;
10
+ getSessionSpawns?: () => string | null;
11
+ hasEditTool?: boolean;
12
+ }
13
+ export interface EvalToolDescriptionOptions {
14
+ py?: boolean;
15
+ js?: boolean;
16
+ }
17
+ export declare function getEvalToolDescription(options?: EvalToolDescriptionOptions): string;
18
+ export declare function evalToolDescriptionForSession(session: DescriptionSession | null | undefined): string;
19
+ export declare function searchToolDescriptionForSession(session: DescriptionSession): string;
20
+ export declare function renderTaskDescription(session: DescriptionSession): string;
21
+ export declare function taskParametersForSession(session?: DescriptionSession): TaskToolSchemaInstance;
22
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.18.0",
4
+ "version": "0.18.5",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -57,12 +57,12 @@
57
57
  "@agentclientprotocol/sdk": "1.3.0",
58
58
  "@babel/parser": "^7.29.3",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "@gajae-code/stats": "0.18.0",
61
- "@gajae-code/agent-core": "0.18.0",
62
- "@gajae-code/ai": "0.18.0",
63
- "@gajae-code/natives": "0.18.0",
64
- "@gajae-code/tui": "0.18.0",
65
- "@gajae-code/utils": "0.18.0",
60
+ "@gajae-code/stats": "0.18.5",
61
+ "@gajae-code/agent-core": "0.18.5",
62
+ "@gajae-code/ai": "0.18.5",
63
+ "@gajae-code/natives": "0.18.5",
64
+ "@gajae-code/tui": "0.18.5",
65
+ "@gajae-code/utils": "0.18.5",
66
66
  "@puppeteer/browsers": "^2.13.0",
67
67
  "@types/turndown": "5.0.6",
68
68
  "@xterm/headless": "^6.0.0",
@@ -203,6 +203,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
203
203
  "agent_session:queueDeferredMessage": "internal accessor/plumbing, not a user-facing control seam",
204
204
  "agent_session:queueDeferredMessageForTests": "internal accessor/plumbing, not a user-facing control seam",
205
205
  "agent_session:getContextUsageObservabilityForTests": "test-only observability, not a user-facing control seam",
206
+ "agent_session:getAutoCompactionThresholdTokens":
207
+ "internal compaction-threshold accessor feeding the /context breakdown; exposed through context.get, not an independent SDK operation",
206
208
  "agent_session:purgeQueuedCustomMessages": "internal accessor/plumbing, not a user-facing control seam",
207
209
  "agent_session:clearQueue": "internal accessor/plumbing, not a user-facing control seam",
208
210
  "agent_session:popLastQueuedMessage": "internal accessor/plumbing, not a user-facing control seam",
@@ -217,11 +217,11 @@ export async function describeManagedLink(
217
217
  const parent = await fs.lstat(parentPath, { bigint: true }).catch(() => undefined);
218
218
  const trustedReceipt =
219
219
  !!body && ownedReceipt(receipt) && publicCandidates.some(candidate => candidate.path === body.source);
220
+ // The recorded st_dev is not compared: macOS APFS gives the same volume a new st_dev after a reboot,
221
+ // which made every pre-reboot receipt look foreign (#5990). The inode binds the link, and the volume
222
+ // is re-derived from the parent directory observed in the same snapshot.
220
223
  const identityMatches =
221
- !!body &&
222
- !!link &&
223
- body.identity.dev === link.stat.dev.toString() &&
224
- body.identity.ino === link.stat.ino.toString();
224
+ !!body && !!link && body.identity.ino === link.stat.ino.toString() && link.stat.dev === link.parent.dev;
225
225
  const owned = trustedReceipt && (link ? identityMatches : occupant === undefined);
226
226
  const recordedCandidate = body && publicCandidates.find(candidate => candidate.path === body.source);
227
227
  let status: ManagedLinkDescriptor["status"] = "unknown";
@@ -892,7 +892,15 @@ export async function executeSelectedDoctorRepair(
892
892
  outcome =
893
893
  result.kind === "restarted"
894
894
  ? { kind: result.kind, successorIncarnation: result.successor.incarnation }
895
- : { kind: result.kind, ...("reason" in result ? { reason: result.reason } : {}) };
895
+ : {
896
+ kind: result.kind,
897
+ // A broker refusal carries the broker's own code (e.g. restart_busy).
898
+ ...("reason" in result
899
+ ? { reason: result.reason }
900
+ : "code" in result
901
+ ? { reason: result.code }
902
+ : {}),
903
+ };
896
904
  } else {
897
905
  settings = await Settings.loadForScope({ cwd: context.cwd, agentDir: context.agentRoot.locator });
898
906
  const result = await restartDaemonForDoctor({
@@ -70,41 +70,6 @@ export interface StatsCommandArgs {
70
70
  summary: boolean;
71
71
  }
72
72
 
73
- // =============================================================================
74
- // Argument Parser
75
- // =============================================================================
76
-
77
- /**
78
- * Parse stats subcommand arguments.
79
- * Returns undefined if not a stats command.
80
- */
81
- export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
82
- if (args.length === 0 || args[0] !== "stats") {
83
- return undefined;
84
- }
85
-
86
- const result: StatsCommandArgs = {
87
- port: 3847,
88
- json: false,
89
- summary: false,
90
- };
91
-
92
- for (let i = 1; i < args.length; i++) {
93
- const arg = args[i];
94
- if (arg === "--json" || arg === "-j") {
95
- result.json = true;
96
- } else if (arg === "--summary" || arg === "-s") {
97
- result.summary = true;
98
- } else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
99
- result.port = parseInt(args[++i], 10);
100
- } else if (arg.startsWith("--port=")) {
101
- result.port = parseInt(arg.split("=")[1], 10);
102
- }
103
- }
104
-
105
- return result;
106
- }
107
-
108
73
  function formatCost(n: number): string {
109
74
  if (n < 0.01) return `$${n.toFixed(4)}`;
110
75
  if (n < 1) return `$${n.toFixed(3)}`;
@@ -131,7 +96,10 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
131
96
  const { processed, files } = await syncAllSessions({ onProgress: progress.onProgress });
132
97
  progress.finish();
133
98
  const total = await getTotalMessageCount();
134
- console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
99
+ const synced = `Synced ${processed} new entries from ${files} files (${total} total)\n`;
100
+ // `--json` stdout must be exactly the JSON document so it can be piped into a parser.
101
+ if (cmd.json) process.stderr.write(`${synced}\n`);
102
+ else console.log(synced);
135
103
 
136
104
  if (cmd.json) {
137
105
  const stats = await getDashboardStats();
package/src/cli-main.ts CHANGED
@@ -95,14 +95,10 @@ async function showHelp(config: CliConfig): Promise<void> {
95
95
  }
96
96
 
97
97
  async function installRuntimeGlobals(): Promise<void> {
98
- const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
99
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
100
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
101
- // can't toggle from JS, so we patch globalThis.fetch to pass
102
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
103
- // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
104
- installH2Fetch();
105
-
98
+ // fetch() deliberately stays on HTTP/1.1. Bun's HTTP/2 client (per-request
99
+ // `protocol: "http2"`) wedges a pooled connection while uploading large
100
+ // request bodies (~300KB): every later request on it waits forever for
101
+ // response headers. Do not re-enable HTTP/2 without re-running that probe.
106
102
  const { warnIfMacOSNoFileLimitTooLow } = await import("./cli/nofile-limit");
107
103
  warnIfMacOSNoFileLimitTooLow();
108
104
 
package/src/cli.ts CHANGED
@@ -37,14 +37,10 @@ export const commands: CommandEntry[] = [
37
37
  ];
38
38
 
39
39
  async function installRuntimeGlobals(writeNoFileWarning?: (text: string) => void): Promise<void> {
40
- const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
41
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
42
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
43
- // can't toggle from JS, so we patch globalThis.fetch to pass
44
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
45
- // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
46
- installH2Fetch();
47
-
40
+ // fetch() deliberately stays on HTTP/1.1. Bun's HTTP/2 client (per-request
41
+ // `protocol: "http2"`) wedges a pooled connection while uploading large
42
+ // request bodies (~300KB): every later request on it waits forever for
43
+ // response headers. Do not re-enable HTTP/2 without re-running that probe.
48
44
  const { warnIfMacOSNoFileLimitTooLow } = await import("./cli/nofile-limit");
49
45
  warnIfMacOSNoFileLimitTooLow({ writeStderr: writeNoFileWarning });
50
46
 
@@ -32,8 +32,8 @@ export default class Commit extends Command {
32
32
  };
33
33
 
34
34
  await initTheme();
35
- // The agentic commit flow opens HTTP/2 keep-alive sockets to the model
36
- // provider (via `installH2Fetch`) and spins up an AgentSession with
35
+ // The agentic commit flow opens keep-alive sockets to the model
36
+ // provider and spins up an AgentSession with
37
37
  // background async-job + extension machinery. `session.dispose()` releases
38
38
  // what it can, but Bun's fetch keeps idle connections warm and a few
39
39
  // timers (Settings autosave, OAuth refresh) stay armed long enough to
@@ -642,8 +642,7 @@ export async function runSessionHost(
642
642
  },
643
643
  absent.result,
644
644
  );
645
-
646
- throw new Error("SDK startup did not complete before readiness cutoff.");
645
+ return;
647
646
  }
648
647
 
649
648
  const rollback = new SdkStartupRollbackTracker();
@@ -45,8 +45,9 @@ export const TIER_MAP_VERSION = 1;
45
45
 
46
46
  export const CURATED_TIER_LABELS = {
47
47
  "anthropic/claude-haiku-4-5": [{ tier: "fast", rank: 1 }],
48
- "anthropic/claude-sonnet-5": [{ tier: "balanced", rank: 1 }],
49
- "anthropic/claude-sonnet-4-6": [{ tier: "balanced", rank: 2 }],
48
+ "anthropic/claude-sonnet-5-5": [{ tier: "balanced", rank: 1 }],
49
+ "anthropic/claude-sonnet-5": [{ tier: "balanced", rank: 2 }],
50
+ "anthropic/claude-sonnet-4-6": [{ tier: "balanced", rank: 3 }],
50
51
  "anthropic/claude-opus-5-5": [{ tier: "strong", effort: "high", rank: 1 }],
51
52
  "anthropic/claude-opus-5": [{ tier: "strong", effort: "high", rank: 2 }],
52
53
  "anthropic/claude-opus-4-8": [{ tier: "strong", effort: "high", rank: 3 }],
@@ -55,6 +56,7 @@ export const CURATED_TIER_LABELS = {
55
56
  { tier: "balanced", effort: "medium", rank: 1 },
56
57
  ],
57
58
  "openai-codex/gpt-6-sol": [{ tier: "strong", effort: "high", rank: 1 }],
59
+ "openai-codex/gpt-6.1-sol": [{ tier: "strong", effort: "high", rank: 2 }],
58
60
  "google/gemini-3.5-flash-lite": [{ tier: "fast", rank: 1 }],
59
61
  "google/gemini-2.5-flash-lite": [{ tier: "fast", rank: 2 }],
60
62
  "google/gemini-3.5-flash": [{ tier: "balanced", rank: 1 }],
@@ -75,6 +77,22 @@ export const CURATED_TIER_LABELS = {
75
77
 
76
78
  /** Generated from packages/ai/src/models.json at feature land: 3936 in-scope baseline skips. */
77
79
  export const TIER_MAP_SKIP_LIST = {
80
+ "amazon-bedrock/anthropic.claude-sonnet-5-5": { rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated" },
81
+ "amazon-bedrock/au.anthropic.claude-sonnet-5-5": {
82
+ rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
83
+ },
84
+ "amazon-bedrock/eu.anthropic.claude-sonnet-5-5": {
85
+ rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
86
+ },
87
+ "amazon-bedrock/global.anthropic.claude-sonnet-5-5": {
88
+ rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
89
+ },
90
+ "amazon-bedrock/jp.anthropic.claude-sonnet-5-5": {
91
+ rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
92
+ },
93
+ "amazon-bedrock/us.anthropic.claude-sonnet-5-5": {
94
+ rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated",
95
+ },
78
96
  "kiro/claude-opus-5-5": { rationale: "post-rebase catalog addition from dev; not yet curated" },
79
97
  "kiro/claude-opus-5.5": { rationale: "post-rebase catalog addition from dev; not yet curated" },
80
98
  "alibaba-token-plan/qwen3.8-flash": { rationale: "post-rebase catalog addition from dev; not yet curated" },
@@ -652,6 +652,9 @@ function assertRegistryIdentityPolicy(value: unknown): void {
652
652
  }
653
653
 
654
654
  function assertCanonicalUnicode(value: string): void {
655
+ // Native well-formedness check first; the scan below only runs to name the
656
+ // offending surrogate when the string is malformed.
657
+ if (value.isWellFormed()) return;
655
658
  for (let index = 0; index < value.length; index++) {
656
659
  const code = value.charCodeAt(index);
657
660
  if (code >= 0xd800 && code <= 0xdbff) {
@@ -665,6 +668,18 @@ function assertCanonicalUnicode(value: string): void {
665
668
  }
666
669
  }
667
670
 
671
+ /** Characters JSON.stringify escapes in strings (lone surrogates are rejected earlier). */
672
+ const JSON_STRING_ESCAPES = /["\\\u0000-\u001f]/;
673
+
674
+ /**
675
+ * Quote a string exactly as JSON.stringify does. Registry documents are almost
676
+ * entirely plain ASCII identifiers, so skipping the per-call JSON.stringify for
677
+ * strings with nothing to escape removes most of the serializer's cost.
678
+ */
679
+ function quoteCanonicalString(value: string): string {
680
+ return JSON_STRING_ESCAPES.test(value) ? JSON.stringify(value) : `"${value}"`;
681
+ }
682
+
668
683
  function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
669
684
  if (value === null) return "null";
670
685
  if (typeof value === "boolean") return value ? "true" : "false";
@@ -674,7 +689,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
674
689
  }
675
690
  if (typeof value === "string") {
676
691
  assertCanonicalUnicode(value);
677
- return JSON.stringify(value);
692
+ return quoteCanonicalString(value);
678
693
  }
679
694
  if (typeof value !== "object") throw new TypeError(`Canonical JSON rejects ${typeof value}.`);
680
695
  if (stack.has(value)) throw new TypeError("Canonical JSON rejects cycles.");
@@ -694,7 +709,7 @@ function serializeCanonicalJson(value: unknown, stack: Set<object>): string {
694
709
  const entries: string[] = [];
695
710
  for (const key of Object.keys(record).sort()) {
696
711
  assertCanonicalUnicode(key);
697
- entries.push(`${JSON.stringify(key)}:${serializeCanonicalJson(record[key], stack)}`);
712
+ entries.push(`${quoteCanonicalString(key)}:${serializeCanonicalJson(record[key], stack)}`);
698
713
  }
699
714
  return `{${entries.join(",")}}`;
700
715
  } finally {
@@ -17,6 +17,7 @@ import {
17
17
  export { resolveModelProfileName } from "./model-profile-contract";
18
18
 
19
19
  import {
20
+ createRegistrySelectorIndex,
20
21
  GJC_MODEL_ASSIGNMENT_TARGETS,
21
22
  type GjcModelAssignmentTargetId,
22
23
  isAuthenticated,
@@ -675,6 +676,7 @@ export async function resolveMissingSessionModelRecovery(options: {
675
676
  options.skips.every(skip => skip.reason === "unknown_model");
676
677
  if (!allSelectorsUnknown) return undefined;
677
678
  const fullCatalog = options.modelRegistry.getAll();
679
+ const catalogIndex = createRegistrySelectorIndex(fullCatalog);
678
680
  const savedSelectorsMissingFromCatalog = resolveConfiguredModelPatterns(
679
681
  options.defaultEntries,
680
682
  options.settings,
@@ -688,7 +690,7 @@ export async function resolveMissingSessionModelRecovery(options: {
688
690
  }).model
689
691
  )
690
692
  return false;
691
- return !registrySelectorResolvesToModel(selector, fullCatalog);
693
+ return !registrySelectorResolvesToModel(selector, catalogIndex);
692
694
  });
693
695
  const savedConcreteDefault = options.savedDefault ? parseModelString(options.savedDefault) : undefined;
694
696
  const savedConcreteDefaultMissingFromCatalog =
@@ -93,6 +93,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
93
93
  critic: "openai-codex/gpt-6-sol:max",
94
94
  architect: "openai-codex/gpt-6-sol:xhigh",
95
95
  }),
96
+ profile("codex-sol61", ["openai-codex"], {
97
+ default: "openai-codex/gpt-6.1-sol:medium",
98
+ executor: "openai-codex/gpt-6.1-sol:medium",
99
+ planner: "openai-codex/gpt-6.1-sol:high",
100
+ critic: "openai-codex/gpt-6.1-sol:xhigh",
101
+ architect: "openai-codex/gpt-6-astra:xhigh",
102
+ }),
96
103
  profile("lunamaxxing", ["openai-codex"], {
97
104
  default: "openai-codex/gpt-6-luna:medium",
98
105
  executor: "openai-codex/gpt-6-luna:xhigh",
@@ -256,14 +263,14 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
256
263
  }),
257
264
  profile("claude-opus", ["anthropic"], {
258
265
  default: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:xhigh"],
259
- executor: "anthropic/claude-sonnet-5",
266
+ executor: "anthropic/claude-sonnet-5-5",
260
267
  planner: ["anthropic/claude-opus-5-5:medium", "anthropic/claude-opus-4-6:low"],
261
268
  critic: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:high"],
262
269
  architect: ["anthropic/claude-opus-5-5:high", "anthropic/claude-opus-4-6:xhigh"],
263
270
  }),
264
271
  profile("claude-fable", ["anthropic"], {
265
272
  default: "anthropic/claude-fable-5-1:xhigh",
266
- executor: "anthropic/claude-sonnet-5",
273
+ executor: "anthropic/claude-sonnet-5-5",
267
274
  planner: "anthropic/claude-fable-5-1:low",
268
275
  critic: "anthropic/claude-fable-5-1:high",
269
276
  architect: "anthropic/claude-fable-5-1:xhigh",
@@ -491,7 +498,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
491
498
  profile("opus-codex", ["anthropic", "openai-codex"], {
492
499
  default: "anthropic/claude-opus-5-5:medium",
493
500
  executor: "openai-codex/gpt-5.6-terra:low",
494
- planner: "anthropic/claude-sonnet-5",
501
+ planner: "anthropic/claude-sonnet-5-5",
495
502
  critic: "openai-codex/gpt-6-sol:xhigh",
496
503
  architect: "openai-codex/gpt-6-sol:high",
497
504
  }),
@@ -555,6 +562,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
555
562
  "codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
556
563
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
557
564
  "codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
565
+ "codex-sol61": { displayName: "Codex Sol 6.1", providerGroup: "CODEX" },
558
566
  lunamaxxing: { displayName: "LunaMaxxing", providerGroup: "CODEX" },
559
567
  "astra-lite": { displayName: "ASTRA-Lite", providerGroup: "CODEX" },
560
568
  "astra-default": { displayName: "ASTRA-Default", providerGroup: "CODEX" },
@@ -782,26 +782,53 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
782
782
  return metadata;
783
783
  }
784
784
 
785
- export function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
785
+ /**
786
+ * Lowercased lookup keys for a model catalog, so selector resolution is a set
787
+ * lookup instead of a scan that re-lowercases every model id per selector.
788
+ */
789
+ export interface RegistrySelectorIndex {
790
+ /** `id` and `provider/id`. */
791
+ exact: ReadonlySet<string>;
792
+ /** `id` alone. */
793
+ ids: ReadonlySet<string>;
794
+ /** Model ids per provider, for parsed `provider/id` selectors (nested, so no delimiter can collide). */
795
+ providerIds: ReadonlyMap<string, ReadonlySet<string>>;
796
+ /** Every `/`-delimited suffix of each id (`a/b/c` → `b/c`, `c`). */
797
+ idSuffixes: ReadonlySet<string>;
798
+ }
799
+
800
+ export function createRegistrySelectorIndex(models: readonly Model<Api>[]): RegistrySelectorIndex {
801
+ const exact = new Set<string>();
802
+ const ids = new Set<string>();
803
+ const providerIds = new Map<string, Set<string>>();
804
+ const idSuffixes = new Set<string>();
805
+ for (const model of models) {
806
+ const id = model.id.toLowerCase();
807
+ const provider = model.provider.toLowerCase();
808
+ ids.add(id);
809
+ exact.add(id);
810
+ exact.add(`${provider}/${id}`);
811
+ let providerModelIds = providerIds.get(provider);
812
+ if (!providerModelIds) {
813
+ providerModelIds = new Set<string>();
814
+ providerIds.set(provider, providerModelIds);
815
+ }
816
+ providerModelIds.add(id);
817
+ for (let slash = id.indexOf("/"); slash !== -1; slash = id.indexOf("/", slash + 1)) {
818
+ idSuffixes.add(id.slice(slash + 1));
819
+ }
820
+ }
821
+ return { exact, ids, providerIds, idSuffixes };
822
+ }
823
+
824
+ export function registrySelectorResolvesToModel(selector: string, index: RegistrySelectorIndex): boolean {
786
825
  const normalizedSelector = selector.trim().toLowerCase();
787
- if (
788
- models.some(
789
- model =>
790
- model.id.toLowerCase() === normalizedSelector ||
791
- `${model.provider}/${model.id}`.toLowerCase() === normalizedSelector,
792
- )
793
- )
794
- return true;
826
+ if (index.exact.has(normalizedSelector)) return true;
795
827
  const suffix = splitSelectorThinkingSuffix(normalizedSelector);
796
828
  const baseSelector = suffix.thinkingLevel === undefined ? normalizedSelector : suffix.selector;
797
829
  const parsed = parseModelString(baseSelector);
798
- if (parsed)
799
- return models.some(
800
- model => model.provider.toLowerCase() === parsed.provider && model.id.toLowerCase() === parsed.id,
801
- );
802
- return models.some(
803
- model => model.id.toLowerCase() === baseSelector || model.id.toLowerCase().endsWith(`/${baseSelector}`),
804
- );
830
+ if (parsed) return index.providerIds.get(parsed.provider)?.has(parsed.id) ?? false;
831
+ return index.ids.has(baseSelector) || index.idSuffixes.has(baseSelector);
805
832
  }
806
833
 
807
834
  function filterMaterializedRegistryProfiles(
@@ -810,6 +837,7 @@ function filterMaterializedRegistryProfiles(
810
837
  dynamicProviders: ReadonlySet<string>,
811
838
  ): Map<string, ModelProfileDefinition> {
812
839
  const filtered = new Map<string, ModelProfileDefinition>();
840
+ const index = createRegistrySelectorIndex(models);
813
841
  for (const [name, profile] of profiles) {
814
842
  if (
815
843
  profile.source === "registry" &&
@@ -818,7 +846,7 @@ function filterMaterializedRegistryProfiles(
818
846
  return (
819
847
  selectors.length > 0 &&
820
848
  !selectors.some(selector => {
821
- if (registrySelectorResolvesToModel(selector, models)) return true;
849
+ if (registrySelectorResolvesToModel(selector, index)) return true;
822
850
  const suffix = splitSelectorThinkingSuffix(selector);
823
851
  const parsed = parseModelString(suffix.thinkingLevel ? suffix.selector : selector);
824
852
  return parsed !== undefined && dynamicProviders.has(parsed.provider);
@@ -2050,9 +2050,15 @@ export const SETTINGS_SCHEMA = {
2050
2050
  ui: {
2051
2051
  tab: "context",
2052
2052
  label: "Compaction Threshold",
2053
- description: "Percent threshold for context maintenance; set to Default to use legacy reserve-based behavior",
2053
+ description:
2054
+ "Percent threshold for context maintenance; the 300,000-token default cap applies only with adaptive mode disabled (except during context promotion)",
2054
2055
  options: [
2055
- { value: "default", label: "Default", description: "Legacy reserve-based threshold" },
2056
+ {
2057
+ value: "default",
2058
+ label: "Default",
2059
+ description:
2060
+ "300,000-token default cap applies only with adaptive mode disabled (except during context promotion)",
2061
+ },
2056
2062
  { value: "10", label: "10%", description: "Extremely early maintenance" },
2057
2063
  { value: "20", label: "20%", description: "Very early maintenance" },
2058
2064
  { value: "30", label: "30%", description: "Early maintenance" },