@sema-agent/core 2.10.0 → 2.12.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/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +3 -0
- package/dist/core/consolidate-scope.js +4 -2
- package/dist/core/context-edit.js +8 -12
- package/dist/core/mcp.d.ts +2 -0
- package/dist/core/mcp.js +89 -6
- package/dist/core/memory.d.ts +4 -2
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/remote-env.d.ts +3 -0
- package/dist/core/remote-env.js +19 -1
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +5 -0
- package/dist/core/runner/prepare-task.js +126 -34
- package/dist/core/runner/runtask.js +22 -2
- 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/core/types.d.ts +9 -0
- package/dist/engine/harness/types.d.ts +1 -1
- package/dist/engine/session/session.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tools/fs/fs-bash.js +14 -0
- 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/dist/tools/scheduler-tools.js +29 -15
- package/package.json +1 -1
|
@@ -49,7 +49,8 @@ import { capAggregateToolResults } from "../tool-result-budget.js";
|
|
|
49
49
|
import { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "../media-byte-cap.js";
|
|
50
50
|
import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
|
|
51
51
|
import { StubExecutionEnv } from "../stub-env.js";
|
|
52
|
-
import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable } from "../remote-env.js";
|
|
52
|
+
import { hasDestroy, isIsolated, isRemoteExecutionEnv, isRetryableRemoteErrorCode, isSuspendable, missingRestoreSurface, RETRYABLE_REMOTE_ERROR_CODES } from "../remote-env.js";
|
|
53
|
+
import { withRetry } from "../with-retry.js";
|
|
53
54
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
54
55
|
import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
|
|
55
56
|
import { createMonitorTool } from "../../tools/monitor.js";
|
|
@@ -173,6 +174,27 @@ export function rebaseWorkspacePath(p, fromRaw, toRaw) {
|
|
|
173
174
|
const suffix = p.slice(fromPrefix.length).replace(/^\/+/, "");
|
|
174
175
|
return to === "/" ? `${to}${suffix}` : `${to}/${suffix}`;
|
|
175
176
|
}
|
|
177
|
+
function remoteEnvFailureNote(op, error, attempts) {
|
|
178
|
+
return { op, code: error.code, retryable: isRetryableRemoteErrorCode(error.code), attempts, message: error.message };
|
|
179
|
+
}
|
|
180
|
+
const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
|
|
181
|
+
const REMOTE_RESTORE_BACKOFF_MS = 200;
|
|
182
|
+
async function restoreWorkspaceWithRetry(env, snapshotId, options) {
|
|
183
|
+
let attempts = 0;
|
|
184
|
+
const outcome = await withRetry(async (attempt) => {
|
|
185
|
+
attempts = attempt;
|
|
186
|
+
return env.resumeVM(snapshotId, options);
|
|
187
|
+
}, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
|
|
188
|
+
return { outcome, attempts };
|
|
189
|
+
}
|
|
190
|
+
export function rebaseWorkspacePathAcross(p, froms, to) {
|
|
191
|
+
for (const from of froms) {
|
|
192
|
+
const out = rebaseWorkspacePath(p, from, to);
|
|
193
|
+
if (out !== p)
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
176
198
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
177
199
|
const toolFaceSnapshot = {
|
|
178
200
|
exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
|
|
@@ -409,7 +431,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
409
431
|
}));
|
|
410
432
|
}
|
|
411
433
|
}
|
|
412
|
-
const taskScope = spec.principal ?? "default";
|
|
434
|
+
const taskScope = internals?.registryScope ?? spec.principal ?? "default";
|
|
413
435
|
defaultTaskRegistry.maybeGc();
|
|
414
436
|
const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
|
|
415
437
|
const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
|
|
@@ -558,11 +580,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
558
580
|
}
|
|
559
581
|
}
|
|
560
582
|
let restoredRootRebase;
|
|
561
|
-
const rebaseRestoredPath = (p) => restoredRootRebase === undefined ? p :
|
|
583
|
+
const rebaseRestoredPath = (p) => restoredRootRebase === undefined ? p : rebaseWorkspacePathAcross(p, restoredRootRebase.from, restoredRootRebase.to);
|
|
562
584
|
if (resume?.workspaceHandle !== undefined) {
|
|
563
|
-
const failResume = (message, cause) => {
|
|
585
|
+
const failResume = (message, cause, note) => {
|
|
564
586
|
const e = new Error(message, cause ? { cause } : undefined);
|
|
565
587
|
e.code = "resume.env_failed";
|
|
588
|
+
if (note !== undefined)
|
|
589
|
+
e.remoteEnvFailure = note;
|
|
566
590
|
throw e;
|
|
567
591
|
};
|
|
568
592
|
const handle = resume.workspaceHandle;
|
|
@@ -570,7 +594,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
570
594
|
failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
|
|
571
595
|
}
|
|
572
596
|
else if (handle.snapshotId === undefined) {
|
|
573
|
-
if (handle.restoreMode !== "park_only" &&
|
|
597
|
+
if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
|
|
574
598
|
failResume("checkpoint workspaceHandle has no snapshotId and is not a park_only handle, but the resumed env is suspendable — refusing to resume on a possibly-unrestored workspace (corrupt checkpoint?)");
|
|
575
599
|
}
|
|
576
600
|
if (handle.mountPath && handle.mountPath !== taskRootPath) {
|
|
@@ -581,12 +605,37 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
581
605
|
const restoreSignal = spec.signal
|
|
582
606
|
? AbortSignal.any([abortController.signal, spec.signal])
|
|
583
607
|
: abortController.signal;
|
|
584
|
-
const
|
|
608
|
+
const missingHere = missingRestoreSurface(ownedEnv);
|
|
609
|
+
if (missingHere.length > 0) {
|
|
610
|
+
failResume(`the resumed execution env cannot restore a workspace snapshot: its adapter does not implement ${missingHere.join(" or ")}. The checkpoint holds snapshot "${handle.snapshotId}" — wire an adapter that implements the full RemoteExecutionEnv restore surface and re-resume.`);
|
|
611
|
+
}
|
|
612
|
+
const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
|
|
613
|
+
abortSignal: restoreSignal,
|
|
614
|
+
priorHandle: handle,
|
|
615
|
+
});
|
|
585
616
|
if (!restored.ok) {
|
|
586
|
-
failResume(`resumeVM failed (${restored.error.code}): ${restored.error.message}`, restored.error);
|
|
617
|
+
failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
|
|
587
618
|
}
|
|
588
619
|
else {
|
|
620
|
+
const restoredEnv = ownedEnv;
|
|
621
|
+
const canonicalInEnv = async (p) => {
|
|
622
|
+
try {
|
|
623
|
+
const r = await restoredEnv.canonicalPath(p, restoreSignal);
|
|
624
|
+
return r.ok ? r.value : undefined;
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
let checkpointedCanonical;
|
|
631
|
+
let sameRootUnderAlias = false;
|
|
589
632
|
if (restored.value.mountPath !== handle.mountPath) {
|
|
633
|
+
checkpointedCanonical = await canonicalInEnv(handle.mountPath);
|
|
634
|
+
const restoredCanonical = await canonicalInEnv(restored.value.mountPath);
|
|
635
|
+
sameRootUnderAlias =
|
|
636
|
+
checkpointedCanonical !== undefined && restoredCanonical !== undefined && checkpointedCanonical === restoredCanonical;
|
|
637
|
+
}
|
|
638
|
+
if (restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
|
|
590
639
|
if (resume.executesApprovedAction === true) {
|
|
591
640
|
failResume(`resumeVM workspace-root divergence with a pending approved action: checkpointed mountPath "${handle.mountPath}" but the restored handle reports "${restored.value.mountPath}" — the approved args are bound to the checkpointed root; refusing to execute them against a moved workspace (adapter should honor priorHandle)`);
|
|
592
641
|
}
|
|
@@ -599,13 +648,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
599
648
|
if (restored.value.mountPath && restored.value.mountPath !== taskRootPath) {
|
|
600
649
|
taskRootPath = restored.value.mountPath;
|
|
601
650
|
}
|
|
602
|
-
if (restored.value.mountPath && restored.value.mountPath !== handle.mountPath) {
|
|
603
|
-
|
|
651
|
+
if (restored.value.mountPath && restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
|
|
652
|
+
const from = checkpointedCanonical !== undefined && checkpointedCanonical !== handle.mountPath
|
|
653
|
+
? [handle.mountPath, checkpointedCanonical]
|
|
654
|
+
: [handle.mountPath];
|
|
655
|
+
restoredRootRebase = { from, to: restored.value.mountPath };
|
|
656
|
+
try {
|
|
657
|
+
deps.onError?.(new Error(from.length > 1
|
|
658
|
+
? `resumeVM workspace-root rebase accepts both spellings of the checkpointed root (${from.map((f) => `"${f}"`).join(" and ")}) when migrating persisted paths to "${restored.value.mountPath}"`
|
|
659
|
+
: `resumeVM workspace-root rebase is spelling-exact: it matches the checkpointed root "${handle.mountPath}" only, so persisted paths recorded under an equivalent alias of it are NOT migrated to "${restored.value.mountPath}" and stay as written`), { phase: "config", sessionId });
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
}
|
|
604
663
|
}
|
|
605
664
|
}
|
|
606
665
|
const init = await ownedEnv.postResumeInit();
|
|
607
666
|
if (!init.ok) {
|
|
608
|
-
failResume(`postResumeInit failed (${init.error.code}): ${init.error.message}`, init.error);
|
|
667
|
+
failResume(`postResumeInit failed (${init.error.code}): ${init.error.message}`, init.error, remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
609
668
|
}
|
|
610
669
|
}
|
|
611
670
|
}
|
|
@@ -618,6 +677,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
618
677
|
}
|
|
619
678
|
const rewindNotes = [];
|
|
620
679
|
const rewindCaptureRequested = spec.rewindFiles === true;
|
|
680
|
+
if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
|
|
681
|
+
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.`);
|
|
682
|
+
e.code = "rewind.conflicting_targets";
|
|
683
|
+
throw e;
|
|
684
|
+
}
|
|
621
685
|
let rewindTarget = spec.resumeAt !== undefined ? (rewindCaptureRequested ? spec.resumeAt : undefined) : spec.rewindFilesTo;
|
|
622
686
|
const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
|
|
623
687
|
if (spec.resumeAt !== undefined && !rewindCaptureRequested) {
|
|
@@ -943,6 +1007,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
943
1007
|
}
|
|
944
1008
|
const suspendRef = {};
|
|
945
1009
|
const reviewRef = {};
|
|
1010
|
+
const remoteEnvFailures = [];
|
|
946
1011
|
const suspendLoopRef = { hit: false };
|
|
947
1012
|
const compactionReuseRef = { consecutive: 0 };
|
|
948
1013
|
const trimPressureRef = { droppedMessages: false };
|
|
@@ -1833,15 +1898,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1833
1898
|
}
|
|
1834
1899
|
const activeTools = new Set();
|
|
1835
1900
|
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
1901
|
const turnSnapshotRef = {};
|
|
1846
1902
|
let harnessTools = tools;
|
|
1847
1903
|
const failedMcpServers = mcp.statuses
|
|
@@ -1908,7 +1964,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1908
1964
|
if (deferred.has(n))
|
|
1909
1965
|
activeTools.add(n);
|
|
1910
1966
|
}
|
|
1911
|
-
|
|
1967
|
+
const callableToolNames = () => {
|
|
1912
1968
|
const s = new Set(tools.map((t) => t.name));
|
|
1913
1969
|
for (const n of deferred)
|
|
1914
1970
|
if (!activeTools.has(n))
|
|
@@ -1916,6 +1972,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1916
1972
|
s.add(TOOL_SEARCH_NAME);
|
|
1917
1973
|
return s;
|
|
1918
1974
|
};
|
|
1975
|
+
offloadReachableToolsRef.current = callableToolNames;
|
|
1919
1976
|
let toolSearch;
|
|
1920
1977
|
const buildToolList = (active) => {
|
|
1921
1978
|
const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
|
|
@@ -1957,11 +2014,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1957
2014
|
active: activeTools,
|
|
1958
2015
|
rematerialize,
|
|
1959
2016
|
listingRide: (newly) => listingRideRef.current?.(newly),
|
|
1960
|
-
mountedNames:
|
|
2017
|
+
mountedNames: callableToolNames,
|
|
1961
2018
|
directCallEnabled: spec.deferSelfResolve !== false,
|
|
1962
2019
|
});
|
|
1963
2020
|
harnessTools = buildToolList(activeTools);
|
|
1964
2021
|
}
|
|
2022
|
+
else {
|
|
2023
|
+
rebuildHarnessToolsRef.current = async () => {
|
|
2024
|
+
const list = [...tools];
|
|
2025
|
+
await harnessRef.current.setTools(list, list.map((t) => t.name));
|
|
2026
|
+
if (fpRef.current)
|
|
2027
|
+
fpRef.current.tools = toolsToFingerprintInputs(list);
|
|
2028
|
+
turnSnapshotRef.current?.refreshTools(toolsToFingerprintInputs(list));
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
1965
2031
|
if (spec.agents !== undefined && spec.agents.length > 0) {
|
|
1966
2032
|
const known = new Set(tools.flatMap((t) => [canonicalToolName(t.name), ...(t.aliases ?? []).map((a) => canonicalToolName(a))]));
|
|
1967
2033
|
for (const def of spec.agents) {
|
|
@@ -2535,16 +2601,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2535
2601
|
const preToolContexts = new Map();
|
|
2536
2602
|
const blockedToolCalls = new Set();
|
|
2537
2603
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
2604
|
+
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
2605
|
+
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
2538
2606
|
const resourceSuspendEligible = spec.resourceSuspend !== undefined &&
|
|
2539
2607
|
(spec.checkpointStore ?? deps.checkpointStore) !== undefined &&
|
|
2540
2608
|
!(offloadStore !== undefined && isVolatileOffloadStore(offloadStore)) &&
|
|
2541
|
-
(ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv))
|
|
2609
|
+
(ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv)) &&
|
|
2610
|
+
incompleteSuspendAdapter === undefined;
|
|
2542
2611
|
if (spec.resourceSuspend !== undefined && !resourceSuspendEligible) {
|
|
2543
2612
|
const why = (spec.checkpointStore ?? deps.checkpointStore) === undefined
|
|
2544
2613
|
? "no CheckpointStore is wired"
|
|
2545
2614
|
: offloadStore !== undefined && isVolatileOffloadStore(offloadStore)
|
|
2546
2615
|
? "tool-result offload uses the in-memory store (a resume needs durable results)"
|
|
2547
|
-
:
|
|
2616
|
+
: incompleteSuspendAdapter !== undefined
|
|
2617
|
+
? `the per-task execution env declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")} (a snapshot nothing can restore is worse than no snapshot)`
|
|
2618
|
+
: "the per-task execution env is not a RemoteExecutionEnv (it would be destroyed on suspend)";
|
|
2548
2619
|
deps.onError?.(new Error(`resourceSuspend is set but INACTIVE: ${why}; resource limits will hard-fail, not suspend`), {
|
|
2549
2620
|
phase: "config",
|
|
2550
2621
|
sessionId,
|
|
@@ -2720,14 +2791,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2720
2791
|
catch (putErr) {
|
|
2721
2792
|
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
2722
2793
|
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
2723
|
-
const back = await remoteEnv
|
|
2794
|
+
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
|
|
2795
|
+
abortSignal: abortController.signal,
|
|
2796
|
+
});
|
|
2724
2797
|
if (back.ok) {
|
|
2725
2798
|
const init = await remoteEnv.postResumeInit();
|
|
2726
2799
|
if (init.ok)
|
|
2727
2800
|
return false;
|
|
2801
|
+
remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
2728
2802
|
deps.onError?.(init.error, { phase: "config", sessionId });
|
|
2729
2803
|
}
|
|
2730
2804
|
else {
|
|
2805
|
+
remoteEnvFailures.push(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
|
|
2731
2806
|
deps.onError?.(back.error, { phase: "config", sessionId });
|
|
2732
2807
|
}
|
|
2733
2808
|
abortController.abort();
|
|
@@ -2737,10 +2812,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2737
2812
|
return false;
|
|
2738
2813
|
}
|
|
2739
2814
|
};
|
|
2740
|
-
const publishCommittedSuspend = (token, gate, scope) => {
|
|
2815
|
+
const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
|
|
2741
2816
|
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? reviewRef : suspendRef;
|
|
2742
2817
|
ref.token = token;
|
|
2743
2818
|
ref.gate = gate;
|
|
2819
|
+
if (remoteHandle !== undefined)
|
|
2820
|
+
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
2744
2821
|
ref.scope = scope;
|
|
2745
2822
|
};
|
|
2746
2823
|
const suspendLoopCapHit = (count, cap, detail) => {
|
|
@@ -2757,7 +2834,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2757
2834
|
return true;
|
|
2758
2835
|
};
|
|
2759
2836
|
const suspendableEnv = ownedEnv !== undefined && isSuspendable(ownedEnv) ? ownedEnv : undefined;
|
|
2760
|
-
const parkOnlyRemoteEnv = suspendableEnv === undefined && ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv)
|
|
2837
|
+
const parkOnlyRemoteEnv = suspendableEnv === undefined && ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && incompleteSuspendAdapter === undefined
|
|
2838
|
+
? ownedEnv
|
|
2839
|
+
: undefined;
|
|
2761
2840
|
const parkOnlyHandle = (env) => {
|
|
2762
2841
|
const { snapshotId: _lineage, ...identity } = env.workspaceHandle();
|
|
2763
2842
|
return { ...identity, restoreMode: "park_only" };
|
|
@@ -2782,6 +2861,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2782
2861
|
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
2783
2862
|
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2784
2863
|
if (!snap.ok) {
|
|
2864
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2785
2865
|
deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
2786
2866
|
return false;
|
|
2787
2867
|
}
|
|
@@ -2816,7 +2896,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2816
2896
|
};
|
|
2817
2897
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
|
|
2818
2898
|
return false;
|
|
2819
|
-
publishCommittedSuspend(token, gate, rs.scope);
|
|
2899
|
+
publishCommittedSuspend(token, gate, rs.scope, remoteHandle);
|
|
2820
2900
|
try {
|
|
2821
2901
|
await sessions.pin?.(sessionId);
|
|
2822
2902
|
}
|
|
@@ -2833,6 +2913,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2833
2913
|
return false;
|
|
2834
2914
|
if (abortController.signal.aborted)
|
|
2835
2915
|
return false;
|
|
2916
|
+
if (incompleteSuspendAdapter !== undefined) {
|
|
2917
|
+
try {
|
|
2918
|
+
deps.onError?.(new Error(`plan_review park skipped: the per-task execution env declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so its workspace could not be restored on resume (the plan-review request was dropped; the run continues without pausing)`), { phase: "config", sessionId });
|
|
2919
|
+
}
|
|
2920
|
+
catch {
|
|
2921
|
+
}
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2836
2924
|
if (suspendLoopCapHit(priorSuspendCount, maxSuspends, " for a plan_review (likely a resume/restart loop)."))
|
|
2837
2925
|
return false;
|
|
2838
2926
|
const leafId = await session.getLeafId();
|
|
@@ -2852,6 +2940,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2852
2940
|
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
2853
2941
|
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2854
2942
|
if (!snap.ok) {
|
|
2943
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2855
2944
|
deps.onError?.(new Error(`plan_review suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
2856
2945
|
return false;
|
|
2857
2946
|
}
|
|
@@ -2890,7 +2979,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2890
2979
|
};
|
|
2891
2980
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
|
|
2892
2981
|
return false;
|
|
2893
|
-
publishCommittedSuspend(token, gate, scope);
|
|
2982
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
2894
2983
|
try {
|
|
2895
2984
|
await sessions.pin?.(sessionId);
|
|
2896
2985
|
}
|
|
@@ -2924,10 +3013,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2924
3013
|
"RunnerDeps.toolResultStore or disable offload.");
|
|
2925
3014
|
}
|
|
2926
3015
|
if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
|
|
2927
|
-
throw new Error(
|
|
2928
|
-
|
|
2929
|
-
"
|
|
2930
|
-
|
|
3016
|
+
throw new Error(incompleteSuspendAdapter !== undefined
|
|
3017
|
+
? `durable suspend is not supported with this per-task executionEnvFactory env: it declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so a snapshot taken now could never be restored. Implement the full RemoteExecutionEnv restore surface, or declare capabilities.suspendable:false if the workspace is externally durable (the park-only lane).`
|
|
3018
|
+
: "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
|
|
3019
|
+
"minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
|
|
3020
|
+
"(empty) env. Use a RemoteExecutionEnv factory (it is paused, not destroyed) or a static, " +
|
|
3021
|
+
"caller-owned RunnerDeps.executionEnv.");
|
|
2931
3022
|
}
|
|
2932
3023
|
const leafId = await session.getLeafId();
|
|
2933
3024
|
if (!leafId) {
|
|
@@ -2945,6 +3036,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2945
3036
|
await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
|
|
2946
3037
|
const snap = await remoteEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2947
3038
|
if (!snap.ok) {
|
|
3039
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2948
3040
|
throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
|
|
2949
3041
|
}
|
|
2950
3042
|
remoteHandle = { ...remoteEnv.workspaceHandle(), snapshotId: snap.value };
|
|
@@ -3033,7 +3125,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3033
3125
|
if (!(await commitSuspendSaga(token, cp, remoteEnv, remoteHandle))) {
|
|
3034
3126
|
return undefined;
|
|
3035
3127
|
}
|
|
3036
|
-
publishCommittedSuspend(token, gate, cp.scope);
|
|
3128
|
+
publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
|
|
3037
3129
|
try {
|
|
3038
3130
|
await sessions.pin?.(sessionId);
|
|
3039
3131
|
}
|
|
@@ -3382,7 +3474,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3382
3474
|
: undefined;
|
|
3383
3475
|
overheadState.promptChars = systemPrompt.length;
|
|
3384
3476
|
const preparedHolder = {};
|
|
3385
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3477
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3386
3478
|
const prepared = buildPrepared();
|
|
3387
3479
|
preparedHolder.current = prepared;
|
|
3388
3480
|
return prepared;
|
|
@@ -1345,6 +1345,16 @@ export class Runner {
|
|
|
1345
1345
|
queue.close();
|
|
1346
1346
|
return;
|
|
1347
1347
|
}
|
|
1348
|
+
const remoteEnvFailure = (() => {
|
|
1349
|
+
let cur = err;
|
|
1350
|
+
for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
|
|
1351
|
+
const note = cur.remoteEnvFailure;
|
|
1352
|
+
if (note !== undefined)
|
|
1353
|
+
return [note];
|
|
1354
|
+
cur = cur.cause;
|
|
1355
|
+
}
|
|
1356
|
+
return undefined;
|
|
1357
|
+
})();
|
|
1348
1358
|
resultValue = {
|
|
1349
1359
|
taskId: taskIdRef.current ?? "unknown",
|
|
1350
1360
|
sessionId: taskIdRef.sessionId ?? "unknown",
|
|
@@ -1352,6 +1362,7 @@ export class Runner {
|
|
|
1352
1362
|
result: "",
|
|
1353
1363
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1354
1364
|
errorCode: code,
|
|
1365
|
+
...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
|
|
1355
1366
|
stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
|
|
1356
1367
|
};
|
|
1357
1368
|
emitTrace(spec.tracer ?? this.deps.tracer, () => ({
|
|
@@ -2765,6 +2776,7 @@ export class Runner {
|
|
|
2765
2776
|
model: prepared.model.id,
|
|
2766
2777
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
2767
2778
|
rewindNotes: prepared.rewindNotes,
|
|
2779
|
+
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
2768
2780
|
abortedForTimeout: timeout.fired,
|
|
2769
2781
|
abortedForTurns: rs.limits.turnsExceeded,
|
|
2770
2782
|
abortedLive,
|
|
@@ -2774,10 +2786,18 @@ export class Runner {
|
|
|
2774
2786
|
outputInvalid: rs.degrade.outputInvalid,
|
|
2775
2787
|
suspendLoop: prepared.suspendLoopRef.hit,
|
|
2776
2788
|
suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
|
|
2777
|
-
? {
|
|
2789
|
+
? {
|
|
2790
|
+
token: prepared.suspendRef.token,
|
|
2791
|
+
gate: prepared.suspendRef.gate,
|
|
2792
|
+
...(prepared.suspendRef.restoreMode !== undefined ? { restoreMode: prepared.suspendRef.restoreMode } : {}),
|
|
2793
|
+
}
|
|
2778
2794
|
: undefined,
|
|
2779
2795
|
reviewRef: prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined
|
|
2780
|
-
? {
|
|
2796
|
+
? {
|
|
2797
|
+
token: prepared.reviewRef.token,
|
|
2798
|
+
gate: prepared.reviewRef.gate,
|
|
2799
|
+
...(prepared.reviewRef.restoreMode !== undefined ? { restoreMode: prepared.reviewRef.restoreMode } : {}),
|
|
2800
|
+
}
|
|
2781
2801
|
: undefined,
|
|
2782
2802
|
});
|
|
2783
2803
|
if (resume !== undefined &&
|
|
@@ -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
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -370,6 +370,13 @@ export interface TaskSpec {
|
|
|
370
370
|
signal?: AbortSignal;
|
|
371
371
|
}
|
|
372
372
|
export type TaskStatus = "completed" | "blocked" | "failed" | "timeout" | "suspended" | "needs_review";
|
|
373
|
+
export interface RemoteEnvFailureNote {
|
|
374
|
+
op: "suspendVM" | "resumeVM" | "postResumeInit";
|
|
375
|
+
code: import("./remote-env.js").RemoteExecutionErrorCode;
|
|
376
|
+
retryable: boolean;
|
|
377
|
+
attempts: number;
|
|
378
|
+
message: string;
|
|
379
|
+
}
|
|
373
380
|
export interface TaskResult {
|
|
374
381
|
taskId: string;
|
|
375
382
|
sessionId: string;
|
|
@@ -380,6 +387,8 @@ export interface TaskResult {
|
|
|
380
387
|
blockedReason?: string;
|
|
381
388
|
checkpointToken?: import("./checkpoint-store.js").CheckpointToken;
|
|
382
389
|
checkpointGate?: import("./checkpoint-store.js").CheckpointGate;
|
|
390
|
+
workspaceRestoreMode?: "snapshot" | "park_only";
|
|
391
|
+
remoteEnvFailures?: RemoteEnvFailureNote[];
|
|
383
392
|
errorMessage?: string;
|
|
384
393
|
errorCode?: string;
|
|
385
394
|
degraded?: {
|
|
@@ -47,7 +47,7 @@ export declare class FileError extends Error {
|
|
|
47
47
|
path?: string;
|
|
48
48
|
constructor(code: FileErrorCode, message: string, path?: string, cause?: Error);
|
|
49
49
|
}
|
|
50
|
-
export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "unknown";
|
|
50
|
+
export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "suspended" | "auth_failed" | "unknown";
|
|
51
51
|
export declare class ExecutionError extends Error {
|
|
52
52
|
code: ExecutionErrorCode;
|
|
53
53
|
partialStdout?: string;
|
|
@@ -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 {
|
package/dist/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
|
|
|
56
56
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
57
57
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
58
58
|
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
|
|
59
|
-
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
|
|
59
|
+
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
60
60
|
export { withRetry } from "./core/with-retry.js";
|
|
61
61
|
export type { RetryPolicy, RetryResult } from "./core/with-retry.js";
|
|
62
62
|
export type { RemoteExecutionEnv, WorkspaceHandle, SnapshotId, SessionToken, SandboxTier, OutputChunk, ExecStreamOptions, RemoteConnectConfig, VmLifecycleOptions, SecretRef, RemoteExecutionErrorCode, ExecutionEnvFactory, ExecutionEnvFactoryContext, } from "./core/remote-env.js";
|
|
@@ -189,7 +189,7 @@ export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
|
|
|
189
189
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
190
190
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
191
191
|
export type { AssistantMessage, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
192
|
-
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
|
|
192
|
+
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
|
|
193
193
|
export { Type } from "typebox";
|
|
194
194
|
export type { TSchema, Static } from "typebox";
|
|
195
195
|
export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
|
|
|
51
51
|
export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
|
|
52
52
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
53
53
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
54
|
-
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
|
|
54
|
+
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
55
55
|
export { withRetry } from "./core/with-retry.js";
|
|
56
56
|
export { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "./core/git-worktree-env.js";
|
|
57
57
|
export { runExecGate } from "./core/exec-gate.js";
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -229,6 +229,20 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
229
229
|
isError: true,
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
+
if (res.error.code === "suspended") {
|
|
233
|
+
return {
|
|
234
|
+
content: `Error (${toolName}): the execution environment is suspended (its workspace VM is paused), so the command did NOT run and no command can run in this leg. Do not retry it here — the task must be resumed first; report that the workspace is suspended. (${res.error.message})`,
|
|
235
|
+
details: { type: "bash", envSuspended: true },
|
|
236
|
+
isError: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (res.error.code === "auth_failed") {
|
|
240
|
+
return {
|
|
241
|
+
content: `Error (${toolName}): authentication to the execution environment was rejected, so the command did NOT run. Do NOT retry — this is a permanent credential failure that an operator must fix; report it instead of trying other commands. (${res.error.message})`,
|
|
242
|
+
details: { type: "bash", authFailed: true },
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
232
246
|
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
233
247
|
const rawStdout = res.error.partialStdout ?? "";
|
|
234
248
|
const rawStderr = res.error.partialStderr ?? "";
|
|
@@ -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;
|