zidane 5.3.1 → 5.3.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 (44) hide show
  1. package/README.md +2 -0
  2. package/dist/{agent-bKs7MRT2.d.ts → agent-BXRCCHeq.d.ts} +129 -2
  3. package/dist/agent-BXRCCHeq.d.ts.map +1 -0
  4. package/dist/chat.d.ts +4 -4
  5. package/dist/chat.js +1 -1
  6. package/dist/{index-BlMvPh9X.d.ts → index-BPk8-Slm.d.ts} +26 -2
  7. package/dist/index-BPk8-Slm.d.ts.map +1 -0
  8. package/dist/{index-CTmNaIDb.d.ts → index-CT5_p-3P.d.ts} +2 -2
  9. package/dist/{index-CTmNaIDb.d.ts.map → index-CT5_p-3P.d.ts.map} +1 -1
  10. package/dist/index.d.ts +4 -4
  11. package/dist/index.js +5 -5
  12. package/dist/{login-CNS9_8Ue.js → login-DrBZ15G7.js} +3 -3
  13. package/dist/{login-CNS9_8Ue.js.map → login-DrBZ15G7.js.map} +1 -1
  14. package/dist/{mcp-ZsSFo4Dp.js → mcp-DhmmJfxK.js} +15 -2
  15. package/dist/mcp-DhmmJfxK.js.map +1 -0
  16. package/dist/mcp.d.ts +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/{presets-h5i3kpOP.js → presets-0_IRJAYF.js} +2 -2
  19. package/dist/{presets-h5i3kpOP.js.map → presets-0_IRJAYF.js.map} +1 -1
  20. package/dist/presets.d.ts +2 -2
  21. package/dist/presets.js +1 -1
  22. package/dist/providers.d.ts +1 -1
  23. package/dist/session/sqlite.d.ts +1 -1
  24. package/dist/session.d.ts +1 -1
  25. package/dist/skills.d.ts +2 -2
  26. package/dist/{tools-CWEDS2ZT.js → tools-CCsL5SCO.js} +148 -104
  27. package/dist/tools-CCsL5SCO.js.map +1 -0
  28. package/dist/tools.d.ts +3 -3
  29. package/dist/tools.js +2 -2
  30. package/dist/{transcript-anchors-DOUqyvXR.d.ts → transcript-anchors-DSk8LlWt.d.ts} +3 -3
  31. package/dist/{transcript-anchors-DOUqyvXR.d.ts.map → transcript-anchors-DSk8LlWt.d.ts.map} +1 -1
  32. package/dist/tui.d.ts +2 -2
  33. package/dist/tui.js +4 -4
  34. package/dist/{turn-operations-D9HvatsR.js → turn-operations-CutZin8X.js} +4 -4
  35. package/dist/{turn-operations-D9HvatsR.js.map → turn-operations-CutZin8X.js.map} +1 -1
  36. package/dist/types-IcokUOyC.js.map +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/docs/ARCHITECTURE.md +1 -1
  39. package/docs/SKILL.md +23 -1
  40. package/package.json +1 -1
  41. package/dist/agent-bKs7MRT2.d.ts.map +0 -1
  42. package/dist/index-BlMvPh9X.d.ts.map +0 -1
  43. package/dist/mcp-ZsSFo4Dp.js.map +0 -1
  44. package/dist/tools-CWEDS2ZT.js.map +0 -1
@@ -2,7 +2,7 @@ import { n as createProcessContext } from "./contexts-BwiHIr2w.js";
2
2
  import { a as AgentToolPairingError, l as toTypedError, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-Byb0F8B9.js";
3
3
  import { t as toolOutputByteLength } from "./types-IcokUOyC.js";
4
4
  import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-D0xT979U.js";
5
- import { t as connectMcpServers } from "./mcp-ZsSFo4Dp.js";
5
+ import { t as connectMcpServers } from "./mcp-DhmmJfxK.js";
6
6
  import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-ERgZUxgg.js";
7
7
  import { n as formatTokenUsage, t as flattenTurns } from "./stats-DgOvY7wd.js";
8
8
  import { createHooks } from "hookable";
@@ -107,8 +107,14 @@ function rewriteMessagesToWire(messages, maps) {
107
107
  const STATE = /* @__PURE__ */ new WeakMap();
108
108
  /**
109
109
  * Get or lazily create the per-session read-state map. Returns `undefined`
110
- * when no session is provided — tools should treat that as "no dedup, no
111
- * guard": the state has nowhere to live, so every read is fresh.
110
+ * when no session is provided.
111
+ *
112
+ * Most tool callers should prefer {@link resolveReadStateMap}, which
113
+ * additionally honors an explicit `ctx.readState` (the path
114
+ * `spawn`'s `shareReadState: true` uses to forward the parent's map
115
+ * into a sessionless child). Use this helper directly only when the
116
+ * Session-keyed map is exactly what you want and you don't need to
117
+ * accept an override.
112
118
  */
113
119
  function getReadState(session) {
114
120
  if (!session) return void 0;
@@ -119,19 +125,38 @@ function getReadState(session) {
119
125
  }
120
126
  return map;
121
127
  }
122
- const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
123
128
  /**
124
- * Get or lazily create the per-session tool-dedup map. Returns `undefined`
125
- * when no session is provided middleware should treat that as "no dedup".
129
+ * Resolve the active read-state map from a tool context. An explicit
130
+ * `ctx.readState` wins (used by `spawn`'s `shareReadState` opt-in to
131
+ * forward the parent's map into a sessionless child); otherwise the
132
+ * usual `Session`-keyed lookup applies.
133
+ *
134
+ * Tools should call this helper instead of `getReadState(ctx.session)`
135
+ * directly so the `shareReadState` plumbing is honored uniformly.
126
136
  */
127
- function getToolDedupState(session) {
128
- if (!session) return void 0;
129
- let map = TOOL_DEDUP_STATE.get(session);
130
- if (!map) {
131
- map = /* @__PURE__ */ new Map();
132
- TOOL_DEDUP_STATE.set(session, map);
133
- }
134
- return map;
137
+ function resolveReadStateMap(ctx) {
138
+ return ctx.readState ?? getReadState(ctx.session);
139
+ }
140
+ /**
141
+ * Canonical read-state key for a `(cwd, path)` pair.
142
+ *
143
+ * `ctx.execution.readFile(handle, path)` resolves the model-provided
144
+ * path against `handle.cwd` before touching disk — so `src/App.tsx`,
145
+ * `./src/App.tsx`, and `/abs/cwd/src/App.tsx` all read the **same**
146
+ * bytes. The read-state map MUST mirror that resolution: without it,
147
+ * a model that reads `src/App.tsx` and then edits `./src/App.tsx`
148
+ * trips the `requireReadBeforeEdit` gate for a file it demonstrably
149
+ * already saw (the gate's "has not been read in this session" message
150
+ * fires on a stale key).
151
+ *
152
+ * `node:path`'s `resolve(cwd, path)` short-circuits when `path` is
153
+ * already absolute, so absolute and relative shapes converge on the
154
+ * same canonical form. We don't `realpath()` symlinks — the file IS
155
+ * the path the model addressed, not the realpath behind it; the host's
156
+ * execution context decides how to dereference.
157
+ */
158
+ function readStateKey(cwd, path) {
159
+ return resolve(cwd, path);
135
160
  }
136
161
  /**
137
162
  * FNV-1a 32-bit hash, hex-encoded. Fast, non-cryptographic — we only need
@@ -147,6 +172,20 @@ function hashContent(text) {
147
172
  }
148
173
  return h.toString(16).padStart(8, "0");
149
174
  }
175
+ const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
176
+ /**
177
+ * Get or lazily create the per-session tool-dedup map. Returns `undefined`
178
+ * when no session is provided — middleware should treat that as "no dedup".
179
+ */
180
+ function getToolDedupState(session) {
181
+ if (!session) return void 0;
182
+ let map = TOOL_DEDUP_STATE.get(session);
183
+ if (!map) {
184
+ map = /* @__PURE__ */ new Map();
185
+ TOOL_DEDUP_STATE.set(session, map);
186
+ }
187
+ return map;
188
+ }
150
189
  //#endregion
151
190
  //#region src/dedup-tools.ts
152
191
  /**
@@ -1067,19 +1106,21 @@ function applyStaleReadElision(messages) {
1067
1106
  }
1068
1107
  /**
1069
1108
  * Drop read-state entries for paths whose reads got elided. Keys are
1070
- * `${cwd}::${path}` (see `src/tools/read-file.ts`); we don't know the
1071
- * cwd in this context, so we match by `::<path>` suffix. Safe because
1072
- * paths inside the read-state map come from a single session bound to
1073
- * one cwd in practice; an accidental cross-cwd collision would also
1074
- * have to share an identical relative path, in which case force-fresh
1075
- * is the correct behavior anyway.
1109
+ * canonical absolute paths produced by `readStateKey(cwd, path)` (see
1110
+ * `src/tools/read-state.ts`); the loop has `ctx.handle.cwd` in scope,
1111
+ * so we re-derive each elided path's canonical key and delete by
1112
+ * direct lookup. No suffix matching, no cross-cwd ambiguity.
1113
+ *
1114
+ * Resolves the map via `resolveReadStateMap` so a child agent running
1115
+ * with a parent's shared map (via `shareReadState`) invalidates the
1116
+ * shared entries too — otherwise a child's `read → edit → re-read`
1117
+ * would dedup-hit on the now-stale parent entry.
1076
1118
  */
1077
- function invalidateReadStateForElidedPaths(session, elidedPaths) {
1078
- if (!session || elidedPaths.length === 0) return;
1079
- const readState = getReadState(session);
1119
+ function invalidateReadStateForElidedPaths(ctx, cwd, elidedPaths) {
1120
+ if (elidedPaths.length === 0) return;
1121
+ const readState = resolveReadStateMap(ctx);
1080
1122
  if (!readState || readState.size === 0) return;
1081
- const suffixes = elidedPaths.map((p) => `::${p}`);
1082
- for (const key of [...readState.keys()]) if (suffixes.some((s) => key.endsWith(s))) readState.delete(key);
1123
+ for (const p of elidedPaths) readState.delete(readStateKey(cwd, p));
1083
1124
  }
1084
1125
  /**
1085
1126
  * Run {@link ensureToolResultPairing} with the loop's hook + strict-mode
@@ -1265,7 +1306,7 @@ async function executeTurn(ctx, turn) {
1265
1306
  if (ctx.elideStaleReads === true) {
1266
1307
  const elision = applyStaleReadElision(canonicalMessages);
1267
1308
  canonicalMessages = elision.messages;
1268
- invalidateReadStateForElidedPaths(ctx.session, elision.elidedPaths);
1309
+ invalidateReadStateForElidedPaths(ctx, ctx.handle.cwd, elision.elidedPaths);
1269
1310
  }
1270
1311
  const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
1271
1312
  let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
@@ -1539,17 +1580,35 @@ function stripImagesForNonVision(provider, output) {
1539
1580
  if (provider.meta.capabilities?.vision !== false) return output;
1540
1581
  return output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
1541
1582
  }
1583
+ /**
1584
+ * Build the per-call base for every `tool:*` hook ctx (and the
1585
+ * matching shape for `mcp:tool:*`). Centralized so the `runId` /
1586
+ * `parentRunId` / `depth` identity fields land uniformly on every
1587
+ * event the loop fires — without one helper they drift across ~14
1588
+ * inline construction sites. The returned object IS the
1589
+ * {@link ToolHookContext} canonical shape; specialized hook payloads
1590
+ * (`gateCtx`, `transformCtx`, etc.) spread it and append their own
1591
+ * fields.
1592
+ */
1593
+ function buildToolHookBase(ctx, turnId, callId, name, displayName, input) {
1594
+ return {
1595
+ turnId,
1596
+ callId,
1597
+ name,
1598
+ displayName,
1599
+ input,
1600
+ ...ctx.runId !== void 0 ? { runId: ctx.runId } : {},
1601
+ ...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
1602
+ ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
1603
+ };
1604
+ }
1542
1605
  async function executeSingleTool(ctx, call, turnId) {
1543
1606
  const toolDef = ctx.tools[call.name];
1544
1607
  const callId = call.id;
1545
1608
  const displayName = toWireName(call.name, ctx.aliasMaps);
1546
1609
  const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
1547
1610
  const gateCtx = {
1548
- turnId,
1549
- callId,
1550
- name: call.name,
1551
- displayName,
1552
- input: call.input,
1611
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, call.input),
1553
1612
  block: false,
1554
1613
  reason: "Tool execution was blocked",
1555
1614
  runToolCounts
@@ -1602,11 +1661,7 @@ async function executeSingleTool(ctx, call, turnId) {
1602
1661
  let effectiveInput = gateCtx.input;
1603
1662
  if (!toolDef) {
1604
1663
  const unknownCtx = {
1605
- turnId,
1606
- callId,
1607
- name: call.name,
1608
- displayName,
1609
- input: effectiveInput,
1664
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1610
1665
  suppressError: false
1611
1666
  };
1612
1667
  await ctx.hooks.callHook("tool:unknown", unknownCtx);
@@ -1615,11 +1670,7 @@ async function executeSingleTool(ctx, call, turnId) {
1615
1670
  if (!unknownCtx.suppressError) {
1616
1671
  const err = /* @__PURE__ */ new Error(`Unknown tool: ${call.name}`);
1617
1672
  await ctx.hooks.callHook("tool:error", {
1618
- turnId,
1619
- callId,
1620
- name: call.name,
1621
- displayName,
1622
- input: effectiveInput,
1673
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1623
1674
  error: err
1624
1675
  });
1625
1676
  }
@@ -1641,11 +1692,7 @@ async function executeSingleTool(ctx, call, turnId) {
1641
1692
  const validation = validateToolArgs(effectiveInput, toolDef.spec.inputSchema);
1642
1693
  if (!validation.valid) {
1643
1694
  await ctx.hooks.callHook("validation:reject", {
1644
- turnId,
1645
- callId,
1646
- name: call.name,
1647
- displayName,
1648
- input: effectiveInput,
1695
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1649
1696
  reason: validation.error ?? "invalid input",
1650
1697
  schema: toolDef.spec.inputSchema
1651
1698
  });
@@ -1667,11 +1714,7 @@ async function executeSingleTool(ctx, call, turnId) {
1667
1714
  effectiveInput = validation.coercedInput ?? effectiveInput;
1668
1715
  const coercions = validation.coercions && validation.coercions.length > 0 ? validation.coercions : void 0;
1669
1716
  if (coercions) await ctx.hooks.callHook("validation:coerce", {
1670
- turnId,
1671
- callId,
1672
- name: call.name,
1673
- displayName,
1674
- input: effectiveInput,
1717
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1675
1718
  coercions,
1676
1719
  schema: toolDef.spec.inputSchema
1677
1720
  });
@@ -1685,11 +1728,7 @@ async function executeSingleTool(ctx, call, turnId) {
1685
1728
  runToolCounts
1686
1729
  });
1687
1730
  await ctx.hooks.callHook("tool:before", {
1688
- turnId,
1689
- callId,
1690
- name: call.name,
1691
- displayName,
1692
- input: effectiveInput,
1731
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1693
1732
  runToolCounts,
1694
1733
  ...coercions ? { coercions } : {}
1695
1734
  });
@@ -1712,18 +1751,16 @@ async function executeSingleTool(ctx, call, turnId) {
1712
1751
  turnId,
1713
1752
  callId,
1714
1753
  runId: ctx.runId,
1754
+ ...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
1715
1755
  ...ctx.session ? { session: ctx.session } : {},
1756
+ ...ctx.readState ? { readState: ctx.readState } : {},
1716
1757
  ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
1717
1758
  };
1718
1759
  output = await toolDef.execute(effectiveInput, toolCtx);
1719
1760
  } catch (err) {
1720
1761
  const error = err instanceof Error ? err : new Error(String(err));
1721
1762
  const errorCtx = {
1722
- turnId,
1723
- callId,
1724
- name: call.name,
1725
- displayName,
1726
- input: effectiveInput,
1763
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1727
1764
  error
1728
1765
  };
1729
1766
  await ctx.hooks.callHook("tool:error", errorCtx);
@@ -1761,9 +1798,11 @@ async function executeSingleTool(ctx, call, turnId) {
1761
1798
  * across five call sites.
1762
1799
  */
1763
1800
  async function fireDispatched(ctx, params) {
1764
- const { reason, ...rest } = params;
1801
+ const { turnId, callId, name, displayName, input, outcome, reason, runToolCounts } = params;
1765
1802
  await ctx.hooks.callHook("tool:dispatched", {
1766
- ...rest,
1803
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1804
+ outcome,
1805
+ runToolCounts,
1767
1806
  ...reason !== void 0 ? { reason } : {}
1768
1807
  });
1769
1808
  }
@@ -1784,11 +1823,7 @@ async function emitToolResult(ctx, params) {
1784
1823
  let output = params.output;
1785
1824
  let isError = params.isError;
1786
1825
  const transformCtx = {
1787
- turnId,
1788
- callId,
1789
- name,
1790
- displayName,
1791
- input,
1826
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1792
1827
  result: output,
1793
1828
  isError,
1794
1829
  outputBytes: toolOutputByteLength(output),
@@ -1813,11 +1848,7 @@ async function emitToolResult(ctx, params) {
1813
1848
  }
1814
1849
  output = stripImagesForNonVision(ctx.provider, output);
1815
1850
  await ctx.hooks.callHook("tool:after", {
1816
- turnId,
1817
- callId,
1818
- name,
1819
- displayName,
1820
- input,
1851
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1821
1852
  result: output,
1822
1853
  outputBytes: toolOutputByteLength(output),
1823
1854
  runToolCounts,
@@ -2745,7 +2776,7 @@ function initialRunCounter(session) {
2745
2776
  for (const t of session.turns) consider(t.runId);
2746
2777
  return max;
2747
2778
  }
2748
- function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
2779
+ function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, readState: agentReadState, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
2749
2780
  const hooks = createHooks();
2750
2781
  const executionContext = execution ?? createProcessContext();
2751
2782
  const sourceTools = agentTools ?? {};
@@ -3061,6 +3092,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
3061
3092
  maxTurns,
3062
3093
  maxTokens,
3063
3094
  ...session ? { session } : {},
3095
+ ...agentReadState ? { readState: agentReadState } : {},
3096
+ ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
3064
3097
  depth: runDepth,
3065
3098
  thinkingBudget,
3066
3099
  schema,
@@ -3624,12 +3657,14 @@ const edit = {
3624
3657
  } catch {
3625
3658
  return `Edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
3626
3659
  }
3627
- if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
3628
- const readState = getReadState(ctx.session);
3629
- const absKey = `${ctx.handle.cwd}::${target}`;
3630
- const prior = readState?.get(absKey);
3631
- if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
3632
- if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
3660
+ if (ctx.behavior?.requireReadBeforeEdit) {
3661
+ const readState = resolveReadStateMap(ctx);
3662
+ if (readState) {
3663
+ const absKey = readStateKey(ctx.handle.cwd, target);
3664
+ const prior = readState.get(absKey);
3665
+ if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
3666
+ if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
3667
+ }
3633
3668
  }
3634
3669
  const match = resolveOldString(original, find);
3635
3670
  if (!match) {
@@ -3642,11 +3677,11 @@ const edit = {
3642
3677
  const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
3643
3678
  if (updated === original) return `Edit error: replacement produced no change in ${target}.`;
3644
3679
  await ctx.execution.writeFile(ctx.handle, target, updated);
3645
- if (ctx.session) {
3646
- const readState = getReadState(ctx.session);
3647
- const absKey = `${ctx.handle.cwd}::${target}`;
3648
- const prior = readState?.get(absKey);
3649
- if (readState && prior) readState.set(absKey, {
3680
+ const readState = resolveReadStateMap(ctx);
3681
+ if (readState) {
3682
+ const absKey = readStateKey(ctx.handle.cwd, target);
3683
+ const prior = readState.get(absKey);
3684
+ if (prior) readState.set(absKey, {
3650
3685
  ...prior,
3651
3686
  contentHash: hashContent(updated),
3652
3687
  mtimeMs: Date.now()
@@ -4085,12 +4120,14 @@ const multiEdit = {
4085
4120
  } catch {
4086
4121
  return `multi_edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
4087
4122
  }
4088
- if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
4089
- const readState = getReadState(ctx.session);
4090
- const absKey = `${ctx.handle.cwd}::${target}`;
4091
- const prior = readState?.get(absKey);
4092
- if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
4093
- if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
4123
+ if (ctx.behavior?.requireReadBeforeEdit) {
4124
+ const readState = resolveReadStateMap(ctx);
4125
+ if (readState) {
4126
+ const absKey = readStateKey(ctx.handle.cwd, target);
4127
+ const prior = readState.get(absKey);
4128
+ if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
4129
+ if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
4130
+ }
4094
4131
  }
4095
4132
  let totalReplacements = 0;
4096
4133
  for (let i = 0; i < steps.length; i++) {
@@ -4110,11 +4147,11 @@ const multiEdit = {
4110
4147
  totalReplacements += occurrences;
4111
4148
  }
4112
4149
  await ctx.execution.writeFile(ctx.handle, target, current);
4113
- if (ctx.session) {
4114
- const readState = getReadState(ctx.session);
4115
- const absKey = `${ctx.handle.cwd}::${target}`;
4116
- const prior = readState?.get(absKey);
4117
- if (readState && prior) readState.set(absKey, {
4150
+ const readState = resolveReadStateMap(ctx);
4151
+ if (readState) {
4152
+ const absKey = readStateKey(ctx.handle.cwd, target);
4153
+ const prior = readState.get(absKey);
4154
+ if (prior) readState.set(absKey, {
4118
4155
  ...prior,
4119
4156
  contentHash: hashContent(current),
4120
4157
  mtimeMs: Date.now()
@@ -4264,14 +4301,16 @@ const readFile$1 = {
4264
4301
  return `File not found: ${path}.${await suggestionFor(ctx.execution, ctx.handle, path)}`;
4265
4302
  }
4266
4303
  const totalBytes = Buffer.byteLength(raw);
4267
- const readState = ctx.behavior?.dedupReads !== false ? getReadState(ctx.session) : void 0;
4268
- const absKey = `${ctx.handle.cwd}::${path}`;
4304
+ const dedupEnabled = ctx.behavior?.dedupReads !== false;
4305
+ const gateEnabled = ctx.behavior?.requireReadBeforeEdit === true;
4306
+ const readState = dedupEnabled || gateEnabled ? resolveReadStateMap(ctx) : void 0;
4307
+ const absKey = readStateKey(ctx.handle.cwd, path);
4269
4308
  const offsetForKey = normalizeInteger(offset, 1);
4270
4309
  const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
4271
4310
  const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
4272
4311
  const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
4273
4312
  const currentHash = readState ? hashContent(raw) : "";
4274
- if (readState) {
4313
+ if (dedupEnabled && readState) {
4275
4314
  const prior = readState.get(absKey);
4276
4315
  if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers) return `File ${path} unchanged since the previous read in this session — the prior result is still current.`;
4277
4316
  }
@@ -4717,18 +4756,23 @@ function createSpawnTool(options = {}) {
4717
4756
  let result = "";
4718
4757
  let unbubble;
4719
4758
  try {
4720
- const agent = createAgent({
4759
+ const parentPreset = {
4721
4760
  ...ctx.name !== void 0 ? { name: ctx.name } : {},
4722
4761
  ...ctx.system !== void 0 ? { system: ctx.system } : {},
4723
4762
  tools: ctx.tools,
4724
4763
  ...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
4725
4764
  ...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
4726
4765
  ...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
4727
- ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {},
4766
+ ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
4767
+ };
4768
+ const sharedReadState = options.shareReadState ? resolveReadStateMap(ctx) : void 0;
4769
+ const agent = createAgent({
4770
+ ...parentPreset,
4728
4771
  ...options.preset,
4729
4772
  provider: ctx.provider,
4730
4773
  execution: ctx.execution,
4731
- ...options.persist && ctx.session ? { session: ctx.session } : {}
4774
+ ...options.persist && ctx.session ? { session: ctx.session } : {},
4775
+ ...sharedReadState ? { readState: sharedReadState } : {}
4732
4776
  });
4733
4777
  if (forwardHooks) {
4734
4778
  const unregisterEnricher = agent.hooks.hook("tool:before", async (toolCtx) => {
@@ -4903,6 +4947,6 @@ const writeFile$1 = {
4903
4947
  }
4904
4948
  };
4905
4949
  //#endregion
4906
- export { PERSISTENCE_PREVIEW_BYTES as C, resolvePersistDir as D, maybePersistToolResult as E, getReadState as O, PERSISTED_STUB_PREFIX as S, cleanupPersistedSession as T, createSkillsReadTool as _, multiEdit as a, TOOL_USE_SKIPPED_MESSAGE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, buildPersistedStub as w, validateToolArgs as x, TOOL_USE_AFTER_ERROR_MESSAGE as y };
4950
+ export { readStateKey as A, PERSISTENCE_PREVIEW_BYTES as C, resolvePersistDir as D, maybePersistToolResult as E, getReadState as O, PERSISTED_STUB_PREFIX as S, cleanupPersistedSession as T, createSkillsReadTool as _, multiEdit as a, TOOL_USE_SKIPPED_MESSAGE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveReadStateMap as j, hashContent as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, buildPersistedStub as w, validateToolArgs as x, TOOL_USE_AFTER_ERROR_MESSAGE as y };
4907
4951
 
4908
- //# sourceMappingURL=tools-CWEDS2ZT.js.map
4952
+ //# sourceMappingURL=tools-CCsL5SCO.js.map