github-router 0.3.212 → 0.3.229

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.
@@ -23,12 +23,13 @@ import { Worker } from "node:worker_threads";
23
23
  import { gunzipSync, inflateRawSync } from "node:zlib";
24
24
  import WebSocket from "ws";
25
25
  import { events } from "fetch-event-stream";
26
- import { Type } from "typebox";
26
+ import { Type, Type as Type$1 } from "typebox";
27
27
  import "partial-json";
28
28
  import { Compile } from "typebox/compile";
29
29
  import { Value } from "typebox/value";
30
30
  import "yaml";
31
31
  import "ignore";
32
+ import "diff";
32
33
 
33
34
  //#region rolldown:runtime
34
35
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -637,9 +638,9 @@ function resolveModel(modelId) {
637
638
  if (oneMMatch) {
638
639
  const stripped = oneMMatch[1];
639
640
  const resolved = resolveModel(stripped);
640
- if (!/-1m(?:$|-)/.test(resolved) && !warnedOneMDowngrade.has(modelId)) {
641
+ if (!(/-1m(?:$|-)/.test(resolved) || (models.find((m) => m.id === resolved)?.capabilities?.limits?.max_context_window_tokens ?? 0) >= 1e6) && !warnedOneMDowngrade.has(modelId)) {
641
642
  warnedOneMDowngrade.add(modelId);
642
- consola.warn(`Model "${modelId}" requested 1M context but no -1m backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
643
+ consola.warn(`Model "${modelId}" requested 1M context but no 1M backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
643
644
  }
644
645
  return resolved;
645
646
  }
@@ -649,8 +650,8 @@ function resolveModel(modelId) {
649
650
  if (ciMatch) return ciMatch.id;
650
651
  if (lower.includes("opus")) {
651
652
  const oneMs = models.filter((m) => m.id.includes("opus") && /-1m(?:$|-)/.test(m.id));
652
- const versionMatch = lower.match(/opus-(\d+)[.-](\d+)/);
653
- const requestedVersion = versionMatch ? `${versionMatch[1]}.${versionMatch[2]}` : void 0;
653
+ const versionMatch = lower.match(/opus-(\d+)(?:[.-](\d+))?/);
654
+ const requestedVersion = versionMatch ? versionMatch[2] ? `${versionMatch[1]}.${versionMatch[2]}` : versionMatch[1] : void 0;
654
655
  const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? (requestedVersion ? void 0 : oneMs[0]);
655
656
  if (oneM) return oneM.id;
656
657
  }
@@ -1005,15 +1006,15 @@ function getPackageVersion() {
1005
1006
  //#region src/lib/port.ts
1006
1007
  const DEFAULT_PORT = 8787;
1007
1008
  const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1009
+ "claude-opus-4-8",
1008
1010
  "claude-opus-4-7",
1009
- "claude-opus-4-6",
1010
- "claude-opus-4-5"
1011
+ "claude-opus-4-6"
1011
1012
  ];
1012
1013
  /**
1013
1014
  * Cap-aware default picker for `ANTHROPIC_MODEL` on the implicit-default
1014
1015
  * path. Returns `claude-opus-${family}[1m]` when the live Copilot catalog
1015
1016
  * shows the family is 1M-capable, else the bare `claude-opus-${family}`
1016
- * slug. `family` defaults to `"4.8"` so the no-arg call selects the
1017
+ * slug. `family` defaults to `"5"` so the no-arg call selects the
1017
1018
  * current default; explicit values like `"4.7"` or `"4.6"` are used to
1018
1019
  * honor the `github-router claude -m <version>` family shorthand.
1019
1020
  *
@@ -1027,8 +1028,8 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1027
1028
  * 2. **Base-slug capability signal** — the catalog entry whose id IS
1028
1029
  * the base `opus-${family}` slug advertises
1029
1030
  * `capabilities.limits.max_context_window_tokens >= 1_000_000`. This
1030
- * is how 4.8 ships — there is no `-1m` sibling; the single
1031
- * `claude-opus-4.8` id is the 1M variant.
1031
+ * is how 4.8 and 5 ship — there is no `-1m` sibling; the single
1032
+ * `claude-opus-4.8` / `claude-opus-5` id is itself the 1M variant.
1032
1033
  * Either signal flips on the `[1m]` decoration. Both signals together
1033
1034
  * also flip it on (no double-counting). The breadcrumb log names which
1034
1035
  * signal fired so users can spot catalog shape changes.
@@ -1058,7 +1059,7 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1058
1059
  * can't tell the difference between "no catalog yet" and "no 1M
1059
1060
  * variant" — defaulting safe-side preserves the pre-change behavior).
1060
1061
  */
1061
- const DEFAULT_OPUS_FAMILY = "4.8";
1062
+ const DEFAULT_OPUS_FAMILY = "5";
1062
1063
  const ONE_M_TOKENS = 1e6;
1063
1064
  function pickClaudeDefault(opusFamily = DEFAULT_OPUS_FAMILY) {
1064
1065
  const dotted = opusFamily.replace(/-/g, ".");
@@ -6187,19 +6188,19 @@ function resolveTierModel(tier$1) {
6187
6188
 
6188
6189
  //#endregion
6189
6190
  //#region src/lib/first-mate/classifier.ts
6190
- function isRecord$2(value) {
6191
+ function isRecord$1(value) {
6191
6192
  return typeof value === "object" && value !== null && !Array.isArray(value);
6192
6193
  }
6193
6194
  function firstMessageContent$1(value) {
6194
- if (!isRecord$2(value) || !Array.isArray(value.choices)) return null;
6195
+ if (!isRecord$1(value) || !Array.isArray(value.choices)) return null;
6195
6196
  const first = value.choices[0];
6196
- if (!isRecord$2(first) || !isRecord$2(first.message)) return null;
6197
+ if (!isRecord$1(first) || !isRecord$1(first.message)) return null;
6197
6198
  return typeof first.message.content === "string" ? first.message.content : null;
6198
6199
  }
6199
6200
  function parseJsonObject(value) {
6200
6201
  try {
6201
6202
  const parsed = JSON.parse(value);
6202
- return isRecord$2(parsed) ? parsed : null;
6203
+ return isRecord$1(parsed) ? parsed : null;
6203
6204
  } catch {
6204
6205
  return null;
6205
6206
  }
@@ -6269,7 +6270,7 @@ async function classifyPlanReady(logExcerpt) {
6269
6270
  schemaHint: "{\"planReady\":boolean,\"planExcerpt\":\"<=1200 chars from the completed plan, or empty\",\"confidence\":number}",
6270
6271
  maxTokens: 500,
6271
6272
  validate(value) {
6272
- if (!isRecord$2(value)) return null;
6273
+ if (!isRecord$1(value)) return null;
6273
6274
  const planReady = booleanValue$1(value.planReady);
6274
6275
  const planExcerpt = stringValue$1(value.planExcerpt);
6275
6276
  if (planReady === null || planExcerpt === null) return null;
@@ -6286,7 +6287,7 @@ async function classifyQuestionAnswerable(question, acceptanceCriteria) {
6286
6287
  user: `Acceptance criteria:\n${acceptanceCriteria}\n\nAgent question:\n${question}`,
6287
6288
  schemaHint: "{\"answerable\":boolean,\"answer\":\"present only when answerable\",\"confidence\":number}",
6288
6289
  validate(value) {
6289
- if (!isRecord$2(value)) return null;
6290
+ if (!isRecord$1(value)) return null;
6290
6291
  const answerable = booleanValue$1(value.answerable);
6291
6292
  if (answerable === null) return null;
6292
6293
  const answer = stringValue$1(value.answer);
@@ -6304,7 +6305,7 @@ async function classifyFixAddressed(failureSummary, latestLogExcerpt) {
6304
6305
  user: `Failure summary:\n${failureSummary}\n\nLatest log excerpt:\n${latestLogExcerpt}`,
6305
6306
  schemaHint: "{\"addressed\":boolean,\"confidence\":number}",
6306
6307
  validate(value) {
6307
- if (!isRecord$2(value)) return null;
6308
+ if (!isRecord$1(value)) return null;
6308
6309
  const addressed = booleanValue$1(value.addressed);
6309
6310
  return addressed === null ? null : { addressed };
6310
6311
  }
@@ -6316,7 +6317,7 @@ async function classifyStuck(logExcerpt) {
6316
6317
  user: `Log excerpt:\n${logExcerpt}`,
6317
6318
  schemaHint: "{\"stuck\":boolean,\"confidence\":number}",
6318
6319
  validate(value) {
6319
- if (!isRecord$2(value)) return null;
6320
+ if (!isRecord$1(value)) return null;
6320
6321
  const stuck = booleanValue$1(value.stuck);
6321
6322
  return stuck === null ? null : { stuck };
6322
6323
  }
@@ -12715,7 +12716,7 @@ function parsePackageJson(text) {
12715
12716
  if (text === void 0) return void 0;
12716
12717
  try {
12717
12718
  const parsed = JSON.parse(text);
12718
- if (!isRecord$1(parsed)) return void 0;
12719
+ if (!isRecord(parsed)) return void 0;
12719
12720
  return {
12720
12721
  scripts: stringRecord(parsed.scripts),
12721
12722
  dependencies: stringRecord(parsed.dependencies),
@@ -12727,9 +12728,9 @@ function parsePackageJson(text) {
12727
12728
  return;
12728
12729
  }
12729
12730
  }
12730
- function detectCommands(packageJson, overrides) {
12731
+ function detectCommands(packageJson, overrides$1) {
12731
12732
  const commands = {};
12732
- const pm = overrides?.package_manager ?? packageJson?.packageManager?.split("@")[0];
12733
+ const pm = overrides$1?.package_manager ?? packageJson?.packageManager?.split("@")[0];
12733
12734
  const runner = pm === "bun" || pm === "pnpm" || pm === "yarn" || pm === "npm" ? pm : "npm";
12734
12735
  const scripts = packageJson?.scripts ?? {};
12735
12736
  if (packageJson !== void 0) {
@@ -12742,11 +12743,11 @@ function detectCommands(packageJson, overrides) {
12742
12743
  "dev"
12743
12744
  ]) if (scripts[key] !== void 0) commands[key] = runScriptCommand(runner, key);
12744
12745
  }
12745
- if (overrides?.build_command !== void 0) commands.build = overrides.build_command;
12746
- if (overrides?.typecheck_command !== void 0) commands.typecheck = overrides.typecheck_command;
12747
- if (overrides?.lint_command !== void 0) commands.lint = overrides.lint_command;
12748
- if (overrides?.test_command !== void 0) commands.test = overrides.test_command;
12749
- if (overrides?.dev_command !== void 0) commands.dev = overrides.dev_command;
12746
+ if (overrides$1?.build_command !== void 0) commands.build = overrides$1.build_command;
12747
+ if (overrides$1?.typecheck_command !== void 0) commands.typecheck = overrides$1.typecheck_command;
12748
+ if (overrides$1?.lint_command !== void 0) commands.lint = overrides$1.lint_command;
12749
+ if (overrides$1?.test_command !== void 0) commands.test = overrides$1.test_command;
12750
+ if (overrides$1?.dev_command !== void 0) commands.dev = overrides$1.dev_command;
12750
12751
  return commands;
12751
12752
  }
12752
12753
  function detectPackageManager(rootNames, packageJsonText) {
@@ -12915,11 +12916,11 @@ function buildScaffoldPrBody(reports) {
12915
12916
  "No factory protocol files are seeded; orchestration remains external to the repository."
12916
12917
  ].join("\n");
12917
12918
  }
12918
- function isRecord$1(value) {
12919
+ function isRecord(value) {
12919
12920
  return typeof value === "object" && value !== null && !Array.isArray(value);
12920
12921
  }
12921
12922
  function stringRecord(value) {
12922
- if (!isRecord$1(value)) return {};
12923
+ if (!isRecord(value)) return {};
12923
12924
  const result = {};
12924
12925
  for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") result[key] = entry;
12925
12926
  return result;
@@ -13334,40 +13335,47 @@ function deriveDefinitionName(node) {
13334
13335
  return null;
13335
13336
  }
13336
13337
  /**
13337
- * Collect EVERY definition node from the parse tree top-level AND
13338
- * nested (class methods, methods' inner functions, nested classes, …) —
13339
- * so the outline is a COMPLETE structural map the model can rely on to
13340
- * decide what to read. Recurses through non-definition wrappers (TS
13341
- * `export_statement`, Python `decorated_definition`, C++
13342
- * `template_declaration`, …) at the same depth, and INTO each definition
13343
- * at depth+1 to surface its members.
13344
- *
13345
- * `defTypes` is the language's definition-node-type set. Each node yields
13346
- * one entry; the `name` is derived per `deriveDefinitionName` (a node
13347
- * with no recoverable name is skipped, but the walk still descends into
13348
- * it so its named members aren't lost). Bounded at `MAX_OUTLINE_ENTRIES`.
13349
- */
13338
+ * Collect declarations that help a model navigate a file: top-level symbols
13339
+ * and members of class-like containers. Function-local variables and nested
13340
+ * helpers are deliberately omitted because they inflate the summary without
13341
+ * identifying useful read targets. Non-definition wrappers (exports,
13342
+ * decorators, templates) preserve the surrounding depth.
13343
+ */
13344
+ const OUTLINE_MEMBER_CONTAINERS = new Set([
13345
+ "class_declaration",
13346
+ "class_definition",
13347
+ "class_specifier",
13348
+ "interface_declaration",
13349
+ "annotation_type_declaration",
13350
+ "impl_item",
13351
+ "trait_item",
13352
+ "struct_item",
13353
+ "namespace_definition",
13354
+ "module"
13355
+ ]);
13350
13356
  function collectDefinitions(root, defTypes, signal) {
13351
13357
  const out = [];
13352
- const visit = (node, depth) => {
13358
+ const visit = (node, depth, includeDefinitions) => {
13353
13359
  if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
13354
13360
  for (const child of node.namedChildren) {
13355
13361
  if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
13356
13362
  if (defTypes.has(child.type)) {
13357
- const name = deriveDefinitionName(child);
13358
- if (name !== null) out.push({
13359
- kind: child.type,
13360
- name,
13361
- line: child.startPosition.row + 1,
13362
- depth
13363
- });
13364
- visit(child, depth + 1);
13363
+ if (includeDefinitions) {
13364
+ const name = deriveDefinitionName(child);
13365
+ if (name !== null) out.push({
13366
+ kind: child.type,
13367
+ name,
13368
+ line: child.startPosition.row + 1,
13369
+ depth
13370
+ });
13371
+ }
13372
+ if (includeDefinitions && OUTLINE_MEMBER_CONTAINERS.has(child.type)) visit(child, depth + 1, true);
13365
13373
  continue;
13366
13374
  }
13367
- visit(child, depth);
13375
+ visit(child, depth, includeDefinitions);
13368
13376
  }
13369
13377
  };
13370
- visit(root, 0);
13378
+ visit(root, 0, true);
13371
13379
  return out;
13372
13380
  }
13373
13381
  /**
@@ -13511,10 +13519,10 @@ function confirmDefinitionSites(tree, source, language, hits, signal) {
13511
13519
  return confirmed;
13512
13520
  }
13513
13521
  /**
13514
- * Full structural outline of a single file — EVERY definition, top-level
13515
- * AND nested (functions, classes, methods, nested functions, interfaces,
13516
- * type aliases, enums, including exported / decorated / templated
13517
- * wrappers). Each entry carries a `depth` (0 = top-level). Reuses the
13522
+ * Navigational structural outline of a single file: top-level declarations
13523
+ * plus class-like members (methods, fields, interface members), excluding
13524
+ * function-local variables and nested implementation helpers. Each entry
13525
+ * carries a `depth` (0 = top-level). Reuses the
13518
13526
  * shared grammar bundle and the same `Parser` the structural pass uses
13519
13527
  * — no second `Parser.init()`.
13520
13528
  *
@@ -13679,6 +13687,14 @@ var TreeSitterPool = class {
13679
13687
  spawned: this.workersSpawned
13680
13688
  };
13681
13689
  }
13690
+ /**
13691
+ * Launch-time latency optimization: ready ONE worker and its grammar bundle.
13692
+ * A single worker keeps startup CPU/memory bounded while removing the first
13693
+ * query's initialization wait; normal demand still grows the pool lazily.
13694
+ */
13695
+ async warm() {
13696
+ return await this.ensureWorkers(1) > 0;
13697
+ }
13682
13698
  queue = [];
13683
13699
  inflight = /* @__PURE__ */ new Map();
13684
13700
  constructor() {
@@ -13694,18 +13710,19 @@ var TreeSitterPool = class {
13694
13710
  * callers onto one in-flight ensure so 8 simultaneous searches don't each
13695
13711
  * spawn a fresh batch. Returns the live worker count (0 → caller must fall
13696
13712
  * back to the in-process path). */
13697
- ensureWorkers() {
13713
+ ensureWorkers(target = this.size) {
13698
13714
  if (this.unavailable || this.shuttingDown) return Promise.resolve(0);
13715
+ const desired = Math.max(1, Math.min(target, this.size));
13699
13716
  const liveNow = this.workers.filter((w) => w.ready && w.loaded.size > 0).length;
13700
- if (liveNow >= this.size) return Promise.resolve(liveNow);
13701
- if (this.ensuring) return this.ensuring;
13702
- this.ensuring = this.doEnsureWorkers().finally(() => {
13717
+ if (liveNow >= desired) return Promise.resolve(liveNow);
13718
+ if (this.ensuring) return liveNow > 0 ? Promise.resolve(liveNow) : this.ensuring;
13719
+ this.ensuring = this.doEnsureWorkers(desired).finally(() => {
13703
13720
  this.ensuring = null;
13704
13721
  });
13705
- return this.ensuring;
13722
+ return liveNow > 0 ? Promise.resolve(liveNow) : this.ensuring;
13706
13723
  }
13707
- async doEnsureWorkers() {
13708
- const need = this.size - this.workers.length;
13724
+ async doEnsureWorkers(target) {
13725
+ const need = target - this.workers.length;
13709
13726
  const spawns = [];
13710
13727
  for (let i = 0; i < need; i++) spawns.push(this.spawnWorker());
13711
13728
  const spawned = await Promise.all(spawns);
@@ -13926,13 +13943,23 @@ var TreeSitterPool = class {
13926
13943
  budgetHit: false
13927
13944
  };
13928
13945
  opts.signal.addEventListener("abort", onAbort, { once: true });
13929
- const budgetTimer = setTimeout(() => {
13930
- budgetHit = true;
13931
- stop();
13932
- }, opts.budgetMs);
13933
- budgetTimer.unref?.();
13946
+ let budgetTimer;
13934
13947
  try {
13935
13948
  if (await this.ensureWorkers() === 0) return null;
13949
+ if (opts.signal.aborted) return {
13950
+ byFile,
13951
+ budgetHit: false
13952
+ };
13953
+ if (opts.budgetMs <= 0) {
13954
+ budgetHit = true;
13955
+ stop();
13956
+ } else {
13957
+ budgetTimer = setTimeout(() => {
13958
+ budgetHit = true;
13959
+ stop();
13960
+ }, opts.budgetMs);
13961
+ budgetTimer.unref?.();
13962
+ }
13936
13963
  const dispatchFile = async (job, retried) => {
13937
13964
  if (stopped) return;
13938
13965
  const id = this.nextJobId++;
@@ -13963,7 +13990,7 @@ var TreeSitterPool = class {
13963
13990
  };
13964
13991
  await Promise.all(jobs.map((job) => dispatchFile(job, false)));
13965
13992
  } finally {
13966
- clearTimeout(budgetTimer);
13993
+ if (budgetTimer) clearTimeout(budgetTimer);
13967
13994
  opts.signal.removeEventListener("abort", onAbort);
13968
13995
  }
13969
13996
  const stillLive = this.workers.some((w) => w.ready && w.loaded.size > 0);
@@ -14009,6 +14036,7 @@ function resolveWorkerPath() {
14009
14036
  let _pool = null;
14010
14037
  let _shutdownRegistered = false;
14011
14038
  let _testCrashOnceArmed = false;
14039
+ let _warmOverride;
14012
14040
  /**
14013
14041
  * The pool is ON by default for real (non-CI) runs and OFF under CI.
14014
14042
  *
@@ -14054,6 +14082,24 @@ function getTreeSitterPool() {
14054
14082
  }
14055
14083
  return _pool;
14056
14084
  }
14085
+ /**
14086
+ * Best-effort launch warm-up. Returns immediately when disabled/unavailable and
14087
+ * never rejects; lazy initialization remains the fallback after any failure.
14088
+ */
14089
+ async function warmTreeSitterPool() {
14090
+ if (process.env.GH_ROUTER_DISABLE_TS_POOL_WARMUP === "1") return;
14091
+ const pool = getTreeSitterPool();
14092
+ if (!pool) return;
14093
+ try {
14094
+ if (await (_warmOverride?.(pool) ?? pool.warm())) return;
14095
+ } catch (err) {
14096
+ consola.debug(`[code_search] tree-sitter pool warm-up failed; using lazy initialization: ${err.message}`);
14097
+ }
14098
+ if (_pool === pool) {
14099
+ pool.shutdown();
14100
+ _pool = null;
14101
+ }
14102
+ }
14057
14103
 
14058
14104
  //#endregion
14059
14105
  //#region src/lib/worker-agent/paths.ts
@@ -14256,9 +14302,9 @@ const WALL_TIME_MS = 3e4;
14256
14302
  * Structural-pass settings. The wall-clock budget is checked between
14257
14303
  * files (NOT mid-parse — tree-sitter doesn't surface a usable cancel
14258
14304
  * hook in the web-tree-sitter binding we're on), so a single
14259
- * pathological file can overrun by one file's parse-time. In practice
14260
- * a single source file parses in well under 50ms; 200ms gives us
14261
- * comfortable headroom for ~5-10 files even on cold cache.
14305
+ * pathological file can overrun by one file's parse-time. Worker/grammar
14306
+ * initialization is completed before this timer starts; the 200ms budget
14307
+ * therefore measures parsing only, on cold and warm queries alike.
14262
14308
  */
14263
14309
  const STRUCTURAL_BUDGET_MS = 200;
14264
14310
  let _structuralBudgetTestOverride = null;
@@ -14853,7 +14899,7 @@ async function runStructuralPassPooled(opts) {
14853
14899
  }
14854
14900
  return {
14855
14901
  confirmedHitIndexes,
14856
- fallback: run.budgetHit ? `structural budget exceeded after parsing ${run.byFile.size}/${opts.cap} hits; retry with structural: "topN" or narrow your query` : null,
14902
+ fallback: run.budgetHit ? `structural budget exceeded after parsing ${run.byFile.size}/${jobs.length} files; retry with structural: "topN" or narrow your query` : null,
14857
14903
  outlinesByFile
14858
14904
  };
14859
14905
  }
@@ -15183,26 +15229,23 @@ function dropAstGrepSecrets(env) {
15183
15229
  return env;
15184
15230
  }
15185
15231
  /**
15186
- * Resolve the ast-grep binary. Checks the toolbelt bin dir (where the
15187
- * proxy materializes `sg` + `ast-grep`) AND the system PATH, trying `sg`
15188
- * first then `ast-grep`. Returns an ABSOLUTE path or `null` when neither
15189
- * is found. `resolveExecutable` honors PATHEXT on Windows and excludes
15190
- * the cwd (no planted-`sg.exe` vector). The toolbelt dir is searched by
15191
- * prepending it to a PATH copy so the same resolver handles both sources.
15232
+ * Resolve the ast-grep binary. Checks the known router-owned toolbelt paths
15233
+ * directly before consulting PATH for the unambiguous `ast-grep` name.
15234
+ * Returns an ABSOLUTE path or `null` when neither source has it.
15192
15235
  */
15193
15236
  function resolveAstGrep() {
15194
15237
  const toolbeltDir = PATHS.TOOLBELT_BIN_DIR;
15195
- const sgInToolbelt = resolveExecutable("sg", { env: {
15196
- ...process.env,
15197
- PATH: toolbeltDir
15198
- } });
15199
- if (sgInToolbelt) return sgInToolbelt;
15200
- const astGrep = resolveExecutable("ast-grep", { env: {
15201
- ...process.env,
15202
- PATH: `${toolbeltDir}${path.delimiter}${pathEnvValue()}`
15203
- } });
15204
- if (astGrep) return astGrep;
15205
- return null;
15238
+ const toolbeltNames = process.platform === "win32" ? [
15239
+ "sg.exe",
15240
+ "ast-grep.exe",
15241
+ "sg",
15242
+ "ast-grep"
15243
+ ] : ["sg", "ast-grep"];
15244
+ for (const name of toolbeltNames) {
15245
+ const candidate = path.resolve(toolbeltDir, name);
15246
+ if (existsSync(candidate)) return candidate;
15247
+ }
15248
+ return resolveExecutable("ast-grep", { env: process.env });
15206
15249
  }
15207
15250
  /**
15208
15251
  * Test-only override for the ast-grep resolver. `undefined` = use the real
@@ -15216,11 +15259,6 @@ let _astGrepResolverOverride;
15216
15259
  function resolveAstGrepForRun() {
15217
15260
  return (_astGrepResolverOverride ?? resolveAstGrep)();
15218
15261
  }
15219
- /** Read PATH case-insensitively from the live env. */
15220
- function pathEnvValue() {
15221
- for (const key of Object.keys(process.env)) if (key.toLowerCase() === "path") return process.env[key] ?? "";
15222
- return "";
15223
- }
15224
15262
  /**
15225
15263
  * Run ast-grep with `pattern` over `workspaceCanonical` and return its
15226
15264
  * matches in `RawHit` shape (relativized, 1-indexed). Read-only,
@@ -15242,7 +15280,7 @@ async function runAstGrep(opts) {
15242
15280
  const binary = resolveAstGrepForRun();
15243
15281
  if (!binary) return {
15244
15282
  hits: [],
15245
- notice: "ast_pattern requires ast-grep (sg), which isn't available here; the model can run ast-grep directly or omit ast_pattern"
15283
+ notice: `ast_pattern requires ast-grep, but no executable was found at ${PATHS.TOOLBELT_BIN_DIR} (sg/ast-grep) or on PATH (ast-grep); install ast-grep or omit ast_pattern`
15246
15284
  };
15247
15285
  if (!opts.lang || !/^[A-Za-z0-9_+-]{1,20}$/.test(opts.lang)) return {
15248
15286
  hits: [],
@@ -15823,318 +15861,6 @@ function modelDirName() {
15823
15861
  /** Short model id used in the sidecar metadata `model` field. */
15824
15862
  const MODEL_ID = "LateOn-Code-edge";
15825
15863
 
15826
- //#endregion
15827
- //#region src/lib/colbert/index-store.ts
15828
- const GIT_TIMEOUT_MS = 4e3;
15829
- /** Grace window after a `building` write before a workspace with no live
15830
- * build PID is declared `crashed` — covers the cross-process window where
15831
- * one proxy wrote `building` but hasn't yet recorded the colgrep child PID. */
15832
- const BUILD_SPAWN_GRACE_MS = 3e4;
15833
- /**
15834
- * Hash a workspace path the same way the metadata sidecar is keyed.
15835
- * NOTE: this is the ROUTER-OWNED meta key, independent of colgrep's
15836
- * internal xxh3 physical-dir key (we never need to predict colgrep's
15837
- * key because we pass the workspace as colgrep's PATH arg and let it
15838
- * route). A stable sha256-prefix of the canonical path is sufficient.
15839
- */
15840
- function metaHashForWorkspace(workspace) {
15841
- const canonical = process$1.platform === "win32" ? nodePath.resolve(workspace).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(workspace);
15842
- let h = 2166136261;
15843
- for (let i = 0; i < canonical.length; i++) {
15844
- h ^= canonical.charCodeAt(i);
15845
- h = Math.imul(h, 16777619);
15846
- }
15847
- return (h >>> 0).toString(16).padStart(8, "0");
15848
- }
15849
- function metaPath(workspace) {
15850
- return nodePath.join(PATHS.COLBERT_META_DIR, `${metaHashForWorkspace(workspace)}.json`);
15851
- }
15852
- /** Read the sidecar metadata for a workspace (null if none yet). */
15853
- async function readColbertMeta(workspace) {
15854
- try {
15855
- const raw = await fs.readFile(metaPath(workspace), "utf8");
15856
- const parsed = JSON.parse(raw);
15857
- if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed;
15858
- return null;
15859
- } catch {
15860
- return null;
15861
- }
15862
- }
15863
- /**
15864
- * Per-workspace write serializer. `runInit` issues a pre-spawn
15865
- * `building` write, an `onSpawn` write that patches in the colgrep child
15866
- * PID, and a final `ready`/`failed` write. Chaining them per workspace
15867
- * guarantees the final write lands AFTER the (fire-and-forget) onSpawn
15868
- * write, so a `ready` result is never clobbered back to `building` by a
15869
- * late atomic-rename.
15870
- */
15871
- const _metaWriteChains = /* @__PURE__ */ new Map();
15872
- /** Atomically write the sidecar metadata for a workspace (serialized). */
15873
- async function writeColbertMeta(meta) {
15874
- const key = metaHashForWorkspace(meta.workspace);
15875
- const next = (_metaWriteChains.get(key) ?? Promise.resolve()).then(() => writeColbertMetaUnchained(meta));
15876
- _metaWriteChains.set(key, next.then(() => void 0, () => void 0));
15877
- return next;
15878
- }
15879
- async function writeColbertMetaUnchained(meta) {
15880
- await fs.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
15881
- const dest = metaPath(meta.workspace);
15882
- const tmp = `${dest}.${process$1.pid}.${Math.random().toString(16).slice(2, 10)}.tmp`;
15883
- try {
15884
- await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
15885
- await fs.rename(tmp, dest);
15886
- } catch (err) {
15887
- await fs.rm(tmp, { force: true }).catch(() => {});
15888
- throw err;
15889
- }
15890
- }
15891
- /**
15892
- * Whether a COMPLETED colgrep index exists on disk for this workspace.
15893
- * The preflight uses this to distinguish `building`/`absent` (no
15894
- * completed index → don't spawn a foreground colgrep) from a real
15895
- * index. We scan COLGREP_DATA_DIR for any per-project dir containing a
15896
- * `project.json` whose canonical path matches this workspace AND an
15897
- * `index/metadata.json` marker.
15898
- */
15899
- async function completedIndexOnDisk(workspace) {
15900
- const indicesDir = PATHS.COLBERT_INDICES_DIR;
15901
- let names;
15902
- try {
15903
- names = await fs.readdir(indicesDir);
15904
- } catch {
15905
- return false;
15906
- }
15907
- const wantCanonical = await realpathForCompare(workspace);
15908
- for (const name of names) {
15909
- if (name === ".gh-router-meta") continue;
15910
- const projJson = nodePath.join(indicesDir, name, "project.json");
15911
- let proj;
15912
- try {
15913
- proj = JSON.parse(await fs.readFile(projJson, "utf8"));
15914
- } catch {
15915
- continue;
15916
- }
15917
- const projPath = proj.path ?? proj.project_path;
15918
- if (!projPath) continue;
15919
- if (await realpathForCompare(projPath) !== wantCanonical) continue;
15920
- if (existsSync(nodePath.join(indicesDir, name, "index", "metadata.json"))) return true;
15921
- if (existsSync(nodePath.join(indicesDir, name, "index"))) try {
15922
- if ((await fs.readdir(nodePath.join(indicesDir, name, "index"))).length > 0) return true;
15923
- } catch {}
15924
- }
15925
- return false;
15926
- }
15927
- function canonicalForCompare(p) {
15928
- return process$1.platform === "win32" ? nodePath.resolve(p).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(p);
15929
- }
15930
- /** Sync realpath-aware canonicalization (sibling of `realpathForCompare`,
15931
- * for the on-a-timer inactivity probe which must be synchronous). */
15932
- function canonicalRealpathSync(p) {
15933
- try {
15934
- return canonicalForCompare(realpathSync(p));
15935
- } catch {
15936
- return canonicalForCompare(p);
15937
- }
15938
- }
15939
- /** Recursive (bytes, fileCount) of a directory; sync + best-effort. A
15940
- * colgrep index is a bounded set of shards so the walk stays small. */
15941
- function dirSizeSync(dir) {
15942
- let bytes = 0;
15943
- let count = 0;
15944
- let entries;
15945
- try {
15946
- entries = readdirSync(dir, { withFileTypes: true });
15947
- } catch {
15948
- return [0, 0];
15949
- }
15950
- for (const e of entries) {
15951
- const p = nodePath.join(dir, e.name);
15952
- if (e.isDirectory()) {
15953
- const [b, c] = dirSizeSync(p);
15954
- bytes += b;
15955
- count += c;
15956
- } else try {
15957
- bytes += statSync(p).size;
15958
- count += 1;
15959
- } catch {}
15960
- }
15961
- return [bytes, count];
15962
- }
15963
- /**
15964
- * (sync) Progress signature of a workspace's colgrep index dir for the init
15965
- * inactivity watchdog: `${totalBytes}:${fileCount}` of the project dir, or
15966
- * `null` if it isn't on disk yet. colgrep is SILENT on a non-TTY pipe
15967
- * during the (potentially multi-hour) encode phase, so output is useless as
15968
- * a progress signal — but it writes index shards incrementally, so a
15969
- * changing signature means "still progressing" and a frozen one means
15970
- * "hung". Successive signatures drive the watchdog: change ⇒ re-arm, frozen
15971
- * ⇒ kill. Sync because it's called from a `setTimeout` (not awaited).
15972
- */
15973
- function indexDirSignature(workspace) {
15974
- const indicesDir = PATHS.COLBERT_INDICES_DIR;
15975
- let names;
15976
- try {
15977
- names = readdirSync(indicesDir);
15978
- } catch {
15979
- return null;
15980
- }
15981
- const want = canonicalRealpathSync(workspace);
15982
- for (const name of names) {
15983
- if (name === ".gh-router-meta") continue;
15984
- const dir = nodePath.join(indicesDir, name);
15985
- let proj;
15986
- try {
15987
- proj = JSON.parse(readFileSync(nodePath.join(dir, "project.json"), "utf8"));
15988
- } catch {
15989
- continue;
15990
- }
15991
- const projPath = proj.path ?? proj.project_path;
15992
- if (!projPath || canonicalRealpathSync(projPath) !== want) continue;
15993
- const [bytes, count] = dirSizeSync(dir);
15994
- return `${bytes}:${count}`;
15995
- }
15996
- return null;
15997
- }
15998
- /**
15999
- * Realpath-aware canonicalization for matching a workspace against
16000
- * colgrep's stored `project_path`. colgrep stores the OS realpath (e.g.
16001
- * macOS `/tmp` → `/private/tmp`, Windows 8.3 short names), so a plain
16002
- * `path.resolve` comparison misses. Falls back to `canonicalForCompare`
16003
- * when realpath fails (path doesn't exist yet).
16004
- */
16005
- async function realpathForCompare(p) {
16006
- try {
16007
- return canonicalForCompare(await fs.realpath(p));
16008
- } catch {
16009
- return canonicalForCompare(p);
16010
- }
16011
- }
16012
- /**
16013
- * Compute the freshness verdict for a query against a workspace.
16014
- *
16015
- * Routing (per the dropped-fallback contract):
16016
- * - `failed` — sidecar says failed → caller returns isError.
16017
- * - `building` — a tracked init is live OR no completed index on disk
16018
- * → caller returns building notice (NO results).
16019
- * - `absent` — never indexed → caller kicks a debounced background
16020
- * init, returns absent (isError).
16021
- * - `stale` — ready but HEAD moved / tree dirty since index → caller
16022
- * returns stale notice (NO results, NO re-search).
16023
- * - `fresh` — ready + completed index + HEAD matches + not newly
16024
- * dirty → caller spawns colgrep search.
16025
- */
16026
- async function freshnessVerdict(workspace) {
16027
- const meta = await readColbertMeta(workspace);
16028
- if (!meta || meta.status === "absent") return {
16029
- verdict: "absent",
16030
- meta
16031
- };
16032
- if (meta.status === "failed") return {
16033
- verdict: "failed",
16034
- meta
16035
- };
16036
- if (meta.status === "building") {
16037
- const pid = typeof meta.buildPid === "number" ? meta.buildPid : 0;
16038
- if (isInitInFlight(workspace) || pid > 0 && isPidAlive(pid)) return {
16039
- verdict: "building",
16040
- meta
16041
- };
16042
- const startedMs = meta.lastIndexedAt ? Date.parse(meta.lastIndexedAt) : NaN;
16043
- if (Number.isFinite(startedMs) && Date.now() - startedMs < BUILD_SPAWN_GRACE_MS) return {
16044
- verdict: "building",
16045
- meta
16046
- };
16047
- if (!await completedIndexOnDisk(workspace)) return {
16048
- verdict: "crashed",
16049
- meta
16050
- };
16051
- }
16052
- if (!await completedIndexOnDisk(workspace)) return {
16053
- verdict: "building",
16054
- meta
16055
- };
16056
- const git = await gitState(workspace);
16057
- if (!git.isRepo) return {
16058
- verdict: "fresh",
16059
- meta
16060
- };
16061
- const headMoved = meta.lastIndexedHead !== void 0 && git.head !== meta.lastIndexedHead;
16062
- const newlyDirty = git.dirty && meta.lastIndexedDirty !== true;
16063
- if (headMoved || newlyDirty) return {
16064
- verdict: "stale",
16065
- meta,
16066
- head: git.head,
16067
- dirty: git.dirty
16068
- };
16069
- return {
16070
- verdict: "fresh",
16071
- meta,
16072
- head: git.head,
16073
- dirty: git.dirty
16074
- };
16075
- }
16076
- /** Cheap, bounded git probe via the native-exe runner. */
16077
- async function gitState(workspace) {
16078
- const git = resolveExecutable("git");
16079
- if (!git) return { isRepo: false };
16080
- try {
16081
- const inside = await runManagedExeCapture(git, [
16082
- "-C",
16083
- workspace,
16084
- "rev-parse",
16085
- "--is-inside-work-tree"
16086
- ], {
16087
- timeoutMs: GIT_TIMEOUT_MS,
16088
- maxStdoutBytes: 64 * 1024
16089
- });
16090
- if (inside.code !== 0 || inside.stdout.trim() !== "true") return { isRepo: false };
16091
- const head = await runManagedExeCapture(git, [
16092
- "-C",
16093
- workspace,
16094
- "rev-parse",
16095
- "HEAD"
16096
- ], {
16097
- timeoutMs: GIT_TIMEOUT_MS,
16098
- maxStdoutBytes: 64 * 1024
16099
- });
16100
- const status = await runManagedExeCapture(git, [
16101
- "-C",
16102
- workspace,
16103
- "status",
16104
- "--porcelain"
16105
- ], {
16106
- timeoutMs: GIT_TIMEOUT_MS,
16107
- maxStdoutBytes: 1024 * 1024
16108
- });
16109
- return {
16110
- isRepo: true,
16111
- head: head.code === 0 ? head.stdout.trim() || void 0 : void 0,
16112
- dirty: status.code === 0 ? status.stdout.trim().length > 0 : void 0
16113
- };
16114
- } catch {
16115
- return { isRepo: false };
16116
- }
16117
- }
16118
- const _initInFlight = /* @__PURE__ */ new Set();
16119
- /** True iff a background init for this workspace is already in flight. */
16120
- function isInitInFlight(workspace) {
16121
- return _initInFlight.has(initKey(workspace));
16122
- }
16123
- /** Mark a background init started (debounce). Returns false if already running. */
16124
- function tryClaimInit(workspace) {
16125
- const k = initKey(workspace);
16126
- if (_initInFlight.has(k)) return false;
16127
- _initInFlight.add(k);
16128
- return true;
16129
- }
16130
- /** Release the debounce claim (call in the init's finally). */
16131
- function releaseInit(workspace) {
16132
- _initInFlight.delete(initKey(workspace));
16133
- }
16134
- function initKey(workspace) {
16135
- return `${MODEL_ID}::${canonicalForCompare(workspace)}`;
16136
- }
16137
-
16138
15864
  //#endregion
16139
15865
  //#region src/lib/toolbelt/extract.ts
16140
15866
  function baseName(p) {
@@ -16167,13 +15893,13 @@ function baseName(p) {
16167
15893
  */
16168
15894
  async function extractTarXzMember(buf, wantBasename, tmpDir) {
16169
15895
  const { spawn: spawn$1 } = await import("node:child_process");
16170
- const fs$1 = await import("node:fs/promises");
15896
+ const fs$2 = await import("node:fs/promises");
16171
15897
  const path$1 = await import("node:path");
16172
15898
  const archivePath = path$1.join(tmpDir, "archive.tar.xz");
16173
15899
  const extractDir = path$1.join(tmpDir, "x");
16174
15900
  try {
16175
- await fs$1.mkdir(extractDir, { recursive: true });
16176
- await fs$1.writeFile(archivePath, buf);
15901
+ await fs$2.mkdir(extractDir, { recursive: true });
15902
+ await fs$2.writeFile(archivePath, buf);
16177
15903
  } catch {
16178
15904
  return null;
16179
15905
  }
@@ -16210,25 +15936,25 @@ async function extractTarXzMember(buf, wantBasename, tmpDir) {
16210
15936
  resolve(code === 0);
16211
15937
  });
16212
15938
  })) return null;
16213
- const found = await findRegularFile(fs$1, path$1, extractDir, new Set([wantBasename, `${wantBasename}.exe`]), 6);
15939
+ const found = await findRegularFile(fs$2, path$1, extractDir, new Set([wantBasename, `${wantBasename}.exe`]), 6);
16214
15940
  if (!found) return null;
16215
15941
  try {
16216
- return await fs$1.readFile(found);
15942
+ return await fs$2.readFile(found);
16217
15943
  } catch {
16218
15944
  return null;
16219
15945
  }
16220
15946
  }
16221
- async function findRegularFile(fs$1, path$1, dir, wants, depthBudget) {
15947
+ async function findRegularFile(fs$2, path$1, dir, wants, depthBudget) {
16222
15948
  if (depthBudget < 0) return null;
16223
15949
  let entries;
16224
15950
  try {
16225
- entries = await fs$1.readdir(dir, { withFileTypes: true });
15951
+ entries = await fs$2.readdir(dir, { withFileTypes: true });
16226
15952
  } catch {
16227
15953
  return null;
16228
15954
  }
16229
15955
  for (const e of entries) if (e.isFile() && wants.has(e.name)) return path$1.join(dir, e.name);
16230
15956
  for (const e of entries) if (e.isDirectory()) {
16231
- const hit = await findRegularFile(fs$1, path$1, path$1.join(dir, e.name), wants, depthBudget - 1);
15957
+ const hit = await findRegularFile(fs$2, path$1, path$1.join(dir, e.name), wants, depthBudget - 1);
16232
15958
  if (hit) return hit;
16233
15959
  }
16234
15960
  return null;
@@ -16339,6 +16065,14 @@ function colgrepBinaryPath() {
16339
16065
  function colbertModelDir() {
16340
16066
  return nodePath.join(PATHS.COLBERT_MODELS_DIR, "LateOn-Code-edge", modelDirName());
16341
16067
  }
16068
+ /**
16069
+ * Canonical model argument for every colgrep invocation and project lookup.
16070
+ * colgrep hashes the raw model string, so slash variants fork one physical
16071
+ * index into distinct project keys on Windows.
16072
+ */
16073
+ function canonicalColbertModelDir() {
16074
+ return nodePath.resolve(colbertModelDir());
16075
+ }
16342
16076
  /** Absolute path the provisioned ORT dylib lives at. */
16343
16077
  function colbertOrtDylibPath() {
16344
16078
  const lib = ortLibAsset()?.member ?? "libonnxruntime.so";
@@ -16461,7 +16195,7 @@ async function provisionColbert() {
16461
16195
  result.status = "incomplete";
16462
16196
  });
16463
16197
  if (result.binaryPath && result.ortDylibPath && result.modelDir) {
16464
- const smoke = await runSmokeTest(result.binaryPath, result.ortDylibPath, result.modelDir);
16198
+ const smoke = await runSmokeTest(result.binaryPath, result.ortDylibPath);
16465
16199
  if (smoke.ok) {
16466
16200
  await writeFile(smokeMarkerPath(), expectedSmokeMarker()).catch(() => {});
16467
16201
  result.status = "ready";
@@ -16590,7 +16324,7 @@ async function sidecarMatches(sidecar, sha256) {
16590
16324
  * scanning stderr for colgrep's exact "ignoring" warning — if present,
16591
16325
  * the dylib didn't load and we fail the smoke test even on exit 0.
16592
16326
  */
16593
- async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16327
+ async function runSmokeTest(binaryPath, ortDylibPath) {
16594
16328
  const tmp = nodePath.join(PATHS.COLBERT_DIR, `smoke-${process$1.pid}-${randomBytes(4).toString("hex")}`);
16595
16329
  const fixtureDir = nodePath.join(tmp, "fixture");
16596
16330
  const dataDir = nodePath.join(tmp, "data");
@@ -16619,7 +16353,7 @@ async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16619
16353
  "never",
16620
16354
  "--force-cpu",
16621
16355
  "--model",
16622
- modelDir,
16356
+ canonicalColbertModelDir(),
16623
16357
  "-y",
16624
16358
  "-k",
16625
16359
  "1",
@@ -16657,6 +16391,395 @@ async function runSmokeTest(binaryPath, ortDylibPath, modelDir) {
16657
16391
  }
16658
16392
  }
16659
16393
 
16394
+ //#endregion
16395
+ //#region src/lib/colbert/index-store.ts
16396
+ const GIT_TIMEOUT_MS = 4e3;
16397
+ /** Grace window after a `building` write before a workspace with no live
16398
+ * build PID is declared `crashed` — covers the cross-process window where
16399
+ * one proxy wrote `building` but hasn't yet recorded the colgrep child PID. */
16400
+ const BUILD_SPAWN_GRACE_MS = 3e4;
16401
+ /**
16402
+ * Hash a workspace path the same way the metadata sidecar is keyed.
16403
+ * NOTE: this is the ROUTER-OWNED meta key, independent of colgrep's
16404
+ * internal xxh3 physical-dir key (we never need to predict colgrep's
16405
+ * key because we pass the workspace as colgrep's PATH arg and let it
16406
+ * route). A stable sha256-prefix of the canonical path is sufficient.
16407
+ */
16408
+ function metaHashForWorkspace(workspace) {
16409
+ const canonical = process$1.platform === "win32" ? nodePath.resolve(workspace).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(workspace);
16410
+ let h = 2166136261;
16411
+ for (let i = 0; i < canonical.length; i++) {
16412
+ h ^= canonical.charCodeAt(i);
16413
+ h = Math.imul(h, 16777619);
16414
+ }
16415
+ return (h >>> 0).toString(16).padStart(8, "0");
16416
+ }
16417
+ function metaPath(workspace) {
16418
+ return nodePath.join(PATHS.COLBERT_META_DIR, `${metaHashForWorkspace(workspace)}.json`);
16419
+ }
16420
+ /** Read the sidecar metadata for a workspace (null if none yet). */
16421
+ async function readColbertMeta(workspace) {
16422
+ try {
16423
+ const raw = await fs.readFile(metaPath(workspace), "utf8");
16424
+ const parsed = JSON.parse(raw);
16425
+ if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed;
16426
+ return null;
16427
+ } catch {
16428
+ return null;
16429
+ }
16430
+ }
16431
+ /**
16432
+ * Per-workspace write serializer. `runInit` issues a pre-spawn
16433
+ * `building` write, an `onSpawn` write that patches in the colgrep child
16434
+ * PID, and a final `ready`/`failed` write. Chaining them per workspace
16435
+ * guarantees the final write lands AFTER the (fire-and-forget) onSpawn
16436
+ * write, so a `ready` result is never clobbered back to `building` by a
16437
+ * late atomic-rename.
16438
+ */
16439
+ const _metaWriteChains = /* @__PURE__ */ new Map();
16440
+ /** Atomically write the sidecar metadata for a workspace (serialized). */
16441
+ async function writeColbertMeta(meta) {
16442
+ const key = metaHashForWorkspace(meta.workspace);
16443
+ const next = (_metaWriteChains.get(key) ?? Promise.resolve()).then(() => writeColbertMetaUnchained(meta));
16444
+ _metaWriteChains.set(key, next.then(() => void 0, () => void 0));
16445
+ return next;
16446
+ }
16447
+ async function writeColbertMetaUnchained(meta) {
16448
+ await fs.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
16449
+ const dest = metaPath(meta.workspace);
16450
+ const tmp = `${dest}.${process$1.pid}.${Math.random().toString(16).slice(2, 10)}.tmp`;
16451
+ try {
16452
+ await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
16453
+ await fs.rename(tmp, dest);
16454
+ } catch (err) {
16455
+ await fs.rm(tmp, { force: true }).catch(() => {});
16456
+ throw err;
16457
+ }
16458
+ }
16459
+ /**
16460
+ * Whether a COMPLETED colgrep index exists on disk for this workspace.
16461
+ * The preflight uses this to distinguish `building`/`absent` (no
16462
+ * completed index → don't spawn a foreground colgrep) from a real
16463
+ * index. We scan COLGREP_DATA_DIR for any per-project dir containing a
16464
+ * `project.json` whose canonical path matches this workspace AND an
16465
+ * `index/metadata.json` marker.
16466
+ */
16467
+ async function colbertProjectDir(workspace) {
16468
+ const indicesDir = PATHS.COLBERT_INDICES_DIR;
16469
+ let names;
16470
+ try {
16471
+ names = await fs.readdir(indicesDir);
16472
+ } catch {
16473
+ return null;
16474
+ }
16475
+ const wantCanonical = await realpathForCompare(workspace);
16476
+ for (const name of names) {
16477
+ if (name === ".gh-router-meta" || name.includes(".corrupt-")) continue;
16478
+ const dir = nodePath.join(indicesDir, name);
16479
+ let proj;
16480
+ try {
16481
+ proj = JSON.parse(await fs.readFile(nodePath.join(dir, "project.json"), "utf8"));
16482
+ } catch {
16483
+ continue;
16484
+ }
16485
+ const projPath = proj.path ?? proj.project_path;
16486
+ if (!projPath || await realpathForCompare(projPath) !== wantCanonical) continue;
16487
+ if (proj.model !== canonicalColbertModelDir()) continue;
16488
+ return dir;
16489
+ }
16490
+ return null;
16491
+ }
16492
+ /** Validate the contiguous embedding interval encoded by PLAID shard metadata. */
16493
+ function validateIndexIntegrity(projectIndexDir) {
16494
+ let names;
16495
+ try {
16496
+ names = readdirSync(projectIndexDir).filter((name) => /^\d+\.metadata\.json$/.test(name));
16497
+ } catch (err) {
16498
+ return err.code === "ENOENT" ? { verdict: "not-built" } : {
16499
+ verdict: "corrupt",
16500
+ reason: "index directory unreadable"
16501
+ };
16502
+ }
16503
+ if (names.length === 0) return { verdict: "not-built" };
16504
+ const intervals = [];
16505
+ for (const name of names) try {
16506
+ const parsed = JSON.parse(readFileSync(nodePath.join(projectIndexDir, name), "utf8"));
16507
+ const start = parsed.embedding_offset;
16508
+ const count = parsed.num_embeddings;
16509
+ if (typeof start !== "number" || typeof count !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(count) || start < 0 || count < 0) return {
16510
+ verdict: "corrupt",
16511
+ reason: `invalid shard metadata: ${name}`
16512
+ };
16513
+ intervals.push({
16514
+ start,
16515
+ count
16516
+ });
16517
+ } catch {
16518
+ return {
16519
+ verdict: "corrupt",
16520
+ reason: `unreadable shard metadata: ${name}`
16521
+ };
16522
+ }
16523
+ intervals.sort((a, b) => a.start - b.start || a.count - b.count);
16524
+ let cursorEnd = 0;
16525
+ let sum = 0;
16526
+ let gapped = false;
16527
+ for (const interval of intervals) {
16528
+ if (interval.start < cursorEnd) return {
16529
+ verdict: "corrupt",
16530
+ reason: "overlapping shard intervals"
16531
+ };
16532
+ if (interval.start > cursorEnd) gapped = true;
16533
+ cursorEnd = interval.start + interval.count;
16534
+ sum += interval.count;
16535
+ }
16536
+ if (gapped) return {
16537
+ verdict: "suspect",
16538
+ reason: "gap between shard intervals"
16539
+ };
16540
+ return {
16541
+ verdict: "coherent",
16542
+ shardCount: intervals.length,
16543
+ embeddingCount: sum
16544
+ };
16545
+ }
16546
+ async function completedIndexOnDisk(workspace) {
16547
+ const projectDir = await colbertProjectDir(workspace);
16548
+ if (!projectDir) return false;
16549
+ return validateIndexIntegrity(nodePath.join(projectDir, "index")).verdict !== "not-built";
16550
+ }
16551
+ function canonicalForCompare(p) {
16552
+ return process$1.platform === "win32" ? nodePath.resolve(p).toLowerCase().replace(/\\/g, "/") : nodePath.resolve(p);
16553
+ }
16554
+ /** Sync realpath-aware canonicalization (sibling of `realpathForCompare`,
16555
+ * for the on-a-timer inactivity probe which must be synchronous). */
16556
+ function canonicalRealpathSync(p) {
16557
+ try {
16558
+ return canonicalForCompare(realpathSync(p));
16559
+ } catch {
16560
+ return canonicalForCompare(p);
16561
+ }
16562
+ }
16563
+ /** Recursive (bytes, fileCount) of a directory; sync + best-effort. A
16564
+ * colgrep index is a bounded set of shards so the walk stays small. */
16565
+ function dirSizeSync(dir) {
16566
+ let bytes = 0;
16567
+ let count = 0;
16568
+ let entries;
16569
+ try {
16570
+ entries = readdirSync(dir, { withFileTypes: true });
16571
+ } catch {
16572
+ return [0, 0];
16573
+ }
16574
+ for (const e of entries) {
16575
+ const p = nodePath.join(dir, e.name);
16576
+ if (e.isDirectory()) {
16577
+ const [b, c] = dirSizeSync(p);
16578
+ bytes += b;
16579
+ count += c;
16580
+ } else try {
16581
+ bytes += statSync(p).size;
16582
+ count += 1;
16583
+ } catch {}
16584
+ }
16585
+ return [bytes, count];
16586
+ }
16587
+ /**
16588
+ * (sync) Progress signature of a workspace's colgrep index dir for the init
16589
+ * inactivity watchdog: `${totalBytes}:${fileCount}` of the project dir, or
16590
+ * `null` if it isn't on disk yet. colgrep is SILENT on a non-TTY pipe
16591
+ * during the (potentially multi-hour) encode phase, so output is useless as
16592
+ * a progress signal — but it writes index shards incrementally, so a
16593
+ * changing signature means "still progressing" and a frozen one means
16594
+ * "hung". Successive signatures drive the watchdog: change ⇒ re-arm, frozen
16595
+ * ⇒ kill. Sync because it's called from a `setTimeout` (not awaited).
16596
+ */
16597
+ function indexDirSignature(workspace) {
16598
+ const indicesDir = PATHS.COLBERT_INDICES_DIR;
16599
+ let names;
16600
+ try {
16601
+ names = readdirSync(indicesDir);
16602
+ } catch {
16603
+ return null;
16604
+ }
16605
+ const want = canonicalRealpathSync(workspace);
16606
+ for (const name of names) {
16607
+ if (name === ".gh-router-meta") continue;
16608
+ if (name.includes(".corrupt-")) continue;
16609
+ const dir = nodePath.join(indicesDir, name);
16610
+ let proj;
16611
+ try {
16612
+ proj = JSON.parse(readFileSync(nodePath.join(dir, "project.json"), "utf8"));
16613
+ } catch {
16614
+ continue;
16615
+ }
16616
+ const projPath = proj.path ?? proj.project_path;
16617
+ if (!projPath || canonicalRealpathSync(projPath) !== want) continue;
16618
+ const [bytes, count] = dirSizeSync(dir);
16619
+ return `${bytes}:${count}`;
16620
+ }
16621
+ return null;
16622
+ }
16623
+ /**
16624
+ * Realpath-aware canonicalization for matching a workspace against
16625
+ * colgrep's stored `project_path`. colgrep stores the OS realpath (e.g.
16626
+ * macOS `/tmp` → `/private/tmp`, Windows 8.3 short names), so a plain
16627
+ * `path.resolve` comparison misses. Falls back to `canonicalForCompare`
16628
+ * when realpath fails (path doesn't exist yet).
16629
+ */
16630
+ async function realpathForCompare(p) {
16631
+ try {
16632
+ return canonicalForCompare(await fs.realpath(p));
16633
+ } catch {
16634
+ return canonicalForCompare(p);
16635
+ }
16636
+ }
16637
+ /**
16638
+ * Compute the freshness verdict for a query against a workspace.
16639
+ *
16640
+ * Routing (per the dropped-fallback contract):
16641
+ * - `failed` — sidecar says failed → caller returns isError.
16642
+ * - `building` — a tracked init is live OR no completed index on disk
16643
+ * → caller returns building notice (NO results).
16644
+ * - `absent` — never indexed → caller kicks a debounced background
16645
+ * init, returns absent (isError).
16646
+ * - `stale` — ready but HEAD moved / tree dirty since index → caller
16647
+ * returns stale notice (NO results, NO re-search).
16648
+ * - `fresh` — ready + completed index + HEAD matches + not newly
16649
+ * dirty → caller spawns colgrep search.
16650
+ */
16651
+ async function freshnessVerdict(workspace) {
16652
+ const meta = await readColbertMeta(workspace);
16653
+ if (!meta || meta.status === "absent") return {
16654
+ verdict: "absent",
16655
+ meta
16656
+ };
16657
+ if (meta.status === "failed") return {
16658
+ verdict: "failed",
16659
+ meta
16660
+ };
16661
+ if (meta.status === "building") {
16662
+ const pid = typeof meta.buildPid === "number" ? meta.buildPid : 0;
16663
+ if (isInitInFlight(workspace) || pid > 0 && isPidAlive(pid)) return {
16664
+ verdict: "building",
16665
+ meta
16666
+ };
16667
+ const startedMs = meta.lastIndexedAt ? Date.parse(meta.lastIndexedAt) : NaN;
16668
+ if (Number.isFinite(startedMs) && Date.now() - startedMs < BUILD_SPAWN_GRACE_MS) return {
16669
+ verdict: "building",
16670
+ meta
16671
+ };
16672
+ if (!await completedIndexOnDisk(workspace)) return {
16673
+ verdict: "crashed",
16674
+ meta
16675
+ };
16676
+ }
16677
+ const projectDir = await colbertProjectDir(workspace);
16678
+ if (!projectDir) return {
16679
+ verdict: "building",
16680
+ meta
16681
+ };
16682
+ const integrity = validateIndexIntegrity(nodePath.join(projectDir, "index"));
16683
+ if (integrity.verdict === "not-built") return {
16684
+ verdict: "building",
16685
+ meta
16686
+ };
16687
+ if (integrity.verdict === "corrupt") return {
16688
+ verdict: "corrupt",
16689
+ meta
16690
+ };
16691
+ if (integrity.verdict === "suspect") return {
16692
+ verdict: "stale",
16693
+ meta
16694
+ };
16695
+ const binarySha = colgrepBinAsset()?.sha256;
16696
+ const ortSha = ortLibAsset()?.sha256;
16697
+ if (binarySha && meta.binarySha !== binarySha || ortSha && meta.ortSha !== ortSha) return {
16698
+ verdict: "stale",
16699
+ meta
16700
+ };
16701
+ const git = await gitState(workspace);
16702
+ if (!git.isRepo) return {
16703
+ verdict: "fresh",
16704
+ meta
16705
+ };
16706
+ const headMoved = meta.lastIndexedHead !== void 0 && git.head !== meta.lastIndexedHead;
16707
+ const newlyDirty = git.dirty && meta.lastIndexedDirty !== true;
16708
+ if (headMoved || newlyDirty) return {
16709
+ verdict: "stale",
16710
+ meta,
16711
+ head: git.head,
16712
+ dirty: git.dirty
16713
+ };
16714
+ return {
16715
+ verdict: "fresh",
16716
+ meta,
16717
+ head: git.head,
16718
+ dirty: git.dirty
16719
+ };
16720
+ }
16721
+ /** Cheap, bounded git probe via the native-exe runner. */
16722
+ async function gitState(workspace) {
16723
+ const git = resolveExecutable("git");
16724
+ if (!git) return { isRepo: false };
16725
+ try {
16726
+ const inside = await runManagedExeCapture(git, [
16727
+ "-C",
16728
+ workspace,
16729
+ "rev-parse",
16730
+ "--is-inside-work-tree"
16731
+ ], {
16732
+ timeoutMs: GIT_TIMEOUT_MS,
16733
+ maxStdoutBytes: 64 * 1024
16734
+ });
16735
+ if (inside.code !== 0 || inside.stdout.trim() !== "true") return { isRepo: false };
16736
+ const head = await runManagedExeCapture(git, [
16737
+ "-C",
16738
+ workspace,
16739
+ "rev-parse",
16740
+ "HEAD"
16741
+ ], {
16742
+ timeoutMs: GIT_TIMEOUT_MS,
16743
+ maxStdoutBytes: 64 * 1024
16744
+ });
16745
+ const status = await runManagedExeCapture(git, [
16746
+ "-C",
16747
+ workspace,
16748
+ "status",
16749
+ "--porcelain"
16750
+ ], {
16751
+ timeoutMs: GIT_TIMEOUT_MS,
16752
+ maxStdoutBytes: 1024 * 1024
16753
+ });
16754
+ return {
16755
+ isRepo: true,
16756
+ head: head.code === 0 ? head.stdout.trim() || void 0 : void 0,
16757
+ dirty: status.code === 0 ? status.stdout.trim().length > 0 : void 0
16758
+ };
16759
+ } catch {
16760
+ return { isRepo: false };
16761
+ }
16762
+ }
16763
+ const _initInFlight = /* @__PURE__ */ new Set();
16764
+ /** True iff a background init for this workspace is already in flight. */
16765
+ function isInitInFlight(workspace) {
16766
+ return _initInFlight.has(initKey(workspace));
16767
+ }
16768
+ /** Mark a background init started (debounce). Returns false if already running. */
16769
+ function tryClaimInit(workspace) {
16770
+ const k = initKey(workspace);
16771
+ if (_initInFlight.has(k)) return false;
16772
+ _initInFlight.add(k);
16773
+ return true;
16774
+ }
16775
+ /** Release the debounce claim (call in the init's finally). */
16776
+ function releaseInit(workspace) {
16777
+ _initInFlight.delete(initKey(workspace));
16778
+ }
16779
+ function initKey(workspace) {
16780
+ return `${MODEL_ID}::${canonicalForCompare(workspace)}`;
16781
+ }
16782
+
16660
16783
  //#endregion
16661
16784
  //#region src/lib/colbert/runner.ts
16662
16785
  /** Caller responsiveness budget for a search. A warm search is sub-second;
@@ -16726,6 +16849,10 @@ function makeIndexProgressProbe(workspace) {
16726
16849
  * colgrep writer per workspace" goal as the init debounce. Cleared when the
16727
16850
  * detached search completes. */
16728
16851
  const _searchIndexInFlight = /* @__PURE__ */ new Set();
16852
+ const _initPromises = /* @__PURE__ */ new Map();
16853
+ /** In-flight quarantine deletions, drained by teardown (Windows EBUSY). */
16854
+ const _quarantineRemovals = /* @__PURE__ */ new Set();
16855
+ let _runManagedExeCapture = runManagedExeCapture;
16729
16856
  /** Build the isolating env for any colgrep child (search or init). */
16730
16857
  function colgrepEnv() {
16731
16858
  const ortDir = nodePath.dirname(colbertOrtDylibPath());
@@ -16763,6 +16890,7 @@ async function runSemanticSearch(opts) {
16763
16890
  };
16764
16891
  case "failed": return handleFailure(workspace, fresh.meta, false);
16765
16892
  case "crashed": return handleFailure(workspace, fresh.meta, true);
16893
+ case "corrupt": return repairCorruptIndex(workspace, fresh.meta);
16766
16894
  case "building": return {
16767
16895
  status: "building",
16768
16896
  notice: "semantic index is being built for this workspace; retry shortly (or use code_search now)"
@@ -16795,6 +16923,64 @@ async function runSemanticSearch(opts) {
16795
16923
  * by the inactivity watchdog) retries at most once — re-running a hung build
16796
16924
  * usually hangs again; transient classes retry up to `MAX_FAILED_ATTEMPTS`.
16797
16925
  */
16926
+ async function quarantineProjectDir(projectDir) {
16927
+ const quarantine = `${projectDir}.corrupt-${process$1.pid}-${randomBytes(4).toString("hex")}`;
16928
+ try {
16929
+ await fs.rename(projectDir, quarantine);
16930
+ } catch (err) {
16931
+ consola.debug("colbert: corrupt index quarantine rename failed:", err);
16932
+ return false;
16933
+ }
16934
+ const removal = fs.rm(quarantine, {
16935
+ recursive: true,
16936
+ force: true
16937
+ }).catch((err) => {
16938
+ consola.debug("colbert: corrupt index quarantine cleanup failed:", err);
16939
+ }).finally(() => {
16940
+ _quarantineRemovals.delete(removal);
16941
+ });
16942
+ _quarantineRemovals.add(removal);
16943
+ return true;
16944
+ }
16945
+ async function repairCorruptIndex(workspace, meta) {
16946
+ const wsKey = nodePath.resolve(workspace);
16947
+ const attempts = (meta?.failureClass === "corrupt" ? meta.failedAttempts ?? 0 : 0) + 1;
16948
+ const failedMeta = {
16949
+ workspace,
16950
+ model: meta?.model ?? MODEL_ID,
16951
+ modelRev: meta?.modelRev ?? MODEL_REVISION,
16952
+ binarySha: colgrepBinAsset()?.sha256,
16953
+ ortSha: ortLibAsset()?.sha256,
16954
+ status: "failed",
16955
+ failureClass: "corrupt",
16956
+ failedAttempts: attempts,
16957
+ lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString(),
16958
+ lastIndexedHead: meta?.lastIndexedHead,
16959
+ lastIndexedDirty: meta?.lastIndexedDirty,
16960
+ ownerInstanceId: getColbertInstanceUuid()
16961
+ };
16962
+ if (_searchIndexInFlight.has(wsKey) || isInitInFlight(workspace)) return {
16963
+ status: "building",
16964
+ notice: "semantic index was found corrupt but a writer is still active; returned results are disabled until it exits"
16965
+ };
16966
+ const projectDir = await colbertProjectDir(workspace);
16967
+ if (!projectDir) return handleFailure(workspace, failedMeta, false);
16968
+ if (!await quarantineProjectDir(projectDir)) {
16969
+ await writeColbertMeta(failedMeta).catch(() => {});
16970
+ return {
16971
+ status: "failed",
16972
+ isError: true,
16973
+ notice: "semantic index is corrupt but could not be quarantined; returned lexical results — close active colgrep processes and retry"
16974
+ };
16975
+ }
16976
+ await writeColbertMeta(failedMeta).catch(() => {});
16977
+ if (attempts < 2) kickBackgroundInit(workspace);
16978
+ return {
16979
+ status: "failed",
16980
+ isError: true,
16981
+ notice: attempts < 2 ? "semantic index was found corrupt and quarantined; a clean rebuild was started — retry mode:\"semantic\" shortly" : "semantic index repeatedly failed integrity checks; automatic rebuild is capped — do NOT retry mode:\"semantic\", use lexical search with specific symbol/keyword terms and see proxy logs"
16982
+ };
16983
+ }
16798
16984
  async function handleFailure(workspace, meta, crashedVerdict) {
16799
16985
  const cls = crashedVerdict ? "crashed" : meta?.failureClass ?? "error";
16800
16986
  const attempts = crashedVerdict ? (meta?.failedAttempts ?? 0) + 1 : meta?.failedAttempts ?? 1;
@@ -16811,7 +16997,7 @@ async function handleFailure(workspace, meta, crashedVerdict) {
16811
16997
  lastIndexedDirty: meta?.lastIndexedDirty,
16812
16998
  ownerInstanceId: getColbertInstanceUuid()
16813
16999
  }).catch(() => {});
16814
- const cap = cls === "stuck" ? 2 : MAX_FAILED_ATTEMPTS;
17000
+ const cap = cls === "stuck" || cls === "corrupt" ? 2 : MAX_FAILED_ATTEMPTS;
16815
17001
  const lastMs = lastAt ? Date.parse(lastAt) : NaN;
16816
17002
  const backoffElapsed = !Number.isFinite(lastMs) || Date.now() - lastMs >= FAILED_RETRY_BACKOFF_MS;
16817
17003
  if (attempts < cap && backoffElapsed) {
@@ -16854,7 +17040,7 @@ async function spawnSearch(opts) {
16854
17040
  "never",
16855
17041
  "--force-cpu",
16856
17042
  "--model",
16857
- colbertModelDir(),
17043
+ canonicalColbertModelDir(),
16858
17044
  "-y",
16859
17045
  "-k",
16860
17046
  String(opts.limit)
@@ -16869,7 +17055,7 @@ async function spawnSearch(opts) {
16869
17055
  _searchIndexInFlight.add(wsKey);
16870
17056
  let searchPromise;
16871
17057
  try {
16872
- searchPromise = runManagedExeCapture(binary, args, {
17058
+ searchPromise = _runManagedExeCapture(binary, args, {
16873
17059
  env: colgrepEnv(),
16874
17060
  inactivityTimeoutMs: INIT_STALL_MS,
16875
17061
  onInactivityCheck: makeIndexProgressProbe(opts.workspace),
@@ -16985,14 +17171,24 @@ function buildSnippet(unit) {
16985
17171
  const sig = typeof unit.signature === "string" ? unit.signature.trim() : "";
16986
17172
  const code = typeof unit.code === "string" ? unit.code : "";
16987
17173
  if (!code) return sig;
16988
- const body = code.split("\n").slice(0, 6).join("\n");
16989
- const snippet = sig && !body.startsWith(sig) ? `${sig}\n${body}` : body;
17174
+ const lines = code.split("\n");
17175
+ const body = lines.slice(0, 6).join("\n");
17176
+ const firstBodyLine = lines[0]?.trim() ?? "";
17177
+ const snippet = sig && firstBodyLine !== sig ? `${sig}\n${body}` : body;
16990
17178
  return snippet.length > 600 ? snippet.slice(0, 600) + "…" : snippet;
16991
17179
  }
17180
+ function stripExtendedPathPrefix(value) {
17181
+ if (/^\\\\\?\\UNC\\/i.test(value)) return `\\\\${value.slice(8)}`;
17182
+ if (/^\\\\\?\\[a-z]:\\/i.test(value)) return value.slice(4);
17183
+ return value;
17184
+ }
16992
17185
  function relativize(file, workspace, workspaceReal) {
16993
- for (const base of [workspace, workspaceReal]) try {
16994
- const rel = nodePath.relative(base, file);
16995
- if (rel && !rel.startsWith("..") && !nodePath.isAbsolute(rel)) return rel;
17186
+ const normalizedFile = stripExtendedPathPrefix(file);
17187
+ for (const rawBase of [workspace, workspaceReal]) try {
17188
+ const base = stripExtendedPathPrefix(rawBase);
17189
+ const flavor = /^[a-z]:[\\/]/i.test(base) || base.startsWith("\\\\") ? nodePath.win32 : nodePath;
17190
+ const rel = flavor.relative(base, normalizedFile);
17191
+ if (rel && !rel.startsWith("..") && !flavor.isAbsolute(rel)) return rel;
16996
17192
  } catch {}
16997
17193
  return file;
16998
17194
  }
@@ -17019,9 +17215,31 @@ function clampLimit(limit) {
17019
17215
  function kickBackgroundInit(workspace) {
17020
17216
  if (isInitInFlight(workspace)) return;
17021
17217
  if (!tryClaimInit(workspace)) return;
17022
- runInit(workspace).catch((err) => {
17023
- consola.debug("colbert: background init failed:", err);
17218
+ const key = nodePath.resolve(workspace);
17219
+ const promise = runInit(workspace).catch(async (err) => {
17220
+ releaseInit(workspace);
17221
+ consola.error("colbert: background init failed:", err);
17222
+ const prior = await readColbertMeta(workspace);
17223
+ await writeColbertMeta({
17224
+ workspace,
17225
+ model: prior?.model ?? MODEL_ID,
17226
+ modelRev: prior?.modelRev ?? MODEL_REVISION,
17227
+ binarySha: colgrepBinAsset()?.sha256,
17228
+ ortSha: ortLibAsset()?.sha256,
17229
+ status: "failed",
17230
+ failureClass: "launch",
17231
+ failedAttempts: (prior?.failedAttempts ?? 0) + 1,
17232
+ lastIndexedAt: (/* @__PURE__ */ new Date()).toISOString(),
17233
+ lastIndexedHead: prior?.lastIndexedHead,
17234
+ lastIndexedDirty: prior?.lastIndexedDirty,
17235
+ ownerInstanceId: getColbertInstanceUuid()
17236
+ }).catch((writeErr) => {
17237
+ consola.error("colbert: failed to record background init failure:", writeErr);
17238
+ });
17239
+ }).finally(() => {
17240
+ _initPromises.delete(key);
17024
17241
  });
17242
+ _initPromises.set(key, promise);
17025
17243
  }
17026
17244
  /**
17027
17245
  * Whether the STARTUP auto-kick should fire for a workspace. Skips a build
@@ -17034,25 +17252,72 @@ function kickBackgroundInit(workspace) {
17034
17252
  async function startupKickAllowed(workspace) {
17035
17253
  const meta = await readColbertMeta(workspace);
17036
17254
  if (!meta || meta.status !== "failed") return true;
17037
- if ((meta.failedAttempts ?? 0) >= MAX_FAILED_ATTEMPTS) return false;
17255
+ const cap = meta.failureClass === "stuck" || meta.failureClass === "corrupt" ? 2 : MAX_FAILED_ATTEMPTS;
17256
+ if ((meta.failedAttempts ?? 0) >= cap) return false;
17038
17257
  if (meta.failureClass === "stuck") return false;
17039
17258
  return true;
17040
17259
  }
17260
+ /**
17261
+ * Encoding sessions colgrep may run in parallel: 25% of the machine's
17262
+ * threads, never fewer than 2.
17263
+ *
17264
+ * colgrep defaults this to the FULL CPU count, so a background index build
17265
+ * saturates the machine — the exact opposite of what a background build
17266
+ * should do during an interactive agent session (the proxy even holds a
17267
+ * keep-awake assertion so those sessions run long and unattended). The floor
17268
+ * of 2 keeps a 1- to 7-thread box from dropping to a single session and
17269
+ * taking proportionally forever.
17270
+ *
17271
+ * Override with `GH_ROUTER_COLBERT_PARALLEL` (a positive integer).
17272
+ */
17273
+ function colbertParallelSessions() {
17274
+ const raw = Number(process$1.env.GH_ROUTER_COLBERT_PARALLEL);
17275
+ if (Number.isSafeInteger(raw) && raw > 0) return raw;
17276
+ const threads = os.availableParallelism?.() ?? os.cpus?.().length ?? 4;
17277
+ return Math.max(2, Math.floor(threads * .25));
17278
+ }
17279
+ /**
17280
+ * Persist the parallelism cap into the router-owned colgrep config.
17281
+ *
17282
+ * `--parallel` exists only on the `settings` subcommand — there is no
17283
+ * per-run flag and no env var. It writes `parallel_sessions` to
17284
+ * `<COLGREP_DATA_DIR>/../config.json`, which for us is
17285
+ * `<APP_DIR>/colbert/config.json`: router-owned, and NOT the user's own
17286
+ * colgrep config (verified — after we write ours, a plain `colgrep
17287
+ * settings` with no COLGREP_DATA_DIR still reports `auto`).
17288
+ *
17289
+ * Best-effort: failing here means colgrep encodes at its default (all
17290
+ * threads), which is greedy but not incorrect, so it must never block a
17291
+ * build.
17292
+ */
17293
+ async function applyParallelismCap(binary) {
17294
+ const sessions$1 = colbertParallelSessions();
17295
+ try {
17296
+ await _runManagedExeCapture(binary, [
17297
+ "settings",
17298
+ "--parallel",
17299
+ String(sessions$1)
17300
+ ], {
17301
+ env: colgrepEnv(),
17302
+ timeoutMs: 3e4,
17303
+ maxStdoutBytes: MAX_STDOUT_BYTES
17304
+ });
17305
+ } catch (err) {
17306
+ consola.debug("colbert: could not cap colgrep parallelism:", err);
17307
+ }
17308
+ }
17041
17309
  async function runInit(workspace) {
17042
17310
  const binary = colgrepBinaryPath();
17043
- if (!existsSync(binary)) {
17044
- releaseInit(workspace);
17045
- return;
17046
- }
17047
- if (!existsSync(colbertOrtDylibPath())) {
17048
- releaseInit(workspace);
17049
- return;
17050
- }
17311
+ if (!existsSync(binary)) throw new Error("colgrep binary is missing");
17312
+ if (!existsSync(colbertOrtDylibPath())) throw new Error("ColBERT ONNX runtime is missing");
17313
+ await applyParallelismCap(binary);
17051
17314
  const prior = await readColbertMeta(workspace);
17052
17315
  const baseMeta = {
17053
17316
  workspace,
17054
17317
  model: MODEL_ID,
17055
17318
  modelRev: MODEL_REVISION,
17319
+ binarySha: colgrepBinAsset()?.sha256,
17320
+ ortSha: ortLibAsset()?.sha256,
17056
17321
  status: "building",
17057
17322
  buildPid: void 0,
17058
17323
  ownerInstanceId: getColbertInstanceUuid(),
@@ -17074,7 +17339,7 @@ async function runInit(workspace) {
17074
17339
  "never",
17075
17340
  "--force-cpu",
17076
17341
  "--model",
17077
- colbertModelDir(),
17342
+ canonicalColbertModelDir(),
17078
17343
  workspace
17079
17344
  ];
17080
17345
  const onInactivityCheck = makeIndexProgressProbe(workspace);
@@ -17082,7 +17347,7 @@ async function runInit(workspace) {
17082
17347
  let ok$3 = false;
17083
17348
  let failureClass;
17084
17349
  try {
17085
- const res = await runManagedExeCapture(binary, args, {
17350
+ const res = await _runManagedExeCapture(binary, args, {
17086
17351
  env: colgrepEnv(),
17087
17352
  timeoutMs: INIT_TIMEOUT_MS,
17088
17353
  inactivityTimeoutMs: INIT_STALL_MS,
@@ -17097,10 +17362,18 @@ async function runInit(workspace) {
17097
17362
  }
17098
17363
  });
17099
17364
  ok$3 = !res.stalled && !res.timedOut && res.code === 0;
17100
- if (!ok$3) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
17101
- } catch {
17365
+ if (ok$3) {
17366
+ const projectDir = await colbertProjectDir(workspace);
17367
+ ok$3 = projectDir !== null && validateIndexIntegrity(nodePath.join(projectDir, "index")).verdict === "coherent";
17368
+ if (!ok$3) {
17369
+ failureClass = "corrupt";
17370
+ if (projectDir) await quarantineProjectDir(projectDir);
17371
+ }
17372
+ } else failureClass = res.stalled || res.timedOut ? "stuck" : "error";
17373
+ } catch (err) {
17102
17374
  ok$3 = false;
17103
17375
  failureClass = "launch";
17376
+ consola.error("colbert: init failed to launch:", err);
17104
17377
  } finally {
17105
17378
  releaseInit(workspace);
17106
17379
  }
@@ -17203,8 +17476,10 @@ function lexicalSearchCodeMode(mode) {
17203
17476
  * it both levers: retry `mode:"semantic"` shortly (the index is self-healing
17204
17477
  * in the background) OR re-query now with specific symbol/keyword terms.
17205
17478
  */
17479
+ const FALLBACK_GUIDANCE_MARKER = "retry mode:\"semantic\"";
17480
+ const FALLBACK_GUIDANCE = `${FALLBACK_GUIDANCE_MARKER} shortly, or re-query now with specific symbol/keyword terms`;
17206
17481
  function fallbackNoticeFor(status) {
17207
- const tail = "retry mode:\"semantic\" shortly, or re-query now with specific symbol/keyword terms";
17482
+ const tail = FALLBACK_GUIDANCE;
17208
17483
  switch (status) {
17209
17484
  case "building": return `semantic index is building; returned lexical keyword matches — ${tail}`;
17210
17485
  case "stale": return `semantic index predates the current HEAD/tree (a background re-index was started); returned lexical keyword matches — ${tail}`;
@@ -17223,6 +17498,38 @@ function joinNotice(primary, secondary) {
17223
17498
  if (primary && secondary) return `${primary} (${secondary})`;
17224
17499
  return primary || secondary || void 0;
17225
17500
  }
17501
+ /** Preserve runner-specific context while guaranteeing actionable guidance once. */
17502
+ function semanticFallbackNotice(sem) {
17503
+ if (!sem.notice) return fallbackNoticeFor(sem.status);
17504
+ if (sem.notice.includes(FALLBACK_GUIDANCE_MARKER)) return sem.notice;
17505
+ return `${sem.notice} — ${FALLBACK_GUIDANCE}`;
17506
+ }
17507
+ async function outlinesForSemanticResults(input, results, signal) {
17508
+ if (input.summary === false) return void 0;
17509
+ const seen = /* @__PURE__ */ new Set();
17510
+ const files = [];
17511
+ for (const result of results) {
17512
+ if (seen.has(result.file)) continue;
17513
+ seen.add(result.file);
17514
+ files.push(result.file);
17515
+ if (files.length >= 10) break;
17516
+ }
17517
+ const outlines = [];
17518
+ const deadline = Date.now() + 2e3;
17519
+ const workspace = nodePath.resolve(input.workspace);
17520
+ for (const file of files) {
17521
+ if (signal?.aborted || Date.now() > deadline) break;
17522
+ const abs = nodePath.resolve(workspace, file);
17523
+ const rel = nodePath.relative(workspace, abs);
17524
+ if (!rel || rel.startsWith("..") || nodePath.isAbsolute(rel)) continue;
17525
+ const outlined = await outlineFile(abs, signal);
17526
+ outlines.push({
17527
+ file,
17528
+ outline: outlined.outline
17529
+ });
17530
+ }
17531
+ return outlines;
17532
+ }
17226
17533
  async function runLexical(input, mode, source, signal) {
17227
17534
  const isAst = mode === "ast";
17228
17535
  const resp = await searchCode({
@@ -17284,22 +17591,26 @@ async function runUnifiedCodeSearch(input, signal) {
17284
17591
  notice: joinNotice(r$1.notice, "semantic search errored; returned lexical results")
17285
17592
  };
17286
17593
  }
17287
- if (sem.status === "ready") return {
17288
- source: "semantic",
17289
- results: (sem.results ?? []).map((r$1) => ({
17594
+ if (sem.status === "ready") {
17595
+ const results = (sem.results ?? []).map((r$1) => ({
17290
17596
  file: r$1.file,
17291
17597
  line: r$1.line,
17292
17598
  snippet: r$1.snippet,
17293
17599
  ...r$1.endLine !== void 0 ? { endLine: r$1.endLine } : {},
17294
17600
  ...r$1.name !== void 0 ? { name: r$1.name } : {},
17295
17601
  ...r$1.score !== void 0 ? { score: r$1.score } : {}
17296
- })),
17297
- ...sem.notice ? { notice: sem.notice } : {}
17298
- };
17602
+ }));
17603
+ return {
17604
+ source: "semantic",
17605
+ results,
17606
+ outlines: await outlinesForSemanticResults(input, results, signal),
17607
+ ...sem.notice ? { notice: sem.notice } : {}
17608
+ };
17609
+ }
17299
17610
  const r = await runLexical(input, "lexical", "lexical-fallback", signal);
17300
17611
  return {
17301
17612
  ...r,
17302
- notice: joinNotice(r.notice, fallbackNoticeFor(sem.status))
17613
+ notice: joinNotice(r.notice, semanticFallbackNotice(sem))
17303
17614
  };
17304
17615
  }
17305
17616
 
@@ -18495,16 +18806,16 @@ function logAudit$1(record) {
18495
18806
  if (process.env.GH_ROUTER_LOG_BROWSER_MCP !== "1") return;
18496
18807
  (async () => {
18497
18808
  try {
18498
- const fs$1 = await import("node:fs/promises");
18809
+ const fs$2 = await import("node:fs/promises");
18499
18810
  const path$1 = await import("node:path");
18500
18811
  const { PATHS: PATHS$1 } = await import("./paths-B5k78n0d.js");
18501
18812
  const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
18502
- await fs$1.mkdir(dir, { recursive: true });
18813
+ await fs$2.mkdir(dir, { recursive: true });
18503
18814
  const line = JSON.stringify({
18504
18815
  ts: (/* @__PURE__ */ new Date()).toISOString(),
18505
18816
  ...record
18506
18817
  }) + "\n";
18507
- await fs$1.appendFile(path$1.join(dir, "audit.log"), line, "utf8");
18818
+ await fs$2.appendFile(path$1.join(dir, "audit.log"), line, "utf8");
18508
18819
  } catch {}
18509
18820
  })();
18510
18821
  }
@@ -21340,58 +21651,6 @@ function registerExitHandlers$1() {
21340
21651
  }
21341
21652
  registerExitHandlers$1();
21342
21653
 
21343
- //#endregion
21344
- //#region src/vendor/pi/ai/api-registry.ts
21345
- const apiProviderRegistry = /* @__PURE__ */ new Map();
21346
- function getApiProvider(api) {
21347
- return apiProviderRegistry.get(api)?.provider;
21348
- }
21349
-
21350
- //#endregion
21351
- //#region src/vendor/pi/ai/env-api-keys.ts
21352
- let _existsSync = null;
21353
- let _homedir = null;
21354
- let _join = null;
21355
- const dynamicImport = (specifier) => import(specifier);
21356
- const NODE_FS_SPECIFIER = "node:fs";
21357
- const NODE_OS_SPECIFIER = "node:os";
21358
- const NODE_PATH_SPECIFIER = "node:path";
21359
- if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
21360
- dynamicImport(NODE_FS_SPECIFIER).then((m) => {
21361
- _existsSync = m.existsSync;
21362
- });
21363
- dynamicImport(NODE_OS_SPECIFIER).then((m) => {
21364
- _homedir = m.homedir;
21365
- });
21366
- dynamicImport(NODE_PATH_SPECIFIER).then((m) => {
21367
- _join = m.join;
21368
- });
21369
- }
21370
-
21371
- //#endregion
21372
- //#region src/vendor/pi/ai/models.generated.ts
21373
- const MODELS = {};
21374
-
21375
- //#endregion
21376
- //#region src/vendor/pi/ai/models.ts
21377
- const modelRegistry = /* @__PURE__ */ new Map();
21378
- for (const [provider, models] of Object.entries(MODELS)) {
21379
- const providerModels = /* @__PURE__ */ new Map();
21380
- for (const [id, model] of Object.entries(models)) providerModels.set(id, model);
21381
- modelRegistry.set(provider, providerModels);
21382
- }
21383
-
21384
- //#endregion
21385
- //#region src/vendor/pi/ai/stream.ts
21386
- function resolveApiProvider(api) {
21387
- const provider = getApiProvider(api);
21388
- if (!provider) throw new Error(`No API provider registered for api: ${api}`);
21389
- return provider;
21390
- }
21391
- function streamSimple(model, context, options) {
21392
- return resolveApiProvider(model.api).streamSimple(model, context, options);
21393
- }
21394
-
21395
21654
  //#endregion
21396
21655
  //#region src/vendor/pi/ai/utils/event-stream.ts
21397
21656
  var EventStream = class {
@@ -21453,19 +21712,89 @@ var AssistantMessageEventStream = class extends EventStream {
21453
21712
  }
21454
21713
  };
21455
21714
 
21715
+ //#endregion
21716
+ //#region src/vendor/pi/ai/env-api-keys.ts
21717
+ let _existsSync = null;
21718
+ let _homedir = null;
21719
+ let _join = null;
21720
+ const dynamicImport = (specifier) => import(specifier);
21721
+ const NODE_FS_SPECIFIER = "node:fs";
21722
+ const NODE_OS_SPECIFIER = "node:os";
21723
+ const NODE_PATH_SPECIFIER = "node:path";
21724
+ if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
21725
+ dynamicImport(NODE_FS_SPECIFIER).then((m) => {
21726
+ _existsSync = m.existsSync;
21727
+ });
21728
+ dynamicImport(NODE_OS_SPECIFIER).then((m) => {
21729
+ _homedir = m.homedir;
21730
+ });
21731
+ dynamicImport(NODE_PATH_SPECIFIER).then((m) => {
21732
+ _join = m.join;
21733
+ });
21734
+ }
21735
+
21736
+ //#endregion
21737
+ //#region src/vendor/pi/ai/utils/retry.ts
21738
+ function buildProviderErrorPattern(patterns) {
21739
+ return new RegExp(patterns.join("|"), "i");
21740
+ }
21741
+ const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
21742
+ "GoUsageLimitError",
21743
+ "FreeUsageLimitError",
21744
+ "Monthly usage limit reached",
21745
+ "available balance",
21746
+ "insufficient_quota",
21747
+ "out of budget",
21748
+ "quota exceeded",
21749
+ "billing"
21750
+ ]);
21751
+ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
21752
+ "overloaded",
21753
+ "rate.?limit",
21754
+ "too many requests",
21755
+ "429",
21756
+ "500",
21757
+ "502",
21758
+ "503",
21759
+ "504",
21760
+ "524",
21761
+ "service.?unavailable",
21762
+ "server.?error",
21763
+ "internal.?error",
21764
+ "provider.?returned.?error",
21765
+ "network.?error",
21766
+ "connection.?error",
21767
+ "connection.?refused",
21768
+ "connection.?lost",
21769
+ "other side closed",
21770
+ "fetch failed",
21771
+ "getaddrinfo",
21772
+ "ENOTFOUND",
21773
+ "EAI_AGAIN",
21774
+ "upstream.?connect",
21775
+ "reset before headers",
21776
+ "socket hang up",
21777
+ "socket connection was closed",
21778
+ "timed? out",
21779
+ "timeout",
21780
+ "terminated",
21781
+ "websocket.?closed",
21782
+ "websocket.?error",
21783
+ "ended without",
21784
+ "stream ended before message_stop",
21785
+ "stream ended before a terminal response event",
21786
+ "http2 request did not get a response",
21787
+ "retry delay",
21788
+ "you can retry your request",
21789
+ "try your request again",
21790
+ "please retry your request",
21791
+ "ResourceExhausted"
21792
+ ]);
21793
+
21456
21794
  //#endregion
21457
21795
  //#region src/vendor/pi/ai/utils/validation.ts
21458
21796
  const validatorCache = /* @__PURE__ */ new WeakMap();
21459
21797
  const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
21460
- function isRecord(value) {
21461
- return typeof value === "object" && value !== null;
21462
- }
21463
- function isJsonSchemaObject(value) {
21464
- return isRecord(value);
21465
- }
21466
- function hasTypeBoxMetadata(schema) {
21467
- return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
21468
- }
21469
21798
  function getSchemaTypes(schema) {
21470
21799
  if (typeof schema.type === "string") return [schema.type];
21471
21800
  if (Array.isArray(schema.type)) return schema.type.filter((type) => typeof type === "string");
@@ -21479,15 +21808,11 @@ function matchesJsonType(value, type) {
21479
21808
  case "string": return typeof value === "string";
21480
21809
  case "null": return value === null;
21481
21810
  case "array": return Array.isArray(value);
21482
- case "object": return isRecord(value) && !Array.isArray(value);
21811
+ case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
21483
21812
  default: return false;
21484
21813
  }
21485
21814
  }
21486
- function isValidatorSchema(value) {
21487
- return isRecord(value);
21488
- }
21489
21815
  function getSubSchemaValidator(schema) {
21490
- if (!isValidatorSchema(schema)) return;
21491
21816
  try {
21492
21817
  return getValidator(schema);
21493
21818
  } catch {
@@ -21540,7 +21865,7 @@ function applySchemaObjectCoercion(value, schema) {
21540
21865
  if (!(key in value)) continue;
21541
21866
  value[key] = coerceWithJsonSchema(value[key], propertySchema);
21542
21867
  }
21543
- if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) for (const [key, propertyValue] of Object.entries(value)) {
21868
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") for (const [key, propertyValue] of Object.entries(value)) {
21544
21869
  if (definedKeys.has(key)) continue;
21545
21870
  value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
21546
21871
  }
@@ -21554,7 +21879,7 @@ function applySchemaArrayCoercion(value, schema) {
21554
21879
  }
21555
21880
  return;
21556
21881
  }
21557
- if (isJsonSchemaObject(schema.items)) for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
21882
+ if (schema.items && typeof schema.items === "object") for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
21558
21883
  }
21559
21884
  function coerceWithUnionSchema(value, schemas) {
21560
21885
  for (const schema of schemas) {
@@ -21577,7 +21902,7 @@ function coerceWithJsonSchema(value, schema) {
21577
21902
  break;
21578
21903
  }
21579
21904
  }
21580
- if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
21905
+ if (schemaTypes.includes("object") && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
21581
21906
  if (schemaTypes.includes("array") && Array.isArray(nextValue)) applySchemaArrayCoercion(nextValue, schema);
21582
21907
  return nextValue;
21583
21908
  }
@@ -21610,9 +21935,9 @@ function validateToolArguments(tool$1, toolCall) {
21610
21935
  const args = structuredClone(toolCall.arguments);
21611
21936
  Value.Convert(tool$1.parameters, args);
21612
21937
  const validator = getValidator(tool$1.parameters);
21613
- if (!hasTypeBoxMetadata(tool$1.parameters) && isJsonSchemaObject(tool$1.parameters)) {
21938
+ if (!Object.getOwnPropertySymbols(tool$1.parameters).includes(TYPEBOX_KIND)) {
21614
21939
  const coerced = coerceWithJsonSchema(args, tool$1.parameters);
21615
- if (coerced !== args) if (isRecord(args) && isRecord(coerced)) {
21940
+ if (coerced !== args) if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) {
21616
21941
  for (const key of Object.keys(args)) delete args[key];
21617
21942
  Object.assign(args, coerced);
21618
21943
  } else return validator.Check(coerced) ? coerced : args;
@@ -21623,6 +21948,14 @@ function validateToolArguments(tool$1, toolCall) {
21623
21948
  throw new Error(errorMessage);
21624
21949
  }
21625
21950
 
21951
+ //#endregion
21952
+ //#region src/vendor/pi/agent/stream-fn.ts
21953
+ let defaultStreamFn;
21954
+ function getDefaultStreamFn() {
21955
+ if (!defaultStreamFn) throw new Error("No default stream function configured. Pass streamFn explicitly or call setDefaultStreamFn().");
21956
+ return defaultStreamFn;
21957
+ }
21958
+
21626
21959
  //#endregion
21627
21960
  //#region src/vendor/pi/agent/agent-loop.ts
21628
21961
  async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
@@ -21643,7 +21976,7 @@ async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
21643
21976
  message: prompt
21644
21977
  });
21645
21978
  }
21646
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
21979
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
21647
21980
  return newMessages;
21648
21981
  }
21649
21982
  async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
@@ -21653,13 +21986,13 @@ async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
21653
21986
  const currentContext = { ...context };
21654
21987
  await emit({ type: "agent_start" });
21655
21988
  await emit({ type: "turn_start" });
21656
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
21989
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
21657
21990
  return newMessages;
21658
21991
  }
21659
21992
  /**
21660
21993
  * Main loop logic shared by agentLoop and agentLoopContinue.
21661
21994
  */
21662
- async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFn) {
21995
+ async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
21663
21996
  let currentContext = initialContext;
21664
21997
  let config = initialConfig;
21665
21998
  let firstTurn = true;
@@ -21684,7 +22017,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21684
22017
  }
21685
22018
  pendingMessages = [];
21686
22019
  }
21687
- const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
22020
+ const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
21688
22021
  newMessages.push(message);
21689
22022
  if (message.stopReason === "error" || message.stopReason === "aborted") {
21690
22023
  await emit({
@@ -21702,7 +22035,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21702
22035
  const toolResults = [];
21703
22036
  hasMoreToolCalls = false;
21704
22037
  if (toolCalls.length > 0) {
21705
- const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
22038
+ const executedToolBatch = message.stopReason === "length" ? await failToolCallsFromTruncatedMessage(toolCalls, emit) : await executeToolCalls(currentContext, message, config, signal, emit);
21706
22039
  toolResults.push(...executedToolBatch.messages);
21707
22040
  hasMoreToolCalls = !executedToolBatch.terminate;
21708
22041
  for (const result of toolResults) {
@@ -21760,7 +22093,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21760
22093
  * Stream an assistant response from the LLM.
21761
22094
  * This is where AgentMessage[] gets transformed to Message[] for the LLM.
21762
22095
  */
21763
- async function streamAssistantResponse(context, config, signal, emit, streamFn) {
22096
+ async function streamAssistantResponse(context, config, signal, emit, streamFunction) {
21764
22097
  let messages = context.messages;
21765
22098
  if (config.transformContext) messages = await config.transformContext(messages, signal);
21766
22099
  const llmMessages = await config.convertToLlm(messages);
@@ -21769,7 +22102,6 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
21769
22102
  messages: llmMessages,
21770
22103
  tools: context.tools
21771
22104
  };
21772
- const streamFunction = streamFn || streamSimple;
21773
22105
  const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : void 0) || config.apiKey;
21774
22106
  const response = await streamFunction(config.model, llmContext, {
21775
22107
  ...config,
@@ -21839,6 +22171,37 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
21839
22171
  return finalMessage;
21840
22172
  }
21841
22173
  /**
22174
+ * Fail all tool calls from an assistant message that was truncated by the
22175
+ * output token limit. Streamed tool-call arguments are finalized with a
22176
+ * best-effort JSON salvage parser, so a truncated message can yield tool calls
22177
+ * whose arguments parse and validate but are silently incomplete. None of them
22178
+ * are safe to execute; report each as an error so the model can re-issue them.
22179
+ */
22180
+ async function failToolCallsFromTruncatedMessage(toolCalls, emit) {
22181
+ const messages = [];
22182
+ for (const toolCall of toolCalls) {
22183
+ await emit({
22184
+ type: "tool_execution_start",
22185
+ toolCallId: toolCall.id,
22186
+ toolName: toolCall.name,
22187
+ args: toolCall.arguments
22188
+ });
22189
+ const finalized = {
22190
+ toolCall,
22191
+ result: createErrorToolResult(`Tool call "${toolCall.name}" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.`),
22192
+ isError: true
22193
+ };
22194
+ await emitToolExecutionEnd(finalized, emit);
22195
+ const toolResultMessage = createToolResultMessage(finalized);
22196
+ await emitToolResultMessage(toolResultMessage, emit);
22197
+ messages.push(toolResultMessage);
22198
+ }
22199
+ return {
22200
+ messages,
22201
+ terminate: false
22202
+ };
22203
+ }
22204
+ /**
21842
22205
  * Execute tool calls from an assistant message.
21843
22206
  */
21844
22207
  async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
@@ -21977,8 +22340,10 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
21977
22340
  }
21978
22341
  async function executePreparedToolCall(prepared, signal, emit) {
21979
22342
  const updateEvents = [];
22343
+ let acceptingUpdates = true;
21980
22344
  try {
21981
22345
  const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
22346
+ if (!acceptingUpdates) return;
21982
22347
  updateEvents.push(Promise.resolve(emit({
21983
22348
  type: "tool_execution_update",
21984
22349
  toolCallId: prepared.toolCall.id,
@@ -21987,17 +22352,21 @@ async function executePreparedToolCall(prepared, signal, emit) {
21987
22352
  partialResult
21988
22353
  })));
21989
22354
  });
22355
+ acceptingUpdates = false;
21990
22356
  await Promise.all(updateEvents);
21991
22357
  return {
21992
22358
  result,
21993
22359
  isError: false
21994
22360
  };
21995
22361
  } catch (error) {
22362
+ acceptingUpdates = false;
21996
22363
  await Promise.all(updateEvents);
21997
22364
  return {
21998
22365
  result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
21999
22366
  isError: true
22000
22367
  };
22368
+ } finally {
22369
+ acceptingUpdates = false;
22001
22370
  }
22002
22371
  }
22003
22372
  async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
@@ -22014,8 +22383,10 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
22014
22383
  }, signal);
22015
22384
  if (afterResult) {
22016
22385
  result = {
22386
+ ...result,
22017
22387
  content: afterResult.content ?? result.content,
22018
22388
  details: afterResult.details ?? result.details,
22389
+ usage: afterResult.usage ?? result.usage,
22019
22390
  terminate: afterResult.terminate ?? result.terminate
22020
22391
  };
22021
22392
  isError = afterResult.isError ?? isError;
@@ -22053,8 +22424,10 @@ function createToolResultMessage(finalized) {
22053
22424
  role: "toolResult",
22054
22425
  toolCallId: finalized.toolCall.id,
22055
22426
  toolName: finalized.toolCall.name,
22056
- content: finalized.result.content,
22427
+ content: finalized.result.content ?? [],
22057
22428
  details: finalized.result.details,
22429
+ usage: finalized.result.usage,
22430
+ ...finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {},
22058
22431
  isError: finalized.isError,
22059
22432
  timestamp: Date.now()
22060
22433
  };
@@ -22171,13 +22544,15 @@ var Agent$1 = class {
22171
22544
  followUpQueue;
22172
22545
  convertToLlm;
22173
22546
  transformContext;
22174
- streamFn;
22547
+ streamFunction;
22175
22548
  getApiKey;
22176
22549
  onPayload;
22177
22550
  onResponse;
22178
22551
  beforeToolCall;
22179
22552
  afterToolCall;
22553
+ shouldStopAfterTurn;
22180
22554
  prepareNextTurn;
22555
+ prepareNextTurnWithContext;
22181
22556
  activeRun;
22182
22557
  /** Session identifier forwarded to providers for cache-aware backends. */
22183
22558
  sessionId;
@@ -22189,24 +22564,27 @@ var Agent$1 = class {
22189
22564
  maxRetryDelayMs;
22190
22565
  /** Tool execution strategy for assistant messages that contain multiple tool calls. */
22191
22566
  toolExecution;
22192
- constructor(options = {}) {
22193
- this._state = createMutableAgentState(options.initialState);
22194
- this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
22195
- this.transformContext = options.transformContext;
22196
- this.streamFn = options.streamFn ?? streamSimple;
22197
- this.getApiKey = options.getApiKey;
22198
- this.onPayload = options.onPayload;
22199
- this.onResponse = options.onResponse;
22200
- this.beforeToolCall = options.beforeToolCall;
22201
- this.afterToolCall = options.afterToolCall;
22202
- this.prepareNextTurn = options.prepareNextTurn;
22203
- this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
22204
- this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
22205
- this.sessionId = options.sessionId;
22206
- this.thinkingBudgets = options.thinkingBudgets;
22207
- this.transport = options.transport ?? "auto";
22208
- this.maxRetryDelayMs = options.maxRetryDelayMs;
22209
- this.toolExecution = options.toolExecution ?? "parallel";
22567
+ constructor(options) {
22568
+ const runtimeOptions = options ?? {};
22569
+ this._state = createMutableAgentState(runtimeOptions.initialState);
22570
+ this.convertToLlm = runtimeOptions.convertToLlm ?? defaultConvertToLlm;
22571
+ this.transformContext = runtimeOptions.transformContext;
22572
+ this.streamFunction = runtimeOptions.streamFn ?? getDefaultStreamFn();
22573
+ this.getApiKey = runtimeOptions.getApiKey;
22574
+ this.onPayload = runtimeOptions.onPayload;
22575
+ this.onResponse = runtimeOptions.onResponse;
22576
+ this.beforeToolCall = runtimeOptions.beforeToolCall;
22577
+ this.afterToolCall = runtimeOptions.afterToolCall;
22578
+ this.shouldStopAfterTurn = runtimeOptions.shouldStopAfterTurn;
22579
+ this.prepareNextTurn = runtimeOptions.prepareNextTurn;
22580
+ this.prepareNextTurnWithContext = runtimeOptions.prepareNextTurnWithContext;
22581
+ this.steeringQueue = new PendingMessageQueue(runtimeOptions.steeringMode ?? "one-at-a-time");
22582
+ this.followUpQueue = new PendingMessageQueue(runtimeOptions.followUpMode ?? "one-at-a-time");
22583
+ this.sessionId = runtimeOptions.sessionId;
22584
+ this.thinkingBudgets = runtimeOptions.thinkingBudgets;
22585
+ this.transport = runtimeOptions.transport ?? "auto";
22586
+ this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
22587
+ this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
22210
22588
  }
22211
22589
  /**
22212
22590
  * Subscribe to agent lifecycle events.
@@ -22336,12 +22714,12 @@ var Agent$1 = class {
22336
22714
  }
22337
22715
  async runPromptMessages(messages, options = {}) {
22338
22716
  await this.runWithLifecycle(async (signal) => {
22339
- await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
22717
+ await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFunction);
22340
22718
  });
22341
22719
  }
22342
22720
  async runContinuation() {
22343
22721
  await this.runWithLifecycle(async (signal) => {
22344
- await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
22722
+ await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFunction);
22345
22723
  });
22346
22724
  }
22347
22725
  createContextSnapshot() {
@@ -22365,7 +22743,11 @@ var Agent$1 = class {
22365
22743
  toolExecution: this.toolExecution,
22366
22744
  beforeToolCall: this.beforeToolCall,
22367
22745
  afterToolCall: this.afterToolCall,
22368
- prepareNextTurn: this.prepareNextTurn ? async () => await this.prepareNextTurn?.(this.signal) : void 0,
22746
+ shouldStopAfterTurn: this.shouldStopAfterTurn,
22747
+ prepareNextTurn: this.prepareNextTurnWithContext || this.prepareNextTurn ? async (context) => {
22748
+ if (this.prepareNextTurnWithContext) return await this.prepareNextTurnWithContext(context, this.signal);
22749
+ return await this.prepareNextTurn?.(this.signal);
22750
+ } : void 0,
22369
22751
  convertToLlm: this.convertToLlm,
22370
22752
  transformContext: this.transformContext,
22371
22753
  getApiKey: this.getApiKey,
@@ -22490,28 +22872,49 @@ var Agent$1 = class {
22490
22872
  const DEFAULT_MAX_BYTES = 50 * 1024;
22491
22873
  const runtimeBuffer = globalThis.Buffer;
22492
22874
 
22875
+ //#endregion
22876
+ //#region src/vendor/pi/agent/harness/tools/bash.ts
22877
+ const MAX_TIMEOUT_SECONDS = 2147483647 / 1e3;
22878
+ const bashSchema = Type.Object({
22879
+ command: Type.String({ description: "Bash command to execute" }),
22880
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }))
22881
+ });
22882
+
22883
+ //#endregion
22884
+ //#region src/vendor/pi/agent/harness/tools/edit.ts
22885
+ const replaceEditSchema = Type.Object({
22886
+ oldText: Type.String({ description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call." }),
22887
+ newText: Type.String({ description: "Replacement text for this targeted edit." })
22888
+ }, {});
22889
+ const editSchema = Type.Object({
22890
+ path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
22891
+ edits: Type.Array(replaceEditSchema, { description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead." })
22892
+ }, {});
22893
+
22894
+ //#endregion
22895
+ //#region src/vendor/pi/agent/harness/tools/read.ts
22896
+ const readSchema = Type.Object({
22897
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
22898
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
22899
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
22900
+ });
22901
+
22902
+ //#endregion
22903
+ //#region src/vendor/pi/agent/harness/tools/write.ts
22904
+ const writeSchema = Type.Object({
22905
+ path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
22906
+ content: Type.String({ description: "Content to write to the file" })
22907
+ });
22908
+
22493
22909
  //#endregion
22494
22910
  //#region src/lib/worker-agent/budget.ts
22495
22911
  const DEFAULT_MAX_TURNS = 500;
22496
22912
  const DEFAULT_MAX_WALLCLOCK_MS = 360 * 6e4;
22913
+ const DEFAULT_MODEL_CALL_TIMEOUT_MS = 15 * 6e4;
22497
22914
  const DEFAULT_MAX_TOOL_BYTES = 16 * 1024 * 1024;
22498
22915
  const DEFAULT_MAX_TOOL_CALLS = 250;
22499
22916
  const DEFAULT_MAX_REPEATED_CALLS = 3;
22500
22917
  /**
22501
- * Thrown when the wall-clock budget is exceeded. Engine catches this
22502
- * around `agent.prompt()` / `agent.continue()` and converts it to a
22503
- * terse `[halted: wallclock]` reply. Carries no extra metadata — by
22504
- * design (no advice).
22505
- */
22506
- var WorkerAbort = class extends Error {
22507
- reason;
22508
- constructor(reason) {
22509
- super(`[halted: ${reason}]`);
22510
- this.reason = reason;
22511
- this.name = "WorkerAbort";
22512
- }
22513
- };
22514
- /**
22515
22918
  * Read a positive-integer env override. Returns `undefined` if the
22516
22919
  * env var is unset, empty, or doesn't parse to a positive integer —
22517
22920
  * keeping the constructor defaults intact. We don't throw on bad input
@@ -22540,6 +22943,8 @@ const DEFAULT_MCP_TOOL_TIMEOUT_MS = 225e5;
22540
22943
  * hard-killed by the harness.
22541
22944
  */
22542
22945
  const MCP_TIMEOUT_HEADROOM_MS = 15 * 6e4;
22946
+ /** Smallest useful worker deadline when the MCP timeout is misconfigured below headroom. */
22947
+ const MIN_WORKER_WALLCLOCK_MS = 1e3;
22543
22948
  /**
22544
22949
  * The MCP per-tool-call timeout the proxy injects into the spawned CLI, in ms.
22545
22950
  * Positive-integer override via `GH_ROUTER_MCP_TOOL_TIMEOUT_MS`; falls back to
@@ -22551,6 +22956,10 @@ const MCP_TIMEOUT_HEADROOM_MS = 15 * 6e4;
22551
22956
  function resolveMcpToolTimeoutMs() {
22552
22957
  return envInt("GH_ROUTER_MCP_TOOL_TIMEOUT_MS") ?? DEFAULT_MCP_TOOL_TIMEOUT_MS;
22553
22958
  }
22959
+ /** Whole-call deadline for one worker model turn, including SSE consumption. */
22960
+ function resolveWorkerModelCallTimeoutMs() {
22961
+ return envInt("GH_ROUTER_WORKER_MODEL_CALL_TIMEOUT_MS") ?? DEFAULT_MODEL_CALL_TIMEOUT_MS;
22962
+ }
22554
22963
  /**
22555
22964
  * The maximum wall-clock a single worker call may be granted: the MCP
22556
22965
  * tool-call timeout minus the teardown headroom. A per-call `maxWallClockMs`
@@ -22560,7 +22969,7 @@ function resolveMcpToolTimeoutMs() {
22560
22969
  * the default MCP timeout (22_500_000 − 900_000 === 21_600_000).
22561
22970
  */
22562
22971
  function workerWallClockCeilingMs() {
22563
- return resolveMcpToolTimeoutMs() - MCP_TIMEOUT_HEADROOM_MS;
22972
+ return Math.max(MIN_WORKER_WALLCLOCK_MS, resolveMcpToolTimeoutMs() - MCP_TIMEOUT_HEADROOM_MS);
22564
22973
  }
22565
22974
  /**
22566
22975
  * Resolve a `BudgetConfig` from defaults + env overrides + caller-
@@ -22570,13 +22979,14 @@ function workerWallClockCeilingMs() {
22570
22979
  * can introspect the merged config without spinning up the `Budget`
22571
22980
  * class.
22572
22981
  */
22573
- function resolveBudgetConfig(overrides) {
22982
+ function resolveBudgetConfig(overrides$1) {
22983
+ const requestedWallClockMs = overrides$1?.maxWallClockMs ?? envInt("GH_ROUTER_WORKER_MAX_WALLCLOCK_MS") ?? DEFAULT_MAX_WALLCLOCK_MS;
22574
22984
  return {
22575
- maxTurns: overrides?.maxTurns ?? envInt("GH_ROUTER_WORKER_MAX_TURNS") ?? DEFAULT_MAX_TURNS,
22576
- maxWallClockMs: overrides?.maxWallClockMs ?? envInt("GH_ROUTER_WORKER_MAX_WALLCLOCK_MS") ?? DEFAULT_MAX_WALLCLOCK_MS,
22577
- maxToolBytes: overrides?.maxToolBytes ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_BYTES") ?? DEFAULT_MAX_TOOL_BYTES,
22578
- maxToolCalls: overrides?.maxToolCalls ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_CALLS") ?? DEFAULT_MAX_TOOL_CALLS,
22579
- maxRepeatedCalls: overrides?.maxRepeatedCalls ?? envInt("GH_ROUTER_WORKER_MAX_REPEATED_CALLS") ?? DEFAULT_MAX_REPEATED_CALLS
22985
+ maxTurns: overrides$1?.maxTurns ?? envInt("GH_ROUTER_WORKER_MAX_TURNS") ?? DEFAULT_MAX_TURNS,
22986
+ maxWallClockMs: Math.min(requestedWallClockMs, workerWallClockCeilingMs()),
22987
+ maxToolBytes: overrides$1?.maxToolBytes ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_BYTES") ?? DEFAULT_MAX_TOOL_BYTES,
22988
+ maxToolCalls: overrides$1?.maxToolCalls ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_CALLS") ?? DEFAULT_MAX_TOOL_CALLS,
22989
+ maxRepeatedCalls: overrides$1?.maxRepeatedCalls ?? envInt("GH_ROUTER_WORKER_MAX_REPEATED_CALLS") ?? DEFAULT_MAX_REPEATED_CALLS
22580
22990
  };
22581
22991
  }
22582
22992
  /**
@@ -22588,11 +22998,10 @@ function resolveBudgetConfig(overrides) {
22588
22998
  * - `checkBeforeCall(name, args)` is called from Pi's
22589
22999
  * `beforeToolCall` hook. Returns `{block: true, reason: "[halted:
22590
23000
  * turns]"}` etc. when a cap fires.
22591
- * - `recordToolBytes(result)` is called from Pi's `afterToolCall`
22592
- * hook.
22593
- * - `checkWallClock()` is called by the engine around blocking
22594
- * awaits and from `beforeToolCall` throws `WorkerAbort` when
22595
- * `Date.now() - startMs > maxWallClockMs`.
23001
+ * - `recordToolBytes(result)` is called from Pi's `afterToolCall` hook.
23002
+ * - A hard cap latches `hardStopReason`; the engine's
23003
+ * `shouldStopAfterTurn` hook reads it and ends the loop before another
23004
+ * provider request. The repeated-call guard remains a non-terminal block.
22596
23005
  */
22597
23006
  var Budget = class {
22598
23007
  config;
@@ -22600,10 +23009,11 @@ var Budget = class {
22600
23009
  turnCount = 0;
22601
23010
  toolBytes = 0;
22602
23011
  toolCallCount = 0;
23012
+ hardStopReasonValue = null;
22603
23013
  lastCallKey = null;
22604
23014
  consecutiveRepeats = 0;
22605
- constructor(overrides) {
22606
- this.config = resolveBudgetConfig(overrides);
23015
+ constructor(overrides$1) {
23016
+ this.config = resolveBudgetConfig(overrides$1);
22607
23017
  this.startMs = Date.now();
22608
23018
  }
22609
23019
  /** Record a turn. Does NOT throw — `checkBeforeCall` surfaces the cap. */
@@ -22622,16 +23032,16 @@ var Budget = class {
22622
23032
  get elapsedMs() {
22623
23033
  return Date.now() - this.startMs;
22624
23034
  }
22625
- /**
22626
- * Throw `WorkerAbort("wallclock")` if elapsed time exceeds
22627
- * `maxWallClockMs`. Engine wraps long awaits in `await
22628
- * Promise.race([..., wallClockTimer])` for prompt cancellation; this
22629
- * is the fallback for cases where the timer hasn't fired yet but a
22630
- * call site wants to be sure (e.g. before sending the next LLM
22631
- * request).
22632
- */
22633
- checkWallClock() {
22634
- if (this.elapsedMs > this.config.maxWallClockMs) throw new WorkerAbort("wallclock");
23035
+ /** First hard cap reached during this run, or null while work may continue. */
23036
+ get hardStopReason() {
23037
+ return this.hardStopReasonValue;
23038
+ }
23039
+ halt(reason) {
23040
+ this.hardStopReasonValue ??= reason;
23041
+ return {
23042
+ block: true,
23043
+ reason: `[halted: ${reason}]`
23044
+ };
22635
23045
  }
22636
23046
  /**
22637
23047
  * Pi `beforeToolCall` integration. Returns `{block: true, reason}`
@@ -22650,23 +23060,12 @@ var Budget = class {
22650
23060
  * signature in Pi without forcing the engine into a wrapper.
22651
23061
  */
22652
23062
  checkBeforeCall(toolName, args) {
22653
- if (this.turnCount > this.config.maxTurns) return {
22654
- block: true,
22655
- reason: "[halted: turns]"
22656
- };
22657
- if (this.elapsedMs > this.config.maxWallClockMs) return {
22658
- block: true,
22659
- reason: "[halted: wallclock]"
22660
- };
22661
- if (this.toolBytes > this.config.maxToolBytes) return {
22662
- block: true,
22663
- reason: "[halted: tool-bytes]"
22664
- };
23063
+ if (this.hardStopReasonValue) return this.halt(this.hardStopReasonValue);
23064
+ if (this.turnCount > this.config.maxTurns) return this.halt("turns");
23065
+ if (this.elapsedMs > this.config.maxWallClockMs) return this.halt("wallclock");
23066
+ if (this.toolBytes > this.config.maxToolBytes) return this.halt("tool-bytes");
22665
23067
  this.toolCallCount += 1;
22666
- if (this.toolCallCount > this.config.maxToolCalls) return {
22667
- block: true,
22668
- reason: "[halted: tool-calls]"
22669
- };
23068
+ if (this.toolCallCount > this.config.maxToolCalls) return this.halt("tool-calls");
22670
23069
  const key = `${toolName}:${stableArgs(args)}`;
22671
23070
  if (key === this.lastCallKey) this.consecutiveRepeats += 1;
22672
23071
  else {
@@ -22735,14 +23134,15 @@ function extractTextByteLength(result) {
22735
23134
  * the clamp logic. Lower index = less thinking. `"off"` is below
22736
23135
  * everything; `"xhigh"` is the cap.
22737
23136
  */
22738
- const THINKING_ORDER = [
23137
+ const WORKER_THINKING_LEVELS = Object.freeze([
22739
23138
  "off",
22740
23139
  "minimal",
22741
23140
  "low",
22742
23141
  "medium",
22743
23142
  "high",
22744
23143
  "xhigh"
22745
- ];
23144
+ ]);
23145
+ const THINKING_ORDER = WORKER_THINKING_LEVELS;
22746
23146
  function tier(level) {
22747
23147
  const i = THINKING_ORDER.indexOf(level);
22748
23148
  return i < 0 ? THINKING_ORDER.indexOf("high") : i;
@@ -22784,13 +23184,7 @@ function resolveModelAndThinking(opts) {
22784
23184
  });
22785
23185
  const allowedRaw = found.capabilities?.supports?.reasoning_effort;
22786
23186
  if (!allowedRaw || allowedRaw.length === 0) return mkOk("off");
22787
- const allowed = allowedRaw.filter((l) => [
22788
- "minimal",
22789
- "low",
22790
- "medium",
22791
- "high",
22792
- "xhigh"
22793
- ].includes(l)).sort((a, b) => tier(a) - tier(b));
23187
+ const allowed = allowedRaw.filter((l) => WORKER_THINKING_LEVELS.includes(l) && l !== "off").sort((a, b) => tier(a) - tier(b));
22794
23188
  if (allowed.length === 0) return mkOk("off");
22795
23189
  if (opts.thinking === "off") return mkOk("off");
22796
23190
  if (allowed.includes(opts.thinking)) return mkOk(opts.thinking);
@@ -22804,6 +23198,40 @@ function resolveModelAndThinking(opts) {
22804
23198
  return mkOk(clamp$1);
22805
23199
  }
22806
23200
 
23201
+ //#endregion
23202
+ //#region src/lib/worker-agent/session-defaults.ts
23203
+ const MODES = Object.freeze([
23204
+ "explore",
23205
+ "review",
23206
+ "plan",
23207
+ "implement",
23208
+ "test",
23209
+ "browse"
23210
+ ]);
23211
+ /**
23212
+ * Process-global, in-memory overrides. They are intentionally not persisted.
23213
+ * In `serve` mode one process may serve multiple client sessions, so these
23214
+ * values are process-wide rather than isolated to an individual client.
23215
+ */
23216
+ const overrides = {};
23217
+ function getWorkerSessionDefault(mode) {
23218
+ return { ...overrides[mode] };
23219
+ }
23220
+ function setWorkerSessionDefault(mode, value) {
23221
+ const next = { ...overrides[mode] };
23222
+ if (value.model !== void 0) next.model = value.model;
23223
+ if (value.thinking !== void 0) next.thinking = value.thinking;
23224
+ overrides[mode] = next;
23225
+ return { ...next };
23226
+ }
23227
+ function resetWorkerSessionDefault(mode) {
23228
+ delete overrides[mode];
23229
+ }
23230
+ function resetAllWorkerSessionDefaults() {
23231
+ for (const mode of MODES) delete overrides[mode];
23232
+ }
23233
+ const WORKER_MODES = MODES;
23234
+
22807
23235
  //#endregion
22808
23236
  //#region src/lib/worker-agent/prompts.ts
22809
23237
  /**
@@ -23220,6 +23648,13 @@ function assembleResponsesPayload(opts) {
23220
23648
  */
23221
23649
  /** Conservative bytes/token for dense DOM-JSON; over-counts tokens by design. */
23222
23650
  const BYTES_PER_TOKEN = 3;
23651
+ /**
23652
+ * Conservative context floor when the live catalog omits or reports an invalid window.
23653
+ * This is deliberately not a hardcoded per-model window table: duplicating the
23654
+ * live catalog would go stale on every model launch and violate the rule to
23655
+ * gate on catalog capabilities rather than model slugs.
23656
+ */
23657
+ const FALLBACK_WINDOW_TOKENS = 128e3;
23223
23658
  const OUTPUT_RESERVE_TOKENS = 12e3;
23224
23659
  const TOOL_SCHEMA_RESERVE_TOKENS = 6e3;
23225
23660
  const SYSTEM_RESERVE_TOKENS = 2e3;
@@ -23253,17 +23688,19 @@ function tokensFromBytes(bytes) {
23253
23688
  /**
23254
23689
  * Build a per-run budget from the model's catalog context window (tokens).
23255
23690
  *
23256
- * Returns `undefined` when the window is unknown / non-positive — callers
23257
- * MUST no-op (no compaction, no dynamic cap) rather than prune blindly
23258
- * against a guessed window. This is the safe degradation on a catalog that
23259
- * doesn't report `max_context_window_tokens`.
23691
+ * Unknown, non-finite, and non-positive windows use the conservative fallback
23692
+ * floor so compaction and the dynamic result cap remain engaged. The returned
23693
+ * `windowKnown` flag keeps the request backstop advisory in that case: upstream
23694
+ * remains authoritative when the model's real window is unknown.
23260
23695
  */
23261
23696
  function makeContextBudget(windowTokens) {
23262
- if (windowTokens === void 0 || !Number.isFinite(windowTokens) || windowTokens <= 0) return;
23263
- const inputHardLimitTokens = Math.max(0, Math.floor(windowTokens * (1 - ASSEMBLY_MARGIN_FRACTION)) - OUTPUT_RESERVE_TOKENS);
23697
+ const windowKnown = windowTokens !== void 0 && Number.isFinite(windowTokens) && windowTokens > 0;
23698
+ const effectiveWindowTokens = windowKnown ? windowTokens : FALLBACK_WINDOW_TOKENS;
23699
+ const inputHardLimitTokens = Math.max(0, Math.floor(effectiveWindowTokens * (1 - ASSEMBLY_MARGIN_FRACTION)) - OUTPUT_RESERVE_TOKENS);
23264
23700
  const promptBudgetTokens = Math.max(0, inputHardLimitTokens - TOOL_SCHEMA_RESERVE_TOKENS - SYSTEM_RESERVE_TOKENS);
23265
23701
  return {
23266
- windowTokens,
23702
+ windowKnown,
23703
+ windowTokens: effectiveWindowTokens,
23267
23704
  inputHardLimitTokens,
23268
23705
  promptBudgetTokens,
23269
23706
  compactTriggerTokens: Math.floor(promptBudgetTokens * COMPACT_TRIGGER_FRACTION),
@@ -23271,7 +23708,7 @@ function makeContextBudget(windowTokens) {
23271
23708
  hardLimitTokens: Math.floor(promptBudgetTokens * HARD_LIMIT_FRACTION),
23272
23709
  keepRecentTokens: Math.max(KEEP_RECENT_FLOOR_TOKENS, Math.floor(promptBudgetTokens * KEEP_RECENT_FRACTION)),
23273
23710
  maxProtectedTokens: Math.max(Math.max(KEEP_RECENT_FLOOR_TOKENS, Math.floor(promptBudgetTokens * KEEP_RECENT_FRACTION)), Math.floor(promptBudgetTokens * MAX_PROTECTED_FRACTION)),
23274
- perResultCapBytes: clamp(Math.round(windowTokens * PER_RESULT_CAP_FRACTION * BYTES_PER_TOKEN), PER_RESULT_CAP_MIN_BYTES, PER_RESULT_CAP_MAX_BYTES)
23711
+ perResultCapBytes: clamp(Math.round(effectiveWindowTokens * PER_RESULT_CAP_FRACTION * BYTES_PER_TOKEN), PER_RESULT_CAP_MIN_BYTES, PER_RESULT_CAP_MAX_BYTES)
23275
23712
  };
23276
23713
  }
23277
23714
 
@@ -23294,11 +23731,52 @@ function createCopilotStreamFn(opts) {
23294
23731
  return stream;
23295
23732
  };
23296
23733
  }
23734
+ async function runModelCallAttempts(stream, resolved, options, timeoutMs, runAttempt) {
23735
+ for (let attempt = 0; attempt < 2; attempt += 1) {
23736
+ const deadline = new AbortController();
23737
+ let localDeadlineFired = false;
23738
+ let rejectDeadline = () => {};
23739
+ const timeoutPromise = new Promise((_resolve, reject) => {
23740
+ rejectDeadline = reject;
23741
+ });
23742
+ timeoutPromise.catch(() => {});
23743
+ const timer = setTimeout(() => {
23744
+ localDeadlineFired = true;
23745
+ const error = new DOMException("Worker model call timed out", "TimeoutError");
23746
+ deadline.abort(error);
23747
+ rejectDeadline(error);
23748
+ }, timeoutMs);
23749
+ const signal = options?.signal ? AbortSignal.any([options.signal, deadline.signal]) : deadline.signal;
23750
+ const state$1 = {
23751
+ emitted: false,
23752
+ active: true
23753
+ };
23754
+ const attemptPromise = runAttempt(signal, state$1);
23755
+ attemptPromise.catch(() => {});
23756
+ try {
23757
+ await Promise.race([attemptPromise, timeoutPromise]);
23758
+ return;
23759
+ } catch (err) {
23760
+ if (options?.signal?.aborted) {
23761
+ pushTerminalError(stream, resolved, options.signal.reason ?? err);
23762
+ return;
23763
+ }
23764
+ if (!state$1.emitted && attempt === 0) continue;
23765
+ if (localDeadlineFired) pushModelCallTimeoutDiagnostic(stream, resolved, timeoutMs, state$1.emitted);
23766
+ else pushTerminalError(stream, resolved, err);
23767
+ return;
23768
+ } finally {
23769
+ state$1.active = false;
23770
+ clearTimeout(timer);
23771
+ }
23772
+ }
23773
+ }
23297
23774
  async function runStreamLoop(stream, context, opts, options) {
23298
23775
  const { resolved } = opts;
23299
23776
  if (opts.contextBudget) {
23300
23777
  const assembledTokens = tokensFromBytes(estimateContextBytes(context));
23301
- if (assembledTokens > opts.contextBudget.inputHardLimitTokens) {
23778
+ if (assembledTokens > opts.contextBudget.inputHardLimitTokens) if (!opts.contextBudget.windowKnown) consola.warn(`[worker] assembled request estimate ${assembledTokens} tokens exceeds fallback input limit ${opts.contextBudget.inputHardLimitTokens}; proceeding because the catalog window is unknown`);
23779
+ else {
23302
23780
  pushBackstopDiagnostic(stream, resolved, assembledTokens, opts.contextBudget.inputHardLimitTokens);
23303
23781
  return;
23304
23782
  }
@@ -23314,15 +23792,13 @@ async function runStreamLoop(stream, context, opts, options) {
23314
23792
  pushTerminalError(stream, resolved, err);
23315
23793
  return;
23316
23794
  }
23317
- let sseStream;
23318
- try {
23319
- const result = await createChatCompletions(payload, void 0, options?.signal, true);
23320
- if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23321
- sseStream = result;
23322
- } catch (err) {
23323
- pushTerminalError(stream, resolved, err);
23324
- return;
23325
- }
23795
+ await runModelCallAttempts(stream, resolved, options, opts.modelCallTimeoutMs ?? 15 * 6e4, (signal, state$1) => runChatAttempt(stream, payload, opts, signal, state$1));
23796
+ }
23797
+ async function runChatAttempt(stream, payload, opts, signal, state$1) {
23798
+ const { resolved } = opts;
23799
+ const result = await createChatCompletions(payload, void 0, signal, false);
23800
+ if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23801
+ const sseStream = result;
23326
23802
  const accum = {
23327
23803
  blocks: [],
23328
23804
  textChunksByIndex: /* @__PURE__ */ new Map(),
@@ -23331,100 +23807,102 @@ async function runStreamLoop(stream, context, opts, options) {
23331
23807
  let nextContentIndex = 0;
23332
23808
  let activeTextIndex = null;
23333
23809
  const toolPiIndexByOAI = /* @__PURE__ */ new Map();
23334
- try {
23335
- for await (const evt of sseStream) {
23336
- const data = evt?.data;
23337
- if (data == null) continue;
23338
- if (data === "[DONE]") break;
23339
- let chunk;
23340
- try {
23341
- chunk = JSON.parse(data);
23342
- } catch {
23343
- continue;
23810
+ for await (const evt of sseStream) {
23811
+ const data = evt?.data;
23812
+ if (data == null) continue;
23813
+ if (data === "[DONE]") break;
23814
+ let chunk;
23815
+ try {
23816
+ chunk = JSON.parse(data);
23817
+ } catch {
23818
+ continue;
23819
+ }
23820
+ try {
23821
+ opts.onChunk?.(chunk);
23822
+ } catch {}
23823
+ if (chunk.usage) accum.usage = chunk.usage;
23824
+ const choice = chunk.choices?.[0];
23825
+ if (!choice) continue;
23826
+ const delta = choice.delta ?? {};
23827
+ if (typeof delta.content === "string" && delta.content.length > 0) {
23828
+ if (activeTextIndex == null) {
23829
+ state$1.emitted = true;
23830
+ activeTextIndex = nextContentIndex++;
23831
+ accum.blocks.push({
23832
+ kind: "text",
23833
+ contentIndex: activeTextIndex
23834
+ });
23835
+ accum.textChunksByIndex.set(activeTextIndex, []);
23836
+ if (!state$1.active) return;
23837
+ stream.push({
23838
+ type: "text_start",
23839
+ contentIndex: activeTextIndex,
23840
+ partial: buildPartial(resolved, accum)
23841
+ });
23344
23842
  }
23345
- try {
23346
- opts.onChunk?.(chunk);
23347
- } catch {}
23348
- if (chunk.usage) accum.usage = chunk.usage;
23349
- const choice = chunk.choices?.[0];
23350
- if (!choice) continue;
23351
- const delta = choice.delta ?? {};
23352
- if (typeof delta.content === "string" && delta.content.length > 0) {
23353
- if (activeTextIndex == null) {
23354
- activeTextIndex = nextContentIndex++;
23355
- accum.blocks.push({
23356
- kind: "text",
23357
- contentIndex: activeTextIndex
23358
- });
23359
- accum.textChunksByIndex.set(activeTextIndex, []);
23360
- stream.push({
23361
- type: "text_start",
23362
- contentIndex: activeTextIndex,
23363
- partial: buildPartial(resolved, accum)
23364
- });
23365
- }
23366
- accum.textChunksByIndex.get(activeTextIndex).push(delta.content);
23843
+ accum.textChunksByIndex.get(activeTextIndex).push(delta.content);
23844
+ stream.push({
23845
+ type: "text_delta",
23846
+ contentIndex: activeTextIndex,
23847
+ delta: delta.content,
23848
+ partial: buildPartial(resolved, accum)
23849
+ });
23850
+ }
23851
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
23852
+ if (activeTextIndex != null) {
23853
+ if (!state$1.active) return;
23367
23854
  stream.push({
23368
- type: "text_delta",
23855
+ type: "text_end",
23369
23856
  contentIndex: activeTextIndex,
23370
- delta: delta.content,
23857
+ content: joinTextChunks(accum, activeTextIndex),
23371
23858
  partial: buildPartial(resolved, accum)
23372
23859
  });
23860
+ activeTextIndex = null;
23373
23861
  }
23374
- if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
23375
- if (activeTextIndex != null) {
23862
+ for (const tcd of delta.tool_calls) {
23863
+ if (tcd == null || tcd.index == null) continue;
23864
+ let piIdx = toolPiIndexByOAI.get(tcd.index);
23865
+ if (piIdx == null) {
23866
+ state$1.emitted = true;
23867
+ piIdx = nextContentIndex++;
23868
+ toolPiIndexByOAI.set(tcd.index, piIdx);
23869
+ accum.blocks.push({
23870
+ kind: "tool",
23871
+ contentIndex: piIdx,
23872
+ openaiIndex: tcd.index
23873
+ });
23874
+ accum.toolByIndex.set(piIdx, {
23875
+ id: "",
23876
+ name: "",
23877
+ argumentChunks: []
23878
+ });
23879
+ if (!state$1.active) return;
23376
23880
  stream.push({
23377
- type: "text_end",
23378
- contentIndex: activeTextIndex,
23379
- content: joinTextChunks(accum, activeTextIndex),
23881
+ type: "toolcall_start",
23882
+ contentIndex: piIdx,
23380
23883
  partial: buildPartial(resolved, accum)
23381
23884
  });
23382
- activeTextIndex = null;
23383
23885
  }
23384
- for (const tcd of delta.tool_calls) {
23385
- if (tcd == null || tcd.index == null) continue;
23386
- let piIdx = toolPiIndexByOAI.get(tcd.index);
23387
- if (piIdx == null) {
23388
- piIdx = nextContentIndex++;
23389
- toolPiIndexByOAI.set(tcd.index, piIdx);
23390
- accum.blocks.push({
23391
- kind: "tool",
23392
- contentIndex: piIdx,
23393
- openaiIndex: tcd.index
23394
- });
23395
- accum.toolByIndex.set(piIdx, {
23396
- id: "",
23397
- name: "",
23398
- argumentChunks: []
23399
- });
23400
- stream.push({
23401
- type: "toolcall_start",
23402
- contentIndex: piIdx,
23403
- partial: buildPartial(resolved, accum)
23404
- });
23405
- }
23406
- const entry = accum.toolByIndex.get(piIdx);
23407
- if (!entry) continue;
23408
- if (tcd.id) entry.id = tcd.id;
23409
- if (tcd.function?.name) entry.name = tcd.function.name;
23410
- const argDelta = tcd.function?.arguments;
23411
- if (typeof argDelta === "string" && argDelta.length > 0) {
23412
- entry.argumentChunks.push(argDelta);
23413
- stream.push({
23414
- type: "toolcall_delta",
23415
- contentIndex: piIdx,
23416
- delta: argDelta,
23417
- partial: buildPartial(resolved, accum)
23418
- });
23419
- }
23886
+ const entry = accum.toolByIndex.get(piIdx);
23887
+ if (!entry) continue;
23888
+ if (tcd.id) entry.id = tcd.id;
23889
+ if (tcd.function?.name) entry.name = tcd.function.name;
23890
+ const argDelta = tcd.function?.arguments;
23891
+ if (typeof argDelta === "string" && argDelta.length > 0) {
23892
+ entry.argumentChunks.push(argDelta);
23893
+ if (!state$1.active) return;
23894
+ stream.push({
23895
+ type: "toolcall_delta",
23896
+ contentIndex: piIdx,
23897
+ delta: argDelta,
23898
+ partial: buildPartial(resolved, accum)
23899
+ });
23420
23900
  }
23421
23901
  }
23422
- if (choice.finish_reason) accum.finishReason = choice.finish_reason;
23423
23902
  }
23424
- } catch (err) {
23425
- pushTerminalError(stream, resolved, err);
23426
- return;
23903
+ if (choice.finish_reason) accum.finishReason = choice.finish_reason;
23427
23904
  }
23905
+ if (!state$1.active) return;
23428
23906
  if (activeTextIndex != null) {
23429
23907
  stream.push({
23430
23908
  type: "text_end",
@@ -23587,15 +24065,13 @@ async function runResponsesStreamLoop(stream, context, opts, options) {
23587
24065
  pushTerminalError(stream, resolved, err);
23588
24066
  return;
23589
24067
  }
23590
- let sseStream;
23591
- try {
23592
- const result = await createResponses(payload, void 0, options?.signal, true);
23593
- if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23594
- sseStream = result;
23595
- } catch (err) {
23596
- pushTerminalError(stream, resolved, err);
23597
- return;
23598
- }
24068
+ await runModelCallAttempts(stream, resolved, options, opts.modelCallTimeoutMs ?? 15 * 6e4, (signal, state$1) => runResponsesAttempt(stream, payload, opts, signal, state$1));
24069
+ }
24070
+ async function runResponsesAttempt(stream, payload, opts, signal, state$1) {
24071
+ const { resolved } = opts;
24072
+ const result = await createResponses(payload, void 0, signal, false);
24073
+ if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
24074
+ const sseStream = result;
23599
24075
  const accum = {
23600
24076
  blocks: [],
23601
24077
  textChunksByIndex: /* @__PURE__ */ new Map(),
@@ -23615,166 +24091,173 @@ async function runResponsesStreamLoop(stream, context, opts, options) {
23615
24091
  });
23616
24092
  activeTextIndex = null;
23617
24093
  };
23618
- try {
23619
- for await (const evt of sseStream) {
23620
- const data = evt?.data;
23621
- if (data == null) continue;
23622
- if (data === "[DONE]") break;
23623
- let ev;
23624
- try {
23625
- ev = JSON.parse(data);
23626
- } catch {
23627
- continue;
23628
- }
23629
- switch (ev.type) {
23630
- case "response.output_text.delta": {
23631
- const delta = ev.delta;
23632
- if (typeof delta !== "string" || delta.length === 0) break;
23633
- if (activeTextIndex == null) {
23634
- activeTextIndex = nextContentIndex++;
23635
- accum.blocks.push({
23636
- kind: "text",
23637
- contentIndex: activeTextIndex
23638
- });
23639
- accum.textChunksByIndex.set(activeTextIndex, []);
23640
- stream.push({
23641
- type: "text_start",
23642
- contentIndex: activeTextIndex,
23643
- partial: buildPartial(resolved, accum)
23644
- });
23645
- }
23646
- accum.textChunksByIndex.get(activeTextIndex).push(delta);
24094
+ for await (const evt of sseStream) {
24095
+ const data = evt?.data;
24096
+ if (data == null) continue;
24097
+ if (data === "[DONE]") break;
24098
+ let ev;
24099
+ try {
24100
+ ev = JSON.parse(data);
24101
+ } catch {
24102
+ continue;
24103
+ }
24104
+ switch (ev.type) {
24105
+ case "response.output_text.delta": {
24106
+ const delta = ev.delta;
24107
+ if (typeof delta !== "string" || delta.length === 0) break;
24108
+ if (activeTextIndex == null) {
24109
+ state$1.emitted = true;
24110
+ activeTextIndex = nextContentIndex++;
24111
+ accum.blocks.push({
24112
+ kind: "text",
24113
+ contentIndex: activeTextIndex
24114
+ });
24115
+ accum.textChunksByIndex.set(activeTextIndex, []);
24116
+ if (!state$1.active) return;
23647
24117
  stream.push({
23648
- type: "text_delta",
24118
+ type: "text_start",
23649
24119
  contentIndex: activeTextIndex,
23650
- delta,
23651
24120
  partial: buildPartial(resolved, accum)
23652
24121
  });
23653
- break;
23654
24122
  }
23655
- case "response.output_text.done":
23656
- if (activeTextIndex == null && typeof ev.text === "string" && ev.text.length > 0) {
23657
- activeTextIndex = nextContentIndex++;
23658
- accum.blocks.push({
23659
- kind: "text",
23660
- contentIndex: activeTextIndex
23661
- });
23662
- accum.textChunksByIndex.set(activeTextIndex, []);
23663
- stream.push({
23664
- type: "text_start",
23665
- contentIndex: activeTextIndex,
23666
- partial: buildPartial(resolved, accum)
23667
- });
23668
- accum.textChunksByIndex.get(activeTextIndex).push(ev.text);
23669
- stream.push({
23670
- type: "text_delta",
23671
- contentIndex: activeTextIndex,
23672
- delta: ev.text,
23673
- partial: buildPartial(resolved, accum)
23674
- });
23675
- }
23676
- closeActiveText();
23677
- break;
23678
- case "response.output_item.added": {
23679
- const item = ev.item;
23680
- if (item?.type !== "function_call") break;
23681
- const key = responsesToolKey(ev.output_index, item.id);
23682
- if (key == null) break;
23683
- if (toolPiIndexByKey.has(key)) break;
23684
- closeActiveText();
23685
- const piIdx = nextContentIndex++;
23686
- toolPiIndexByKey.set(key, piIdx);
24123
+ accum.textChunksByIndex.get(activeTextIndex).push(delta);
24124
+ if (!state$1.active) return;
24125
+ stream.push({
24126
+ type: "text_delta",
24127
+ contentIndex: activeTextIndex,
24128
+ delta,
24129
+ partial: buildPartial(resolved, accum)
24130
+ });
24131
+ break;
24132
+ }
24133
+ case "response.output_text.done":
24134
+ if (activeTextIndex == null && typeof ev.text === "string" && ev.text.length > 0) {
24135
+ state$1.emitted = true;
24136
+ activeTextIndex = nextContentIndex++;
23687
24137
  accum.blocks.push({
23688
- kind: "tool",
23689
- contentIndex: piIdx,
23690
- openaiIndex: piIdx
23691
- });
23692
- accum.toolByIndex.set(piIdx, {
23693
- id: item.call_id ?? item.id ?? key,
23694
- name: item.name ?? "",
23695
- argumentChunks: []
23696
- });
23697
- stream.push({
23698
- type: "toolcall_start",
23699
- contentIndex: piIdx,
23700
- partial: buildPartial(resolved, accum)
24138
+ kind: "text",
24139
+ contentIndex: activeTextIndex
23701
24140
  });
23702
- break;
23703
- }
23704
- case "response.function_call_arguments.delta": {
23705
- const key = responsesToolKey(ev.output_index, ev.item_id);
23706
- if (key == null) break;
23707
- const piIdx = toolPiIndexByKey.get(key);
23708
- if (piIdx == null) break;
23709
- const entry = accum.toolByIndex.get(piIdx);
23710
- if (!entry) break;
23711
- const delta = ev.delta;
23712
- if (typeof delta !== "string" || delta.length === 0) break;
23713
- entry.argumentChunks.push(delta);
24141
+ accum.textChunksByIndex.set(activeTextIndex, []);
24142
+ if (!state$1.active) return;
23714
24143
  stream.push({
23715
- type: "toolcall_delta",
23716
- contentIndex: piIdx,
23717
- delta,
24144
+ type: "text_start",
24145
+ contentIndex: activeTextIndex,
23718
24146
  partial: buildPartial(resolved, accum)
23719
24147
  });
23720
- break;
23721
- }
23722
- case "response.function_call_arguments.done": {
23723
- const key = responsesToolKey(ev.output_index, ev.item_id);
23724
- if (key == null) break;
23725
- const piIdx = toolPiIndexByKey.get(key);
23726
- if (piIdx == null) break;
23727
- const entry = accum.toolByIndex.get(piIdx);
23728
- if (entry && typeof ev.arguments === "string") entry.argumentChunks = [ev.arguments];
23729
- break;
23730
- }
23731
- case "response.output_item.done": {
23732
- const item = ev.item;
23733
- if (item?.type !== "function_call") break;
23734
- const key = responsesToolKey(ev.output_index, item.id);
23735
- if (key == null) break;
23736
- const piIdx = toolPiIndexByKey.get(key);
23737
- if (piIdx == null) break;
23738
- const entry = accum.toolByIndex.get(piIdx);
23739
- if (!entry) break;
23740
- if (item.call_id) entry.id = item.call_id;
23741
- if (item.name) entry.name = item.name;
23742
- if (typeof item.arguments === "string") entry.argumentChunks = [item.arguments];
24148
+ accum.textChunksByIndex.get(activeTextIndex).push(ev.text);
24149
+ if (!state$1.active) return;
23743
24150
  stream.push({
23744
- type: "toolcall_end",
23745
- contentIndex: piIdx,
23746
- toolCall: makePiToolCall(entry),
24151
+ type: "text_delta",
24152
+ contentIndex: activeTextIndex,
24153
+ delta: ev.text,
23747
24154
  partial: buildPartial(resolved, accum)
23748
24155
  });
23749
- closedToolItems.add(piIdx);
23750
- break;
23751
24156
  }
23752
- case "response.completed":
23753
- case "response.incomplete":
23754
- accum.usage = mapResponsesUsage(ev.response?.usage);
23755
- if (ev.type === "response.incomplete" && ev.response?.incomplete_details?.reason === "max_output_tokens") accum.finishReason = "length";
23756
- if (opts.onChunk && accum.usage) try {
23757
- opts.onChunk({
23758
- id: "",
23759
- object: "chat.completion.chunk",
23760
- created: 0,
23761
- model: resolved.modelId,
23762
- choices: [],
23763
- usage: accum.usage
23764
- });
23765
- } catch {}
23766
- break;
23767
- case "response.failed":
23768
- closeActiveText();
23769
- pushTerminalError(stream, resolved, new Error(ev.response?.error?.message ?? "response.failed"));
23770
- return;
23771
- default: break;
24157
+ closeActiveText();
24158
+ break;
24159
+ case "response.output_item.added": {
24160
+ const item = ev.item;
24161
+ if (item?.type !== "function_call") break;
24162
+ const key = responsesToolKey(ev.output_index, item.id);
24163
+ if (key == null) break;
24164
+ if (toolPiIndexByKey.has(key)) break;
24165
+ closeActiveText();
24166
+ state$1.emitted = true;
24167
+ const piIdx = nextContentIndex++;
24168
+ toolPiIndexByKey.set(key, piIdx);
24169
+ accum.blocks.push({
24170
+ kind: "tool",
24171
+ contentIndex: piIdx,
24172
+ openaiIndex: piIdx
24173
+ });
24174
+ accum.toolByIndex.set(piIdx, {
24175
+ id: item.call_id ?? item.id ?? key,
24176
+ name: item.name ?? "",
24177
+ argumentChunks: []
24178
+ });
24179
+ if (!state$1.active) return;
24180
+ stream.push({
24181
+ type: "toolcall_start",
24182
+ contentIndex: piIdx,
24183
+ partial: buildPartial(resolved, accum)
24184
+ });
24185
+ break;
24186
+ }
24187
+ case "response.function_call_arguments.delta": {
24188
+ const key = responsesToolKey(ev.output_index, ev.item_id);
24189
+ if (key == null) break;
24190
+ const piIdx = toolPiIndexByKey.get(key);
24191
+ if (piIdx == null) break;
24192
+ const entry = accum.toolByIndex.get(piIdx);
24193
+ if (!entry) break;
24194
+ const delta = ev.delta;
24195
+ if (typeof delta !== "string" || delta.length === 0) break;
24196
+ entry.argumentChunks.push(delta);
24197
+ if (!state$1.active) return;
24198
+ stream.push({
24199
+ type: "toolcall_delta",
24200
+ contentIndex: piIdx,
24201
+ delta,
24202
+ partial: buildPartial(resolved, accum)
24203
+ });
24204
+ break;
24205
+ }
24206
+ case "response.function_call_arguments.done": {
24207
+ const key = responsesToolKey(ev.output_index, ev.item_id);
24208
+ if (key == null) break;
24209
+ const piIdx = toolPiIndexByKey.get(key);
24210
+ if (piIdx == null) break;
24211
+ const entry = accum.toolByIndex.get(piIdx);
24212
+ if (entry && typeof ev.arguments === "string") entry.argumentChunks = [ev.arguments];
24213
+ break;
24214
+ }
24215
+ case "response.output_item.done": {
24216
+ const item = ev.item;
24217
+ if (item?.type !== "function_call") break;
24218
+ const key = responsesToolKey(ev.output_index, item.id);
24219
+ if (key == null) break;
24220
+ const piIdx = toolPiIndexByKey.get(key);
24221
+ if (piIdx == null) break;
24222
+ const entry = accum.toolByIndex.get(piIdx);
24223
+ if (!entry) break;
24224
+ if (item.call_id) entry.id = item.call_id;
24225
+ if (item.name) entry.name = item.name;
24226
+ if (typeof item.arguments === "string") entry.argumentChunks = [item.arguments];
24227
+ if (!state$1.active) return;
24228
+ stream.push({
24229
+ type: "toolcall_end",
24230
+ contentIndex: piIdx,
24231
+ toolCall: makePiToolCall(entry),
24232
+ partial: buildPartial(resolved, accum)
24233
+ });
24234
+ closedToolItems.add(piIdx);
24235
+ break;
23772
24236
  }
24237
+ case "response.completed":
24238
+ case "response.incomplete":
24239
+ accum.usage = mapResponsesUsage(ev.response?.usage);
24240
+ if (ev.type === "response.incomplete" && ev.response?.incomplete_details?.reason === "max_output_tokens") accum.finishReason = "length";
24241
+ if (opts.onChunk && accum.usage) try {
24242
+ opts.onChunk({
24243
+ id: "",
24244
+ object: "chat.completion.chunk",
24245
+ created: 0,
24246
+ model: resolved.modelId,
24247
+ choices: [],
24248
+ usage: accum.usage
24249
+ });
24250
+ } catch {}
24251
+ break;
24252
+ case "response.failed":
24253
+ if (!state$1.active) return;
24254
+ closeActiveText();
24255
+ pushTerminalError(stream, resolved, new Error(ev.response?.error?.message ?? "response.failed"));
24256
+ return;
24257
+ default: break;
23773
24258
  }
23774
- } catch (err) {
23775
- pushTerminalError(stream, resolved, err);
23776
- return;
23777
24259
  }
24260
+ if (!state$1.active) return;
23778
24261
  closeActiveText();
23779
24262
  for (const block of accum.blocks) {
23780
24263
  if (block.kind !== "tool") continue;
@@ -24092,6 +24575,23 @@ function fieldBytes(v) {
24092
24575
  * the engine marks the result isError). No upstream call is made — this
24093
24576
  * replaces an opaque upstream 4xx with an actionable, sanitized message.
24094
24577
  */
24578
+ function pushModelCallTimeoutDiagnostic(stream, resolved, timeoutMs, emitted) {
24579
+ const text = `Worker model call timed out after ${timeoutMs}ms${emitted ? " after emitting partial output" : " after two attempts with no output"}. The upstream stream stopped making progress. Retry the worker call; if it repeats, choose a different model.`;
24580
+ const final = {
24581
+ ...makeBaseMessage(resolved),
24582
+ content: [{
24583
+ type: "text",
24584
+ text
24585
+ }],
24586
+ stopReason: "error",
24587
+ errorMessage: "worker model-call deadline exceeded"
24588
+ };
24589
+ stream.push({
24590
+ type: "error",
24591
+ reason: "error",
24592
+ error: final
24593
+ });
24594
+ }
24095
24595
  function pushBackstopDiagnostic(stream, resolved, assembledTokens, limitTokens) {
24096
24596
  const text = `Request too large: the assembled input is ~${assembledTokens} tokens, over the ~${limitTokens}-token budget for ${resolved.modelId}. The run was stopped before an overflow error. Retry with a narrower task — target a specific section / file / element rather than reading everything at once.`;
24097
24597
  const final = {
@@ -25143,7 +25643,7 @@ function shimDefaultsToXhigh(id) {
25143
25643
  * ALL THREE peer models the consensus protocol needs:
25144
25644
  * - an OpenAI frontier model (`gpt-5.6-sol`, else `gpt-5.5` — see
25145
25645
  * `resolveOpenAiFrontier`)
25146
- * - `claude-opus-4-7` (opus_critic's model)
25646
+ * - `claude-opus-5` (stand_in's Anthropic slot)
25147
25647
  * - any `gemini-3.X.*pro` (gemini_critic's model family — matches the
25148
25648
  * same regex `geminiAvailable()` uses, so the gate stays in sync if
25149
25649
  * the GA slug renames `gemini-3.1-pro-preview` → `gemini-3.1-pro`)
@@ -25152,11 +25652,8 @@ function shimDefaultsToXhigh(id) {
25152
25652
  * fails `tools/call` with -32601 (mirroring the `worker` capability's
25153
25653
  * defense-in-depth pattern — the gated tool is functionally invisible).
25154
25654
  *
25155
- * Tier-mismatch on `claude-opus-4-7`: the proxy's `resolveModel` will
25156
- * fuzzy-match `claude-opus-4-7` to `claude-opus-4.7` (Copilot's dotted
25157
- * slug). For the catalog probe we use the Anthropic-published dashed
25158
- * slug too — `state.models?.data` mirrors Copilot's catalog where these
25159
- * land under the dotted slug, so we match by Copilot's actual id shape.
25655
+ * `claude-opus-5` is a single-segment slug (dotted == dashed), so the
25656
+ * catalog probe matches Copilot's actual id shape directly.
25160
25657
  */
25161
25658
  function geminiAvailable(source = state) {
25162
25659
  const models = source.models?.data;
@@ -25183,7 +25680,7 @@ function standInToolEnabled() {
25183
25680
  const models = state.models?.data;
25184
25681
  if (!models) return false;
25185
25682
  const hasOpenAi = resolveOpenAiFrontier() != null;
25186
- const hasOpus = models.some((m) => m.id === "claude-opus-4-7" || m.id === "claude-opus-4.7");
25683
+ const hasOpus = models.some((m) => m.id === "claude-opus-5");
25187
25684
  const hasGeminiPro = geminiAvailable();
25188
25685
  return hasOpenAi && hasOpus && hasGeminiPro;
25189
25686
  }
@@ -25461,27 +25958,42 @@ function checkAuth(c) {
25461
25958
  return { ok: true };
25462
25959
  }
25463
25960
  /**
25464
- * The 1M-context Opus 4.6 variant (`claude-opus-4.6-1m`, `max_prompt_tokens`
25465
- * 936K). opus_critic prefers it so it can take large artifacts in one shot
25466
- * (the whole point of pairing it with gpt-5.6-sol as the big-window peers);
25467
- * falls back to the 200K `claude-opus-4-6` when the catalog doesn't carry
25468
- * a 1M 4.6 slug. The regex is version-anchored to 4.6 AND requires a
25469
- * `-1m` suffix boundary (not a permissive `.*1m`), so it does NOT
25470
- * false-positive on `claude-opus-4.7-1m-internal` (stand_in's pinned
25471
- * 4.7 row), `claude-opus-4.6-1max` (hypothetical), or `claude-opus-4.8`
25472
- * (1M-without-sibling). Tolerates dotted (`opus-4.6-1m`) and dashed
25473
- * (`opus-4-6-1m`) catalog separators.
25961
+ * opus_critic's effective model, resolved against the live catalog.
25962
+ *
25963
+ * Prefers `claude-opus-5` a single-segment slug that is natively 1M
25964
+ * (no `-1m` sibling), so it takes large artifacts in one shot (the whole
25965
+ * point of pairing it with gpt-5.6-sol as the big-window peers). When opus-5
25966
+ * isn't in the catalog (e.g. a lesser tier), falls back to the older
25967
+ * 1M-context Opus 4.6 variant (`claude-opus-4.6-1m`, `max_prompt_tokens`
25968
+ * 936K), then to the 200K `claude-opus-4-6`. The 4.6 regex is
25969
+ * version-anchored AND requires a `-1m` suffix boundary (not a permissive
25970
+ * `.*1m`), so it does NOT false-positive on `claude-opus-4.7-1m-internal`,
25971
+ * `claude-opus-4.6-1max`, or a 1M-without-sibling base slug. Tolerates
25972
+ * dotted (`opus-4.6-1m`) and dashed (`opus-4-6-1m`) catalog separators.
25474
25973
  */
25475
25974
  const OPUS_1M_RE = /opus-4[.-]6-1m(?:$|-)/i;
25476
25975
  function resolveOpusCriticModel() {
25477
- const oneM = state.models?.data?.find((m) => OPUS_1M_RE.test(m.id));
25976
+ const models = state.models?.data;
25977
+ if (models?.some((m) => m.id === "claude-opus-5")) return "claude-opus-5";
25978
+ const oneM = models?.find((m) => OPUS_1M_RE.test(m.id));
25478
25979
  return oneM ? oneM.id : "claude-opus-4-6";
25479
25980
  }
25480
25981
  function activePersonas() {
25481
- return PERSONAS_READ.filter((p) => !p.requiresGeminiCatalog || geminiAvailable()).map((p) => p.toolNameHttp === "opus_critic" ? {
25482
- ...p,
25483
- model: resolveOpusCriticModel()
25484
- } : p);
25982
+ return PERSONAS_READ.filter((p) => !p.requiresGeminiCatalog || geminiAvailable()).map((p) => {
25983
+ if (p.toolNameHttp !== "opus_critic") return p;
25984
+ const model = resolveOpusCriticModel();
25985
+ const allowedEfforts = model === "claude-opus-5" ? [
25986
+ "low",
25987
+ "medium",
25988
+ "high",
25989
+ "xhigh"
25990
+ ] : p.allowedEfforts;
25991
+ return {
25992
+ ...p,
25993
+ model,
25994
+ allowedEfforts
25995
+ };
25996
+ });
25485
25997
  }
25486
25998
  function toolEntries(scope) {
25487
25999
  const personaEntries = scope === "all" || scope === "peers" ? activePersonas().map((p) => ({
@@ -25672,7 +26184,9 @@ async function predictedWindowOverflow(persona, prompt, context) {
25672
26184
  return;
25673
26185
  }
25674
26186
  if (tokens <= budget) return void 0;
25675
- const opusHint = OPUS_1M_RE.test(id) ? "" : " / `opus_critic` (Opus-4.7 1M ≈ 936K tokens, when the enterprise catalog carries it)";
26187
+ const criticModel = resolveOpusCriticModel();
26188
+ const criticIs1M = criticModel === "claude-opus-5" || OPUS_1M_RE.test(criticModel);
26189
+ const opusHint = id === "claude-opus-5" || OPUS_1M_RE.test(id) || !criticIs1M ? "" : " / `opus_critic` (Opus 5, 1M context ≈ 1M tokens)";
25676
26190
  return `pre-flight rejected: this ${persona.toolNameHttp} brief is ≈${tokens} tokens, over the ${budget}-token budget for ${persona.model} (its ${maxPromptTokens}-token prompt window minus a ${PEER_PROMPT_TOKEN_RESERVE}-token framing reserve). Do NOT summarize or truncate the artifact to fit. Route the full artifact to a larger-window peer — \`codex_critic\` (gpt-5.6-sol ≈ 1M tokens)${opusHint} — or split it into focused sub-calls BY CONCERN and call them in parallel, then aggregate.`;
25677
26191
  }
25678
26192
  /**
@@ -25732,7 +26246,7 @@ function jsonPathPreflightCap(body, scope) {
25732
26246
  * the `stand_in` orchestrator in `src/lib/stand-in.ts` — can reuse the
25733
26247
  * same per-endpoint request shaping without re-implementing it. The
25734
26248
  * stand_in tool needs to drive its own per-round system prompts across
25735
- * three concrete models (gpt-5.6-sol, claude-opus-4-7, gemini-3.1-pro-preview),
26249
+ * three concrete models (gpt-5.6-sol, claude-opus-5, gemini-3.1-pro-preview),
25736
26250
  * each on a different endpoint; doing that with a `PersonaSpec` would
25737
26251
  * require either inventing throwaway personas per round or duplicating
25738
26252
  * the dispatch switch.
@@ -28033,13 +28547,13 @@ function atomicWriteSync(absPath, contents) {
28033
28547
  throw err;
28034
28548
  }
28035
28549
  }
28036
- const READ_PARAMS = Type.Object({
28037
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28038
- offset: Type.Optional(Type.Integer({
28550
+ const READ_PARAMS = Type$1.Object({
28551
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28552
+ offset: Type$1.Optional(Type$1.Integer({
28039
28553
  minimum: 0,
28040
28554
  description: "Line offset (0-indexed)."
28041
28555
  })),
28042
- limit: Type.Optional(Type.Integer({
28556
+ limit: Type$1.Optional(Type$1.Integer({
28043
28557
  minimum: 1,
28044
28558
  description: "Max lines to return."
28045
28559
  }))
@@ -28064,9 +28578,9 @@ function readTool(workspace) {
28064
28578
  }
28065
28579
  };
28066
28580
  }
28067
- const GLOB_PARAMS = Type.Object({
28068
- pattern: Type.String({ description: "ripgrep glob pattern, e.g. `src/**/*.ts`." }),
28069
- limit: Type.Optional(Type.Integer({
28581
+ const GLOB_PARAMS = Type$1.Object({
28582
+ pattern: Type$1.String({ description: "ripgrep glob pattern, e.g. `src/**/*.ts`." }),
28583
+ limit: Type$1.Optional(Type$1.Integer({
28070
28584
  minimum: 1,
28071
28585
  maximum: SEARCH_HARD_MAX
28072
28586
  }))
@@ -28093,11 +28607,11 @@ function globTool(workspace) {
28093
28607
  }
28094
28608
  };
28095
28609
  }
28096
- const GREP_PARAMS = Type.Object({
28097
- query: Type.String({ description: "Pattern to search for." }),
28098
- mode: Type.Optional(Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "`literal` (default) = fixed-string; `regex` = PCRE2." })),
28099
- file_glob: Type.Optional(Type.String({ description: "ripgrep glob filter, e.g. `*.ts`." })),
28100
- limit: Type.Optional(Type.Integer({
28610
+ const GREP_PARAMS = Type$1.Object({
28611
+ query: Type$1.String({ description: "Pattern to search for." }),
28612
+ mode: Type$1.Optional(Type$1.Union([Type$1.Literal("literal"), Type$1.Literal("regex")], { description: "`literal` (default) = fixed-string; `regex` = PCRE2." })),
28613
+ file_glob: Type$1.Optional(Type$1.String({ description: "ripgrep glob filter, e.g. `*.ts`." })),
28614
+ limit: Type$1.Optional(Type$1.Integer({
28101
28615
  minimum: 1,
28102
28616
  maximum: SEARCH_HARD_MAX
28103
28617
  }))
@@ -28133,10 +28647,10 @@ function grepTool(workspace) {
28133
28647
  }
28134
28648
  };
28135
28649
  }
28136
- const EDIT_PARAMS = Type.Object({
28137
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28138
- old_string: Type.String({ description: "Exact text to find. Must match exactly once; tool returns `not found` (0 matches) or `matches N times` (>1) without editing." }),
28139
- new_string: Type.String({ description: "Replacement text. May be empty (deletes old_string)." })
28650
+ const EDIT_PARAMS = Type$1.Object({
28651
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28652
+ old_string: Type$1.String({ description: "Exact text to find. Must match exactly once; tool returns `not found` (0 matches) or `matches N times` (>1) without editing." }),
28653
+ new_string: Type$1.String({ description: "Replacement text. May be empty (deletes old_string)." })
28140
28654
  });
28141
28655
  function editTool(workspace) {
28142
28656
  return {
@@ -28164,9 +28678,9 @@ function editTool(workspace) {
28164
28678
  }
28165
28679
  };
28166
28680
  }
28167
- const WRITE_PARAMS = Type.Object({
28168
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28169
- contents: Type.String({ description: "Full file contents. Refused if >10 MiB." })
28681
+ const WRITE_PARAMS = Type$1.Object({
28682
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28683
+ contents: Type$1.String({ description: "Full file contents. Refused if >10 MiB." })
28170
28684
  });
28171
28685
  function writeTool(workspace) {
28172
28686
  return {
@@ -28182,9 +28696,9 @@ function writeTool(workspace) {
28182
28696
  }
28183
28697
  };
28184
28698
  }
28185
- const BASH_PARAMS = Type.Object({
28186
- cmd: Type.String({ description: "Shell command line." }),
28187
- timeout_ms: Type.Optional(Type.Integer({
28699
+ const BASH_PARAMS = Type$1.Object({
28700
+ cmd: Type$1.String({ description: "Shell command line." }),
28701
+ timeout_ms: Type$1.Optional(Type$1.Integer({
28188
28702
  minimum: 100,
28189
28703
  maximum: BASH_MAX_TIMEOUT_MS,
28190
28704
  description: `Per-call timeout (default ${BASH_DEFAULT_TIMEOUT_MS} ms).`
@@ -28217,7 +28731,7 @@ function bashTool(workspace) {
28217
28731
  }
28218
28732
  };
28219
28733
  }
28220
- const WEB_SEARCH_PARAMS = Type.Object({ query: Type.String({ description: "Natural-language search query." }) });
28734
+ const WEB_SEARCH_PARAMS = Type$1.Object({ query: Type$1.String({ description: "Natural-language search query." }) });
28221
28735
  function webSearchTool() {
28222
28736
  return {
28223
28737
  name: "web_search",
@@ -28233,7 +28747,7 @@ function webSearchTool() {
28233
28747
  }
28234
28748
  };
28235
28749
  }
28236
- const FETCH_URL_PARAMS = Type.Object({ url: Type.String({ description: "Absolute URL (http/https only)." }) });
28750
+ const FETCH_URL_PARAMS = Type$1.Object({ url: Type$1.String({ description: "Absolute URL (http/https only)." }) });
28237
28751
  function fetchUrlTool() {
28238
28752
  return {
28239
28753
  name: "fetch_url",
@@ -28287,26 +28801,26 @@ function fetchUrlTool() {
28287
28801
  }
28288
28802
  };
28289
28803
  }
28290
- const CODE_SEARCH_PARAMS = Type.Object({
28291
- query: Type.String({ description: "Search text. Natural-language intent in the default `semantic` mode; a literal string in `lexical`/`exact`; a PCRE2 regex in `regex`." }),
28292
- mode: Type.Optional(Type.Union([
28293
- Type.Literal("semantic"),
28294
- Type.Literal("lexical"),
28295
- Type.Literal("exact"),
28296
- Type.Literal("regex"),
28297
- Type.Literal("ast")
28804
+ const CODE_SEARCH_PARAMS = Type$1.Object({
28805
+ query: Type$1.String({ description: "Search text. Natural-language intent in the default `semantic` mode; a literal string in `lexical`/`exact`; a PCRE2 regex in `regex`." }),
28806
+ mode: Type$1.Optional(Type$1.Union([
28807
+ Type$1.Literal("semantic"),
28808
+ Type$1.Literal("lexical"),
28809
+ Type$1.Literal("exact"),
28810
+ Type$1.Literal("regex"),
28811
+ Type$1.Literal("ast")
28298
28812
  ], { description: "Search mode. `semantic` (DEFAULT): ColBERT meaning-based ranking, falls back to lexical when the index isn't ready (response `source` says which engine ran). `lexical`: BM25F + tree-sitter (best for exact symbols). `exact`: fixed-string. `regex`: PCRE2. `ast`: ast-grep structural (needs `ast_pattern` + `ast_lang`)." })),
28299
- pattern: Type.Optional(Type.String({ description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank semantically. Ignored in lexical modes." })),
28300
- file_glob: Type.Optional(Type.String({ description: "ripgrep glob filter." })),
28301
- limit: Type.Optional(Type.Integer({
28813
+ pattern: Type$1.Optional(Type$1.String({ description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank semantically. Ignored in lexical modes." })),
28814
+ file_glob: Type$1.Optional(Type$1.String({ description: "ripgrep glob filter." })),
28815
+ limit: Type$1.Optional(Type$1.Integer({
28302
28816
  minimum: 1,
28303
28817
  description: "Max hits to return."
28304
28818
  })),
28305
- structural: Type.Optional(Type.Union([Type.Literal("full"), Type.Literal("topN")], { description: "Structural-ranking depth (lexical mode only)." })),
28306
- complete: Type.Optional(Type.Boolean({ description: "Lexical mode: when true, return the COMPLETE match set (every line ripgrep would find, capped only by `limit`) — disables the default precision shoulder cut + per-file cap. Use it when you must not miss any occurrence (every caller of X, a rename, an audit). The default response `notice` says when matches were hidden." })),
28307
- multiline: Type.Optional(Type.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/lexical queries can't contain a newline.)" })),
28308
- ast_pattern: Type.Optional(Type.String({ description: "mode:'ast' structural pattern (e.g. 'function $F($$$) { $$$ }'). Matches come from ast-grep instead of ripgrep — for multi-line AST shapes the regex modes can't express. Takes precedence over `query`. REQUIRES `ast_lang`. If ast-grep isn't installed you get a `notice`; it never falls back to regex." })),
28309
- ast_lang: Type.Optional(Type.String({ description: "Language grammar for `ast_pattern` (REQUIRED with it): 'ts' | 'tsx' | 'js' | 'py' | 'rust' | 'go' | … Without it ast-grep cross-matches every language and returns garbage." }))
28819
+ structural: Type$1.Optional(Type$1.Union([Type$1.Literal("full"), Type$1.Literal("topN")], { description: "Structural-ranking depth (lexical mode only)." })),
28820
+ complete: Type$1.Optional(Type$1.Boolean({ description: "Lexical mode: when true, return the COMPLETE match set (every line ripgrep would find, capped only by `limit`) — disables the default precision shoulder cut + per-file cap. Use it when you must not miss any occurrence (every caller of X, a rename, an audit). The default response `notice` says when matches were hidden." })),
28821
+ multiline: Type$1.Optional(Type$1.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/lexical queries can't contain a newline.)" })),
28822
+ ast_pattern: Type$1.Optional(Type$1.String({ description: "mode:'ast' structural pattern (e.g. 'function $F($$$) { $$$ }'). Matches come from ast-grep instead of ripgrep — for multi-line AST shapes the regex modes can't express. Takes precedence over `query`. REQUIRES `ast_lang`. If ast-grep isn't installed you get a `notice`; it never falls back to regex." })),
28823
+ ast_lang: Type$1.Optional(Type$1.String({ description: "Language grammar for `ast_pattern` (REQUIRED with it): 'ts' | 'tsx' | 'js' | 'py' | 'rust' | 'go' | … Without it ast-grep cross-matches every language and returns garbage." }))
28310
28824
  });
28311
28825
  function codeSearchTool(workspace) {
28312
28826
  return {
@@ -28465,9 +28979,9 @@ const GIT_DIFF_PRODUCING = new Set([
28465
28979
  "show",
28466
28980
  "diff"
28467
28981
  ]);
28468
- const TOOLBELT_PARAMS = Type.Object({
28469
- tool: Type.Union(TOOLBELT_TOOLS.map((t) => Type.Literal(t)), { description: "Which read-only analysis CLI to run: rg (ripgrep search), fd (file find), sg (ast-grep structural search), jq (JSON), yq (YAML/TOML/XML), gron (flatten JSON to greppable lines), scc (code stats: LOC + complexity), tokei (code stats), difft (difftastic structural diff), git (read-only subcommands only)." }),
28470
- args: Type.Optional(Type.Array(Type.String(), { description: "Arguments passed LITERALLY to the tool (no shell: no pipes, redirects, chaining, or glob expansion). For git, args[0] must be a read-only subcommand (log/show/diff/blame/ls-files/…)." }))
28982
+ const TOOLBELT_PARAMS = Type$1.Object({
28983
+ tool: Type$1.Union(TOOLBELT_TOOLS.map((t) => Type$1.Literal(t)), { description: "Which read-only analysis CLI to run: rg (ripgrep search), fd (file find), sg (ast-grep structural search), jq (JSON), yq (YAML/TOML/XML), gron (flatten JSON to greppable lines), scc (code stats: LOC + complexity), tokei (code stats), difft (difftastic structural diff), git (read-only subcommands only)." }),
28984
+ args: Type$1.Optional(Type$1.Array(Type$1.String(), { description: "Arguments passed LITERALLY to the tool (no shell: no pipes, redirects, chaining, or glob expansion). For git, args[0] must be a read-only subcommand (log/show/diff/blame/ls-files/…)." }))
28471
28985
  });
28472
28986
  /**
28473
28987
  * True iff `arg` triggers a denied flag. Long flags (`--foo`) match on the
@@ -28576,10 +29090,10 @@ function toolbeltTool(workspace) {
28576
29090
  };
28577
29091
  }
28578
29092
  const PEER_CRITIC_TUPLE = [
28579
- Type.Literal("codex_critic"),
28580
- Type.Literal("gemini_critic"),
28581
- Type.Literal("codex_reviewer"),
28582
- Type.Literal("opus_critic")
29093
+ Type$1.Literal("codex_critic"),
29094
+ Type$1.Literal("gemini_critic"),
29095
+ Type$1.Literal("codex_reviewer"),
29096
+ Type$1.Literal("opus_critic")
28583
29097
  ];
28584
29098
  /**
28585
29099
  * Critic names accepted by `peer_review.critic`. Exported for the
@@ -28587,17 +29101,17 @@ const PEER_CRITIC_TUPLE = [
28587
29101
  * against `PERSONAS_READ` from `~/lib/peer-mcp-personas`.
28588
29102
  */
28589
29103
  const PEER_CRITIC_NAMES = PEER_CRITIC_TUPLE.map((l) => l.const);
28590
- const PEER_EFFORT_UNION = Type.Union([
28591
- Type.Literal("low"),
28592
- Type.Literal("medium"),
28593
- Type.Literal("high"),
28594
- Type.Literal("xhigh")
29104
+ const PEER_EFFORT_UNION = Type$1.Union([
29105
+ Type$1.Literal("low"),
29106
+ Type$1.Literal("medium"),
29107
+ Type$1.Literal("high"),
29108
+ Type$1.Literal("xhigh")
28595
29109
  ], { description: "Reasoning depth. Per-critic allowedEfforts gate; out-of-band values are clamped to the critic's default." });
28596
- const PEER_REVIEW_PARAMS = Type.Object({
28597
- critic: Type.Union([...PEER_CRITIC_TUPLE], { description: "Critic tool name. One of " + PEER_CRITIC_NAMES.map((n) => `\`${n}\``).join(", ") + ". `gemini_critic` is only valid when gemini-3.x is in the Copilot catalog; otherwise the call is refused." }),
28598
- prompt: Type.String({ description: "The brief — artifact under review plus constraints. Pasted verbatim into the critic's user message." }),
28599
- context: Type.Optional(Type.String({ description: "Optional extra context concatenated to the brief." })),
28600
- effort: Type.Optional(PEER_EFFORT_UNION)
29110
+ const PEER_REVIEW_PARAMS = Type$1.Object({
29111
+ critic: Type$1.Union([...PEER_CRITIC_TUPLE], { description: "Critic tool name. One of " + PEER_CRITIC_NAMES.map((n) => `\`${n}\``).join(", ") + ". `gemini_critic` is only valid when gemini-3.x is in the Copilot catalog; otherwise the call is refused." }),
29112
+ prompt: Type$1.String({ description: "The brief — artifact under review plus constraints. Pasted verbatim into the critic's user message." }),
29113
+ context: Type$1.Optional(Type$1.String({ description: "Optional extra context concatenated to the brief." })),
29114
+ effort: Type$1.Optional(PEER_EFFORT_UNION)
28601
29115
  });
28602
29116
  function lookupPersona(critic) {
28603
29117
  const persona = PERSONAS_READ.find((p) => p.toolNameHttp === critic);
@@ -28618,10 +29132,10 @@ function lookupPersona(critic) {
28618
29132
  * `acquireInFlightSlot`, and `callPersona` keeps the slot accounting,
28619
29133
  * effort clamping, and isError-promotion semantics identical.
28620
29134
  */
28621
- const CODEX_REVIEW_PARAMS = Type.Object({
28622
- prompt: Type.String({ description: "The code-review brief — diff or single file under review plus constraints. Pasted verbatim into codex-reviewer's user message." }),
28623
- context: Type.Optional(Type.String({ description: "Optional extra context concatenated to the brief." })),
28624
- effort: Type.Optional(PEER_EFFORT_UNION)
29135
+ const CODEX_REVIEW_PARAMS = Type$1.Object({
29136
+ prompt: Type$1.String({ description: "The code-review brief — diff or single file under review plus constraints. Pasted verbatim into codex-reviewer's user message." }),
29137
+ context: Type$1.Optional(Type$1.String({ description: "Optional extra context concatenated to the brief." })),
29138
+ effort: Type$1.Optional(PEER_EFFORT_UNION)
28625
29139
  });
28626
29140
  function codexReviewTool() {
28627
29141
  return {
@@ -28632,7 +29146,7 @@ function codexReviewTool() {
28632
29146
  executionMode: "sequential",
28633
29147
  async execute(_toolCallId, params, signal) {
28634
29148
  if (networkDisabled()) throw new Error("rejected: network disabled");
28635
- const persona = lookupPersona("codex-reviewer");
29149
+ const persona = lookupPersona("codex_reviewer");
28636
29150
  const requested = params.effort;
28637
29151
  const effort = requested && persona.allowedEfforts.includes(requested) ? requested : persona.defaultEffort;
28638
29152
  const release = acquireInFlightSlot();
@@ -28655,7 +29169,7 @@ function geminiInCatalog() {
28655
29169
  if (!models) return false;
28656
29170
  return models.some((m) => /^gemini-3\..*pro/i.test(m.id));
28657
29171
  }
28658
- const ADVISOR_PARAMS = Type.Object({ concern: Type.String({
29172
+ const ADVISOR_PARAMS = Type$1.Object({ concern: Type$1.String({
28659
29173
  description: "What you want a second pair of eyes on — your current approach, the blocker you're stuck on, or the decision you're about to commit. Required: the advisor needs a focal point.",
28660
29174
  minLength: 1
28661
29175
  }) });
@@ -28770,22 +29284,22 @@ function advisorTool(getMessages) {
28770
29284
  }
28771
29285
  };
28772
29286
  }
28773
- const UPDATE_PLAN_PARAMS = Type.Object({
28774
- steps: Type.Array(Type.Object({
28775
- title: Type.String({
29287
+ const UPDATE_PLAN_PARAMS = Type$1.Object({
29288
+ steps: Type$1.Array(Type$1.Object({
29289
+ title: Type$1.String({
28776
29290
  minLength: 1,
28777
29291
  description: "Short imperative description of the step."
28778
29292
  }),
28779
- status: Type.Union([
28780
- Type.Literal("pending"),
28781
- Type.Literal("in_progress"),
28782
- Type.Literal("completed")
29293
+ status: Type$1.Union([
29294
+ Type$1.Literal("pending"),
29295
+ Type$1.Literal("in_progress"),
29296
+ Type$1.Literal("completed")
28783
29297
  ], { description: "Current status of this step." })
28784
29298
  }), {
28785
29299
  minItems: 1,
28786
29300
  description: "The FULL ordered plan. Each call replaces the previous plan, so always send every step (not just the changed one)."
28787
29301
  }),
28788
- explanation: Type.Optional(Type.String({ description: "Optional one-line note on what changed this update." }))
29302
+ explanation: Type$1.Optional(Type$1.String({ description: "Optional one-line note on what changed this update." }))
28789
29303
  });
28790
29304
  function createPlanState() {
28791
29305
  return { current: [] };
@@ -29380,15 +29894,21 @@ registerExitHandlers(WORKTREE_REGISTRY);
29380
29894
  * mode. */
29381
29895
  const DEFAULT_MODEL = "gpt-5.4-mini";
29382
29896
  const DEFAULT_THINKING = "xhigh";
29383
- /** Default model for the READ-ONLY `explore` mode. `claude-sonnet-5` at `xhigh`
29384
- * (via `DEFAULT_THINKING`) — a strong, NATIVE (no-shim) tool-caller for repo
29385
- * research. Native Claude models run as workers over `/chat/completions`, the
29386
- * same path proven by `PLAN_DEFAULT_MODEL` (claude-opus-4.8). Like `implement`'s
29387
- * gpt-5.6-sol this is NOT a `workerToolsEnabled` gate input — if absent (e.g. a
29388
- * non-enterprise tier) `explore` errors helpfully at call time rather than
29389
- * vanishing the whole worker surface. The caller (the main model) overrides
29390
- * BOTH the model and the reasoning per call via the `model` / `thinking` args. */
29391
- const EXPLORE_DEFAULT_MODEL = "claude-sonnet-5";
29897
+ /** Default model for the READ-ONLY `explore` mode. `gemini-3.6-flash` at `high`
29898
+ * (via `EXPLORE_DEFAULT_THINKING`; flash advertises no xhigh) — a fast, cheap,
29899
+ * 1M-context tool-caller for read-only repo research. Routes over
29900
+ * `/chat/completions` via the translation shim (the same proven path the
29901
+ * `review` worker uses for gemini). Like `implement`'s gpt-5.6-sol this is NOT a
29902
+ * `workerToolsEnabled` gate input if absent (e.g. a non-enterprise tier)
29903
+ * `explore` errors helpfully at call time rather than vanishing the whole worker
29904
+ * surface. The caller (the main model) overrides BOTH the model and the reasoning
29905
+ * per call via the `model` / `thinking` args — see the tier ladder (gpt-5.6-sol
29906
+ * heavy / gpt-5.6-terra moderate / gemini-3.6-flash light) in the MCP tool desc. */
29907
+ const EXPLORE_DEFAULT_MODEL = "gemini-3.6-flash";
29908
+ /** Default thinking for `explore`. `high` (flash has no xhigh); explicit rather
29909
+ * than inherited from `DEFAULT_THINKING` so the explore effort can't drift if the
29910
+ * shared fallback changes. */
29911
+ const EXPLORE_DEFAULT_THINKING = "high";
29392
29912
  /** Default model + thinking for the READ-ONLY `review` mode.
29393
29913
  * `gemini-3.1-pro-preview` at `xhigh` (clamped to `high` at call time — gemini
29394
29914
  * advertises no xhigh). DELIBERATELY DECORRELATED FROM THE IMPLEMENTER: bounded
@@ -29410,6 +29930,10 @@ const REVIEW_DEFAULT_THINKING = "xhigh";
29410
29930
  * autonomous implementation. An explicit `opts.model` still wins. */
29411
29931
  const IMPLEMENT_DEFAULT_MODEL = "gpt-5.6-sol";
29412
29932
  const IMPLEMENT_DEFAULT_THINKING = "xhigh";
29933
+ /** `test` starts with the same built-in pair as `implement`, but remains an
29934
+ * independent mode so either can be overridden without affecting the other. */
29935
+ const TEST_DEFAULT_MODEL = "gpt-5.6-sol";
29936
+ const TEST_DEFAULT_THINKING = "xhigh";
29413
29937
  /** Default model for `browse` mode. `gpt-5.4-mini` — the Gate-B-winning
29414
29938
  * browse model (small + fast enough to drive a tab at human pace, with
29415
29939
  * enough tool-calling discipline to terminate). This is DISTINCT from the
@@ -29429,14 +29953,53 @@ const BROWSE_DEFAULT_THINKING = "high";
29429
29953
  /** Default model + thinking for the read-only `plan` mode. `claude-opus-4.8`
29430
29954
  * at `xhigh` — planning is the highest-leverage read-only step (the plan
29431
29955
  * shapes everything downstream), so it gets the strongest reasoning model
29432
- * rather than the cheap `gemini-3.5-flash` explore default. Uses the DOTTED
29956
+ * rather than the lightweight `gemini-3.6-flash` explore default. Uses the DOTTED
29433
29957
  * Copilot catalog id (the worker resolver exact-matches `catalog.id`, it does
29434
- * NOT translate the Anthropic dashed slug). Falls back to a helpful
29435
- * unknown-model error at call time if opus-4.8 isn't in the catalog (e.g. a
29436
- * non-enterprise tier), exactly like `implement`'s `gpt-5.6-sol`. Caller's `model`
29437
- * arg still wins. */
29438
- const PLAN_DEFAULT_MODEL = "claude-opus-4.8";
29439
- const PLAN_DEFAULT_THINKING = "xhigh";
29958
+ * NOT translate the Anthropic dashed slug; `claude-opus-5` is a single-segment
29959
+ * slug so dotted == dashed). Falls back to a helpful unknown-model error at call
29960
+ * time if opus-5 isn't in the catalog (e.g. a non-enterprise tier), exactly like
29961
+ * `implement`'s `gpt-5.6-sol`. Caller's `model` arg still wins. */
29962
+ const PLAN_DEFAULT_MODEL = "claude-opus-5";
29963
+ const BUILT_IN_MODE_DEFAULTS = Object.freeze({
29964
+ explore: {
29965
+ model: EXPLORE_DEFAULT_MODEL,
29966
+ thinking: EXPLORE_DEFAULT_THINKING
29967
+ },
29968
+ review: {
29969
+ model: REVIEW_DEFAULT_MODEL,
29970
+ thinking: REVIEW_DEFAULT_THINKING
29971
+ },
29972
+ plan: {
29973
+ model: PLAN_DEFAULT_MODEL,
29974
+ thinking: "xhigh"
29975
+ },
29976
+ implement: {
29977
+ model: IMPLEMENT_DEFAULT_MODEL,
29978
+ thinking: IMPLEMENT_DEFAULT_THINKING
29979
+ },
29980
+ test: {
29981
+ model: TEST_DEFAULT_MODEL,
29982
+ thinking: TEST_DEFAULT_THINKING
29983
+ },
29984
+ browse: {
29985
+ model: BROWSE_DEFAULT_MODEL,
29986
+ thinking: BROWSE_DEFAULT_THINKING
29987
+ }
29988
+ });
29989
+ /** Resolve the effective mode ladder without changing the gate sentinel. */
29990
+ function resolveModeDefaults(mode, ignoreSessionDefaults = false) {
29991
+ const builtIn = BUILT_IN_MODE_DEFAULTS[mode] ?? {
29992
+ model: DEFAULT_MODEL,
29993
+ thinking: DEFAULT_THINKING
29994
+ };
29995
+ const override = ignoreSessionDefaults ? {} : getWorkerSessionDefault(mode);
29996
+ return {
29997
+ model: override.model ?? builtIn.model,
29998
+ thinking: override.thinking ?? builtIn.thinking,
29999
+ modelSource: override.model === void 0 ? "built-in" : "override",
30000
+ thinkingSource: override.thinking === void 0 ? "built-in" : "override"
30001
+ };
30002
+ }
29440
30003
  /**
29441
30004
  * `Model<any>` shim used to satisfy `Agent.initialState.model` typing.
29442
30005
  *
@@ -29450,6 +30013,7 @@ const PLAN_DEFAULT_THINKING = "xhigh";
29450
30013
  * diagnostics (e.g. error-message AssistantMessage's `model` field
29451
30014
  * if Pi ever inspects it) faithful to what the caller asked for.
29452
30015
  */
30016
+ let agentOptionsObserverForTests;
29453
30017
  function makeModelShim(modelId) {
29454
30018
  return {
29455
30019
  id: modelId,
@@ -29487,6 +30051,34 @@ function extractAssistantText(content) {
29487
30051
  for (const part of content) if (part.type === "text") out += part.text;
29488
30052
  return out;
29489
30053
  }
30054
+ const MAX_EMPTY_OUTPUT_NUDGES = 3;
30055
+ const EMPTY_OUTPUT_NUDGES = [
30056
+ "Summarize your findings so far.",
30057
+ "Your previous reply was empty. Provide the answer now in plain text.",
30058
+ "Reply with plain text only. Do not call any tool."
30059
+ ];
30060
+ /**
30061
+ * Resolve the per-run nudge cap. Zero explicitly disables nudging; malformed,
30062
+ * negative, and fractional values fall back to the default.
30063
+ */
30064
+ function resolveMaxEmptyOutputNudges() {
30065
+ const raw = process$1.env.GH_ROUTER_WORKER_MAX_NUDGES;
30066
+ if (raw === void 0 || raw === "") return MAX_EMPTY_OUTPUT_NUDGES;
30067
+ const parsed = Number(raw);
30068
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) return MAX_EMPTY_OUTPUT_NUDGES;
30069
+ return parsed;
30070
+ }
30071
+ function emptyOutputNudge(attempt) {
30072
+ return EMPTY_OUTPUT_NUDGES[Math.min(attempt, EMPTY_OUTPUT_NUDGES.length) - 1];
30073
+ }
30074
+ /** True only for a clean, empty assistant stop with no pending tool calls. */
30075
+ function shouldNudgeForEmptyOutput(message) {
30076
+ if (message.role !== "assistant") return false;
30077
+ const assistant = message;
30078
+ if (assistant.stopReason !== "stop" || !Array.isArray(assistant.content)) return false;
30079
+ if (assistant.content.some((part) => part.type === "toolCall")) return false;
30080
+ return extractAssistantText(assistant.content).trim() === "";
30081
+ }
29490
30082
  /**
29491
30083
  * Trivial stub for the no-worktree path. `dir` is the workspace
29492
30084
  * itself; `finalize` returns an empty diff (the response text won't
@@ -29527,21 +30119,15 @@ async function runWorkerAgentOnce(opts) {
29527
30119
  isError: true
29528
30120
  };
29529
30121
  try {
29530
- const isBrowse = opts.mode === "browse";
29531
- const isPlan = opts.mode === "plan";
29532
- const isReview = opts.mode === "review";
29533
- const isWriteCapable = opts.mode === "implement" || opts.mode === "test";
29534
- const isExplore = opts.mode === "explore";
29535
- const defaultModel = isBrowse ? BROWSE_DEFAULT_MODEL : isPlan ? PLAN_DEFAULT_MODEL : isReview ? REVIEW_DEFAULT_MODEL : isWriteCapable ? IMPLEMENT_DEFAULT_MODEL : isExplore ? EXPLORE_DEFAULT_MODEL : DEFAULT_MODEL;
29536
- const defaultThinking = isBrowse ? BROWSE_DEFAULT_THINKING : isPlan ? PLAN_DEFAULT_THINKING : isReview ? REVIEW_DEFAULT_THINKING : isWriteCapable ? IMPLEMENT_DEFAULT_THINKING : DEFAULT_THINKING;
29537
30122
  const resolved = resolveModelAndThinking({
29538
- model: opts.model ?? defaultModel,
29539
- thinking: opts.thinking ?? defaultThinking
30123
+ model: opts.model,
30124
+ thinking: opts.thinking
29540
30125
  });
29541
30126
  if (!resolved.ok) return {
29542
30127
  text: resolved.error,
29543
30128
  isError: true
29544
30129
  };
30130
+ const isBrowse = opts.mode === "browse";
29545
30131
  const ctxBudget = makeContextBudget(resolved.contextWindow);
29546
30132
  const workspaceInput = opts.workspace ?? (isBrowse ? process$1.cwd() : void 0);
29547
30133
  if (workspaceInput === void 0) return {
@@ -29581,7 +30167,7 @@ async function runWorkerAgentOnce(opts) {
29581
30167
  getMessages,
29582
30168
  planState
29583
30169
  });
29584
- const agent = new Agent$1({
30170
+ const agentOptions = {
29585
30171
  initialState: {
29586
30172
  systemPrompt: systemPromptFor(opts.mode),
29587
30173
  model: makeModelShim(resolved.modelId),
@@ -29590,7 +30176,8 @@ async function runWorkerAgentOnce(opts) {
29590
30176
  },
29591
30177
  streamFn: createCopilotStreamFn({
29592
30178
  resolved,
29593
- contextBudget: ctxBudget
30179
+ contextBudget: ctxBudget,
30180
+ modelCallTimeoutMs: resolveWorkerModelCallTimeoutMs()
29594
30181
  }),
29595
30182
  toolExecution: "parallel",
29596
30183
  transformContext: async (messages) => {
@@ -29623,6 +30210,7 @@ async function runWorkerAgentOnce(opts) {
29623
30210
  if (a.trim()) terminalText = a;
29624
30211
  }
29625
30212
  },
30213
+ shouldStopAfterTurn: () => budget.hardStopReason !== null,
29626
30214
  afterToolCall: async (ctx) => {
29627
30215
  budget.recordToolBytes(ctx.result);
29628
30216
  if (ctxBudget) {
@@ -29633,15 +30221,32 @@ async function runWorkerAgentOnce(opts) {
29633
30221
  prepareNextTurn: async () => {
29634
30222
  budget.addTurn();
29635
30223
  }
29636
- });
30224
+ };
30225
+ agentOptionsObserverForTests?.(agentOptions);
30226
+ const agent = new Agent$1(agentOptions);
29637
30227
  agentHolder.agent = agent;
29638
- const abortHandler = () => agent?.abort();
29639
- if (opts.signal) if (opts.signal.aborted) agent.abort();
29640
- else opts.signal.addEventListener("abort", abortHandler, { once: true });
30228
+ const abortHandler = () => agent.abort();
30229
+ if (opts.signal) opts.signal.addEventListener("abort", abortHandler, { once: true });
29641
30230
  let finalText = "";
29642
30231
  let lastStopReason = null;
30232
+ let nudgeCount = 0;
30233
+ const maxEmptyOutputNudges = resolveMaxEmptyOutputNudges();
29643
30234
  let terminalText = null;
29644
30235
  const unsubscribe = agent.subscribe((event) => {
30236
+ if (event.type === "turn_end") {
30237
+ if (nudgeCount < maxEmptyOutputNudges && shouldNudgeForEmptyOutput(event.message)) {
30238
+ nudgeCount += 1;
30239
+ agent.followUp({
30240
+ role: "user",
30241
+ content: [{
30242
+ type: "text",
30243
+ text: emptyOutputNudge(nudgeCount)
30244
+ }],
30245
+ timestamp: Date.now()
30246
+ });
30247
+ }
30248
+ return;
30249
+ }
29645
30250
  if (event.type !== "message_end") return;
29646
30251
  const msg = event.message;
29647
30252
  if (typeof msg !== "object" || msg === null) return;
@@ -29652,11 +30257,14 @@ async function runWorkerAgentOnce(opts) {
29652
30257
  const sr = msg.stopReason;
29653
30258
  if (typeof sr === "string") lastStopReason = sr;
29654
30259
  });
30260
+ let wallClockExpired = false;
29655
30261
  const wallClockTimer = setTimeout(() => {
29656
- agent?.abort();
30262
+ wallClockExpired = true;
30263
+ agent.abort();
29657
30264
  }, budget.config.maxWallClockMs);
29658
30265
  wallClockTimer.unref?.();
29659
30266
  try {
30267
+ if (opts.signal?.aborted) throw new Error("[halted: cancelled]");
29660
30268
  await agent.prompt(opts.prompt);
29661
30269
  await agent.waitForIdle();
29662
30270
  let diff = "";
@@ -29669,12 +30277,26 @@ async function runWorkerAgentOnce(opts) {
29669
30277
  await ws.remove();
29670
30278
  } catch {}
29671
30279
  const text = isBrowse ? terminalText ?? finalText : diff ? `${finalText}\n\n${diff}` : finalText;
29672
- if (lastStopReason === "error") return {
29673
- text: [(terminalText ?? finalText).trim() || "Worker run failed before producing an answer — the model's input likely overflowed (a large tool result), or the upstream errored. Retry with a narrower task: target a specific section / file / element rather than reading everything at once.", diff].filter(Boolean).join("\n\n"),
30280
+ if (lastStopReason === "error" || lastStopReason === "aborted") {
30281
+ const diag = (terminalText ?? finalText).trim();
30282
+ let diagnostic;
30283
+ if (lastStopReason === "aborted") diagnostic = wallClockExpired ? "[halted: wallclock]" : "[halted: cancelled]";
30284
+ else diagnostic = diag || "Worker run failed before producing an answer — the model's input likely overflowed (a large tool result), or the upstream errored. Retry with a narrower task: target a specific section / file / element rather than reading everything at once.";
30285
+ return {
30286
+ text: lastStopReason === "aborted" ? [
30287
+ diag,
30288
+ diff,
30289
+ diagnostic
30290
+ ].filter(Boolean).join("\n\n") : [diagnostic, diff].filter(Boolean).join("\n\n"),
30291
+ isError: true
30292
+ };
30293
+ }
30294
+ if (budget.hardStopReason) return {
30295
+ text: [text, `[halted: ${budget.hardStopReason}]`].filter(Boolean).join("\n\n"),
29674
30296
  isError: true
29675
30297
  };
29676
30298
  if (!text.trim()) return {
29677
- text: `${NO_OUTPUT_PREFIX} (stopReason=${lastStopReason ?? "unknown"}, turns=${budget.turns}, elapsed=${budget.elapsedMs}ms)]`,
30299
+ text: `${NO_OUTPUT_PREFIX} after ${nudgeCount} nudges (stopReason=${lastStopReason ?? "unknown"}, turns=${budget.turns}, elapsed=${budget.elapsedMs}ms)]; retry with a different model via worker_defaults, or narrow/split the task.`,
29678
30300
  isError: true
29679
30301
  };
29680
30302
  return { text };
@@ -29708,42 +30330,21 @@ async function runWorkerAgentOnce(opts) {
29708
30330
  }
29709
30331
  /**
29710
30332
  * Prefix of the sentinel `runWorkerAgentOnce` returns when a worker stops
29711
- * CLEANLY but emits no usable text the model occasionally ends a turn right
29712
- * after a tool call without summarizing. Stable so the retry wrapper can detect
29713
- * exactly this case. Distinct from a budget cap (`WorkerAbort` → halt message),
29714
- * a stream error (`stopReason="error"` → overflow/upstream diagnostic), and a
29715
- * real failure — none of which carry this prefix, so none are retried.
30333
+ * cleanly but emits no usable text even after its bounded in-run nudges. Kept
30334
+ * stable for callers that recognize the existing sentinel shape.
29716
30335
  */
29717
30336
  const NO_OUTPUT_PREFIX = "[worker exited with no output";
29718
- /** True iff `r` is the transient no-output sentinel (a clean stop with empty
29719
- * text), the one case worth a fresh retry. Keyed on the specific sentinel
29720
- * PREFIX, not on `isError` so the retry can't be silently decoupled if the
29721
- * sentinel's error flag ever changes, and a real worker answer never begins
29722
- * with this string. */
29723
- function isTransientNoOutput(r) {
29724
- return typeof r.text === "string" && r.text.startsWith(NO_OUTPUT_PREFIX);
29725
- }
29726
- /**
29727
- * Run `runOnce`, and on the transient no-output sentinel retry EXACTLY ONCE with
29728
- * a fresh run before surfacing it. Real errors, budget caps, and stream errors
29729
- * are returned as-is (they have distinct, actionable messages and a retry would
29730
- * not help). A consumed abort signal short-circuits the retry. If the retry also
29731
- * produces no output, the ORIGINAL is returned (one is enough signal; the
29732
- * failure isn't hidden). Extracted + injected for unit-testability.
29733
- */
29734
- async function withNoOutputRetry(runOnce, opts) {
29735
- const first = await runOnce(opts);
29736
- if (!isTransientNoOutput(first) || opts.signal?.aborted) return first;
29737
- const second = await runOnce(opts);
29738
- return isTransientNoOutput(second) ? first : second;
30337
+ /** Public entry. Resolve model/thinking once, then run under one transcript and Budget. */
30338
+ function resolveWorkerRunOpts(opts) {
30339
+ const defaults = resolveModeDefaults(opts.mode, opts.ignoreSessionDefaults === true);
30340
+ return {
30341
+ ...opts,
30342
+ model: opts.model ?? defaults.model,
30343
+ thinking: opts.thinking ?? defaults.thinking
30344
+ };
29739
30345
  }
29740
- /**
29741
- * Public entry: a worker run with a single transient-no-output retry. Wraps the
29742
- * implementation (`runWorkerAgentOnce`); the signature is unchanged so every
29743
- * caller (MCP dispatch, the orchestration runner) gets the retry for free.
29744
- */
29745
30346
  async function runWorkerAgent(opts) {
29746
- return withNoOutputRetry(runWorkerAgentOnce, opts);
30347
+ return runWorkerAgentOnce(resolveWorkerRunOpts(opts));
29747
30348
  }
29748
30349
  /**
29749
30350
  * Test-only exports. The public surface of the engine is
@@ -29794,8 +30395,8 @@ const STAND_IN_MODELS = Object.freeze([
29794
30395
  effort: "xhigh"
29795
30396
  },
29796
30397
  {
29797
- key: "claude-opus-4-7",
29798
- model: "claude-opus-4-7",
30398
+ key: "claude-opus-5",
30399
+ model: "claude-opus-5",
29799
30400
  endpoint: "/v1/messages",
29800
30401
  effort: "xhigh"
29801
30402
  },
@@ -31891,7 +32492,8 @@ async function runWorkflowLive(opts) {
31891
32492
  mode,
31892
32493
  prompt,
31893
32494
  workspace,
31894
- signal: opts.signal
32495
+ signal: opts.signal,
32496
+ ignoreSessionDefaults: true
31895
32497
  });
31896
32498
  return {
31897
32499
  text: r.text,
@@ -32011,10 +32613,11 @@ function isMcpGroup(s) {
32011
32613
  * (handler.ts:handleToolsCallSSE). Claude Code's MCP HTTP client honors
32012
32614
  * `text/event-stream` responses without applying the ~60s per-tool-call
32013
32615
  * timer that previously broke xhigh on gpt-5.5 (~56s wall) and on
32014
- * Anthropic Opus families (high+ thinking budgets). opus-critic itself
32015
- * now runs on claude-opus-4-6 which doesn't advertise xhigh, so the
32016
- * SSE long-tail concern there is moot; the SSE machinery still applies
32017
- * to the other personas that do expose xhigh.
32616
+ * Anthropic Opus families (high+ thinking budgets). opus-critic caps its
32617
+ * exposed effort at `high` (its effective model can fall back to
32618
+ * claude-opus-4-6, which doesn't advertise xhigh), so the SSE long-tail
32619
+ * concern there is moot; the SSE machinery still applies to the other
32620
+ * personas that do expose xhigh.
32018
32621
  */
32019
32622
  const EFFORT_LEVELS = [
32020
32623
  "low",
@@ -32147,7 +32750,7 @@ Reply format (markdown):
32147
32750
 
32148
32751
  Resilience reminder:
32149
32752
  If your session terminates abnormally before "Status: complete", the lead will retry once. On recovery, ask the lead to confirm what's already been done before re-applying changes — duplicate edits are worse than a slow restart.`;
32150
- const OPUS_CRITIC_BASE = `You are opus-critic, a fresh-context same-lab adversarial reviewer running on Opus 4.6. The lead orchestrator that just delegated to you runs newer Opus-family context, but you are NOT the lead. You did not see the lead's reasoning trace. You only see the brief.
32753
+ const OPUS_CRITIC_BASE = `You are opus-critic, a fresh-context same-lab adversarial reviewer running on Opus 5. The lead orchestrator that just delegated to you runs Opus-family context too, but you are NOT the lead. You did not see the lead's reasoning trace. You only see the brief.
32151
32754
 
32152
32755
  Your job is to spot what the lead missed because of cognitive momentum, sunk-cost on a plan, or motivated reasoning toward a particular fix. Your blind-spot diversification is LIMITED compared to codex-critic (gpt-5.6-sol) and gemini-critic (gemini-3.1-pro), same lab, adjacent model family, related priors. Use that honestly: don't pretend to find a different perspective when the obvious read is "the lead got it right." Silence on good work is a valid and welcome answer.
32153
32756
 
@@ -32232,9 +32835,9 @@ const PERSONAS_READ = Object.freeze([
32232
32835
  {
32233
32836
  agentName: "opus-critic",
32234
32837
  toolNameHttp: "opus_critic",
32235
- model: "claude-opus-4-6",
32838
+ model: "claude-opus-5",
32236
32839
  endpoint: "/v1/messages",
32237
- description: "Adversarial same-lab critic backed by fresh-context Opus 4.6, with limited blind-spot diversity compared with cross-lab critics. It reviews plans, designs, or code tradeoffs for cognitive momentum, sunk-cost reasoning, and confabulated assumptions, then returns a calibrated objection or no material objection. Use when a same-family sanity check can catch lead-context drift or when comparing against codex_critic / gemini_critic findings. Not a substitute for cross-lab review on security-sensitive or high-risk changes; use codex_critic or gemini_critic for stronger diversity. On enterprise catalogs that carry Opus-4.6-1M it runs with ≈936K input tokens; otherwise ≈168K. Pinned two minors behind the default Opus so the panel spans more of the version curve. Pass artifact verbatim.",
32840
+ description: "Adversarial same-lab critic backed by fresh-context Opus 5, with limited blind-spot diversity compared with cross-lab critics. It reviews plans, designs, or code tradeoffs for cognitive momentum, sunk-cost reasoning, and confabulated assumptions, then returns a calibrated objection or no material objection. Use when a same-family sanity check can catch lead-context drift or when comparing against codex_critic / gemini_critic findings. Not a substitute for cross-lab review on security-sensitive or high-risk changes; use codex_critic or gemini_critic for stronger diversity. Runs with the full 1M-context Opus 5 window (native, no -1m sibling needed). Pass artifact verbatim.",
32238
32841
  baseInstructions: OPUS_CRITIC_BASE,
32239
32842
  agentPrompt: "",
32240
32843
  writeCapable: false,
@@ -32383,7 +32986,7 @@ function buildPeerAwarenessSnippet(opts) {
32383
32986
  criticList.push("`gemini_reviewer` (gemini-3.1-pro, line-level code review)");
32384
32987
  criticList.push("`gemini_critic` (gemini-3.1-pro)");
32385
32988
  }
32386
- criticList.push("`opus_critic` (Opus 4.6)");
32989
+ criticList.push("`opus_critic` (Opus 5)");
32387
32990
  const codexCliClause = opts.codexCli ? " `mcp__codex-cli__codex` dispatches to `codex-implementer` (gpt-5.3-codex with workspace-write) for end-to-end coding tasks." : "";
32388
32991
  const para2Parts = [`\`mcp__${searchKey}__code\` is the one-stop code search (no extra model call). Its DEFAULT mode (or \`mode:"semantic"\`) ranks by MEANING via ColBERT over a per-workspace index, the first thing to reach for on intent/concept questions ("where is retry/backoff handled", "how does auth work"); when that index isn't ready it transparently falls back to lexical (the response \`source\` says which engine ran). Forced modes cover the rest: \`lexical\` (BM25F-ranked + tree-sitter, best for exact symbols), \`exact\`, \`regex\`, \`complete\` (exhaustive set), \`ast_pattern\`+\`ast_lang\` for multi-line AST shapes, \`scan\` for a whole-workspace symbol outline, \`multiline\` for cross-line regex. Multiple queries can run in a single turn. The index covers code-shaped files; for unstructured files (logs, \`.csv\`, \`.env*\`, config-only wiring), \`grep\`/\`glob\` still apply.`];
32389
32992
  if (opts.workerToolsAvailable) para2Parts.push(`\`worker-*\` are background Agent subagents (subagent_type) that run the matching worker in its own context and deliver the result as a completion notification, so a long run never blocks the turn: \`worker-explore\` (read-only research), \`worker-review\` (reads the code to verify a change or claim), \`worker-plan\` (ordered implementation plan), \`worker-implement\` (edit/write/bash; ALWAYS runs in an isolated git worktree and returns the diff via a saved patch file; for in-place edits use the \`implementer\` subagent), \`worker-test\` (independent test author; also always worktree-isolated). The raw \`mcp__${workersKey}__*\` tools they call are guarded (a direct main-thread call is redirected to the matching agent); Workers themselves have \`code_search\`.`);
@@ -32460,6 +33063,15 @@ function formatWebSearchResult(results) {
32460
33063
  const refsLine = results.references.map((r) => `- [${r.title}](${r.url})`).join("\n");
32461
33064
  return `${results.content}\n\n## References\n${refsLine}`;
32462
33065
  }
33066
+ /**
33067
+ * Model-override tier ladder surfaced on the read-heavy workers
33068
+ * (explore / implement / review). The caller picks a model by task weight;
33069
+ * all three tiers are 1M-context, and `high` is the recommended reasoning
33070
+ * depth for the ladder (flash tops out at high; sol/terra go higher if the
33071
+ * caller wants). Appended to those tools' `model` param description so the
33072
+ * lead has actionable override guidance instead of a bare free string.
33073
+ */
33074
+ const WORKER_TIER_GUIDANCE = " Override by task weight: `gpt-5.6-sol` (heavy/deep), `gpt-5.6-terra` (moderate), `gemini-3.6-flash` (light/cheap) — all 1M context; pair with thinking:'high'.";
32463
33075
  const NON_PERSONA_MCP_TOOLS = Object.freeze([
32464
33076
  {
32465
33077
  toolNameHttp: "web",
@@ -32647,11 +33259,84 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32647
33259
  }
32648
33260
  }
32649
33261
  },
33262
+ {
33263
+ toolNameHttp: "worker_defaults",
33264
+ group: "workers",
33265
+ capability: "worker",
33266
+ description: "Sets or clears process-wide worker model/reasoning defaults and returns the full effective table. Omit arguments to inspect current values. Per-call worker arguments still take precedence; values are in-memory and apply to every client served by this process.",
33267
+ inputSchema: {
33268
+ type: "object",
33269
+ additionalProperties: false,
33270
+ properties: {
33271
+ mode: {
33272
+ type: "string",
33273
+ enum: WORKER_MODES,
33274
+ description: "Worker mode to set or clear."
33275
+ },
33276
+ model: {
33277
+ type: "string",
33278
+ description: "Copilot catalog model id to use by default for the mode."
33279
+ },
33280
+ thinking: {
33281
+ type: "string",
33282
+ enum: WORKER_THINKING_LEVELS,
33283
+ description: "Requested default reasoning level; clamped per run for the selected model."
33284
+ },
33285
+ clear: {
33286
+ type: "boolean",
33287
+ description: "When true, clears both overrides for the selected mode."
33288
+ },
33289
+ clearAll: {
33290
+ type: "boolean",
33291
+ description: "When true, clears overrides for every worker mode."
33292
+ }
33293
+ }
33294
+ },
33295
+ async handler(args) {
33296
+ const mode = typeof args.mode === "string" && WORKER_MODES.includes(args.mode) ? args.mode : void 0;
33297
+ const model = typeof args.model === "string" ? args.model : void 0;
33298
+ const thinking = typeof args.thinking === "string" && WORKER_THINKING_LEVELS.includes(args.thinking) ? args.thinking : void 0;
33299
+ const clear = args.clear === true;
33300
+ const clearAll = args.clearAll === true;
33301
+ if (args.mode !== void 0 && mode === void 0 || args.model !== void 0 && model === void 0 || args.thinking !== void 0 && thinking === void 0 || args.clear !== void 0 && typeof args.clear !== "boolean" || args.clearAll !== void 0 && typeof args.clearAll !== "boolean" || clearAll && (mode !== void 0 || model !== void 0 || thinking !== void 0 || args.clear !== void 0) || clear && (mode === void 0 || model !== void 0 || thinking !== void 0) || !clearAll && !clear && (model !== void 0 || thinking !== void 0) && mode === void 0) return {
33302
+ content: [{
33303
+ type: "text",
33304
+ text: "worker_defaults: use mode with model/thinking or clear:true; clearAll:true must stand alone"
33305
+ }],
33306
+ isError: true
33307
+ };
33308
+ if (clearAll) resetAllWorkerSessionDefaults();
33309
+ else if (clear && mode) resetWorkerSessionDefault(mode);
33310
+ else if (mode && (model !== void 0 || thinking !== void 0)) {
33311
+ const current = resolveModeDefaults(mode);
33312
+ const validation = resolveModelAndThinking({
33313
+ model: model ?? current.model,
33314
+ thinking: thinking ?? current.thinking
33315
+ });
33316
+ if (!validation.ok) return {
33317
+ content: [{
33318
+ type: "text",
33319
+ text: validation.error
33320
+ }],
33321
+ isError: true
33322
+ };
33323
+ setWorkerSessionDefault(mode, {
33324
+ model,
33325
+ thinking
33326
+ });
33327
+ }
33328
+ const table = Object.fromEntries(WORKER_MODES.map((workerMode) => [workerMode, resolveModeDefaults(workerMode)]));
33329
+ return { content: [{
33330
+ type: "text",
33331
+ text: JSON.stringify(table)
33332
+ }] };
33333
+ }
33334
+ },
32650
33335
  {
32651
33336
  toolNameHttp: "explore",
32652
33337
  group: "workers",
32653
33338
  capability: "worker",
32654
- description: "Runs as the background `worker-explore` agent. Dispatch via the Agent tool (subagent_type: worker-explore) so the turn is never blocked; the result arrives as a completion notification. Read-only investigation by an autonomous worker (Pi runtime; default model `claude-sonnet-5` at xhigh reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). It has read, glob, grep, semantic-first code search, web search, fetch_url, advisor, update_plan, and read-only toolbelt tools, and it returns a single text answer. Use for bounded research, repo discovery, dependency investigation, or multi-file reading that would otherwise consume the lead context window. Not for implementation, test authoring, or verification of a concrete diff; use implement, test, or review for those scopes. Brief the investigation goal and constraints, not step-by-step tool semantics.",
33339
+ description: "Runs as the background `worker-explore` agent. Dispatch via the Agent tool (subagent_type: worker-explore) so the turn is never blocked; the result arrives as a completion notification. Read-only investigation by an autonomous worker (Pi runtime; default model `gemini-3.6-flash` at high reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). It has read, glob, grep, semantic-first code search, web search, fetch_url, advisor, update_plan, and read-only toolbelt tools, and it returns a single text answer. Use for bounded research, repo discovery, dependency investigation, or multi-file reading that would otherwise consume the lead context window. Not for implementation, test authoring, or verification of a concrete diff; use implement, test, or review for those scopes. Brief the investigation goal and constraints, not step-by-step tool semantics.",
32655
33340
  inputSchema: {
32656
33341
  type: "object",
32657
33342
  required: ["prompt"],
@@ -32663,19 +33348,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32663
33348
  },
32664
33349
  model: {
32665
33350
  type: "string",
32666
- description: "Optional Copilot catalog model id (defaults to claude-sonnet-5). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33351
+ description: "Optional Copilot catalog model id (defaults to gemini-3.6-flash). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32667
33352
  },
32668
33353
  thinking: {
32669
33354
  type: "string",
32670
- enum: [
32671
- "off",
32672
- "minimal",
32673
- "low",
32674
- "medium",
32675
- "high",
32676
- "xhigh"
32677
- ],
32678
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33355
+ enum: WORKER_THINKING_LEVELS,
33356
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32679
33357
  },
32680
33358
  workspace: {
32681
33359
  type: "string",
@@ -32715,19 +33393,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32715
33393
  },
32716
33394
  model: {
32717
33395
  type: "string",
32718
- description: "Optional Copilot catalog model id (defaults to gpt-5.6-sol). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33396
+ description: "Optional Copilot catalog model id (defaults to gpt-5.6-sol). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32719
33397
  },
32720
33398
  thinking: {
32721
33399
  type: "string",
32722
- enum: [
32723
- "off",
32724
- "minimal",
32725
- "low",
32726
- "medium",
32727
- "high",
32728
- "xhigh"
32729
- ],
32730
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33400
+ enum: WORKER_THINKING_LEVELS,
33401
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32731
33402
  },
32732
33403
  workspace: {
32733
33404
  type: "string",
@@ -32763,18 +33434,11 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32763
33434
  },
32764
33435
  model: {
32765
33436
  type: "string",
32766
- description: "Optional Copilot catalog model id (defaults to gemini-3.1-pro-preview). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33437
+ description: "Optional Copilot catalog model id (defaults to gemini-3.1-pro-preview). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32767
33438
  },
32768
33439
  thinking: {
32769
33440
  type: "string",
32770
- enum: [
32771
- "off",
32772
- "minimal",
32773
- "low",
32774
- "medium",
32775
- "high",
32776
- "xhigh"
32777
- ],
33441
+ enum: WORKER_THINKING_LEVELS,
32778
33442
  description: "Optional reasoning depth (defaults to xhigh, clamped to high for the default review model). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32779
33443
  },
32780
33444
  workspace: {
@@ -32799,7 +33463,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32799
33463
  toolNameHttp: "plan",
32800
33464
  group: "workers",
32801
33465
  capability: "worker",
32802
- description: "Runs as the background `worker-plan` agent. Dispatch via the Agent tool (subagent_type: worker-plan) so the turn is never blocked; the result arrives as a completion notification. Read-only implementation planning by an autonomous worker (Pi runtime; default model `claude-opus-4.8` at xhigh reasoning, override via `model` with any Copilot-catalog model that advertises `tool_calls`). It has the same read-only toolset as explore and returns a concrete, ordered implementation plan covering files, approach, risks, and how acceptance criteria will be verified. Use before coding when the task needs repo-grounded sequencing or acceptance criteria translated into implementation steps. Not for editing files, running an implementation, writing tests, or adversarial review; use implement, test, or review for those scopes.",
33466
+ description: "Runs as the background `worker-plan` agent. Dispatch via the Agent tool (subagent_type: worker-plan) so the turn is never blocked; the result arrives as a completion notification. Read-only implementation planning by an autonomous worker (Pi runtime; default model `claude-opus-5` at xhigh reasoning, override via `model` with any Copilot-catalog model that advertises `tool_calls`). It has the same read-only toolset as explore and returns a concrete, ordered implementation plan covering files, approach, risks, and how acceptance criteria will be verified. Use before coding when the task needs repo-grounded sequencing or acceptance criteria translated into implementation steps. Not for editing files, running an implementation, writing tests, or adversarial review; use implement, test, or review for those scopes.",
32803
33467
  inputSchema: {
32804
33468
  type: "object",
32805
33469
  required: ["prompt"],
@@ -32811,19 +33475,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32811
33475
  },
32812
33476
  model: {
32813
33477
  type: "string",
32814
- description: "Optional Copilot catalog model id (defaults to claude-opus-4.8). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33478
+ description: "Optional Copilot catalog model id (defaults to claude-opus-5). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
32815
33479
  },
32816
33480
  thinking: {
32817
33481
  type: "string",
32818
- enum: [
32819
- "off",
32820
- "minimal",
32821
- "low",
32822
- "medium",
32823
- "high",
32824
- "xhigh"
32825
- ],
32826
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33482
+ enum: WORKER_THINKING_LEVELS,
33483
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32827
33484
  },
32828
33485
  workspace: {
32829
33486
  type: "string",
@@ -32867,15 +33524,8 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32867
33524
  },
32868
33525
  thinking: {
32869
33526
  type: "string",
32870
- enum: [
32871
- "off",
32872
- "minimal",
32873
- "low",
32874
- "medium",
32875
- "high",
32876
- "xhigh"
32877
- ],
32878
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33527
+ enum: WORKER_THINKING_LEVELS,
33528
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32879
33529
  },
32880
33530
  workspace: {
32881
33531
  type: "string",
@@ -33248,14 +33898,7 @@ async function runWorkerToolCall(call) {
33248
33898
  isError: true
33249
33899
  };
33250
33900
  const thinkingRaw = args.thinking;
33251
- const ALLOWED_THINKING = [
33252
- "off",
33253
- "minimal",
33254
- "low",
33255
- "medium",
33256
- "high",
33257
- "xhigh"
33258
- ];
33901
+ const ALLOWED_THINKING = WORKER_THINKING_LEVELS;
33259
33902
  let thinking;
33260
33903
  if (thinkingRaw !== void 0) {
33261
33904
  if (typeof thinkingRaw !== "string" || !ALLOWED_THINKING.includes(thinkingRaw)) return {
@@ -33568,5 +34211,5 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
33568
34211
  }
33569
34212
 
33570
34213
  //#endregion
33571
- export { buildAdvisorStream as $, getPackageVersion as $t, trustRepo as A, readResponseBodyCapped as At, runWorkerAgent as B, ArtifactClient as Bt, fileLastPromptStore as C, getTokenCount as Ct, repoRoot as D, createResponses as Dt, repoFingerprint as E, pickEndpoint as Et, EXPLORE_DEFAULT_MODEL as F, extractTarGzMember as Ft, toolbeltEnabled as G, DEFAULT_CLAUDE_MODEL_FALLBACKS as Gt, buildEnv as H, buildWorkspaceHeaderJson as Ht, IMPLEMENT_DEFAULT_MODEL as I, extractZipMember as It, TOOLBELT_TOOLS$1 as J, DEFAULT_PORT as Jt, toolbeltSkipSet as K, DEFAULT_CODEX_MODEL as Kt, PLAN_DEFAULT_MODEL as L, CONDENSED_OPERATING_SEQUENCE as Lt, resolveSealedGate as M, provisionBrowserAssets as Mt, BROWSE_DEFAULT_MODEL as N, hasSupportedBrowserInstalled as Nt, stopGateEnabledForRepo as O, createChatCompletions as Ot, DEFAULT_MODEL as P, provisionAndIndexColbert as Pt, ADVISOR_TOOL_INSTRUCTIONS as Q, pickClaudeDefault as Qt, REVIEW_DEFAULT_MODEL as R, DEFINITION_OF_GREATNESS as Rt, fileFindingsStore as S, state as Sn, createMessages as St, isSubagentContext as T, resolveMcpToolTimeoutMs as Tt, availableToolCommands as U, collapsePathKeys as Ut, withNoOutputRetry as V, buildWorkspaceHeaderHelperCommand as Vt, buildToolbeltAwareness as W, toolbeltPathOverride as Wt, searchWeb as X, UPSTREAM_INACTIVITY_TIMEOUT_MS as Xt, assetFor as Y, UPSTREAM_FETCH_TIMEOUT_MS as Yt, ADVISOR_INTERNAL_TOOL_NAME as Z, generateRandomPort as Zt, stopGateDisabled as _, forwardError as _n, nativeSubagentModel as _t, buildPeerAwarenessSnippet as a, cacheCopilotVersion as an, logStreamError as at, stopReviewEnabled as b, copilotHeaders as bn, shimDefaultsToXhigh as bt, personasFor as c, filterBetaHeader as cn, handleMcpDelete as ct, buildStopHookCommand as d, resolveModel as dn, artifactToolsEnabled as dt, withInstallLock as en, injectAdvisorTool as et, captureLaunchBaseline as f, sleep as fn, browseAgentEnabled as ft, launchBaselineKey as g, HTTPError as gn, geminiAvailable as gt, injectStopHookIntoSettingsFile as h, fetchWithTransientRetry as hn, fleetToolsEnabled as ht, buildAgentPrompt as i, tryRefreshAndRetry as in, isControllerClosedError as it, liveExec as j, parseJsonOrDiagnose as jt, stopReviewStateDir as k, MAX_RESPONSE_BODY_BYTES as kt, buildArtifactOpenHookCommand as l, isNullish as ln, handleMcpPost as lt, fileBlockBudget as m, getGitHubUser as mn, browserToolsEnabled as mt, MCP_GROUPS as n, setupGitHubAgentToken as nn, buildAnthropicErrorEvent as nt, buildPeerAwarenessSummary as o, cacheModels as on, readIteratorWithTimeout as ot, decideStopHook as p, getModels as pn, browserCompoundToolsEnabled as pt, vscodeRipgrepPath as q, DEFAULT_CODEX_MODEL_FALLBACKS as qt, assertMcpToolSurfaceConsistent as r, setupGitHubToken as rn, buildOpenAIErrorEvent as rt, enumerateInjectedMcpToolNames as s, cacheVSCodeVersion as sn, relayAnthropicStream as st, GROUP_META as t, setupCopilotToken as tn, isAdvisorRequested as tt, buildSessionBindHookCommand as u, resolveCodexModel as un, agentToolsEnabled as ut, stopGateId as v, GITHUB_API_BASE_URL as vn, standInToolEnabled as vt, fileReviewDebounce as w, assembleResponsesPayload as wt, fileBaselineStore as x, githubHeaders as xn, countTokens as xt, stopGatePlanMode as y, copilotBaseUrl as yn, workerToolsEnabled as yt, appendPlanReminder as z, shouldUseInsecureTls as zt };
33572
- //# sourceMappingURL=peer-mcp-personas-C1RegCRs.js.map
34214
+ export { searchWeb as $, UPSTREAM_FETCH_TIMEOUT_MS as $t, trustRepo as A, createResponses as At, TEST_DEFAULT_MODEL as B, warmTreeSitterPool as Bt, fileLastPromptStore as C, copilotBaseUrl as Cn, shimDefaultsToXhigh as Ct, repoRoot as D, assembleResponsesPayload as Dt, repoFingerprint as E, state as En, getTokenCount as Et, EXPLORE_DEFAULT_MODEL as F, provisionBrowserAssets as Ft, buildEnv as G, buildWorkspaceHeaderHelperCommand as Gt, resolveModeDefaults as H, DEFINITION_OF_GREATNESS as Ht, EXPLORE_DEFAULT_THINKING as I, hasSupportedBrowserInstalled as It, toolbeltEnabled as J, toolbeltPathOverride as Jt, availableToolCommands as K, buildWorkspaceHeaderJson as Kt, IMPLEMENT_DEFAULT_MODEL as L, provisionAndIndexColbert as Lt, resolveSealedGate as M, MAX_RESPONSE_BODY_BYTES as Mt, BROWSE_DEFAULT_MODEL as N, readResponseBodyCapped as Nt, stopGateEnabledForRepo as O, resolveMcpToolTimeoutMs as Ot, DEFAULT_MODEL as P, parseJsonOrDiagnose as Pt, assetFor as Q, DEFAULT_PORT as Qt, PLAN_DEFAULT_MODEL as R, extractTarGzMember as Rt, fileFindingsStore as S, GITHUB_API_BASE_URL as Sn, workerToolsEnabled as St, isSubagentContext as T, githubHeaders as Tn, createMessages as Tt, resolveWorkerRunOpts as U, shouldUseInsecureTls as Ut, appendPlanReminder as V, CONDENSED_OPERATING_SEQUENCE as Vt, runWorkerAgent as W, ArtifactClient as Wt, vscodeRipgrepPath as X, DEFAULT_CODEX_MODEL as Xt, toolbeltSkipSet as Y, DEFAULT_CLAUDE_MODEL_FALLBACKS as Yt, TOOLBELT_TOOLS$1 as Z, DEFAULT_CODEX_MODEL_FALLBACKS as Zt, stopGateDisabled as _, getModels as _n, browserToolsEnabled as _t, buildPeerAwarenessSnippet as a, setupCopilotToken as an, buildAnthropicErrorEvent as at, stopReviewEnabled as b, HTTPError as bn, nativeSubagentModel as bt, personasFor as c, tryRefreshAndRetry as cn, logStreamError as ct, buildStopHookCommand as d, cacheVSCodeVersion as dn, handleMcpDelete as dt, UPSTREAM_INACTIVITY_TIMEOUT_MS as en, ADVISOR_INTERNAL_TOOL_NAME as et, captureLaunchBaseline as f, filterBetaHeader as fn, handleMcpPost as ft, launchBaselineKey as g, sleep as gn, browserCompoundToolsEnabled as gt, injectStopHookIntoSettingsFile as h, resolveModel as hn, browseAgentEnabled as ht, buildAgentPrompt as i, withInstallLock as in, isAdvisorRequested as it, liveExec as j, createChatCompletions as jt, stopReviewStateDir as k, pickEndpoint as kt, buildArtifactOpenHookCommand as l, cacheCopilotVersion as ln, readIteratorWithTimeout as lt, fileBlockBudget as m, resolveCodexModel as mn, artifactToolsEnabled as mt, MCP_GROUPS as n, pickClaudeDefault as nn, buildAdvisorStream as nt, buildPeerAwarenessSummary as o, setupGitHubAgentToken as on, buildOpenAIErrorEvent as ot, decideStopHook as p, isNullish as pn, agentToolsEnabled as pt, buildToolbeltAwareness as q, collapsePathKeys as qt, assertMcpToolSurfaceConsistent as r, getPackageVersion as rn, injectAdvisorTool as rt, enumerateInjectedMcpToolNames as s, setupGitHubToken as sn, isControllerClosedError as st, GROUP_META as t, generateRandomPort as tn, ADVISOR_TOOL_INSTRUCTIONS as tt, buildSessionBindHookCommand as u, cacheModels as un, relayAnthropicStream as ut, stopGateId as v, getGitHubUser as vn, fleetToolsEnabled as vt, fileReviewDebounce as w, copilotHeaders as wn, countTokens as wt, fileBaselineStore as x, forwardError as xn, standInToolEnabled as xt, stopGatePlanMode as y, fetchWithTransientRetry as yn, geminiAvailable as yt, REVIEW_DEFAULT_MODEL as z, extractZipMember as zt };
34215
+ //# sourceMappingURL=peer-mcp-personas-BKwMCOsl.js.map