@sema-agent/core 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,9 @@ import { createSubagentResume } from "./subagent.js";
8
8
  export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
9
9
  let uplinkSeqGlobal = Date.now();
10
10
  const UPLINK_RESULT_MAX = 8000;
11
+ function clipCarrierMessage(text) {
12
+ return text.length > UPLINK_RESULT_MAX ? `${text.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${text.length} chars total]` : text;
13
+ }
11
14
  export const SEND_MESSAGE_SUMMARY_MAX = 200;
12
15
  export function clipSendMessageSummary(raw) {
13
16
  return raw.length > SEND_MESSAGE_SUMMARY_MAX
@@ -89,6 +92,7 @@ export function createSendMessageTool(opts) {
89
92
  isError: true,
90
93
  };
91
94
  }
95
+ const summary = clipSendMessageSummary(summaryArg);
92
96
  const senderId = ctx.taskId ?? opts.owner;
93
97
  const [parentTaskId, parentSessionId] = ctx.parentTaskId !== undefined ? [ctx.parentTaskId, ctx.parentSessionId] : [opts.parentTaskId, opts.parentSessionId];
94
98
  const senderName = ctx.spawnedAgentName ?? opts.senderName;
@@ -102,14 +106,12 @@ export function createSendMessageTool(opts) {
102
106
  }
103
107
  if (normalizeAgentName(to) === "main") {
104
108
  if (opts.uplink && senderId !== undefined) {
105
- const uplinkSummaryRaw = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : message.slice(0, 80);
106
- const fromLabel = senderName ?? senderId;
107
109
  try {
108
110
  opts.uplink({
109
111
  task_id: senderId,
110
112
  task_type: "background_agent",
111
113
  status: "event",
112
- summary: `message from ${fromLabel}: ${uplinkSummaryRaw}`,
114
+ summary: `message from ${senderLabel}: ${summary}`,
113
115
  result: message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[uplink truncated: ${message.length} chars total — read the agent's transcript for the rest]` : message,
114
116
  seq: ++uplinkSeqGlobal,
115
117
  }, { priority: "next" });
@@ -270,11 +272,9 @@ export function createSendMessageTool(opts) {
270
272
  isError: true,
271
273
  };
272
274
  }
273
- const clipped = message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]` : message;
274
- const t3Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
275
- const t3From = senderLabel;
275
+ const clipped = clipCarrierMessage(message);
276
276
  try {
277
- await opts.mailbox.append(scope, handle, { from: t3From, content: t3Summary !== undefined ? `[${t3Summary}] ${clipped}` : clipped, sentAt: now });
277
+ await opts.mailbox.append(scope, handle, { from: senderLabel, content: `[${summary}] ${clipped}`, sentAt: now });
278
278
  }
279
279
  catch (e) {
280
280
  await rollback();
@@ -418,17 +418,13 @@ export function createSendMessageTool(opts) {
418
418
  }
419
419
  if (row.status === "running" || row.status === "pending") {
420
420
  return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
421
- const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
422
- const fromLabel = senderLabel;
423
- const s2Clipped = message.length > UPLINK_RESULT_MAX
424
- ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]`
425
- : message;
426
- const teammateXml = frameTeammateMessage({ from: fromLabel, ...(s2Summary !== undefined ? { summary: s2Summary } : {}), text: s2Clipped });
421
+ const s2Clipped = clipCarrierMessage(message);
422
+ const teammateXml = frameTeammateMessage({ from: senderLabel, summary, text: s2Clipped });
427
423
  const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
428
424
  task_id: senderLabel,
429
425
  task_type: "background_agent",
430
426
  status: "event",
431
- summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
427
+ summary: `message from ${senderLabel}: ${summary}`,
432
428
  result: teammateXml,
433
429
  seq: ++uplinkSeqGlobal,
434
430
  }, { priority: "next" });
@@ -440,7 +436,7 @@ export function createSendMessageTool(opts) {
440
436
  : `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — you will be notified of its completion either way; resend then if it went unanswered. Continue with other work; do not poll.`;
441
437
  return {
442
438
  content: receiptText,
443
- details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, ...(s2Summary !== undefined ? { summary: s2Summary } : {}) },
439
+ details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, summary },
444
440
  };
445
441
  }
446
442
  if (delivered.reason === "no_channel") {
@@ -531,16 +527,15 @@ export function createSendMessageTool(opts) {
531
527
  ...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
532
528
  ...(opts.notify ? { currentParentNotify: opts.notify } : {}),
533
529
  });
534
- const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
535
- const fromPrefix = parentTaskId !== undefined ? `(message from teammate "${senderName ?? senderId ?? "unknown"}")\n` : "";
530
+ const fromPrefix = senderIsChild ? `(message from teammate "${senderLabel}")\n` : "";
536
531
  try {
537
- const safeSummary = summary !== undefined ? escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary) : undefined;
532
+ const safeSummary = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary);
538
533
  const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
539
- const marker = await resume(`${fromPrefix}${safeSummary ? `[${safeSummary}] ${safeMessage}` : safeMessage}`);
534
+ const marker = await resume(`${fromPrefix}[${safeSummary}] ${safeMessage}`);
540
535
  return {
541
536
  content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
542
537
  `You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
543
- details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, ...(summary !== undefined ? { summary } : {}) },
538
+ details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, summary },
544
539
  };
545
540
  }
546
541
  catch (e) {
@@ -1372,8 +1372,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1372
1372
  const editedFiles = stepRecorder.editedFiles();
1373
1373
  return { ...(recentSteps ? { recentSteps } : {}), ...(editedFiles ? { editedFiles } : {}) };
1374
1374
  };
1375
+ const treeScope = reviveClaim?.row.scope ?? ctx.principal ?? opts.background?.scope;
1375
1376
  const childInternals = {
1376
1377
  ...(inheritedManifestScope ? { inheritedManifestScope } : {}),
1378
+ ...(treeScope !== undefined ? { registryScope: treeScope } : {}),
1377
1379
  ...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
1378
1380
  ...(childDefaultPersona !== undefined ? { defaultSystemPrompt: childDefaultPersona } : {}),
1379
1381
  isDelegatedChild: true,
@@ -1632,7 +1634,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1632
1634
  }
1633
1635
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
1634
1636
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
1635
- const bgScope = ctx.principal ?? bg.scope;
1637
+ const bgScope = treeScope;
1636
1638
  let taskId;
1637
1639
  try {
1638
1640
  taskId = bg.registry.registerBackgroundAgent({
@@ -2037,7 +2039,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2037
2039
  }
2038
2040
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2039
2041
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2040
- const bgScope = reviveRow !== undefined ? reviveRow.scope : ctx.principal ?? bg.scope;
2042
+ const bgScope = treeScope;
2041
2043
  let taskId;
2042
2044
  try {
2043
2045
  taskId = bg.registry.registerBackgroundAgent(reviveRow !== undefined
@@ -23,8 +23,10 @@ export async function consolidateScope(scope, deps, opts = {}) {
23
23
  "(getConsolidationCursor/setConsolidationCursor) — skipped (no-op), NOT degraded to a full re-consolidation"));
24
24
  return undefined;
25
25
  }
26
- if (!supportsConsolidation(store) || typeof store.listStructuredNotes !== "function") {
27
- onWarn?.(new Error("consolidateScope: store is not id-addressable / has no manifest — skipped (no-op)"));
26
+ if (!supportsConsolidation(store) ||
27
+ typeof store.listStructuredNotes !== "function" ||
28
+ typeof store.getByIds !== "function") {
29
+ onWarn?.(new Error("consolidateScope: store is not id-addressable / has no manifest read pair — skipped (no-op)"));
28
30
  return undefined;
29
31
  }
30
32
  const release = deps.acquire ? await deps.acquire(scope) : () => { };
@@ -76,29 +76,28 @@ export function clearStaleToolResults(messages, opts) {
76
76
  }
77
77
  const keep = opts.keepRecentToolResults ?? 3;
78
78
  const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
79
- const toolResultIdx = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName ?? "") ? [i] : []);
80
- const clearable = toolResultIdx.slice(0, Math.max(0, toolResultIdx.length - keep));
79
+ const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
80
+ const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
81
81
  if (clearable.length === 0) {
82
82
  return messages;
83
83
  }
84
84
  const out = messages.slice();
85
85
  let current = total;
86
- for (const idx of clearable) {
86
+ for (const { idx, target } of clearable) {
87
87
  if (current <= opts.budgetTokens) {
88
88
  break;
89
89
  }
90
- const before = estimateTokens(out[idx], cpt);
91
- const msg = out[idx];
92
- const rawContent = Array.isArray(msg.content) ? msg.content : [];
90
+ const before = estimateTokens(target, cpt);
91
+ const rawContent = Array.isArray(target.content) ? target.content : [];
93
92
  let ref;
94
93
  if (opts.offload) {
95
94
  const fullText = rawContent
96
95
  .filter((c) => c?.type === "text" && typeof c.text === "string")
97
96
  .map((c) => c.text)
98
97
  .join("\n");
99
- if (msg.toolCallId && fullText.trim().length > 0) {
98
+ if (target.toolCallId && fullText.trim().length > 0) {
100
99
  try {
101
- ref = refNote(opts.offload.persist(msg.toolCallId, fullText));
100
+ ref = refNote(opts.offload.persist(target.toolCallId, fullText));
102
101
  }
103
102
  catch {
104
103
  ref = undefined;
@@ -107,10 +106,7 @@ export function clearStaleToolResults(messages, opts) {
107
106
  }
108
107
  const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
109
108
  const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
110
- const cleared = {
111
- ...out[idx],
112
- content: [{ type: "text", text: marker }],
113
- };
109
+ const cleared = { ...target, content: [{ type: "text", text: marker }] };
114
110
  out[idx] = cleared;
115
111
  current -= idx > anchorIdx ? before - estimateTokens(cleared, cpt) : 0;
116
112
  }
@@ -47,8 +47,10 @@ export interface MemoryNoteHeader {
47
47
  export interface MemoryNoteRecord extends MemoryNoteHeader {
48
48
  text: string;
49
49
  }
50
- export declare function supportsConsolidation(store: MemoryStore): boolean;
51
- export declare function supportsPeriodicConsolidation(store: MemoryStore): boolean;
50
+ export type ConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "searchScored" | "update" | "delete">>;
51
+ export declare function supportsConsolidation(store: MemoryStore): store is ConsolidationCapableStore;
52
+ export type PeriodicConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "getConsolidationCursor" | "setConsolidationCursor">>;
53
+ export declare function supportsPeriodicConsolidation(store: MemoryStore): store is PeriodicConsolidationCapableStore;
52
54
  export declare const CALLER_AUTHORED_TYPES: ReadonlySet<string>;
53
55
  export declare const DEFAULT_CALLER_AUTHORED_TYPE: MemoryNoteType;
54
56
  export type UtilityGate = (stats: unknown) => boolean;
@@ -1,3 +1,4 @@
1
1
  import type { AgentMessage } from "../internal/harness-types.js";
2
+ import type { ToolResultMessage } from "../internal/llm.js";
2
3
  export declare function messageRole(m: AgentMessage): string | undefined;
3
- export declare function isToolResult(m: AgentMessage): boolean;
4
+ export declare function isToolResult(m: AgentMessage): m is ToolResultMessage;
@@ -308,6 +308,7 @@ export interface RunInternals {
308
308
  parentTaskId?: string;
309
309
  parentSessionId?: string;
310
310
  rootSessionId?: string;
311
+ registryScope?: string;
311
312
  parentCenterArtifactDigest?: string;
312
313
  parentCenterSourceRevision?: string;
313
314
  promptProfile?: "simple" | "classic";
@@ -409,7 +409,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
409
409
  }));
410
410
  }
411
411
  }
412
- const taskScope = spec.principal ?? "default";
412
+ const taskScope = internals?.registryScope ?? spec.principal ?? "default";
413
413
  defaultTaskRegistry.maybeGc();
414
414
  const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
415
415
  const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
@@ -618,6 +618,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
618
618
  }
619
619
  const rewindNotes = [];
620
620
  const rewindCaptureRequested = spec.rewindFiles === true;
621
+ if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
622
+ const e = new Error(`rewind-files: resumeAt ("${spec.resumeAt}") and rewindFilesTo ("${spec.rewindFilesTo}") were both set — resumeAt already anchors the file restore when rewindFiles is true, so a separate rewindFilesTo target is a conflicting request. Drop one of the two.`);
623
+ e.code = "rewind.conflicting_targets";
624
+ throw e;
625
+ }
621
626
  let rewindTarget = spec.resumeAt !== undefined ? (rewindCaptureRequested ? spec.resumeAt : undefined) : spec.rewindFilesTo;
622
627
  const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
623
628
  if (spec.resumeAt !== undefined && !rewindCaptureRequested) {
@@ -1833,15 +1838,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1833
1838
  }
1834
1839
  const activeTools = new Set();
1835
1840
  const fpRef = {};
1836
- if (rebuildHarnessToolsRef.current === undefined) {
1837
- rebuildHarnessToolsRef.current = async () => {
1838
- const list = [...tools];
1839
- await harnessRef.current.setTools(list, list.map((t) => t.name));
1840
- if (fpRef.current)
1841
- fpRef.current.tools = toolsToFingerprintInputs(list);
1842
- turnSnapshotRef.current?.refreshTools(toolsToFingerprintInputs(list));
1843
- };
1844
- }
1845
1841
  const turnSnapshotRef = {};
1846
1842
  let harnessTools = tools;
1847
1843
  const failedMcpServers = mcp.statuses
@@ -1908,7 +1904,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1908
1904
  if (deferred.has(n))
1909
1905
  activeTools.add(n);
1910
1906
  }
1911
- offloadReachableToolsRef.current = () => {
1907
+ const callableToolNames = () => {
1912
1908
  const s = new Set(tools.map((t) => t.name));
1913
1909
  for (const n of deferred)
1914
1910
  if (!activeTools.has(n))
@@ -1916,6 +1912,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1916
1912
  s.add(TOOL_SEARCH_NAME);
1917
1913
  return s;
1918
1914
  };
1915
+ offloadReachableToolsRef.current = callableToolNames;
1919
1916
  let toolSearch;
1920
1917
  const buildToolList = (active) => {
1921
1918
  const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
@@ -1957,11 +1954,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1957
1954
  active: activeTools,
1958
1955
  rematerialize,
1959
1956
  listingRide: (newly) => listingRideRef.current?.(newly),
1960
- mountedNames: () => new Set(buildToolList(activeTools).map((t) => t.name).filter((n) => !registry.has(n) || activeTools.has(n))),
1957
+ mountedNames: callableToolNames,
1961
1958
  directCallEnabled: spec.deferSelfResolve !== false,
1962
1959
  });
1963
1960
  harnessTools = buildToolList(activeTools);
1964
1961
  }
1962
+ else {
1963
+ rebuildHarnessToolsRef.current = async () => {
1964
+ const list = [...tools];
1965
+ await harnessRef.current.setTools(list, list.map((t) => t.name));
1966
+ if (fpRef.current)
1967
+ fpRef.current.tools = toolsToFingerprintInputs(list);
1968
+ turnSnapshotRef.current?.refreshTools(toolsToFingerprintInputs(list));
1969
+ };
1970
+ }
1965
1971
  if (spec.agents !== undefined && spec.agents.length > 0) {
1966
1972
  const known = new Set(tools.flatMap((t) => [canonicalToolName(t.name), ...(t.aliases ?? []).map((a) => canonicalToolName(a))]));
1967
1973
  for (const def of spec.agents) {
@@ -12,6 +12,7 @@ export interface TtlSessionStoreOptions {
12
12
  export declare class TtlSessionStore implements SessionStore {
13
13
  private repo;
14
14
  private entries;
15
+ private owners;
15
16
  private pending;
16
17
  private pinned;
17
18
  private defaultTtlMs;
@@ -31,6 +32,8 @@ export declare class TtlSessionStore implements SessionStore {
31
32
  forget(sessionId: string): void;
32
33
  pin(sessionId: string): void;
33
34
  unpin(sessionId: string): void;
35
+ ownerOf(sessionId: string): Promise<string | null | undefined>;
36
+ register(sessionId: string, owner: string | null): Promise<void>;
34
37
  sweep(now?: number): void;
35
38
  get size(): number;
36
39
  dispose(): void;
@@ -6,6 +6,7 @@ export { SESSION_DEFAULT_TTL_DAYS };
6
6
  export class TtlSessionStore {
7
7
  repo;
8
8
  entries = new Map();
9
+ owners = new Map();
9
10
  pending = new Map();
10
11
  pinned = new Set();
11
12
  defaultTtlMs;
@@ -132,6 +133,7 @@ export class TtlSessionStore {
132
133
  this.entries.delete(sessionId);
133
134
  if (this.evictPolicy === "delete") {
134
135
  await this.repo.delete(await e.session.getMetadata());
136
+ this.owners.delete(sessionId);
135
137
  }
136
138
  }
137
139
  forget(sessionId) {
@@ -143,6 +145,13 @@ export class TtlSessionStore {
143
145
  unpin(sessionId) {
144
146
  this.pinned.delete(sessionId);
145
147
  }
148
+ async ownerOf(sessionId) {
149
+ return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined;
150
+ }
151
+ async register(sessionId, owner) {
152
+ if (!this.owners.has(sessionId))
153
+ this.owners.set(sessionId, owner);
154
+ }
146
155
  sweep(now = Date.now()) {
147
156
  for (const [id, e] of this.entries) {
148
157
  if (this.pinned.has(id)) {
@@ -152,6 +161,7 @@ export class TtlSessionStore {
152
161
  this.entries.delete(id);
153
162
  if (this.evictPolicy === "delete") {
154
163
  void this.repo.delete({ id, createdAt: "" });
164
+ this.owners.delete(id);
155
165
  }
156
166
  }
157
167
  }
@@ -31,6 +31,8 @@ export interface SessionStore {
31
31
  noteTaskRun?(sessionId: string, taskId: string): void | Promise<void>;
32
32
  list?(): Promise<SessionStoreSummary[]>;
33
33
  fork?(sourceId: string, owner?: string | null): Promise<string | null>;
34
+ ownerOf?(sessionId: string): Promise<string | null | undefined>;
35
+ register?(sessionId: string, owner: string | null): Promise<void>;
34
36
  readonly size: number;
35
37
  dispose(): void | Promise<void>;
36
38
  }
@@ -21,6 +21,8 @@ function isOffloadedPreview(content) {
21
21
  return first?.type === "text" && typeof first.text === "string" && first.text.startsWith(PERSISTED_OUTPUT_PREFIX);
22
22
  }
23
23
  function replaceTextBlock(m, text) {
24
+ if (!isToolResult(m))
25
+ return m;
24
26
  const images = m.content.filter((b) => b.type !== "text");
25
27
  return { ...m, content: [{ type: "text", text }, ...images] };
26
28
  }
@@ -34,8 +34,9 @@ export function buildSessionContext(pathEntries, opts) {
34
34
  }
35
35
  }
36
36
  const messages = [];
37
+ const isAssistantMessage = (m) => m.role === "assistant";
37
38
  const stripAssistantUsage = (message) => {
38
- if (message.role !== "assistant") {
39
+ if (!isAssistantMessage(message)) {
39
40
  return message;
40
41
  }
41
42
  return {
@@ -152,6 +152,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
152
152
  return errorResult(`Error (Edit): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`);
153
153
  const original = decoded.text;
154
154
  const entry = state.get(r.key);
155
+ const readWasTruncated = entry?.truncated === true;
155
156
  const stale = checkStale(entry, sha256(original));
156
157
  if (stale)
157
158
  return errorResult(violationText("Edit", stale));
@@ -180,7 +181,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
180
181
  oldS = resolvedEsc;
181
182
  }
182
183
  }
183
- const match = checkEditMatch(working, oldS, e.replace_all === true, entry.truncated);
184
+ const match = checkEditMatch(working, oldS, e.replace_all === true, readWasTruncated);
184
185
  if (match) {
185
186
  const escNote = match.code === "ambiguous_edit" && escapeMatchWasAttempted(e.old_string) && !working.includes(oldS) ? ESCAPE_MATCH_MISS_NOTE : "";
186
187
  return errorResult(batch ? `Error (Edit): ${where}${match.message}${escNote} (no changes written — the batch is atomic).` : `${violationText("Edit", match)}${escNote}`);
@@ -58,7 +58,7 @@ export declare const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read to
58
58
  export declare const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view \u2014 the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
59
59
  export declare const WRITE_ENCODING_DEADLOCK_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead \u2014 e.g. `rm` + rewrite, or `iconv`.)";
60
60
  export declare function checkNoChange(oldString: string, newString: string): FsViolation | undefined;
61
- export declare function checkStale(entry: ReadEntry, currentHash: string): FsViolation | undefined;
61
+ export declare function checkStale(entry: ReadEntry | undefined, currentHash: string): FsViolation | undefined;
62
62
  export declare function countOccurrences(haystack: string, needle: string): number;
63
63
  export declare function similarNameSuggestion(siblingNames: readonly string[], missingName: string): string | undefined;
64
64
  export declare function normalizeQuotes(s: string): string;
@@ -276,7 +276,7 @@ export function checkNoChange(oldString, newString) {
276
276
  return undefined;
277
277
  }
278
278
  export function checkStale(entry, currentHash) {
279
- if (entry.hash !== currentHash) {
279
+ if (entry === undefined || entry.hash !== currentHash) {
280
280
  return { code: "stale", message: "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it." };
281
281
  }
282
282
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",