@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.
- package/dist/agents/send-message-tool.js +15 -20
- package/dist/agents/subagent.js +4 -2
- package/dist/core/consolidate-scope.js +4 -2
- package/dist/core/context-edit.js +8 -12
- package/dist/core/memory.d.ts +4 -2
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/runner/prepare-task.d.ts +1 -0
- package/dist/core/runner/prepare-task.js +18 -12
- package/dist/core/session-store.d.ts +3 -0
- package/dist/core/session-store.js +10 -0
- package/dist/core/session.d.ts +2 -0
- package/dist/core/tool-result-budget.js +2 -0
- package/dist/engine/session/session.js +2 -1
- package/dist/tools/fs/fs-write.js +2 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +1 -1
- package/package.json +1 -1
|
@@ -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 ${
|
|
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
|
|
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:
|
|
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
|
|
422
|
-
const
|
|
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 ${
|
|
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,
|
|
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
|
|
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 =
|
|
532
|
+
const safeSummary = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary);
|
|
538
533
|
const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
|
|
539
|
-
const marker = await resume(`${fromPrefix}
|
|
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,
|
|
538
|
+
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, summary },
|
|
544
539
|
};
|
|
545
540
|
}
|
|
546
541
|
catch (e) {
|
package/dist/agents/subagent.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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) ||
|
|
27
|
-
|
|
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
|
|
80
|
-
const clearable =
|
|
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(
|
|
91
|
-
const
|
|
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 (
|
|
98
|
+
if (target.toolCallId && fullText.trim().length > 0) {
|
|
100
99
|
try {
|
|
101
|
-
ref = refNote(opts.offload.persist(
|
|
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
|
}
|
package/dist/core/memory.d.ts
CHANGED
|
@@ -47,8 +47,10 @@ export interface MemoryNoteHeader {
|
|
|
47
47
|
export interface MemoryNoteRecord extends MemoryNoteHeader {
|
|
48
48
|
text: string;
|
|
49
49
|
}
|
|
50
|
-
export
|
|
51
|
-
export declare function
|
|
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):
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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;
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -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;
|