zidane 5.10.13 → 5.11.1

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 (118) hide show
  1. package/README.md +31 -5
  2. package/dist/{agent-BHkvYIH9.d.ts → agent-D0W9yClt.d.ts} +114 -27
  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/docker.d.ts +1 -1
  8. package/dist/contexts/docker.d.ts.map +1 -1
  9. package/dist/contexts/docker.js +53 -14
  10. package/dist/contexts/docker.js.map +1 -1
  11. package/dist/contexts/e2b.d.ts +168 -0
  12. package/dist/contexts/e2b.d.ts.map +1 -0
  13. package/dist/contexts/e2b.js +261 -0
  14. package/dist/contexts/e2b.js.map +1 -0
  15. package/dist/{contexts-BJVgG0LY.js → contexts-DglWSzmR.js} +59 -9
  16. package/dist/contexts-DglWSzmR.js.map +1 -0
  17. package/dist/contexts.d.ts +3 -3
  18. package/dist/contexts.js +1 -1
  19. package/dist/eval.d.ts +1 -1
  20. package/dist/eval.js +5 -5
  21. package/dist/eval.js.map +1 -1
  22. package/dist/{headless-CPaunZsU.js → headless-Bb5gU8AR.js} +6 -6
  23. package/dist/{headless-CPaunZsU.js.map → headless-Bb5gU8AR.js.map} +1 -1
  24. package/dist/headless.d.ts +1 -1
  25. package/dist/headless.js +1 -1
  26. package/dist/{index-C_t8tW_X.d.ts → index-CrMb8jCE.d.ts} +2 -2
  27. package/dist/{index-C_t8tW_X.d.ts.map → index-CrMb8jCE.d.ts.map} +1 -1
  28. package/dist/{index-BIo67xLV.d.ts → index-D60tX5XC.d.ts} +10 -3
  29. package/dist/index-D60tX5XC.d.ts.map +1 -0
  30. package/dist/{index-C4aT2kO_.d.ts → index-DZR99FD4.d.ts} +30 -111
  31. package/dist/index-DZR99FD4.d.ts.map +1 -0
  32. package/dist/index.d.ts +7 -6
  33. package/dist/index.js +11 -10
  34. package/dist/index.js.map +1 -1
  35. package/dist/{interpolate-Dy7Lunvg.js → interpolate-CTfr0GdR.js} +19 -1
  36. package/dist/{interpolate-Dy7Lunvg.js.map → interpolate-CTfr0GdR.js.map} +1 -1
  37. package/dist/logger-Ktm-lj1s.js +300 -0
  38. package/dist/logger-Ktm-lj1s.js.map +1 -0
  39. package/dist/logger-n4LsLISE.d.ts +102 -0
  40. package/dist/logger-n4LsLISE.d.ts.map +1 -0
  41. package/dist/{login-0jP1pnSJ.js → login-BHhOdTp9.js} +4 -301
  42. package/dist/login-BHhOdTp9.js.map +1 -0
  43. package/dist/{mcp-tevNihk_.js → mcp-Cy9mgCcr.js} +22 -9
  44. package/dist/mcp-Cy9mgCcr.js.map +1 -0
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +1 -1
  47. package/dist/{messages-C_1AmSpk.js → messages-RPKrEPvH.js} +6 -2
  48. package/dist/messages-RPKrEPvH.js.map +1 -0
  49. package/dist/output/stream-json.d.ts +2 -2
  50. package/dist/output/stream-json.js +1 -1
  51. package/dist/output/terminal.d.ts +2 -2
  52. package/dist/output/terminal.js +1 -0
  53. package/dist/output/terminal.js.map +1 -1
  54. package/dist/{presets-Cm2BPJaU.js → presets-D5ibZTml.js} +2 -2
  55. package/dist/{presets-Cm2BPJaU.js.map → presets-D5ibZTml.js.map} +1 -1
  56. package/dist/presets.d.ts +2 -2
  57. package/dist/presets.js +1 -1
  58. package/dist/{providers-BGBB18zz.js → providers-C2cxujp_.js} +85 -20
  59. package/dist/providers-C2cxujp_.js.map +1 -0
  60. package/dist/providers.d.ts +1 -1
  61. package/dist/providers.js +2 -2
  62. package/dist/restate.d.ts +2 -2
  63. package/dist/restate.js +4 -1
  64. package/dist/restate.js.map +1 -1
  65. package/dist/session/sqlite.d.ts +1 -1
  66. package/dist/session/sqlite.d.ts.map +1 -1
  67. package/dist/session/sqlite.js +36 -4
  68. package/dist/session/sqlite.js.map +1 -1
  69. package/dist/{session-CtAWwwkn.js → session-Do_TQV7c.js} +70 -22
  70. package/dist/session-Do_TQV7c.js.map +1 -0
  71. package/dist/session.d.ts +2 -2
  72. package/dist/session.js +3 -3
  73. package/dist/shell-quote-BmnhZmdM.js +33 -0
  74. package/dist/shell-quote-BmnhZmdM.js.map +1 -0
  75. package/dist/skills.d.ts +3 -3
  76. package/dist/skills.js +1 -1
  77. package/dist/skills.js.map +1 -1
  78. package/dist/{tool-formatters-D_fX6FGl.d.ts → tool-formatters-RT5-gyE2.d.ts} +2 -2
  79. package/dist/{tool-formatters-D_fX6FGl.d.ts.map → tool-formatters-RT5-gyE2.d.ts.map} +1 -1
  80. package/dist/tools/fetch-url.d.ts +1 -1
  81. package/dist/tools/web-search.d.ts +1 -1
  82. package/dist/{tools-NxnEmzYg.js → tools-ZHKOh44k.js} +342 -123
  83. package/dist/tools-ZHKOh44k.js.map +1 -0
  84. package/dist/tools.d.ts +2 -2
  85. package/dist/tools.js +1 -1
  86. package/dist/{transcript-anchors-DA6XawEU.d.ts → transcript-anchors-B4FxkG-8.d.ts} +10 -4
  87. package/dist/transcript-anchors-B4FxkG-8.d.ts.map +1 -0
  88. package/dist/{transcript-anchors-B_c7gWot.js → transcript-anchors-CS46ul6X.js} +10 -10
  89. package/dist/transcript-anchors-CS46ul6X.js.map +1 -0
  90. package/dist/tui.d.ts +3 -3
  91. package/dist/tui.d.ts.map +1 -1
  92. package/dist/tui.js +167 -41
  93. package/dist/tui.js.map +1 -1
  94. package/dist/{turn-operations-CCl7rpbT.d.ts → turn-operations-CoRj3mYZ.d.ts} +3 -3
  95. package/dist/{turn-operations-CCl7rpbT.d.ts.map → turn-operations-CoRj3mYZ.d.ts.map} +1 -1
  96. package/dist/{types-BibzMDjX.d.ts → types-B39tBba1.d.ts} +69 -2
  97. package/dist/types-B39tBba1.d.ts.map +1 -0
  98. package/dist/types-BiobHM1D.js.map +1 -1
  99. package/dist/types.d.ts +5 -5
  100. package/docs/ARCHITECTURE.md +1 -1
  101. package/docs/CHAT.md +3 -3
  102. package/docs/EXECUTION_CONTEXT.md +257 -0
  103. package/docs/RUN_IN_BACKGROUND.md +8 -0
  104. package/docs/SKILL.md +3 -3
  105. package/package.json +57 -24
  106. package/dist/agent-BHkvYIH9.d.ts.map +0 -1
  107. package/dist/contexts-BJVgG0LY.js.map +0 -1
  108. package/dist/index-BIo67xLV.d.ts.map +0 -1
  109. package/dist/index-C4aT2kO_.d.ts.map +0 -1
  110. package/dist/login-0jP1pnSJ.js.map +0 -1
  111. package/dist/mcp-tevNihk_.js.map +0 -1
  112. package/dist/messages-C_1AmSpk.js.map +0 -1
  113. package/dist/providers-BGBB18zz.js.map +0 -1
  114. package/dist/session-CtAWwwkn.js.map +0 -1
  115. package/dist/tools-NxnEmzYg.js.map +0 -1
  116. package/dist/transcript-anchors-B_c7gWot.js.map +0 -1
  117. package/dist/transcript-anchors-DA6XawEU.d.ts.map +0 -1
  118. package/dist/types-BibzMDjX.d.ts.map +0 -1
@@ -1,25 +1,26 @@
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, h as FAST_MODE_OPTIONS, m as ANTHROPIC_EXTRA_MODELS, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter, u as arcee } from "./providers-BGBB18zz.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-C_1AmSpk.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
- import { r as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-BJVgG0LY.js";
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
- 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-Dy7Lunvg.js";
13
- import { n as connectMcpServers } from "./mcp-tevNihk_.js";
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-Cy9mgCcr.js";
14
14
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
15
- import { dirname, isAbsolute, join, resolve } from "node:path";
15
+ import { n as shellQuote, t as alwaysQuote } from "./shell-quote-BmnhZmdM.js";
16
+ import { isAbsolute, join, resolve } from "node:path";
16
17
  import { createHooks } from "hookable";
17
18
  import { getModel, getModels } from "@earendil-works/pi-ai";
18
19
  import { Buffer } from "node:buffer";
19
20
  import { refreshAnthropicToken, refreshOpenAICodexToken, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
21
+ import { glob, readdir, rm, stat, unlink } from "node:fs/promises";
20
22
  import { createServer } from "node:http";
21
23
  import { randomBytes } from "node:crypto";
22
- import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
23
24
  //#region src/aliasing.ts
24
25
  /**
25
26
  * Build alias lookup maps from a `toolAliases` record.
@@ -82,8 +83,8 @@ function toCanonicalName(wire, maps) {
82
83
  * Why: SDK consumers and tool descriptions across the ecosystem
83
84
  * inconsistently reference one form or the other (Anthropic's docs use
84
85
  * double, zidane uses single). A Claude-Code-trained model emitting
85
- * `mcp__supabase__apply_migration` against a server zidane registered as
86
- * `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`
87
88
  * path and waste a turn on the correction.
88
89
  *
89
90
  * Idempotent: a canonical that already has a double-underscore alias
@@ -1403,6 +1404,14 @@ function installDedupToolsGate(hooks, getDedupTools, getSession) {
1403
1404
  */
1404
1405
  const PERSISTENCE_PREVIEW_BYTES = 2 * 1024;
1405
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
+ /**
1406
1415
  * Byte-stable prefix every {@link buildPersistedStub} output starts with.
1407
1416
  * Exported so wire-level passes (tail compaction, future stale-output
1408
1417
  * elision) can recognize a persisted stub and preserve its path attribute
@@ -1632,7 +1641,7 @@ async function enforcePersistDirCap(persistDir, maxBytes, opts = {}) {
1632
1641
  * surface.
1633
1642
  */
1634
1643
  function buildPersistedStub(input) {
1635
- const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, PERSISTENCE_PREVIEW_BYTES);
1644
+ const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, input.previewBytes ?? 2048);
1636
1645
  const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
1637
1646
  return [
1638
1647
  `${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
@@ -1662,30 +1671,76 @@ async function cleanupPersistedSession(persistRoot) {
1662
1671
  }
1663
1672
  }
1664
1673
  /**
1665
- * Write-then-rename for atomicity. A concurrent reader (or a crash mid-
1666
- * write) never observes a partial file: the rename is atomic on every
1667
- * POSIX FS, and the destination is either the fully-written file or
1668
- * absent. Creates the parent directory on demand so the caller doesn't
1669
- * have to track which sessions have already initialized their dir.
1670
- *
1671
- * On write failure (disk full, permissions, signal), removes the `.tmp`
1672
- * file so it doesn't accumulate as debris alongside successful blobs.
1673
- * The unlink itself is best-effort if we can't write to the dir we
1674
- * likely can't unlink from it either, and the original error is what
1675
- * the caller needs to see.
1676
- *
1677
- * Same idiom we use elsewhere (state-store snapshots, session export).
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
+ /**
1736
+ * Write-then-rename for atomicity — delegates to the shared
1737
+ * {@link writeFileAtomicAsync} helper (unique pid+counter tmp suffix,
1738
+ * fsync before rename, tmp cleanup on failure). Creates the parent
1739
+ * directory on demand so the caller doesn't have to track which
1740
+ * sessions have already initialized their dir.
1678
1741
  */
1679
1742
  async function writeAtomic(path, content) {
1680
- await mkdir(dirname(path), { recursive: true });
1681
- const tmp = `${path}.tmp`;
1682
- try {
1683
- await writeFile(tmp, content, "utf8");
1684
- await rename(tmp, path);
1685
- } catch (err) {
1686
- await rm(tmp, { force: true }).catch(() => {});
1687
- throw err;
1688
- }
1743
+ await writeFileAtomicAsync(path, content, { ensureDir: true });
1689
1744
  }
1690
1745
  /**
1691
1746
  * Take the first `cap` bytes of `text` without splitting a UTF-8
@@ -2125,6 +2180,25 @@ const SHELL_CASCADE_CANCEL_MESSAGE = "Cancelled: a sibling `shell` call in the s
2125
2180
  */
2126
2181
  const CANCELLED_BY_USER_SENTINEL = "zidane:tool:cancelled-by-user";
2127
2182
  /**
2183
+ * Grace window after a run-level abort before we force-settle a tool body that
2184
+ * hasn't returned. Well-behaved tools observe `childSignal` and finalize their
2185
+ * own bookkeeping (e.g. `spawn` awaits its child's session-persisting `finally`
2186
+ * block); they settle within this window and the forced rejection never fires.
2187
+ * Only a body that *ignores* its signal hits the timeout — that's the wedge we
2188
+ * guard against. Kept short so a real wedge doesn't stall `drain()` for long,
2189
+ * but long enough for a normal abort-driven cleanup to complete.
2190
+ *
2191
+ * Overridable via `ZIDANE_RUN_ABORT_GRACE_MS` (internal/testing only — lets
2192
+ * the wedge test exercise the forced-rejection path without a multi-second
2193
+ * real wait). Read per-use (not memoized) so tests can set it; falls back to
2194
+ * the 2s default on any non-finite value.
2195
+ */
2196
+ const DEFAULT_RUN_ABORT_GRACE_MS = 2e3;
2197
+ function runAbortGraceMs() {
2198
+ const raw = Number(process.env.ZIDANE_RUN_ABORT_GRACE_MS);
2199
+ return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_RUN_ABORT_GRACE_MS;
2200
+ }
2201
+ /**
2128
2202
  * Compute the effective thinking budget for a given run-relative turn, given
2129
2203
  * the configured decay schedule. Pure helper — exported for tests and so
2130
2204
  * downstream tooling can preview decay curves without spinning up the loop.
@@ -2272,32 +2346,56 @@ function applyCompactSummaryCutoff(turns) {
2272
2346
  * alone — those are consumer-controlled and the consumer is responsible for
2273
2347
  * matching prompt parts to the provider's capabilities.
2274
2348
  */
2275
- 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.]";
2276
2350
  const COMPACTION_STUB_BYTES = toolOutputByteLength(COMPACTION_STUB);
2277
- /**
2278
- * Tail-compaction for non-Anthropic providers: when the cumulative byte size
2279
- * of `tool_result` content across the wire-level message list exceeds
2280
- * `threshold`, replace older `tool_result` outputs with a short stub. The
2281
- * newest `keepTurns` messages (user/assistant alike) are left untouched so
2282
- * the model retains the freshest tool context.
2283
- *
2284
- * Only `tool_result` blocks are touched — text and image blocks pass through
2285
- * unchanged. Mutates a shallow-cloned message array; original `messages` is
2286
- * not modified.
2287
- *
2288
- * For Anthropic users, prefer the server-side `context-management-2025-06-27`
2289
- * beta (token-accurate, no client-side approximation). This function is the
2290
- * client-side fallback for OpenAI-compatible / OpenRouter / Cerebras runs
2291
- * against OSS models that lack a server-side equivalent.
2292
- */
2293
- function applyTailCompaction(messages, threshold, keepTurns) {
2294
- 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
+ };
2295
2362
  let totalBytes = 0;
2296
2363
  for (const msg of messages) for (const block of msg.content) if (block.type === "tool_result") totalBytes += toolOutputByteLength(block.output);
2297
- if (totalBytes <= threshold) return messages;
2364
+ if (totalBytes <= threshold) return {
2365
+ messages,
2366
+ elidedReadPaths,
2367
+ persistableElided
2368
+ };
2298
2369
  const keep = Math.max(0, keepTurns);
2299
- const cutoff = messages.length - keep;
2300
- 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;
2301
2399
  let changed = false;
2302
2400
  const out = messages.slice();
2303
2401
  for (let i = 0; i < cutoff; i++) {
@@ -2305,10 +2403,18 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2305
2403
  let msgChanged = false;
2306
2404
  const newContent = msg.content.map((block) => {
2307
2405
  if (block.type !== "tool_result") return block;
2308
- if (toolOutputByteLength(block.output) <= COMPACTION_STUB_BYTES) return block;
2406
+ const existingBytes = toolOutputByteLength(block.output);
2407
+ if (existingBytes <= COMPACTION_STUB_BYTES) return block;
2309
2408
  if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
2310
2409
  msgChanged = true;
2311
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
+ });
2312
2418
  return {
2313
2419
  ...block,
2314
2420
  output: COMPACTION_STUB
@@ -2319,7 +2425,34 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2319
2425
  content: newContent
2320
2426
  };
2321
2427
  }
2322
- 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
+ };
2323
2456
  }
2324
2457
  /**
2325
2458
  * Replace `read_file` `tool_result` blocks with a short stub when a later
@@ -2338,7 +2471,7 @@ function applyTailCompaction(messages, threshold, keepTurns) {
2338
2471
  * Pure function, exported for tests and so downstream tooling can preview
2339
2472
  * elision without spinning up the loop.
2340
2473
  */
2341
- 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.]";
2342
2475
  function applyStaleReadElision(messages) {
2343
2476
  if (messages.length === 0) return {
2344
2477
  messages,
@@ -2394,7 +2527,7 @@ function applyStaleReadElision(messages) {
2394
2527
  let msgChanged = false;
2395
2528
  const newContent = msg.content.map((block) => {
2396
2529
  if (block.type !== "tool_result" || !staleCallIds.has(block.callId)) return block;
2397
- 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;
2398
2531
  if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
2399
2532
  msgChanged = true;
2400
2533
  changed = true;
@@ -2473,10 +2606,10 @@ function applyPairingRepair(ctx, messages, turnId) {
2473
2606
  ...ctx.providerName ? { provider: ctx.providerName } : {},
2474
2607
  repairs
2475
2608
  });
2476
- for (const repair of repairs) ctx.hooks.callHook("pairing:repair", {
2609
+ for (const repair of repairs) Promise.resolve(ctx.hooks.callHook("pairing:repair", {
2477
2610
  ...repair,
2478
2611
  turnId
2479
- });
2612
+ })).catch(() => {});
2480
2613
  return repaired;
2481
2614
  }
2482
2615
  function sanitizeStoredToolResults(provider, messages) {
@@ -3001,7 +3134,19 @@ async function executeTurn(ctx, turn, priorUsage) {
3001
3134
  if (ctx.compactStrategy === "tail") {
3002
3135
  const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
3003
3136
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
3004
- 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
+ });
3005
3150
  } else if (typeof ctx.compactStrategy === "function") {
3006
3151
  const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
3007
3152
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
@@ -3207,12 +3352,12 @@ async function executeTurn(ctx, turn, priorUsage) {
3207
3352
  if (reminted.reminted.length > 0) {
3208
3353
  canonicalToolCalls = reminted.toolCalls;
3209
3354
  canonicalContent = reminted.content;
3210
- for (const r of reminted.reminted) ctx.hooks.callHook("pairing:repair", {
3355
+ for (const r of reminted.reminted) Promise.resolve(ctx.hooks.callHook("pairing:repair", {
3211
3356
  mode: "duplicate-tool-use-remint",
3212
3357
  callId: r.oldId,
3213
3358
  messageIndex: ctx.turns.length,
3214
3359
  turnId
3215
- });
3360
+ })).catch(() => {});
3216
3361
  }
3217
3362
  }
3218
3363
  if (turnTtftMs !== void 0 && result.usage.timeToFirstTokenMs === void 0) result.usage.timeToFirstTokenMs = turnTtftMs;
@@ -3582,10 +3727,31 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
3582
3727
  perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
3583
3728
  removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
3584
3729
  });
3730
+ let removeRunAbortListener;
3731
+ let runAbortTimer;
3732
+ const runAbortPromise = new Promise((_, reject) => {
3733
+ const forceReject = () => reject(new DOMException("Aborted", "AbortError"));
3734
+ const armGrace = () => {
3735
+ runAbortTimer = setTimeout(forceReject, runAbortGraceMs());
3736
+ runAbortTimer.unref?.();
3737
+ };
3738
+ if (ctx.signal.aborted) {
3739
+ armGrace();
3740
+ return;
3741
+ }
3742
+ ctx.signal.addEventListener("abort", armGrace, { once: true });
3743
+ removeRunAbortListener = () => ctx.signal.removeEventListener("abort", armGrace);
3744
+ });
3585
3745
  try {
3586
- output = await Promise.race([bodyPromise, cancellationPromise]);
3746
+ output = await Promise.race([
3747
+ bodyPromise,
3748
+ cancellationPromise,
3749
+ runAbortPromise
3750
+ ]);
3587
3751
  } finally {
3588
3752
  removeAbortListener?.();
3753
+ removeRunAbortListener?.();
3754
+ if (runAbortTimer !== void 0) clearTimeout(runAbortTimer);
3589
3755
  }
3590
3756
  } catch (err) {
3591
3757
  const isOurSentinel = err instanceof Error && err.message === CANCELLED_BY_USER_SENTINEL;
@@ -4419,13 +4585,43 @@ function buildPromptMessage(provider, parts) {
4419
4585
  const DEFAULT_BLOCK_THRESHOLD = 4;
4420
4586
  const DEFAULT_ABORT_THRESHOLD = 8;
4421
4587
  /**
4422
- * Default tracked-tool predicate: `shell` exactly, plus any tool whose
4423
- * canonical name ends in `_execute_sql` (the MCP SQL-runner shape,
4424
- * `mcp_<server>_execute_sql`). Both are auto-approved, side-effect-light
4425
- * 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}.
4426
4622
  */
4427
4623
  function defaultRepeatGuardTracked(name) {
4428
- return name === "shell" || name.endsWith("_execute_sql");
4624
+ return name === "shell" || name === "read_file";
4429
4625
  }
4430
4626
  function compileMatchers(tools) {
4431
4627
  if (tools === void 0) return defaultRepeatGuardTracked;
@@ -4471,10 +4667,12 @@ function normalizeShellCommand(command) {
4471
4667
  }
4472
4668
  /**
4473
4669
  * Built-in normalizer used when the consumer doesn't supply one. Shell-shaped
4474
- * tools get {@link normalizeShellCommand}; SQL-runner tools get a whitespace-
4475
- * collapsed query; everything else falls back to a stable JSON encoding.
4476
- * Returns `undefined` when there's no meaningful payload to key on (so the
4477
- * 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}.
4478
4676
  */
4479
4677
  function defaultRepeatGuardNormalize(name, input) {
4480
4678
  if (name === "shell") {
@@ -4482,11 +4680,6 @@ function defaultRepeatGuardNormalize(name, input) {
4482
4680
  if (typeof cmd !== "string" || cmd.trim().length === 0) return void 0;
4483
4681
  return `shell:${normalizeShellCommand(cmd)}`;
4484
4682
  }
4485
- if (name.endsWith("_execute_sql")) {
4486
- const q = input.query ?? input.sql ?? input.statement;
4487
- if (typeof q !== "string" || q.trim().length === 0) return void 0;
4488
- return `sql:${q.replace(/\s+/g, " ").trim().toLowerCase()}`;
4489
- }
4490
4683
  try {
4491
4684
  return `json:${stableStringify(input)}`;
4492
4685
  } catch {
@@ -4528,6 +4721,7 @@ function formatBlockReason(reason, name, count) {
4528
4721
  function installRepeatGuard(hooks, getConfig, abort) {
4529
4722
  const streaks = /* @__PURE__ */ new Map();
4530
4723
  const windows = /* @__PURE__ */ new Map();
4724
+ const globalWindows = /* @__PURE__ */ new Map();
4531
4725
  function streakKey(runId, name) {
4532
4726
  return `${runId ?? "-"}::${name}`;
4533
4727
  }
@@ -4540,20 +4734,60 @@ function installRepeatGuard(hooks, getConfig, abort) {
4540
4734
  const reachable = Number.isFinite(abortThreshold) ? abortThreshold : blockThreshold;
4541
4735
  windowSize = Math.max(Math.floor(config.windowSize), reachable);
4542
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);
4543
4741
  return {
4544
4742
  isTracked: compileMatchers(config.tools),
4545
4743
  normalize: config.normalize ?? defaultRepeatGuardNormalize,
4546
4744
  blockThreshold,
4547
4745
  abortThreshold,
4548
4746
  countBlockedCalls: config.countBlockedCalls === true,
4549
- windowSize
4747
+ windowSize,
4748
+ globalCeiling,
4749
+ globalWindow,
4750
+ isGloballyExcluded
4550
4751
  };
4551
4752
  }
4552
4753
  async function gateHandler(ctx) {
4553
4754
  if (ctx.result !== void 0) return;
4554
4755
  const config = getConfig();
4555
4756
  if (!config) return;
4556
- 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
+ }
4557
4791
  if (!isTracked(ctx.name)) return;
4558
4792
  if (ctx.block && !countBlockedCalls) return;
4559
4793
  let key;
@@ -4565,11 +4799,12 @@ function installRepeatGuard(hooks, getConfig, abort) {
4565
4799
  if (typeof key !== "string" || key.length === 0) return;
4566
4800
  const blockedByPriorGate = ctx.block;
4567
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);
4568
4803
  let count;
4569
- if (windowSize !== void 0) {
4804
+ if (effectiveWindow !== void 0) {
4570
4805
  const recent = windows.get(slot) ?? [];
4571
4806
  recent.push(key);
4572
- if (recent.length > windowSize) recent.shift();
4807
+ if (recent.length > effectiveWindow) recent.shift();
4573
4808
  windows.set(slot, recent);
4574
4809
  count = 0;
4575
4810
  for (const k of recent) if (k === key) count++;
@@ -4613,6 +4848,7 @@ function installRepeatGuard(hooks, getConfig, abort) {
4613
4848
  unregister();
4614
4849
  streaks.clear();
4615
4850
  windows.clear();
4851
+ globalWindows.clear();
4616
4852
  };
4617
4853
  }
4618
4854
  //#endregion
@@ -5264,36 +5500,6 @@ function createSkillsReadTool(options) {
5264
5500
  };
5265
5501
  }
5266
5502
  //#endregion
5267
- //#region src/tools/shell-quote.ts
5268
- /**
5269
- * Shared shell-argument quoter for tool implementations.
5270
- *
5271
- * Single source of truth so `grep`, `binary-read`, and `skills-run-script`
5272
- * don't drift on the POSIX `'\''` escape pattern.
5273
- */
5274
- const SAFE_TOKEN_RE = /^[\w@%+=:,./-]+$/;
5275
- const SINGLE_QUOTE_RE = /'/g;
5276
- /**
5277
- * Wrap an argument in single quotes, escaping embedded single quotes via the
5278
- * standard POSIX `'\''` close-escape-reopen trick. Tokens that are already
5279
- * shell-safe pass through unchanged so command lines stay readable in logs.
5280
- *
5281
- * NOT a sandbox — only safe when the caller controls the surrounding shell
5282
- * context. Use it for arguments only, never for the verb / subcommand.
5283
- */
5284
- function shellQuote(arg) {
5285
- if (SAFE_TOKEN_RE.test(arg)) return arg;
5286
- return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
5287
- }
5288
- /**
5289
- * Variant that always quotes — useful when the caller doesn't want a
5290
- * conditional `unquoted-when-safe` branch (consistent log shape, paranoid
5291
- * inputs that contain whitespace by construction).
5292
- */
5293
- function alwaysQuote(arg) {
5294
- return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
5295
- }
5296
- //#endregion
5297
5503
  //#region src/tools/skills-run-script.ts
5298
5504
  const ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
5299
5505
  const COLLAPSE_SLASHES_RE = /\/+/g;
@@ -5441,7 +5647,15 @@ function createSkillsUseTool(options) {
5441
5647
  if (!skill.instructions.includes("!`")) body = skill.instructions;
5442
5648
  else if (options.allowShellInterpolation !== false) {
5443
5649
  const failures = [];
5444
- body = await interpolateShellCommands(skill.instructions, ctx.execution, ctx.handle, { onFailure: (command) => failures.push(command) });
5650
+ const approve = options.approveShellInterpolation;
5651
+ body = await interpolateShellCommands(skill.instructions, ctx.execution, ctx.handle, {
5652
+ onFailure: (command) => failures.push(command),
5653
+ ...approve ? { approve: (command) => approve({
5654
+ skillName,
5655
+ source: skill.source,
5656
+ command
5657
+ }) } : {}
5658
+ });
5445
5659
  if (failures.length > 0) body += `\n\nWarning: ${failures.length} shell interpolation command${failures.length === 1 ? "" : "s"} failed during activation (${failures.map((c) => `\`${c}\``).join(", ")}). The instructions above may be missing dynamic content — see the inline [command failed …] markers.`;
5446
5660
  } else body = stripShellInterpolations(skill.instructions);
5447
5661
  interpolatedBodyCache.set(skillName, body);
@@ -6227,7 +6441,7 @@ function installLazyDisclosureGate(hooks, lazyCanonicalNames, unlocked, discover
6227
6441
  *
6228
6442
  * # MCP Server Instructions
6229
6443
  *
6230
- * ## supabase
6444
+ * ## acme
6231
6445
  * The project is provisioned. Use `apply_migration` directly.
6232
6446
  *
6233
6447
  * ## linear
@@ -6273,12 +6487,12 @@ function childRunIdSet(session) {
6273
6487
  * Throws on an unrecoverable resume request; otherwise returns the filtered
6274
6488
  * turns (or `undefined` when the session has no turns).
6275
6489
  */
6276
- function validateAndPrepareResume(session, prompt) {
6490
+ function validateAndPrepareResume(session, prompt, hasPendingTaskNotifications = false) {
6277
6491
  const hasSessionTurns = !!session && session.turns.length > 0;
6278
- if (!prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
6492
+ if (!prompt && !hasSessionTurns && !hasPendingTaskNotifications) throw new Error("prompt is required when no session with existing turns is provided");
6279
6493
  let resumeFilteredTurns;
6280
6494
  if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
6281
- if (!prompt && resumeFilteredTurns) {
6495
+ if (!prompt && resumeFilteredTurns && !hasPendingTaskNotifications) {
6282
6496
  const lastTurn = resumeFilteredTurns.at(-1);
6283
6497
  if (lastTurn && lastTurn.role !== "user") {
6284
6498
  const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
@@ -6386,7 +6600,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
6386
6600
  const skillActivationState = createSkillActivationState({ maxActive: skillsConfig?.maxActive });
6387
6601
  async function run(options) {
6388
6602
  if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
6389
- const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt);
6603
+ const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt, pendingTaskNotifications.size > 0);
6390
6604
  const clock = options.clock ?? agentClock ?? DEFAULT_AGENT_CLOCK;
6391
6605
  let externalAbortListener;
6392
6606
  const externalSignal = options.signal;
@@ -6472,6 +6686,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
6472
6686
  const thinking = options.thinking ?? "off";
6473
6687
  const model = options.model ?? provider.meta.defaultModel;
6474
6688
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
6689
+ if (provider.meta?.clearsContextServerSide) resolvedBehavior.dedupReads = false;
6475
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;
6476
6691
  const modelOptions = options.modelOptions ?? behaviorModelOptions;
6477
6692
  let system = options.system || agentSystem || "You are a helpful assistant.";
@@ -6492,7 +6707,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
6492
6707
  catalog: resolvedSkills,
6493
6708
  state: skillActivationState,
6494
6709
  hooks,
6495
- allowShellInterpolation: skillsConfig?.allowShellInterpolation
6710
+ allowShellInterpolation: skillsConfig?.allowShellInterpolation,
6711
+ approveShellInterpolation: skillsConfig?.approveShellInterpolation
6496
6712
  }),
6497
6713
  skills_read: createSkillsReadTool({
6498
6714
  catalog: resolvedSkills,
@@ -7309,6 +7525,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
7309
7525
  abort,
7310
7526
  cancelTool,
7311
7527
  killBackgroundTask,
7528
+ get hasPendingTaskNotifications() {
7529
+ return pendingTaskNotifications.size > 0;
7530
+ },
7312
7531
  steer,
7313
7532
  followUp: followUpFn,
7314
7533
  waitForIdle,
@@ -8254,7 +8473,7 @@ const readFile$1 = {
8254
8473
  const prior = readState.get(absKey);
8255
8474
  if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers && prior.elided !== true) {
8256
8475
  rememberRead();
8257
- 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.`;
8258
8477
  }
8259
8478
  }
8260
8479
  if (looksBinary(raw)) return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
@@ -8782,7 +9001,7 @@ function createSpawnTool(options = {}) {
8782
9001
  const childHandle = agent.handle;
8783
9002
  if (childHandle && ctx.execution.reassignBackgroundTasks) try {
8784
9003
  const reassigned = await ctx.execution.reassignBackgroundTasks(childHandle, ctx.handle, (info) => {
8785
- ctx.hooks.callHook("background:exit", info);
9004
+ Promise.resolve(ctx.hooks.callHook("background:exit", info)).catch(() => {});
8786
9005
  });
8787
9006
  for (const entry of reassigned) await ctx.hooks.callHook("background:reassign", {
8788
9007
  taskId: entry.taskId,
@@ -8903,6 +9122,6 @@ const writeFile$1 = {
8903
9122
  }
8904
9123
  };
8905
9124
  //#endregion
8906
- export { piIdOf as $, PERSISTED_STUB_PREFIX as A, anthropicDescriptor as B, normalizeShellCommand as C, TOOL_USE_CANCELLED_MESSAGE as D, SHELL_CASCADE_CANCEL_MESSAGE as E, resolveMcpWarningsDir as F, getContextWindow as G, credKeyOf as H, resolvePersistDir as I, modelOptionsFor as J, getModelInfo as K, resolveTasksDir as L, buildPersistedStub as M, cleanupPersistedSession as N, TOOL_USE_SKIPPED_MESSAGE as O, maybePersistToolResult as P, openrouterDescriptor as Q, BUILTIN_PROVIDERS as R, defaultRepeatGuardTracked as S, INTERRUPT_MESSAGE_FOR_TOOL_USE as T, effectiveContextWindow as U, cerebrasDescriptor as V, enabledModelOptions as W, modelsForDescriptor as X, modelSupportsReasoning as Y, openaiDescriptor as Z, alwaysQuote as _, multiEdit as a, shell as b, grep as c, createAgent as d, restoreModelOptions as et, WAIT_TASK_TIMED_OUT_PREFIX as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, PERSISTENCE_PREVIEW_BYTES as j, validateToolArgs as k, glob$1 as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, waitTask as p, localDescriptor as q, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createSkillsReadTool as v, stableStringify as w, defaultRepeatGuardNormalize as x, createShellTool as y, OUTPUT_RESERVE_TOKENS as z };
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 };
8907
9126
 
8908
- //# sourceMappingURL=tools-NxnEmzYg.js.map
9127
+ //# sourceMappingURL=tools-ZHKOh44k.js.map