zidane 5.11.0 → 5.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +5 -5
  2. package/dist/{agent-B7ilLoh8.d.ts → agent-D0W9yClt.d.ts} +52 -24
  3. package/dist/agent-D0W9yClt.d.ts.map +1 -0
  4. package/dist/chat/pure.d.ts +3 -3
  5. package/dist/chat.d.ts +7 -7
  6. package/dist/chat.js +2 -2
  7. package/dist/contexts/e2b.d.ts +1 -1
  8. package/dist/eval.d.ts +1 -1
  9. package/dist/eval.js +3 -3
  10. package/dist/eval.js.map +1 -1
  11. package/dist/{headless-CYHU4ZB-.js → headless-Bb5gU8AR.js} +5 -5
  12. package/dist/{headless-CYHU4ZB-.js.map → headless-Bb5gU8AR.js.map} +1 -1
  13. package/dist/headless.d.ts +1 -1
  14. package/dist/headless.js +1 -1
  15. package/dist/{index-CsRdmSh6.d.ts → index-D60tX5XC.d.ts} +2 -2
  16. package/dist/{index-CsRdmSh6.d.ts.map → index-D60tX5XC.d.ts.map} +1 -1
  17. package/dist/{index-C-zEbeFI.d.ts → index-DZR99FD4.d.ts} +19 -12
  18. package/dist/{index-C-zEbeFI.d.ts.map → index-DZR99FD4.d.ts.map} +1 -1
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +8 -8
  21. package/dist/{logger-Bd4Wn9Hc.d.ts → logger-n4LsLISE.d.ts} +2 -2
  22. package/dist/{logger-Bd4Wn9Hc.d.ts.map → logger-n4LsLISE.d.ts.map} +1 -1
  23. package/dist/{login-DVLJzf-S.js → login-BHhOdTp9.js} +3 -3
  24. package/dist/{login-DVLJzf-S.js.map → login-BHhOdTp9.js.map} +1 -1
  25. package/dist/{mcp-DPneQbw2.js → mcp-Cy9mgCcr.js} +2 -2
  26. package/dist/mcp-Cy9mgCcr.js.map +1 -0
  27. package/dist/mcp.d.ts +1 -1
  28. package/dist/mcp.js +1 -1
  29. package/dist/{messages-DJ7xdoUg.js → messages-RPKrEPvH.js} +2 -2
  30. package/dist/messages-RPKrEPvH.js.map +1 -0
  31. package/dist/output/stream-json.d.ts +2 -2
  32. package/dist/output/stream-json.js +1 -1
  33. package/dist/output/terminal.d.ts +2 -2
  34. package/dist/{presets-Q0yUDRA6.js → presets-D5ibZTml.js} +2 -2
  35. package/dist/{presets-Q0yUDRA6.js.map → presets-D5ibZTml.js.map} +1 -1
  36. package/dist/presets.d.ts +2 -2
  37. package/dist/presets.js +1 -1
  38. package/dist/{providers-BPdi7cUU.js → providers-C2cxujp_.js} +14 -11
  39. package/dist/providers-C2cxujp_.js.map +1 -0
  40. package/dist/providers.d.ts +1 -1
  41. package/dist/providers.js +2 -2
  42. package/dist/restate.d.ts +1 -1
  43. package/dist/session/sqlite.d.ts +1 -1
  44. package/dist/{session-D4GckETs.js → session-Do_TQV7c.js} +3 -3
  45. package/dist/session-Do_TQV7c.js.map +1 -0
  46. package/dist/session.d.ts +1 -1
  47. package/dist/session.js +2 -2
  48. package/dist/skills.d.ts +2 -2
  49. package/dist/{tool-formatters-DXO8rGut.d.ts → tool-formatters-RT5-gyE2.d.ts} +2 -2
  50. package/dist/{tool-formatters-DXO8rGut.d.ts.map → tool-formatters-RT5-gyE2.d.ts.map} +1 -1
  51. package/dist/tools/fetch-url.d.ts +1 -1
  52. package/dist/tools/web-search.d.ts +1 -1
  53. package/dist/{tools-DqNkGwfd.js → tools-ZHKOh44k.js} +266 -54
  54. package/dist/tools-ZHKOh44k.js.map +1 -0
  55. package/dist/tools.d.ts +2 -2
  56. package/dist/tools.js +1 -1
  57. package/dist/{transcript-anchors-C3tVPR5n.js → transcript-anchors-0zzqcSm5.js} +111 -9
  58. package/dist/transcript-anchors-0zzqcSm5.js.map +1 -0
  59. package/dist/{transcript-anchors-p-ZsBSEF.d.ts → transcript-anchors-B4FxkG-8.d.ts} +4 -4
  60. package/dist/{transcript-anchors-p-ZsBSEF.d.ts.map → transcript-anchors-B4FxkG-8.d.ts.map} +1 -1
  61. package/dist/tui.d.ts +3 -3
  62. package/dist/tui.d.ts.map +1 -1
  63. package/dist/tui.js +24 -8
  64. package/dist/tui.js.map +1 -1
  65. package/dist/{turn-operations-DD1D5VFx.d.ts → turn-operations-CoRj3mYZ.d.ts} +3 -3
  66. package/dist/{turn-operations-DD1D5VFx.d.ts.map → turn-operations-CoRj3mYZ.d.ts.map} +1 -1
  67. package/dist/types-BiobHM1D.js.map +1 -1
  68. package/dist/types.d.ts +2 -2
  69. package/docs/ARCHITECTURE.md +1 -1
  70. package/docs/CHAT.md +3 -3
  71. package/docs/SKILL.md +3 -3
  72. package/package.json +1 -1
  73. package/dist/agent-B7ilLoh8.d.ts.map +0 -1
  74. package/dist/mcp-DPneQbw2.js.map +0 -1
  75. package/dist/messages-DJ7xdoUg.js.map +0 -1
  76. package/dist/providers-BPdi7cUU.js.map +0 -1
  77. package/dist/session-D4GckETs.js.map +0 -1
  78. package/dist/tools-DqNkGwfd.js.map +0 -1
  79. package/dist/transcript-anchors-C3tVPR5n.js.map +0 -1
@@ -1,16 +1,16 @@
1
1
  import { r as utf8ByteLength } from "./utils-ngQzYzZD.js";
2
2
  import { t as buildContextBreakdown } from "./context-breakdown-kO-pDsay.js";
3
3
  import { a as formatTaskStatus, i as formatDuration, o as formatTaskSummary, s as previewLine } from "./format-BNOXpl-1.js";
4
- import { a as createCursorOAuthProvider, c as baseten, d as anthropic, g as FAST_MODE_OPTIONS, h as ANTHROPIC_EXTRA_MODELS, m as writeFileAtomicAsync, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter, u as arcee } from "./providers-BPdi7cUU.js";
4
+ import { a as createCursorOAuthProvider, c as baseten, d as anthropic, g as FAST_MODE_OPTIONS, h as ANTHROPIC_EXTRA_MODELS, m as writeFileAtomicAsync, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter, u as arcee } from "./providers-C2cxujp_.js";
5
5
  import { i as AgentProviderError, l as errorMessage, n as AgentBudgetExceededError, o as AgentToolPairingError, p as toTypedError, t as AgentAbortedError } from "./errors-DkR6GPJw.js";
6
- import { A as renderSystemForWire, D as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, d as remintDuplicateToolCallIds, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-DJ7xdoUg.js";
6
+ import { A as renderSystemForWire, D as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, d as remintDuplicateToolCallIds, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-RPKrEPvH.js";
7
7
  import { a as toolResultToText, i as toolOutputByteLength, n as documentBlockMarker, r as toolOutputBudgetByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-BiobHM1D.js";
8
8
  import { t as reconcileImageMediaType } from "./image-sniff-B7uFSNO1.js";
9
9
  import { r as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-DglWSzmR.js";
10
10
  import { i as styleReplacementForVia, n as resolveOldString, r as stripLineNumberPrefixes, t as describeVia } from "./edit-utils-EGosADZq.js";
11
11
  import { a as markReadStateElided, n as getToolDedupState, o as readStateKey, r as hashContent, s as resolveReadStateMap } from "./read-state-BFqpQRc5.js";
12
12
  import { S as escapeXml, d as buildCatalog, n as stripShellInterpolations, p as installAllowedToolsGate, r as resolveSkills, t as interpolateShellCommands, v as validateResourcePathReal, x as createSkillActivationState } from "./interpolate-CTfr0GdR.js";
13
- import { n as connectMcpServers } from "./mcp-DPneQbw2.js";
13
+ import { n as connectMcpServers } from "./mcp-Cy9mgCcr.js";
14
14
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
15
15
  import { n as shellQuote, t as alwaysQuote } from "./shell-quote-BmnhZmdM.js";
16
16
  import { isAbsolute, join, resolve } from "node:path";
@@ -83,8 +83,8 @@ function toCanonicalName(wire, maps) {
83
83
  * Why: SDK consumers and tool descriptions across the ecosystem
84
84
  * inconsistently reference one form or the other (Anthropic's docs use
85
85
  * double, zidane uses single). A Claude-Code-trained model emitting
86
- * `mcp__supabase__apply_migration` against a server zidane registered as
87
- * `mcp_supabase_apply_migration` would otherwise trip the `tool:unknown`
86
+ * `mcp__acme__apply_migration` against a server zidane registered as
87
+ * `mcp_acme_apply_migration` would otherwise trip the `tool:unknown`
88
88
  * path and waste a turn on the correction.
89
89
  *
90
90
  * Idempotent: a canonical that already has a double-underscore alias
@@ -1404,6 +1404,14 @@ function installDedupToolsGate(hooks, getDedupTools, getSession) {
1404
1404
  */
1405
1405
  const PERSISTENCE_PREVIEW_BYTES = 2 * 1024;
1406
1406
  /**
1407
+ * Preview size for persist-on-elide stubs. Much smaller than
1408
+ * {@link PERSISTENCE_PREVIEW_BYTES}: this content is being COMPACTED OUT (it's
1409
+ * old), so the stub only needs a hint plus the recovery path, not a full
1410
+ * preview. Keeping it small stops the elided region from ballooning ~40× over
1411
+ * the lossy stub it replaces while still being recoverable by reading `path`.
1412
+ */
1413
+ const ELIDE_PERSIST_PREVIEW_BYTES = 256;
1414
+ /**
1407
1415
  * Byte-stable prefix every {@link buildPersistedStub} output starts with.
1408
1416
  * Exported so wire-level passes (tail compaction, future stale-output
1409
1417
  * elision) can recognize a persisted stub and preserve its path attribute
@@ -1633,7 +1641,7 @@ async function enforcePersistDirCap(persistDir, maxBytes, opts = {}) {
1633
1641
  * surface.
1634
1642
  */
1635
1643
  function buildPersistedStub(input) {
1636
- const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, PERSISTENCE_PREVIEW_BYTES);
1644
+ const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, input.previewBytes ?? 2048);
1637
1645
  const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
1638
1646
  return [
1639
1647
  `${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
@@ -1663,6 +1671,68 @@ async function cleanupPersistedSession(persistRoot) {
1663
1671
  }
1664
1672
  }
1665
1673
  /**
1674
+ * Persist-on-elide: belatedly write tool results that tail compaction is
1675
+ * about to replace with the lossy `[…elided…]` stub, swapping in a
1676
+ * recoverable `<persisted-output … path=…>` stub instead. This is the lazy
1677
+ * counterpart to {@link maybePersistToolResult}'s eager (emit-time)
1678
+ * persistence — it only fires when a result is actually being compacted out,
1679
+ * so recent results stay full inline while OLD ones become recoverable rather
1680
+ * than destroyed. Closes the context-loss gap for non-read results between
1681
+ * the lossy stub and re-runnable side effects: after this, a compacted result
1682
+ * is always retrievable by `read_file`-ing its path.
1683
+ *
1684
+ * `items` are the {@link TailCompactionResult.persistableElided} entries (the
1685
+ * caller has already filtered out reads — which recover via the read-state
1686
+ * dedup re-serve — and results too small to be worth persisting). Each
1687
+ * `output` is the ORIGINAL content captured before the lossy stub replaced it.
1688
+ *
1689
+ * Idempotency: blobs are immutable (a tool result never changes), so we write
1690
+ * `<callId>.txt` only when it's absent and otherwise just rebuild the
1691
+ * byte-stable stub. The session keeps the original content; only the wire copy
1692
+ * carries the stub — consistent with compaction being wire-only. Returns the
1693
+ * messages with the lossy stubs swapped; unchanged when nothing was persisted.
1694
+ */
1695
+ async function persistElidedToolResults(messages, items, opts) {
1696
+ if (items.length === 0 || !isAbsolute(opts.persistDir)) return messages;
1697
+ const stubByCallId = /* @__PURE__ */ new Map();
1698
+ let wroteAny = false;
1699
+ for (const item of items) {
1700
+ if (!SAFE_CALL_ID.test(item.callId) || item.callId.includes("..")) continue;
1701
+ const persistedPath = join(opts.persistDir, `${item.callId}.txt`);
1702
+ let exists = false;
1703
+ try {
1704
+ exists = (await stat(persistedPath)).isFile();
1705
+ } catch {
1706
+ exists = false;
1707
+ }
1708
+ if (!exists) try {
1709
+ await writeAtomic(persistedPath, item.output);
1710
+ wroteAny = true;
1711
+ } catch {
1712
+ continue;
1713
+ }
1714
+ stubByCallId.set(item.callId, buildPersistedStub({
1715
+ toolName: item.toolName,
1716
+ originalBytes: toolOutputByteLength(item.output),
1717
+ persistedPath,
1718
+ output: item.output,
1719
+ previewBytes: ELIDE_PERSIST_PREVIEW_BYTES
1720
+ }));
1721
+ }
1722
+ if (wroteAny && typeof opts.maxBytes === "number" && Number.isFinite(opts.maxBytes) && opts.maxBytes > 0) await enforcePersistDirCap(opts.persistDir, opts.maxBytes);
1723
+ if (stubByCallId.size === 0) return messages;
1724
+ return messages.map((msg) => {
1725
+ if (!msg.content.some((b) => b.type === "tool_result" && stubByCallId.has(b.callId))) return msg;
1726
+ return {
1727
+ ...msg,
1728
+ content: msg.content.map((b) => b.type === "tool_result" && stubByCallId.has(b.callId) ? {
1729
+ ...b,
1730
+ output: stubByCallId.get(b.callId)
1731
+ } : b)
1732
+ };
1733
+ });
1734
+ }
1735
+ /**
1666
1736
  * Write-then-rename for atomicity — delegates to the shared
1667
1737
  * {@link writeFileAtomicAsync} helper (unique pid+counter tmp suffix,
1668
1738
  * fsync before rename, tmp cleanup on failure). Creates the parent
@@ -2276,32 +2346,56 @@ function applyCompactSummaryCutoff(turns) {
2276
2346
  * alone — those are consumer-controlled and the consumer is responsible for
2277
2347
  * matching prompt parts to the provider's capabilities.
2278
2348
  */
2279
- const COMPACTION_STUB = "[…elided by client-side tail compaction; ask the user or re-run the tool to retrieve.]";
2349
+ const COMPACTION_STUB = "[…elided: older tool output trimmed to fit the context budget.]";
2280
2350
  const COMPACTION_STUB_BYTES = toolOutputByteLength(COMPACTION_STUB);
2281
- /**
2282
- * Tail-compaction for non-Anthropic providers: when the cumulative byte size
2283
- * of `tool_result` content across the wire-level message list exceeds
2284
- * `threshold`, replace older `tool_result` outputs with a short stub. The
2285
- * newest `keepTurns` messages (user/assistant alike) are left untouched so
2286
- * the model retains the freshest tool context.
2287
- *
2288
- * Only `tool_result` blocks are touched — text and image blocks pass through
2289
- * unchanged. Mutates a shallow-cloned message array; original `messages` is
2290
- * not modified.
2291
- *
2292
- * For Anthropic users, prefer the server-side `context-management-2025-06-27`
2293
- * beta (token-accurate, no client-side approximation). This function is the
2294
- * client-side fallback for OpenAI-compatible / OpenRouter / Cerebras runs
2295
- * against OSS models that lack a server-side equivalent.
2296
- */
2297
- function applyTailCompaction(messages, threshold, keepTurns) {
2298
- if (messages.length === 0) return messages;
2351
+ const COMPACT_CHUNK_TURNS = 8;
2352
+ function applyTailCompaction(messages, threshold, keepTurns, options) {
2353
+ const elidedReadPaths = [];
2354
+ const persistableElided = [];
2355
+ const toolNameByCallId = options?.toolNameByCallId;
2356
+ const persistElideMinBytes = options?.persistElideMinBytes ?? 0;
2357
+ if (messages.length === 0) return {
2358
+ messages,
2359
+ elidedReadPaths,
2360
+ persistableElided
2361
+ };
2299
2362
  let totalBytes = 0;
2300
2363
  for (const msg of messages) for (const block of msg.content) if (block.type === "tool_result") totalBytes += toolOutputByteLength(block.output);
2301
- if (totalBytes <= threshold) return messages;
2364
+ if (totalBytes <= threshold) return {
2365
+ messages,
2366
+ elidedReadPaths,
2367
+ persistableElided
2368
+ };
2302
2369
  const keep = Math.max(0, keepTurns);
2303
- const cutoff = messages.length - keep;
2304
- if (cutoff <= 0) return messages;
2370
+ const floorCutoff = messages.length - keep;
2371
+ if (floorCutoff <= 0) return {
2372
+ messages,
2373
+ elidedReadPaths,
2374
+ persistableElided
2375
+ };
2376
+ let keptBytes = 0;
2377
+ let budgetCutoff = floorCutoff;
2378
+ for (let i = messages.length - 1; i >= 0; i--) {
2379
+ for (const block of messages[i].content) if (block.type === "tool_result") keptBytes += toolOutputByteLength(block.output);
2380
+ if (keptBytes > threshold) {
2381
+ budgetCutoff = i + 1;
2382
+ break;
2383
+ }
2384
+ }
2385
+ const rawCutoff = Math.min(budgetCutoff, floorCutoff);
2386
+ if (rawCutoff <= 0) return {
2387
+ messages,
2388
+ elidedReadPaths,
2389
+ persistableElided
2390
+ };
2391
+ const chunk = Math.max(1, options?.chunkTurns ?? 1);
2392
+ const cutoff = Math.floor(rawCutoff / chunk) * chunk;
2393
+ if (cutoff <= 0) return {
2394
+ messages,
2395
+ elidedReadPaths,
2396
+ persistableElided
2397
+ };
2398
+ const readPathByCallId = options?.readPathByCallId;
2305
2399
  let changed = false;
2306
2400
  const out = messages.slice();
2307
2401
  for (let i = 0; i < cutoff; i++) {
@@ -2309,10 +2403,18 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2309
2403
  let msgChanged = false;
2310
2404
  const newContent = msg.content.map((block) => {
2311
2405
  if (block.type !== "tool_result") return block;
2312
- if (toolOutputByteLength(block.output) <= COMPACTION_STUB_BYTES) return block;
2406
+ const existingBytes = toolOutputByteLength(block.output);
2407
+ if (existingBytes <= COMPACTION_STUB_BYTES) return block;
2313
2408
  if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
2314
2409
  msgChanged = true;
2315
2410
  changed = true;
2411
+ const readPath = readPathByCallId?.get(block.callId);
2412
+ if (readPath !== void 0) elidedReadPaths.push(readPath);
2413
+ else if (persistElideMinBytes > 0 && typeof block.output === "string" && existingBytes > persistElideMinBytes) persistableElided.push({
2414
+ callId: block.callId,
2415
+ toolName: toolNameByCallId?.get(block.callId) ?? "tool",
2416
+ output: block.output
2417
+ });
2316
2418
  return {
2317
2419
  ...block,
2318
2420
  output: COMPACTION_STUB
@@ -2323,7 +2425,34 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2323
2425
  content: newContent
2324
2426
  };
2325
2427
  }
2326
- return changed ? out : messages;
2428
+ return {
2429
+ messages: changed ? out : messages,
2430
+ elidedReadPaths,
2431
+ persistableElided
2432
+ };
2433
+ }
2434
+ /**
2435
+ * Build a call_id → file path map for `read_file` tool calls in `messages`.
2436
+ * Tail compaction uses it to learn which stubbed `tool_result`s were file
2437
+ * reads, so their read-state dedup can be invalidated. Resolve this from the
2438
+ * canonical, pre-alias history — call_ids are stable across the alias rewrite,
2439
+ * so the map still keys the wire-level `tool_result` blocks.
2440
+ */
2441
+ function collectCallMaps(messages) {
2442
+ const readPathByCallId = /* @__PURE__ */ new Map();
2443
+ const toolNameByCallId = /* @__PURE__ */ new Map();
2444
+ for (const msg of messages) for (const block of msg.content) {
2445
+ if (block.type !== "tool_call") continue;
2446
+ toolNameByCallId.set(block.id, block.name);
2447
+ if (block.name === "read_file") {
2448
+ const path = block.input.path;
2449
+ if (typeof path === "string" && path.length > 0) readPathByCallId.set(block.id, path);
2450
+ }
2451
+ }
2452
+ return {
2453
+ readPathByCallId,
2454
+ toolNameByCallId
2455
+ };
2327
2456
  }
2328
2457
  /**
2329
2458
  * Replace `read_file` `tool_result` blocks with a short stub when a later
@@ -2342,7 +2471,7 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2342
2471
  * Pure function, exported for tests and so downstream tooling can preview
2343
2472
  * elision without spinning up the loop.
2344
2473
  */
2345
- const STALE_READ_STUB = "[…elided: file edited later in this run; re-read if still needed.]";
2474
+ const STALE_READ_STUB = "[…elided: this file was edited later in the run, so this earlier read is stale.]";
2346
2475
  function applyStaleReadElision(messages) {
2347
2476
  if (messages.length === 0) return {
2348
2477
  messages,
@@ -2398,7 +2527,7 @@ function applyStaleReadElision(messages) {
2398
2527
  let msgChanged = false;
2399
2528
  const newContent = msg.content.map((block) => {
2400
2529
  if (block.type !== "tool_result" || !staleCallIds.has(block.callId)) return block;
2401
- if (block.output === "[…elided: file edited later in this run; re-read if still needed.]") return block;
2530
+ if (block.output === "[…elided: this file was edited later in the run, so this earlier read is stale.]") return block;
2402
2531
  if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
2403
2532
  msgChanged = true;
2404
2533
  changed = true;
@@ -3005,7 +3134,19 @@ async function executeTurn(ctx, turn, priorUsage) {
3005
3134
  if (ctx.compactStrategy === "tail") {
3006
3135
  const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
3007
3136
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
3008
- sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
3137
+ const { readPathByCallId, toolNameByCallId } = collectCallMaps(canonicalMessages);
3138
+ const compacted = applyTailCompaction(sanitizedMessages, threshold, keep, {
3139
+ readPathByCallId,
3140
+ toolNameByCallId,
3141
+ chunkTurns: COMPACT_CHUNK_TURNS,
3142
+ persistElideMinBytes: ctx.persistDir ? PERSISTENCE_PREVIEW_BYTES : 0
3143
+ });
3144
+ sanitizedMessages = compacted.messages;
3145
+ markReadStateForElidedPaths(ctx, ctx.handle.cwd, compacted.elidedReadPaths);
3146
+ if (ctx.persistDir && compacted.persistableElided.length > 0) sanitizedMessages = await persistElidedToolResults(sanitizedMessages, compacted.persistableElided, {
3147
+ persistDir: ctx.persistDir,
3148
+ maxBytes: ctx.persistMaxBytes
3149
+ });
3009
3150
  } else if (typeof ctx.compactStrategy === "function") {
3010
3151
  const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
3011
3152
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
@@ -4444,13 +4585,43 @@ function buildPromptMessage(provider, parts) {
4444
4585
  const DEFAULT_BLOCK_THRESHOLD = 4;
4445
4586
  const DEFAULT_ABORT_THRESHOLD = 8;
4446
4587
  /**
4447
- * Default tracked-tool predicate: `shell` exactly, plus any tool whose
4448
- * canonical name ends in `_execute_sql` (the MCP SQL-runner shape,
4449
- * `mcp_<server>_execute_sql`). Both are auto-approved, side-effect-light
4450
- * retry magnetsthe exact loop shapes the abort escalation exists for.
4588
+ * Tools watched in sliding-window mode even without an explicit `windowSize`.
4589
+ * A re-read loop cycles through a handful of slices of the same file (offset
4590
+ * 1 52 → 199 → 1 …), so consecutive-streak counting — which resets on every
4591
+ * changed offsetnever trips. Exact-payload keying in a window catches the
4592
+ * revisited slice while leaving linear chunked reads (distinct slices, never
4593
+ * repeated) untouched. Other tracked tools (shell, SQL) keep streak mode.
4594
+ */
4595
+ const READ_LOOP_TOOLS = new Set(["read_file"]);
4596
+ /**
4597
+ * Default window for {@link READ_LOOP_TOOLS} when the consumer sets no global
4598
+ * `windowSize`. Sized so a short revisit cycle still reaches the abort
4599
+ * threshold: a 3-slice cycle fills a 24-call window with 8 occurrences of each
4600
+ * slice.
4601
+ */
4602
+ const DEFAULT_READ_LOOP_WINDOW = 24;
4603
+ /**
4604
+ * Universal repeat ceiling: the last-resort loop breaker. Independent of the
4605
+ * tracked-tool set, it counts exact `(tool, args)` repeats across EVERY tool —
4606
+ * catching loops the per-tool streak/window misses (a `grep`/`glob`/`edit`
4607
+ * cycle, or a model hammering a tool that keeps getting blocked). Set high
4608
+ * enough that legitimate identical repeats (a handful of retries) don't trip,
4609
+ * but a genuine spin does.
4610
+ */
4611
+ const DEFAULT_GLOBAL_CEILING = 12;
4612
+ /** Sliding window (per run+tool) the global ceiling counts repeats within. */
4613
+ const DEFAULT_GLOBAL_CEILING_WINDOW = 40;
4614
+ /**
4615
+ * Default tracked-tool predicate: the built-in `shell` and `read_file` tools.
4616
+ * `shell` is an auto-approved, side-effect-light retry magnet; `read_file` is
4617
+ * the re-read loop magnet — a compaction/dedup interaction can strand the
4618
+ * model re-reading one file forever (see {@link READ_LOOP_TOOLS}). Both are the
4619
+ * loop shapes the abort escalation exists for. Consumers that want to track
4620
+ * additional tools (e.g. an MCP query runner) pass them via
4621
+ * {@link RepeatGuardConfig.tools}.
4451
4622
  */
4452
4623
  function defaultRepeatGuardTracked(name) {
4453
- return name === "shell" || name.endsWith("_execute_sql");
4624
+ return name === "shell" || name === "read_file";
4454
4625
  }
4455
4626
  function compileMatchers(tools) {
4456
4627
  if (tools === void 0) return defaultRepeatGuardTracked;
@@ -4496,10 +4667,12 @@ function normalizeShellCommand(command) {
4496
4667
  }
4497
4668
  /**
4498
4669
  * Built-in normalizer used when the consumer doesn't supply one. Shell-shaped
4499
- * tools get {@link normalizeShellCommand}; SQL-runner tools get a whitespace-
4500
- * collapsed query; everything else falls back to a stable JSON encoding.
4501
- * Returns `undefined` when there's no meaningful payload to key on (so the
4502
- * call is excluded from streak tracking rather than keyed on `'{}'`).
4670
+ * tools get {@link normalizeShellCommand}; everything else falls back to a
4671
+ * stable JSON encoding. Returns `undefined` when there's no meaningful payload
4672
+ * to key on (so the call is excluded from streak tracking rather than keyed on
4673
+ * `'{}'`). Consumers needing semantic keying for their own tools (e.g.
4674
+ * collapsing a query's whitespace) supply a normalizer via
4675
+ * {@link RepeatGuardConfig.normalize}.
4503
4676
  */
4504
4677
  function defaultRepeatGuardNormalize(name, input) {
4505
4678
  if (name === "shell") {
@@ -4507,11 +4680,6 @@ function defaultRepeatGuardNormalize(name, input) {
4507
4680
  if (typeof cmd !== "string" || cmd.trim().length === 0) return void 0;
4508
4681
  return `shell:${normalizeShellCommand(cmd)}`;
4509
4682
  }
4510
- if (name.endsWith("_execute_sql")) {
4511
- const q = input.query ?? input.sql ?? input.statement;
4512
- if (typeof q !== "string" || q.trim().length === 0) return void 0;
4513
- return `sql:${q.replace(/\s+/g, " ").trim().toLowerCase()}`;
4514
- }
4515
4683
  try {
4516
4684
  return `json:${stableStringify(input)}`;
4517
4685
  } catch {
@@ -4553,6 +4721,7 @@ function formatBlockReason(reason, name, count) {
4553
4721
  function installRepeatGuard(hooks, getConfig, abort) {
4554
4722
  const streaks = /* @__PURE__ */ new Map();
4555
4723
  const windows = /* @__PURE__ */ new Map();
4724
+ const globalWindows = /* @__PURE__ */ new Map();
4556
4725
  function streakKey(runId, name) {
4557
4726
  return `${runId ?? "-"}::${name}`;
4558
4727
  }
@@ -4565,20 +4734,60 @@ function installRepeatGuard(hooks, getConfig, abort) {
4565
4734
  const reachable = Number.isFinite(abortThreshold) ? abortThreshold : blockThreshold;
4566
4735
  windowSize = Math.max(Math.floor(config.windowSize), reachable);
4567
4736
  }
4737
+ const rawGlobalCeiling = typeof config.globalCeiling === "number" && Number.isFinite(config.globalCeiling) ? Math.floor(config.globalCeiling) : DEFAULT_GLOBAL_CEILING;
4738
+ const globalCeiling = rawGlobalCeiling > 0 && Number.isFinite(abortThreshold) ? Math.max(2, rawGlobalCeiling, abortThreshold) : 0;
4739
+ const globalWindow = Math.max(globalCeiling, typeof config.globalCeilingWindow === "number" && Number.isFinite(config.globalCeilingWindow) ? Math.floor(config.globalCeilingWindow) : DEFAULT_GLOBAL_CEILING_WINDOW);
4740
+ const isGloballyExcluded = config.globalCeilingExclude === void 0 ? () => false : compileMatchers(config.globalCeilingExclude);
4568
4741
  return {
4569
4742
  isTracked: compileMatchers(config.tools),
4570
4743
  normalize: config.normalize ?? defaultRepeatGuardNormalize,
4571
4744
  blockThreshold,
4572
4745
  abortThreshold,
4573
4746
  countBlockedCalls: config.countBlockedCalls === true,
4574
- windowSize
4747
+ windowSize,
4748
+ globalCeiling,
4749
+ globalWindow,
4750
+ isGloballyExcluded
4575
4751
  };
4576
4752
  }
4577
4753
  async function gateHandler(ctx) {
4578
4754
  if (ctx.result !== void 0) return;
4579
4755
  const config = getConfig();
4580
4756
  if (!config) return;
4581
- const { isTracked, normalize, blockThreshold, abortThreshold, countBlockedCalls, windowSize } = resolve(config);
4757
+ const resolved = resolve(config);
4758
+ const { isTracked, normalize, blockThreshold, abortThreshold, countBlockedCalls, windowSize } = resolved;
4759
+ if (resolved.globalCeiling > 0 && !resolved.isGloballyExcluded(ctx.name)) {
4760
+ let gkey;
4761
+ try {
4762
+ gkey = `${ctx.name}\u0000${stableStringify(ctx.input)}`;
4763
+ } catch {
4764
+ gkey = "";
4765
+ }
4766
+ if (gkey.length > 0) {
4767
+ const gslot = streakKey(ctx.runId, ctx.name);
4768
+ const recent = globalWindows.get(gslot) ?? [];
4769
+ recent.push(gkey);
4770
+ if (recent.length > resolved.globalWindow) recent.shift();
4771
+ globalWindows.set(gslot, recent);
4772
+ let gcount = 0;
4773
+ for (const k of recent) if (k === gkey) gcount++;
4774
+ if (gcount >= resolved.globalCeiling) {
4775
+ if (!ctx.block) {
4776
+ ctx.block = true;
4777
+ ctx.reason = formatBlockReason(config.blockReason, ctx.name, gcount);
4778
+ }
4779
+ await hooks.callHook("repeat-guard:exceeded", {
4780
+ tool: ctx.name,
4781
+ count: gcount,
4782
+ threshold: resolved.globalCeiling,
4783
+ turnId: ctx.turnId,
4784
+ action: "abort"
4785
+ });
4786
+ abort();
4787
+ return;
4788
+ }
4789
+ }
4790
+ }
4582
4791
  if (!isTracked(ctx.name)) return;
4583
4792
  if (ctx.block && !countBlockedCalls) return;
4584
4793
  let key;
@@ -4590,11 +4799,12 @@ function installRepeatGuard(hooks, getConfig, abort) {
4590
4799
  if (typeof key !== "string" || key.length === 0) return;
4591
4800
  const blockedByPriorGate = ctx.block;
4592
4801
  const slot = streakKey(ctx.runId, ctx.name);
4802
+ const effectiveWindow = windowSize ?? (READ_LOOP_TOOLS.has(ctx.name) ? Math.max(DEFAULT_READ_LOOP_WINDOW, Number.isFinite(abortThreshold) ? abortThreshold : DEFAULT_READ_LOOP_WINDOW) : void 0);
4593
4803
  let count;
4594
- if (windowSize !== void 0) {
4804
+ if (effectiveWindow !== void 0) {
4595
4805
  const recent = windows.get(slot) ?? [];
4596
4806
  recent.push(key);
4597
- if (recent.length > windowSize) recent.shift();
4807
+ if (recent.length > effectiveWindow) recent.shift();
4598
4808
  windows.set(slot, recent);
4599
4809
  count = 0;
4600
4810
  for (const k of recent) if (k === key) count++;
@@ -4638,6 +4848,7 @@ function installRepeatGuard(hooks, getConfig, abort) {
4638
4848
  unregister();
4639
4849
  streaks.clear();
4640
4850
  windows.clear();
4851
+ globalWindows.clear();
4641
4852
  };
4642
4853
  }
4643
4854
  //#endregion
@@ -6230,7 +6441,7 @@ function installLazyDisclosureGate(hooks, lazyCanonicalNames, unlocked, discover
6230
6441
  *
6231
6442
  * # MCP Server Instructions
6232
6443
  *
6233
- * ## supabase
6444
+ * ## acme
6234
6445
  * The project is provisioned. Use `apply_migration` directly.
6235
6446
  *
6236
6447
  * ## linear
@@ -6475,6 +6686,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
6475
6686
  const thinking = options.thinking ?? "off";
6476
6687
  const model = options.model ?? provider.meta.defaultModel;
6477
6688
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
6689
+ if (provider.meta?.clearsContextServerSide) resolvedBehavior.dedupReads = false;
6478
6690
  const { maxConcurrentTools, toolBatchExecutor, shouldRethrowToolError, maxTurns, maxTurnsWarning, maxCostUsd, maxTotalTokens, maxWallMs, maxTokens, retry, thinkingBudget, modelOptions: behaviorModelOptions, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, repeatGuard, elideStaleReads, toolDisclosure, mcpToolNameSeparator, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxPairingRepairsPerTurn, maxConsecutivePauseTurns, persistTurns } = resolvedBehavior;
6479
6691
  const modelOptions = options.modelOptions ?? behaviorModelOptions;
6480
6692
  let system = options.system || agentSystem || "You are a helpful assistant.";
@@ -8261,7 +8473,7 @@ const readFile$1 = {
8261
8473
  const prior = readState.get(absKey);
8262
8474
  if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers && prior.elided !== true) {
8263
8475
  rememberRead();
8264
- return `File ${path} unchanged since the previous read in this session — the prior result is still current. If that output is no longer visible in your context (e.g. it was compacted away), re-read with a different offset/limit or lineNumbers value to get the full contents again.`;
8476
+ return `File ${path} unchanged since the previous read in this session — the prior result above is still current, so this duplicate read was skipped to save tokens.`;
8265
8477
  }
8266
8478
  }
8267
8479
  if (looksBinary(raw)) return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
@@ -8912,4 +9124,4 @@ const writeFile$1 = {
8912
9124
  //#endregion
8913
9125
  export { restoreModelOptions as $, PERSISTENCE_PREVIEW_BYTES as A, cerebrasDescriptor as B, stableStringify as C, TOOL_USE_SKIPPED_MESSAGE as D, TOOL_USE_CANCELLED_MESSAGE as E, resolvePersistDir as F, getModelInfo as G, effectiveContextWindow as H, resolveTasksDir as I, modelSupportsReasoning as J, localDescriptor as K, BUILTIN_PROVIDERS as L, cleanupPersistedSession as M, maybePersistToolResult as N, validateToolArgs as O, resolveMcpWarningsDir as P, piIdOf as Q, OUTPUT_RESERVE_TOKENS as R, normalizeShellCommand as S, SHELL_CASCADE_CANCEL_MESSAGE as T, enabledModelOptions as U, credKeyOf as V, getContextWindow as W, openaiDescriptor as X, modelsForDescriptor as Y, openrouterDescriptor as Z, createSkillsReadTool as _, multiEdit as a, defaultRepeatGuardNormalize as b, grep as c, createAgent as d, WAIT_TASK_TIMED_OUT_PREFIX as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, buildPersistedStub as j, PERSISTED_STUB_PREFIX as k, glob$1 as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, waitTask as p, modelOptionsFor as q, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, INTERRUPT_MESSAGE_FOR_TOOL_USE as w, defaultRepeatGuardTracked as x, shell as y, anthropicDescriptor as z };
8914
9126
 
8915
- //# sourceMappingURL=tools-DqNkGwfd.js.map
9127
+ //# sourceMappingURL=tools-ZHKOh44k.js.map