@cydm/pie 2.5.2 → 2.6.0

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.
@@ -1,7 +1,8 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
- Agent
4
- } from "./chunk-RTTGO2A6.js";
3
+ Agent,
4
+ getToolCallerIdentity
5
+ } from "./chunk-Y4CYXJR7.js";
5
6
  import {
6
7
  FileSystemGateway,
7
8
  completeSimple,
@@ -10,7 +11,7 @@ import {
10
11
  getPlatformConfig,
11
12
  hashText,
12
13
  streamSimple
13
- } from "./chunk-UD7KYQ6J.js";
14
+ } from "./chunk-O2DKMLNX.js";
14
15
  import {
15
16
  Type
16
17
  } from "./chunk-XXGYUYXI.js";
@@ -19,22 +20,31 @@ import {
19
20
  __toESM
20
21
  } from "./chunk-LZQXQOPJ.js";
21
22
 
22
- // ../../packages/shared-headless-capabilities/node_modules/shell-quote/quote.js
23
+ // ../../node_modules/shell-quote/quote.js
23
24
  var require_quote = __commonJS({
24
- "../../packages/shared-headless-capabilities/node_modules/shell-quote/quote.js"(exports, module) {
25
+ "../../node_modules/shell-quote/quote.js"(exports, module) {
25
26
  "use strict";
26
27
  var OPS = (
27
28
  /** @type {const} */
28
29
  [
29
30
  "||",
30
31
  "&&",
32
+ ";;&",
31
33
  ";;",
34
+ ";&",
32
35
  "|&",
33
36
  "<(",
37
+ ">(",
34
38
  "<<<",
39
+ "<<-",
40
+ "<<",
35
41
  ">>",
36
42
  ">&",
43
+ ">|",
44
+ "&>>",
45
+ "&>",
37
46
  "<&",
47
+ "<>",
38
48
  "&",
39
49
  ";",
40
50
  "(",
@@ -45,9 +55,13 @@ var require_quote = __commonJS({
45
55
  ]
46
56
  );
47
57
  var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
48
- var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g;
58
+ var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|~]/g;
49
59
  module.exports = function quote(xs) {
60
+ var sawComment = false;
50
61
  return xs.map(function(s) {
62
+ if (sawComment && typeof s === "string" && LINE_TERMINATORS.test(s)) {
63
+ throw new TypeError("a token after a `comment` must not contain line terminators");
64
+ }
51
65
  if (s === "") {
52
66
  return (
53
67
  /** @type {const} */
@@ -62,6 +76,12 @@ var require_quote = __commonJS({
62
76
  if (LINE_TERMINATORS.test(s.pattern)) {
63
77
  throw new TypeError("glob `pattern` must not contain line terminators");
64
78
  }
79
+ if (s.pattern === "") {
80
+ return (
81
+ /** @type {const} */
82
+ "''"
83
+ );
84
+ }
65
85
  return s.pattern.replace(GLOB_SHELL_SPECIAL, "\\$&");
66
86
  }
67
87
  if ("op" in s && typeof s.op === "string") {
@@ -74,15 +94,19 @@ var require_quote = __commonJS({
74
94
  if (LINE_TERMINATORS.test(s.comment)) {
75
95
  throw new TypeError("`comment` must not contain line terminators");
76
96
  }
97
+ sawComment = true;
77
98
  return "#" + s.comment;
78
99
  }
79
100
  throw new TypeError("unrecognized object token shape");
80
101
  }
102
+ if (/'/.test(s) && /!/.test(s)) {
103
+ return "'" + s.replace(/'/g, `'"'"'`) + "'";
104
+ }
81
105
  if (/["\s\\]/.test(s) && !/'/.test(s)) {
82
- return "'" + s.replace(/(['])/g, "\\$1") + "'";
106
+ return "'" + s + "'";
83
107
  }
84
108
  if (/["'\s]/.test(s)) {
85
- return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"';
109
+ return '"' + s.replace(/(["\\$`])/g, "\\$1") + '"';
86
110
  }
87
111
  return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}~])/g, "$1\\$2");
88
112
  }).join(" ");
@@ -90,23 +114,31 @@ var require_quote = __commonJS({
90
114
  }
91
115
  });
92
116
 
93
- // ../../packages/shared-headless-capabilities/node_modules/shell-quote/parse.js
117
+ // ../../node_modules/shell-quote/parse.js
94
118
  var require_parse = __commonJS({
95
- "../../packages/shared-headless-capabilities/node_modules/shell-quote/parse.js"(exports, module) {
119
+ "../../node_modules/shell-quote/parse.js"(exports, module) {
96
120
  "use strict";
97
121
  var CONTROL = (
98
122
  /** @type {const} */
99
123
  "(?:" + /** @type {const} */
100
124
  [
101
125
  "\\|\\|",
102
- "\\&\\&",
103
- ";;",
104
- "\\|\\&",
126
+ "\\&(?:\\&|>>?)",
127
+ // `&&`, `&>`, and `&>>`
128
+ ";;\\&?",
129
+ // `;;` and `;;&`
130
+ "[;|]\\&",
131
+ // `;&` and `|&`
105
132
  "\\<\\(",
106
133
  "\\<\\<\\<",
134
+ "\\<\\<-",
135
+ "\\<\\<(?!\\()",
136
+ // `<<(` stays `<` and `<(`, as zsh reads it; other shells reject it
107
137
  ">>",
108
- ">\\&",
109
- "<\\&",
138
+ ">[&|(]",
139
+ // `>&`, `>|`, and `>(`
140
+ "<[&>]",
141
+ // `<&` and `<>`
110
142
  "[&;()|<>]"
111
143
  ].join(
112
144
  /** @type {const} */
@@ -123,10 +155,11 @@ var require_parse = __commonJS({
123
155
  /** @type {const} */
124
156
  "'([^']*?)'"
125
157
  );
126
- var DOUBLE_QUOTE = (
127
- /** @type {const} */
128
- '"((\\\\"|[^"])*?)"'
129
- );
158
+ var ANSI_C_BODY = "(?:\\\\[\\s\\S]|[^\\\\'])*?";
159
+ var ANSI_C_QUOTE = "\\$'" + ANSI_C_BODY + "'";
160
+ var ansiCAt = new RegExp("\\$'" + ANSI_C_BODY + "(?:(')|\\\\?$)", "g");
161
+ var ANSI_C_LETTERS = "abeEfnrtv";
162
+ var ANSI_C_CHARS = "\x07\b\x1B\x1B\f\n\r \v";
130
163
  var hash = /^#$/;
131
164
  var SQ = (
132
165
  /** @type {const} */
@@ -175,17 +208,46 @@ var require_parse = __commonJS({
175
208
  }
176
209
  return pre + r;
177
210
  }
211
+ var ansiCEscape = /\\([0-7]{1,3}|x[\dA-Fa-f]{1,2}|u[\dA-Fa-f]{1,4}|U[\dA-Fa-f]{1,8}|c(?:\\\\|[\s\S])|[abeEfnrtv\\'"?])/g;
212
+ function expandAnsiCEscape(m, escape2) {
213
+ var kind = escape2.charAt(0);
214
+ if (kind === "c") {
215
+ var ctrl = escape2.charAt(1);
216
+ return ctrl === "?" ? "\x7F" : String.fromCharCode(ctrl.charCodeAt(0) & 31);
217
+ }
218
+ if (kind === "x" || kind === "u" || kind === "U") {
219
+ var cp = parseInt(escape2.slice(1), 16);
220
+ if (cp > 1114111) {
221
+ return m;
222
+ }
223
+ return String.fromCharCode.apply(null, cp > 65535 ? [55232 + (cp >> 10), 56320 + (cp & 1023)] : [cp]);
224
+ }
225
+ if (kind >= "0" && kind <= "7") {
226
+ return String.fromCharCode(parseInt(escape2, 8) & 255);
227
+ }
228
+ var letter = ANSI_C_LETTERS.indexOf(escape2);
229
+ return letter < 0 ? escape2 : ANSI_C_CHARS.charAt(letter);
230
+ }
231
+ function expandAnsiC(body) {
232
+ return body.replace(ansiCEscape, expandAnsiCEscape).split("\0")[0];
233
+ }
234
+ function closesAnsiC(s, i2) {
235
+ ansiCAt.lastIndex = i2;
236
+ return !!/** @type {RegExpExecArray} */
237
+ ansiCAt.exec(s)[1];
238
+ }
178
239
  function parseInternal(string, env, opts) {
179
240
  if (!opts) {
180
241
  opts = {};
181
242
  }
182
243
  var BS = opts.escape || "\\";
183
244
  var ifs = opts.splitUnquoted === true ? " \n" : typeof opts.splitUnquoted === "string" ? opts.splitUnquoted : "";
184
- var BAREWORD = "(\\" + BS + `['"` + META + `]|[^\\s'"` + META + "])+";
245
+ var BAREWORD = "(\\" + BS + `['"$\\` + BS + META + "]|\\$\\$|\\$(?!" + ANSI_C_QUOTE.slice(2) + `)|[^\\s'"$` + META + "])+";
246
+ var DOUBLE_QUOTE = '"(?:\\' + BS + '[\\s\\S]|[^"\\' + BS + '])*"';
185
247
  var chunker = new RegExp([
186
248
  "(" + CONTROL + ")",
187
249
  // control chars
188
- "(" + BAREWORD + "|" + DOUBLE_QUOTE + "|" + SINGLE_QUOTE + ")+"
250
+ "(" + ANSI_C_QUOTE + "|" + BAREWORD + "|" + DOUBLE_QUOTE + "|" + SINGLE_QUOTE + ")+"
189
251
  ].join("|"), "g");
190
252
  var matches = matchAll(string, chunker);
191
253
  if (matches.length === 0) {
@@ -240,9 +302,8 @@ var require_parse = __commonJS({
240
302
  varend -= 1;
241
303
  varname = s.slice(i2, varend);
242
304
  i2 = varend;
243
- } else if (/[*@#?$!_-]/.test(char)) {
305
+ } else if (/[*@#?$!-]/.test(char)) {
244
306
  varname = char;
245
- i2 += 1;
246
307
  } else {
247
308
  var slicedFromI = s.slice(i2);
248
309
  varend = slicedFromI.match(/[^\w\d_]/);
@@ -332,6 +393,11 @@ var require_parse = __commonJS({
332
393
  );
333
394
  } else if (c === BS) {
334
395
  esc = true;
396
+ } else if (c === DS && s.charAt(i2 + 1) === SQ && closesAnsiC(s, i2)) {
397
+ flushRun();
398
+ sawQuote = true;
399
+ out += expandAnsiC(s.slice(i2 + 2, ansiCAt.lastIndex - 1));
400
+ i2 = ansiCAt.lastIndex - 1;
335
401
  } else if (c === DS) {
336
402
  var value = parseEnvVar();
337
403
  if (!ifs) {
@@ -415,9 +481,9 @@ var require_parse = __commonJS({
415
481
  }
416
482
  });
417
483
 
418
- // ../../packages/shared-headless-capabilities/node_modules/shell-quote/index.js
484
+ // ../../node_modules/shell-quote/index.js
419
485
  var require_shell_quote = __commonJS({
420
- "../../packages/shared-headless-capabilities/node_modules/shell-quote/index.js"(exports) {
486
+ "../../node_modules/shell-quote/index.js"(exports) {
421
487
  "use strict";
422
488
  exports.quote = require_quote();
423
489
  exports.parse = require_parse();
@@ -1392,10 +1458,14 @@ Subagent guarded mode uses the same bash risk policy as the parent agent. Destru
1392
1458
  }
1393
1459
  };
1394
1460
  }
1395
- function filterToolsForSubagent(runtimeTools, parentTools, plan) {
1461
+ function filterToolsForSubagent(runtimeTools, parentTools, plan, mode) {
1396
1462
  const parentNames = new Set(parentTools.map((tool) => tool.name));
1397
1463
  const filtered = [];
1398
1464
  for (const tool of runtimeTools) {
1465
+ if (tool.name === "spawn_subagents_parallel") {
1466
+ plan.toolsExcluded.push({ name: tool.name, reason: "recursive delegation is not allowed" });
1467
+ continue;
1468
+ }
1399
1469
  if (!parentNames.has(tool.name)) {
1400
1470
  plan.toolsExcluded.push({ name: tool.name, reason: "not available in parent tool set" });
1401
1471
  continue;
@@ -1404,6 +1474,10 @@ function filterToolsForSubagent(runtimeTools, parentTools, plan) {
1404
1474
  filtered.push(wrapBashGuard(tool, plan));
1405
1475
  continue;
1406
1476
  }
1477
+ if (mode === "edit" && (tool.name === "write_file" || tool.name === "edit_file")) {
1478
+ filtered.push(tool);
1479
+ continue;
1480
+ }
1407
1481
  const policy = getSubagentToolPolicyDecision(tool);
1408
1482
  if (!policy.allowed) {
1409
1483
  plan.toolsExcluded.push({ name: tool.name, reason: policy.reason || "not allowed for subagent" });
@@ -1413,7 +1487,7 @@ function filterToolsForSubagent(runtimeTools, parentTools, plan) {
1413
1487
  }
1414
1488
  return filtered;
1415
1489
  }
1416
- function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOverride, thinkingLevel = DEFAULT_SUBAGENT_THINKING_LEVEL) {
1490
+ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOverride, thinkingLevel = DEFAULT_SUBAGENT_THINKING_LEVEL, mode = "read") {
1417
1491
  const complexity = inferComplexity(task, priority);
1418
1492
  const modelClass = selectModelClass(complexity, priority);
1419
1493
  const overrideResolution = modelOverride ? deps.resolveModelRef?.(modelOverride) ?? { model: void 0, fallbackReason: `model override "${modelOverride}" is not supported by this host` } : void 0;
@@ -1428,6 +1502,7 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOver
1428
1502
  task,
1429
1503
  name,
1430
1504
  priority,
1505
+ mode,
1431
1506
  complexity,
1432
1507
  modelClass,
1433
1508
  modelOverride,
@@ -1439,8 +1514,11 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOver
1439
1514
  timeoutSeconds,
1440
1515
  outputContract: "Return a concise answer with evidence, uncertainty, and any parent follow-up needed."
1441
1516
  };
1442
- const runtimeTools = deps.createRuntimeTools();
1443
- const filteredTools = filterToolsForSubagent(runtimeTools, deps.getParentTools(), partialPlan);
1517
+ const runtimeTools = deps.createRuntimeTools(mode);
1518
+ const filteredTools = filterToolsForSubagent(runtimeTools, deps.getParentTools(), partialPlan, mode);
1519
+ if (mode === "edit" && !filteredTools.some((tool) => tool.name === "write_file" || tool.name === "edit_file")) {
1520
+ throw new Error("Edit delegation requires executable editing tools from the parent's current tool set; it is unavailable in read-only/plan mode.");
1521
+ }
1444
1522
  const plan = {
1445
1523
  ...partialPlan,
1446
1524
  toolsAllowed: filteredTools.map((tool) => tool.name)
@@ -1502,7 +1580,7 @@ function inferUncertainty(text, signals) {
1502
1580
  return "medium";
1503
1581
  }
1504
1582
  function isParentHandoffRequired(task) {
1505
- return task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use");
1583
+ return task.mode === "edit" || task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use");
1506
1584
  }
1507
1585
  async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1508
1586
  const startTime = Date.now();
@@ -1511,9 +1589,9 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1511
1589
  deps.debugLog?.("run", "runSubagentWithProgress START", { taskName: task.name, yoloMode: deps.yoloMode });
1512
1590
  try {
1513
1591
  reportProgress({ message: `[${task.name}] Starting...`, increment: 5 });
1514
- const { plan, tools, apiKey, model } = buildSubagentPlan(deps, task.task, task.name, task.priority, task.timeoutSeconds, task.modelOverride, task.thinkingLevel);
1592
+ const { plan, tools, apiKey, model } = buildSubagentPlan(deps, task.task, task.name, task.priority, task.timeoutSeconds, task.modelOverride, task.thinkingLevel, task.mode);
1515
1593
  task.plan = plan;
1516
- task.needsParentWrite = needsParentWrite(task.task);
1594
+ task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
1517
1595
  for (const excluded of plan.toolsExcluded) {
1518
1596
  if (excluded.reason === "unclassified_tool") {
1519
1597
  task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
@@ -1538,7 +1616,7 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1538
1616
  Task boundary:
1539
1617
  - Work only on the assigned task.
1540
1618
  - Use available tools to gather evidence before making claims.
1541
- - Do not edit files; if writing is needed, say what the parent agent should change.
1619
+ ${task.mode === "edit" ? "- You may edit files and run tests using the inherited parent tools and permissions. This is a shared workspace, not an isolated sandbox. Preserve existing user changes. Do not commit or automatically roll back changes. Report changed files, tests actually run, and remaining work for parent review." : "- Do not edit files; if writing is needed, say what the parent agent should change."}
1542
1620
  - Return a concise answer with evidence and uncertainty.
1543
1621
 
1544
1622
  Output:
@@ -1600,8 +1678,8 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1600
1678
  }
1601
1679
  });
1602
1680
  let prompt = task.task;
1603
- if (task.needsParentWrite) {
1604
- prompt += "\n\nThis task appears to require edits. Do not write files; provide a read-only plan and set clear parent follow-up.";
1681
+ if (task.mode === "read") {
1682
+ prompt += "\n\nThis delegation is read mode. Do not write files. If changes are needed, describe them for the parent.";
1605
1683
  }
1606
1684
  prompt += "\n\nReturn evidence for your conclusion and state uncertainty as low, medium, or high.";
1607
1685
  reportProgress({ message: `[${task.name}] Running plan ${plan.complexity}/${plan.modelClass}...`, increment: 20 });
@@ -1669,7 +1747,7 @@ function buildParallelResult(tasks, totalDuration) {
1669
1747
  const statusLabel = task.status === "completed" ? "completed" : task.status;
1670
1748
  resultText += `**${task.name}** (${statusLabel}`;
1671
1749
  if (task.plan) {
1672
- resultText += `, ${task.plan.complexity}/${task.plan.modelClass}`;
1750
+ resultText += `, mode=${task.mode}, ${task.plan.complexity}/${task.plan.modelClass}`;
1673
1751
  if (task.plan.thinkingLevel !== DEFAULT_SUBAGENT_THINKING_LEVEL) {
1674
1752
  resultText += `, thinking=${task.plan.thinkingLevel}`;
1675
1753
  }
@@ -1693,7 +1771,9 @@ function buildParallelResult(tasks, totalDuration) {
1693
1771
  resultText += `Evidence: ${task.evidence.slice(0, 5).map((entry) => `${entry.type}:${entry.value}`).join("; ")}
1694
1772
  `;
1695
1773
  }
1696
- if (task.needsParentWrite) {
1774
+ if (task.mode === "edit") {
1775
+ resultText += "Parent follow-up: review changes and validate reported tests. Partial edits are preserved on failure; no automatic rollback or task retry.\n";
1776
+ } else if (task.needsParentWrite) {
1697
1777
  resultText += "Parent follow-up: writing appears to be needed; parent agent must make any edits.\n";
1698
1778
  } else if (task.parentHandoffRequired) {
1699
1779
  resultText += "Parent follow-up: subagent result needs parent review or continuation.\n";
@@ -1715,7 +1795,7 @@ function normalizeTaskDefs(args) {
1715
1795
  return { error: "Invalid spawn_subagents_parallel arguments: tasks must be a non-empty array." };
1716
1796
  }
1717
1797
  const normalized = [];
1718
- const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel"];
1798
+ const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel", "mode"];
1719
1799
  for (const [index, def] of taskDefs.entries()) {
1720
1800
  if (!def || typeof def !== "object") {
1721
1801
  return { error: `Invalid task #${index + 1}: expected an object with the objective in "task" (aliases "description"/"prompt"/"objective" accepted).` };
@@ -1737,6 +1817,9 @@ function normalizeTaskDefs(args) {
1737
1817
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
1738
1818
  return { error: `Invalid task #${index + 1}: timeout must be a positive number of seconds.` };
1739
1819
  }
1820
+ if (def.mode !== void 0 && def.mode !== "read" && def.mode !== "edit") {
1821
+ return { error: `Invalid task #${index + 1}: "mode" must be "read" or "edit".` };
1822
+ }
1740
1823
  const priority = def.priority === "thorough" ? "thorough" : "fast";
1741
1824
  const modelOverride = typeof def.model === "string" ? def.model.trim() : void 0;
1742
1825
  if (def.model !== void 0 && !modelOverride) {
@@ -1749,11 +1832,15 @@ function normalizeTaskDefs(args) {
1749
1832
  name: typeof def.name === "string" && def.name.trim() ? def.name.trim() : void 0,
1750
1833
  task,
1751
1834
  priority,
1835
+ mode: def.mode ?? "read",
1752
1836
  timeoutSeconds,
1753
1837
  modelOverride,
1754
1838
  thinkingLevel: def.thinkingLevel
1755
1839
  });
1756
1840
  }
1841
+ if (normalized.length !== 1 && normalized.some((task) => task.mode === "edit")) {
1842
+ return { error: "An edit delegation must contain exactly one task. Wait for it to finish before editing or launching another edit task in the shared workspace." };
1843
+ }
1757
1844
  return normalized;
1758
1845
  }
1759
1846
  var DEFAULT_MAX_CONCURRENT_SUBAGENTS = 4;
@@ -1792,10 +1879,10 @@ function createSubagentCapability(deps) {
1792
1879
  label: "Spawn Subagents",
1793
1880
  description: `Spawn child agents to run bounded sub-tasks in parallel.
1794
1881
 
1795
- Use this for independent investigation, search, review, or triage work. Provide each task as a plain objective. Pie chooses the model class automatically. Use timeout only for unusually long tasks.
1882
+ Default mode="read" is for investigation, search, review, or triage and cannot edit source files. Explicit mode="edit" permits file editing and tests with inherited parent permissions. An edit call must contain exactly one task; wait for it to finish before editing or launching another edit task. The workspace is shared, not isolated. Edit tasks are not automatically retried; the parent must review changes and test results. Provide each task as a plain objective. Pie chooses the model class automatically. Use timeout only for unusually long tasks.
1796
1883
 
1797
1884
  Arguments shape (each tasks[] item puts the objective in "task" \u2014 "description"/"prompt"/"objective" are accepted as aliases):
1798
- { "tasks": [{ "task": "<objective text>", "name": "<optional short label>", "priority": "fast" | "thorough", "model": "<optional explicit model override>", "thinkingLevel": "<optional off|minimal|low|medium|high|xhigh, default medium>", "timeout": 600 }] }
1885
+ { "tasks": [{ "task": "<objective text>", "mode": "read" | "edit", "name": "<optional short label>", "priority": "fast" | "thorough", "model": "<optional explicit model override>", "thinkingLevel": "<optional off|minimal|low|medium|high|xhigh, default medium>", "timeout": 600 }] }
1799
1886
 
1800
1887
  "model" overrides the automatic model-class selection for that task when the user explicitly asks for a specific model (e.g. "anthropic/claude-..."). Prefer "provider/model-id"; a bare model id works only when it is unambiguous across providers. When the override cannot be resolved, the task falls back to automatic selection and reports why.
1801
1888
 
@@ -1809,6 +1896,7 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
1809
1896
  prompt: Type.Optional(Type.String({ description: "Alias for task." })),
1810
1897
  objective: Type.Optional(Type.String({ description: "Alias for task." })),
1811
1898
  name: Type.Optional(Type.String({ description: "Short name for this subagent, shown in progress updates." })),
1899
+ mode: Type.Optional(Type.Union([Type.Literal("read"), Type.Literal("edit")], { description: "Default read. Edit permits inherited file editing and tests in the shared workspace; exactly one task per edit call, parent review required." })),
1812
1900
  priority: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("thorough")], { description: "fast uses lower-cost defaults; thorough raises model class when needed." })),
1813
1901
  model: Type.Optional(Type.String({ description: 'Explicit model override for this task, e.g. "provider/model-id". Only set when the user explicitly requests a specific model; otherwise Pie picks the model class automatically.' })),
1814
1902
  thinkingLevel: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], { description: `Reasoning level for this task. Default: "medium". Only override when the user asks (e.g. "xhigh" for deep review tasks, "off" for cheap lookups). Ignored by non-reasoning models.` })),
@@ -1846,6 +1934,7 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
1846
1934
  status: "pending",
1847
1935
  task: def.task,
1848
1936
  priority: def.priority,
1937
+ mode: def.mode,
1849
1938
  timeoutSeconds: def.timeoutSeconds,
1850
1939
  modelOverride: def.modelOverride,
1851
1940
  thinkingLevel: def.thinkingLevel,
@@ -1864,7 +1953,7 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
1864
1953
  const runTask = async (task) => {
1865
1954
  for (let attempt = 0; ; attempt++) {
1866
1955
  await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
1867
- const canRetry = task.status === "failed" && isTransientNetworkError(task.error ?? "") && attempt < MAX_TRANSIENT_RETRIES && !signal?.aborted;
1956
+ const canRetry = task.mode === "read" && task.status === "failed" && isTransientNetworkError(task.error ?? "") && attempt < MAX_TRANSIENT_RETRIES && !signal?.aborted;
1868
1957
  if (!canRetry) return;
1869
1958
  const failureReason = task.error ?? "unknown";
1870
1959
  task.evidence.length = 0;
@@ -1896,6 +1985,7 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
1896
1985
  id: task.id,
1897
1986
  name: task.name,
1898
1987
  status: task.status,
1988
+ mode: task.mode,
1899
1989
  task: task.task,
1900
1990
  timeoutSeconds: task.timeoutSeconds,
1901
1991
  plan: task.plan,
@@ -9015,14 +9105,15 @@ Focus: ${question.trim()}` : ""}` },
9015
9105
  const readReachedEndOfFile = startLine + range2.selectedLines >= range2.totalLines;
9016
9106
  const isCompleteTextKnown = readStartsAtBeginning && readReachedEndOfFile && !truncation.truncated && !truncation.firstLineExceedsLimit;
9017
9107
  let callerObservation = fileObservationState;
9018
- if (signal) {
9108
+ const callerKey = getToolCallerIdentity(signal) ?? signal;
9109
+ if (callerKey) {
9019
9110
  const generation = fileObservationState.getGeneration();
9020
- const existing = callerObservations.get(signal);
9111
+ const existing = callerObservations.get(callerKey);
9021
9112
  if (existing?.generation === generation) {
9022
9113
  callerObservation = existing.state;
9023
9114
  } else {
9024
9115
  callerObservation = createFileObservationState();
9025
- callerObservations.set(signal, { generation, state: callerObservation });
9116
+ callerObservations.set(callerKey, { generation, state: callerObservation });
9026
9117
  }
9027
9118
  }
9028
9119
  if (fileObservationState.isUnchangedReadRange(absolutePath, rangeKey, snapshot, range2.selectedContentHash) && callerObservation.isUnchangedReadRange(absolutePath, rangeKey, snapshot, range2.selectedContentHash)) {
@@ -13483,6 +13574,29 @@ Focus: ${request.question.trim()}` : ""}` },
13483
13574
  isError: false
13484
13575
  };
13485
13576
  }
13577
+ if ((request.kind === "video" || request.kind === "audio") && agentModel?.api === "openai-completions" && agentModel.input.includes(request.kind)) {
13578
+ const capability = agentModel.fileCapabilities?.[request.kind];
13579
+ const audioSupported = request.kind !== "audio" || ["audio/wav", "audio/mpeg", "audio/mp3"].includes(request.mimeType);
13580
+ if (capability && typeof capability === "object" && capability.supported !== false && capability.nativeInline === true && audioSupported && (!capability.formats || capability.formats.includes(request.mimeType)) && capability.maxDurationSeconds === void 0) {
13581
+ const fs2 = getFileSystem();
13582
+ const size = (await fs2.stat(request.absolutePath)).size;
13583
+ const cap = Math.min(capability.maxFileSizeMb ?? 15, 15) * 1024 * 1024;
13584
+ if (size > 0 && size <= cap) {
13585
+ if (request.signal?.aborted) throw new Error("Operation aborted");
13586
+ const data = await fs2.readFile(request.absolutePath, "base64");
13587
+ if (request.signal?.aborted) throw new Error("Operation aborted");
13588
+ if (data) return {
13589
+ content: [
13590
+ { type: "text", text: `Read ${request.kind} file ${request.displayPath} [${request.mimeType}]. Original media attached for the current model.${request.question?.trim() ? `
13591
+ Focus: ${request.question.trim()}` : ""}` },
13592
+ { type: request.kind, data, mimeType: request.mimeType }
13593
+ ],
13594
+ details: { understanding: { purpose, kind: request.kind, mimeType: request.mimeType, agentModel: getModelLabel(agentModel), routeSource: "current_model", inputMode: "nativeAttachment", attachmentBytes: size } },
13595
+ isError: false
13596
+ };
13597
+ }
13598
+ }
13599
+ }
13486
13600
  const singleDecision = deps.resolveToolModel(purpose);
13487
13601
  const candidateDecisions = deps.resolveToolModelCandidates?.(purpose) ?? [singleDecision];
13488
13602
  const sessionId = deps.getSessionId() || `read-file-${Date.now()}`;
@@ -14602,19 +14716,121 @@ var webFetchSchema = Type.Object({
14602
14716
  var DEFAULT_TIMEOUT_MS = 45e3;
14603
14717
  var DEFAULT_MAX_BYTES2 = 2e6;
14604
14718
  var DEFAULT_MAX_CHARS = 1e5;
14719
+ var DEFAULT_MAX_REDIRECTS = 5;
14720
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
14605
14721
  function modelLabel2(model) {
14606
14722
  return model ? `${model.provider}/${model.id}` : void 0;
14607
14723
  }
14724
+ function parseIpv4Part(part) {
14725
+ if (part === "") return void 0;
14726
+ const lowered = part.toLowerCase();
14727
+ if (lowered.startsWith("0x")) {
14728
+ const digits = lowered.slice(2);
14729
+ return /^[0-9a-f]+$/.test(digits) ? parseInt(digits, 16) : void 0;
14730
+ }
14731
+ if (lowered.length > 1 && lowered.startsWith("0")) {
14732
+ return /^[0-7]+$/.test(lowered) ? parseInt(lowered, 8) : void 0;
14733
+ }
14734
+ if (!/^\d+$/.test(lowered)) return void 0;
14735
+ return parseInt(lowered, 10);
14736
+ }
14737
+ function parseIpv4Literal(host) {
14738
+ const parts = host.split(".");
14739
+ if (parts.length === 0 || parts.length > 4) return void 0;
14740
+ const numbers = [];
14741
+ for (const part of parts) {
14742
+ const value = parseIpv4Part(part);
14743
+ if (value === void 0) return void 0;
14744
+ numbers.push(value);
14745
+ }
14746
+ const count = numbers.length;
14747
+ const last = numbers[count - 1];
14748
+ if (last > 256 ** (5 - count) - 1) return void 0;
14749
+ for (let i = 0; i < count - 1; i += 1) {
14750
+ if (numbers[i] > 255) return void 0;
14751
+ }
14752
+ let address = 0;
14753
+ for (let i = 0; i < count - 1; i += 1) address = address * 256 + numbers[i];
14754
+ address = address * 256 ** (5 - count) + last;
14755
+ return [address >>> 24 & 255, address >>> 16 & 255, address >>> 8 & 255, address & 255];
14756
+ }
14757
+ function isPrivateIpv4(octets) {
14758
+ const [a = 0, b = 0] = octets;
14759
+ if (a === 0) return true;
14760
+ if (a === 10) return true;
14761
+ if (a === 100 && b >= 64 && b <= 127) return true;
14762
+ if (a === 127) return true;
14763
+ if (a === 169 && b === 254) return true;
14764
+ if (a === 172 && b >= 16 && b <= 31) return true;
14765
+ if (a === 192 && b === 0 && octets[2] === 0) return true;
14766
+ if (a === 192 && b === 0 && octets[2] === 2) return true;
14767
+ if (a === 192 && b === 88 && octets[2] === 99) return true;
14768
+ if (a === 192 && b === 168) return true;
14769
+ if (a === 198 && (b === 18 || b === 19)) return true;
14770
+ if (a === 198 && b === 51 && octets[2] === 100) return true;
14771
+ if (a === 203 && b === 0 && octets[2] === 113) return true;
14772
+ if (a >= 224) return true;
14773
+ return false;
14774
+ }
14775
+ function parseIpv6Literal(hostname) {
14776
+ let host = hostname.toLowerCase();
14777
+ if (host.startsWith("[")) host = host.slice(1, host.endsWith("]") ? -1 : void 0);
14778
+ const zoneIndex = host.indexOf("%");
14779
+ if (zoneIndex >= 0) host = host.slice(0, zoneIndex);
14780
+ if (!host.includes(":")) return void 0;
14781
+ const halves = host.split("::");
14782
+ if (halves.length > 2) return void 0;
14783
+ const parseTokens = (tokens) => {
14784
+ const groups = [];
14785
+ for (let i = 0; i < tokens.length; i += 1) {
14786
+ const token = tokens[i];
14787
+ if (token === "") return void 0;
14788
+ if (i === tokens.length - 1 && token.includes(".")) {
14789
+ const v4 = parseIpv4Literal(token);
14790
+ if (!v4) return void 0;
14791
+ groups.push(v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]);
14792
+ continue;
14793
+ }
14794
+ if (!/^[0-9a-f]{1,4}$/.test(token)) return void 0;
14795
+ groups.push(parseInt(token, 16));
14796
+ }
14797
+ return groups;
14798
+ };
14799
+ const head = halves[0] === "" ? [] : parseTokens(halves[0].split(":"));
14800
+ if (head === void 0) return void 0;
14801
+ const tail = halves.length === 2 ? halves[1] === "" ? [] : parseTokens(halves[1].split(":")) : [];
14802
+ if (tail === void 0) return void 0;
14803
+ if (halves.length === 1) return head.length === 8 ? head : void 0;
14804
+ const missing = 8 - head.length - tail.length;
14805
+ if (missing < 1) return void 0;
14806
+ return [...head, ...new Array(missing).fill(0), ...tail];
14807
+ }
14808
+ function isPrivateIpv6(groups) {
14809
+ const first = groups[0];
14810
+ if (groups.every((group) => group === 0)) return true;
14811
+ if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1) return true;
14812
+ if ((first & 65024) === 64512) return true;
14813
+ if ((first & 65472) === 65152) return true;
14814
+ const embeddedV4 = [groups[6] >> 8, groups[6] & 255, groups[7] >> 8, groups[7] & 255];
14815
+ if (groups.slice(0, 5).every((group) => group === 0) && groups[5] === 65535) {
14816
+ return isPrivateIpv4(embeddedV4);
14817
+ }
14818
+ if (groups.slice(0, 6).every((group) => group === 0)) {
14819
+ return isPrivateIpv4(embeddedV4);
14820
+ }
14821
+ if (first === 100 && groups[1] === 65435 && groups.slice(2, 6).every((group) => group === 0)) {
14822
+ return isPrivateIpv4(embeddedV4);
14823
+ }
14824
+ return false;
14825
+ }
14608
14826
  function isPrivateHostname(hostname) {
14609
- const host = hostname.toLowerCase();
14827
+ const host = hostname.toLowerCase().trim().replace(/\.+$/, "");
14610
14828
  if (host === "localhost" || host.endsWith(".localhost")) return true;
14611
- if (host === "0.0.0.0") return true;
14612
- if (host.startsWith("127.")) return true;
14613
- if (host.startsWith("10.")) return true;
14614
- if (host.startsWith("192.168.")) return true;
14615
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
14616
- if (host === "::1" || host === "[::1]") return true;
14617
- if (host === "169.254.169.254") return true;
14829
+ if (host === "local" || host.endsWith(".local")) return true;
14830
+ const v4 = parseIpv4Literal(host);
14831
+ if (v4) return isPrivateIpv4(v4);
14832
+ const v6 = parseIpv6Literal(host);
14833
+ if (v6) return isPrivateIpv6(v6);
14618
14834
  return false;
14619
14835
  }
14620
14836
  function charsetFromContentType(contentType) {
@@ -14753,6 +14969,24 @@ async function cancelResponseBody(response) {
14753
14969
  } catch {
14754
14970
  }
14755
14971
  }
14972
+ var DISCARD_BODY_GRACE_MS = 250;
14973
+ async function discardResponseBody(response) {
14974
+ await new Promise((resolve3) => {
14975
+ let settled = false;
14976
+ const settle = () => {
14977
+ if (settled) return;
14978
+ settled = true;
14979
+ clearTimeout(timer);
14980
+ resolve3();
14981
+ };
14982
+ const timer = setTimeout(settle, DISCARD_BODY_GRACE_MS);
14983
+ try {
14984
+ void cancelResponseBody(response).then(settle, settle);
14985
+ } catch {
14986
+ settle();
14987
+ }
14988
+ });
14989
+ }
14756
14990
  var BODY_TRUNCATED_ERROR_CODE = "body_truncated";
14757
14991
  function isBodyTruncationError(error) {
14758
14992
  return error instanceof Error && error.code === BODY_TRUNCATED_ERROR_CODE;
@@ -14889,7 +15123,8 @@ function createSharedWebFetchTool(deps = {}) {
14889
15123
  isError: true
14890
15124
  };
14891
15125
  }
14892
- if (!deps.allowPrivateHosts?.() && isPrivateHostname(parsed.hostname)) {
15126
+ const allowPrivate = deps.allowPrivateHosts?.() ?? false;
15127
+ if (!allowPrivate && isPrivateHostname(parsed.hostname)) {
14893
15128
  return {
14894
15129
  content: [{ type: "text", text: `Blocked private or local URL: ${url}` }],
14895
15130
  details: { url, sourceUrl: url, durationMs: Date.now() - start, failureCategory: "blocked_private_host" },
@@ -14898,22 +15133,91 @@ function createSharedWebFetchTool(deps = {}) {
14898
15133
  }
14899
15134
  const timeout = createTimeoutSignal(signal, deps.timeoutMs ?? DEFAULT_TIMEOUT_MS);
14900
15135
  try {
14901
- const response = await (deps.fetch ?? fetch)(url, {
15136
+ const requestHeaders = {
15137
+ "user-agent": "Pie web_fetch/1.0",
15138
+ accept: "text/html,application/xhtml+xml,text/plain,application/pdf,*/*;q=0.8"
15139
+ };
15140
+ const doFetch = (target) => (deps.fetch ?? fetch)(target, {
14902
15141
  method: "GET",
14903
- redirect: "follow",
15142
+ // Redirects are followed manually in the loop below so that every
15143
+ // hop is validated (scheme + host) BEFORE its request is issued.
15144
+ // A transport that ignores this and follows internally is caught
15145
+ // post-hoc via the response.url guard below.
15146
+ redirect: "manual",
14904
15147
  signal: timeout.signal,
14905
- headers: {
14906
- "user-agent": "Pie web_fetch/1.0",
14907
- accept: "text/html,application/xhtml+xml,text/plain,application/pdf,*/*;q=0.8"
14908
- }
15148
+ headers: requestHeaders
14909
15149
  });
15150
+ const redirectChain = [url];
15151
+ const maxRedirects = deps.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
15152
+ let currentUrl = url;
15153
+ let response = await doFetch(currentUrl);
15154
+ let redirectsFollowed = 0;
15155
+ while (REDIRECT_STATUSES.has(response.status)) {
15156
+ const location = response.headers.get("location")?.trim();
15157
+ if (!location) break;
15158
+ let next;
15159
+ try {
15160
+ next = new URL(location, currentUrl);
15161
+ } catch {
15162
+ await discardResponseBody(response);
15163
+ return {
15164
+ content: [{ type: "text", text: `web_fetch redirect target is not a valid URL: ${location}` }],
15165
+ details: { url, sourceUrl: url, finalUrl: currentUrl, redirectChain, durationMs: Date.now() - start, failureCategory: "invalid_url" },
15166
+ isError: true
15167
+ };
15168
+ }
15169
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
15170
+ await discardResponseBody(response);
15171
+ return {
15172
+ content: [{ type: "text", text: `web_fetch only follows http(s) redirects: ${next.toString()} (from ${currentUrl})` }],
15173
+ details: { url, sourceUrl: url, finalUrl: next.toString(), redirectChain: [...redirectChain, next.toString()], durationMs: Date.now() - start, failureCategory: "invalid_url" },
15174
+ isError: true
15175
+ };
15176
+ }
15177
+ if (!allowPrivate && isPrivateHostname(next.hostname)) {
15178
+ await discardResponseBody(response);
15179
+ return {
15180
+ content: [{ type: "text", text: `Blocked redirect to private or local URL: ${next.toString()} (while fetching ${url})` }],
15181
+ details: { url, sourceUrl: url, finalUrl: next.toString(), redirectChain: [...redirectChain, next.toString()], durationMs: Date.now() - start, failureCategory: "blocked_private_host" },
15182
+ isError: true
15183
+ };
15184
+ }
15185
+ redirectsFollowed += 1;
15186
+ if (redirectsFollowed > maxRedirects) {
15187
+ await discardResponseBody(response);
15188
+ return {
15189
+ content: [{ type: "text", text: `web_fetch stopped after ${maxRedirects} redirects while fetching ${url} (last hop: ${currentUrl} -> ${location})` }],
15190
+ details: { url, sourceUrl: url, finalUrl: currentUrl, redirectChain, durationMs: Date.now() - start, failureCategory: "too_many_redirects" },
15191
+ isError: true
15192
+ };
15193
+ }
15194
+ await discardResponseBody(response);
15195
+ currentUrl = next.toString();
15196
+ redirectChain.push(currentUrl);
15197
+ response = await doFetch(currentUrl);
15198
+ }
15199
+ const finalUrl = response.url || currentUrl;
15200
+ let transportFinalHostname;
15201
+ try {
15202
+ transportFinalHostname = new URL(finalUrl).hostname;
15203
+ } catch {
15204
+ transportFinalHostname = void 0;
15205
+ }
15206
+ if (!allowPrivate && transportFinalHostname && isPrivateHostname(transportFinalHostname)) {
15207
+ await discardResponseBody(response);
15208
+ return {
15209
+ content: [{ type: "text", text: `Blocked: the fetch transport followed a redirect to private or local URL ${finalUrl} (while fetching ${url})` }],
15210
+ details: { url, sourceUrl: url, finalUrl, redirectChain, durationMs: Date.now() - start, failureCategory: "blocked_private_host" },
15211
+ isError: true
15212
+ };
15213
+ }
14910
15214
  const contentType = response.headers.get("content-type") || "";
14911
15215
  const maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES2;
14912
15216
  const bodyResult = await readResponseBody(response, maxBytes);
14913
15217
  if (bodyResult.exceededLimit) {
14914
15218
  return {
14915
15219
  content: [{ type: "text", text: `Fetched content is too large (${bodyResult.observedBytes} bytes; max ${maxBytes}).` }],
14916
- details: { url, sourceUrl: url, finalUrl: response.url || url, status: response.status, contentType, bytes: bodyResult.observedBytes, truncated: true, truncationReason: "max_bytes", durationMs: Date.now() - start, failureCategory: "too_large" },
15220
+ details: { url, sourceUrl: url, finalUrl, status: response.status, contentType, bytes: bodyResult.observedBytes, truncated: true, truncationReason: "max_bytes", durationMs: Date.now() - start, failureCategory: "too_large" },
14917
15221
  isError: true
14918
15222
  };
14919
15223
  }
@@ -14921,7 +15225,7 @@ function createSharedWebFetchTool(deps = {}) {
14921
15225
  if (!response.ok) {
14922
15226
  return {
14923
15227
  content: [{ type: "text", text: `web_fetch failed with HTTP ${response.status} for ${url}` }],
14924
- details: { url, sourceUrl: url, finalUrl: response.url || url, status: response.status, contentType, bytes: bodyResult.observedBytes, durationMs: Date.now() - start, failureCategory: "http_error" },
15228
+ details: { url, sourceUrl: url, finalUrl, status: response.status, contentType, bytes: bodyResult.observedBytes, redirectChain, durationMs: Date.now() - start, failureCategory: "http_error" },
14925
15229
  isError: true
14926
15230
  };
14927
15231
  }
@@ -14939,7 +15243,7 @@ function createSharedWebFetchTool(deps = {}) {
14939
15243
  details: {
14940
15244
  url,
14941
15245
  sourceUrl: url,
14942
- finalUrl: response.url || url,
15246
+ finalUrl,
14943
15247
  status: response.status,
14944
15248
  contentType,
14945
15249
  charset: decoded.charset,
@@ -14958,15 +15262,14 @@ function createSharedWebFetchTool(deps = {}) {
14958
15262
  if (!text) {
14959
15263
  return {
14960
15264
  content: [{ type: "text", text: `web_fetch could not extract readable text from ${url}` }],
14961
- details: { url, sourceUrl: url, finalUrl: response.url || url, status: response.status, contentType, charset: decoded.charset, decodeWarning: decoded.decodeWarning, bytes: bodyResult.observedBytes, extractionMethod, durationMs: Date.now() - start, failureCategory: "unsupported_content" },
15265
+ details: { url, sourceUrl: url, finalUrl, status: response.status, contentType, charset: decoded.charset, decodeWarning: decoded.decodeWarning, bytes: bodyResult.observedBytes, redirectChain, extractionMethod, durationMs: Date.now() - start, failureCategory: "unsupported_content" },
14962
15266
  isError: true
14963
15267
  };
14964
15268
  }
14965
15269
  const maxChars = deps.maxChars ?? DEFAULT_MAX_CHARS;
14966
15270
  const truncated = text.length > maxChars;
14967
15271
  if (truncated) text = text.slice(0, maxChars) + "\n\n[web_fetch content truncated]";
14968
- const extraction = prompt?.trim() ? await applyPromptWithModel(deps, prompt.trim(), response.url || url, text, timeout.signal) : { text, model: modelLabel2(deps.getModel?.()) };
14969
- const finalUrl = response.url || url;
15272
+ const extraction = prompt?.trim() ? await applyPromptWithModel(deps, prompt.trim(), finalUrl, text, timeout.signal) : { text, model: modelLabel2(deps.getModel?.()) };
14970
15273
  return {
14971
15274
  content: [{ type: "text", text: extraction.text }],
14972
15275
  details: {
@@ -14980,7 +15283,7 @@ function createSharedWebFetchTool(deps = {}) {
14980
15283
  bytes: bodyResult.observedBytes,
14981
15284
  title,
14982
15285
  truncated,
14983
- redirectChain: finalUrl !== url ? [url, finalUrl] : [url],
15286
+ redirectChain,
14984
15287
  extractionMethod: prompt?.trim() ? "model_extraction" : extractionMethod,
14985
15288
  readabilityScore: htmlExtraction?.readabilityScore,
14986
15289
  citationAnchors: buildCitationAnchors(finalUrl, title, text),