@sema-agent/core 2.11.0 → 2.13.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/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +3 -0
- package/dist/core/git-worktree-env.js +5 -0
- package/dist/core/mcp.d.ts +3 -0
- package/dist/core/mcp.js +91 -7
- package/dist/core/remote-env.d.ts +3 -0
- package/dist/core/remote-env.js +19 -1
- package/dist/core/runner/active-skill-scope.js +34 -6
- 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 +4 -0
- package/dist/core/runner/prepare-task.js +120 -25
- package/dist/core/runner/runtask.js +49 -10
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/types.d.ts +11 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/engine/harness/types.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.js +38 -6
- package/dist/tools/fs/fs-bash.js +14 -0
- package/dist/tools/scheduler-tools.js +38 -18
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- 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,
|
|
@@ -410,6 +432,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
410
432
|
}
|
|
411
433
|
}
|
|
412
434
|
const taskScope = internals?.registryScope ?? spec.principal ?? "default";
|
|
435
|
+
const offloadScope = spec.principal ?? "default";
|
|
413
436
|
defaultTaskRegistry.maybeGc();
|
|
414
437
|
const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
|
|
415
438
|
const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
|
|
@@ -434,7 +457,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
434
457
|
const offloadEnabled = Number.isFinite(offloadThreshold) && offloadThreshold > 0;
|
|
435
458
|
const rawOffloadStore = offloadEnabled ? (deps.toolResultStore ?? new InMemoryToolResultStore()) : undefined;
|
|
436
459
|
const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
|
|
437
|
-
? new ScopedToolResultStore(rawOffloadStore,
|
|
460
|
+
? new ScopedToolResultStore(rawOffloadStore, offloadScope)
|
|
438
461
|
: rawOffloadStore;
|
|
439
462
|
const offloadReachableToolsRef = {};
|
|
440
463
|
const maybeOffload = (tool, perTool) => {
|
|
@@ -558,11 +581,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
558
581
|
}
|
|
559
582
|
}
|
|
560
583
|
let restoredRootRebase;
|
|
561
|
-
const rebaseRestoredPath = (p) => restoredRootRebase === undefined ? p :
|
|
584
|
+
const rebaseRestoredPath = (p) => restoredRootRebase === undefined ? p : rebaseWorkspacePathAcross(p, restoredRootRebase.from, restoredRootRebase.to);
|
|
562
585
|
if (resume?.workspaceHandle !== undefined) {
|
|
563
|
-
const failResume = (message, cause) => {
|
|
586
|
+
const failResume = (message, cause, note) => {
|
|
564
587
|
const e = new Error(message, cause ? { cause } : undefined);
|
|
565
588
|
e.code = "resume.env_failed";
|
|
589
|
+
if (note !== undefined)
|
|
590
|
+
e.remoteEnvFailure = note;
|
|
566
591
|
throw e;
|
|
567
592
|
};
|
|
568
593
|
const handle = resume.workspaceHandle;
|
|
@@ -570,7 +595,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
570
595
|
failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
|
|
571
596
|
}
|
|
572
597
|
else if (handle.snapshotId === undefined) {
|
|
573
|
-
if (handle.restoreMode !== "park_only" &&
|
|
598
|
+
if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
|
|
574
599
|
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
600
|
}
|
|
576
601
|
if (handle.mountPath && handle.mountPath !== taskRootPath) {
|
|
@@ -581,12 +606,37 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
581
606
|
const restoreSignal = spec.signal
|
|
582
607
|
? AbortSignal.any([abortController.signal, spec.signal])
|
|
583
608
|
: abortController.signal;
|
|
584
|
-
const
|
|
609
|
+
const missingHere = missingRestoreSurface(ownedEnv);
|
|
610
|
+
if (missingHere.length > 0) {
|
|
611
|
+
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.`);
|
|
612
|
+
}
|
|
613
|
+
const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
|
|
614
|
+
abortSignal: restoreSignal,
|
|
615
|
+
priorHandle: handle,
|
|
616
|
+
});
|
|
585
617
|
if (!restored.ok) {
|
|
586
|
-
failResume(`resumeVM failed (${restored.error.code}): ${restored.error.message}`, restored.error);
|
|
618
|
+
failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
|
|
587
619
|
}
|
|
588
620
|
else {
|
|
621
|
+
const restoredEnv = ownedEnv;
|
|
622
|
+
const canonicalInEnv = async (p) => {
|
|
623
|
+
try {
|
|
624
|
+
const r = await restoredEnv.canonicalPath(p, restoreSignal);
|
|
625
|
+
return r.ok ? r.value : undefined;
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
let checkpointedCanonical;
|
|
632
|
+
let sameRootUnderAlias = false;
|
|
589
633
|
if (restored.value.mountPath !== handle.mountPath) {
|
|
634
|
+
checkpointedCanonical = await canonicalInEnv(handle.mountPath);
|
|
635
|
+
const restoredCanonical = await canonicalInEnv(restored.value.mountPath);
|
|
636
|
+
sameRootUnderAlias =
|
|
637
|
+
checkpointedCanonical !== undefined && restoredCanonical !== undefined && checkpointedCanonical === restoredCanonical;
|
|
638
|
+
}
|
|
639
|
+
if (restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
|
|
590
640
|
if (resume.executesApprovedAction === true) {
|
|
591
641
|
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
642
|
}
|
|
@@ -599,13 +649,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
599
649
|
if (restored.value.mountPath && restored.value.mountPath !== taskRootPath) {
|
|
600
650
|
taskRootPath = restored.value.mountPath;
|
|
601
651
|
}
|
|
602
|
-
if (restored.value.mountPath && restored.value.mountPath !== handle.mountPath) {
|
|
603
|
-
|
|
652
|
+
if (restored.value.mountPath && restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
|
|
653
|
+
const from = checkpointedCanonical !== undefined && checkpointedCanonical !== handle.mountPath
|
|
654
|
+
? [handle.mountPath, checkpointedCanonical]
|
|
655
|
+
: [handle.mountPath];
|
|
656
|
+
restoredRootRebase = { from, to: restored.value.mountPath };
|
|
657
|
+
try {
|
|
658
|
+
deps.onError?.(new Error(from.length > 1
|
|
659
|
+
? `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}"`
|
|
660
|
+
: `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 });
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
}
|
|
604
664
|
}
|
|
605
665
|
}
|
|
606
666
|
const init = await ownedEnv.postResumeInit();
|
|
607
667
|
if (!init.ok) {
|
|
608
|
-
failResume(`postResumeInit failed (${init.error.code}): ${init.error.message}`, init.error);
|
|
668
|
+
failResume(`postResumeInit failed (${init.error.code}): ${init.error.message}`, init.error, remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
609
669
|
}
|
|
610
670
|
}
|
|
611
671
|
}
|
|
@@ -948,6 +1008,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
948
1008
|
}
|
|
949
1009
|
const suspendRef = {};
|
|
950
1010
|
const reviewRef = {};
|
|
1011
|
+
const remoteEnvFailures = [];
|
|
951
1012
|
const suspendLoopRef = { hit: false };
|
|
952
1013
|
const compactionReuseRef = { consecutive: 0 };
|
|
953
1014
|
const trimPressureRef = { droppedMessages: false };
|
|
@@ -1324,6 +1385,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1324
1385
|
owner: hostTaskId,
|
|
1325
1386
|
scope: taskScope,
|
|
1326
1387
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1388
|
+
enrichCtx: enrichSpecToolCtx,
|
|
1327
1389
|
})));
|
|
1328
1390
|
}
|
|
1329
1391
|
}
|
|
@@ -1855,6 +1917,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1855
1917
|
throw e;
|
|
1856
1918
|
}
|
|
1857
1919
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
1920
|
+
let activationChain = Promise.resolve();
|
|
1921
|
+
const serializeActivation = (section) => {
|
|
1922
|
+
const p = activationChain.then(section);
|
|
1923
|
+
activationChain = p.then(() => undefined, () => undefined);
|
|
1924
|
+
return p;
|
|
1925
|
+
};
|
|
1858
1926
|
const directCallFor = (name) => {
|
|
1859
1927
|
if (spec.deferSelfResolve === false)
|
|
1860
1928
|
return undefined;
|
|
@@ -1870,7 +1938,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1870
1938
|
};
|
|
1871
1939
|
},
|
|
1872
1940
|
...(executionMode !== undefined ? { executionMode } : {}),
|
|
1873
|
-
activate: async () => {
|
|
1941
|
+
activate: async () => serializeActivation(async () => {
|
|
1874
1942
|
if (activeTools.has(name))
|
|
1875
1943
|
return undefined;
|
|
1876
1944
|
activeTools.add(name);
|
|
@@ -1882,7 +1950,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1882
1950
|
throw e;
|
|
1883
1951
|
}
|
|
1884
1952
|
return listingRideRef.current?.([name]);
|
|
1885
|
-
},
|
|
1953
|
+
}),
|
|
1886
1954
|
};
|
|
1887
1955
|
};
|
|
1888
1956
|
const placeholders = new Map([...registry.values()].map((i) => [i.name, createPlaceholderTool(i, directCallFor(i.name))]));
|
|
@@ -1956,6 +2024,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1956
2024
|
listingRide: (newly) => listingRideRef.current?.(newly),
|
|
1957
2025
|
mountedNames: callableToolNames,
|
|
1958
2026
|
directCallEnabled: spec.deferSelfResolve !== false,
|
|
2027
|
+
serializeActivation,
|
|
1959
2028
|
});
|
|
1960
2029
|
harnessTools = buildToolList(activeTools);
|
|
1961
2030
|
}
|
|
@@ -2541,16 +2610,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2541
2610
|
const preToolContexts = new Map();
|
|
2542
2611
|
const blockedToolCalls = new Set();
|
|
2543
2612
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
2613
|
+
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
2614
|
+
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
2544
2615
|
const resourceSuspendEligible = spec.resourceSuspend !== undefined &&
|
|
2545
2616
|
(spec.checkpointStore ?? deps.checkpointStore) !== undefined &&
|
|
2546
2617
|
!(offloadStore !== undefined && isVolatileOffloadStore(offloadStore)) &&
|
|
2547
|
-
(ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv))
|
|
2618
|
+
(ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv)) &&
|
|
2619
|
+
incompleteSuspendAdapter === undefined;
|
|
2548
2620
|
if (spec.resourceSuspend !== undefined && !resourceSuspendEligible) {
|
|
2549
2621
|
const why = (spec.checkpointStore ?? deps.checkpointStore) === undefined
|
|
2550
2622
|
? "no CheckpointStore is wired"
|
|
2551
2623
|
: offloadStore !== undefined && isVolatileOffloadStore(offloadStore)
|
|
2552
2624
|
? "tool-result offload uses the in-memory store (a resume needs durable results)"
|
|
2553
|
-
:
|
|
2625
|
+
: incompleteSuspendAdapter !== undefined
|
|
2626
|
+
? `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)`
|
|
2627
|
+
: "the per-task execution env is not a RemoteExecutionEnv (it would be destroyed on suspend)";
|
|
2554
2628
|
deps.onError?.(new Error(`resourceSuspend is set but INACTIVE: ${why}; resource limits will hard-fail, not suspend`), {
|
|
2555
2629
|
phase: "config",
|
|
2556
2630
|
sessionId,
|
|
@@ -2726,14 +2800,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2726
2800
|
catch (putErr) {
|
|
2727
2801
|
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
2728
2802
|
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
2729
|
-
const back = await remoteEnv
|
|
2803
|
+
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
|
|
2804
|
+
abortSignal: abortController.signal,
|
|
2805
|
+
});
|
|
2730
2806
|
if (back.ok) {
|
|
2731
2807
|
const init = await remoteEnv.postResumeInit();
|
|
2732
2808
|
if (init.ok)
|
|
2733
2809
|
return false;
|
|
2810
|
+
remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
2734
2811
|
deps.onError?.(init.error, { phase: "config", sessionId });
|
|
2735
2812
|
}
|
|
2736
2813
|
else {
|
|
2814
|
+
remoteEnvFailures.push(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
|
|
2737
2815
|
deps.onError?.(back.error, { phase: "config", sessionId });
|
|
2738
2816
|
}
|
|
2739
2817
|
abortController.abort();
|
|
@@ -2743,10 +2821,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2743
2821
|
return false;
|
|
2744
2822
|
}
|
|
2745
2823
|
};
|
|
2746
|
-
const publishCommittedSuspend = (token, gate, scope) => {
|
|
2824
|
+
const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
|
|
2747
2825
|
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? reviewRef : suspendRef;
|
|
2748
2826
|
ref.token = token;
|
|
2749
2827
|
ref.gate = gate;
|
|
2828
|
+
if (remoteHandle !== undefined)
|
|
2829
|
+
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
2750
2830
|
ref.scope = scope;
|
|
2751
2831
|
};
|
|
2752
2832
|
const suspendLoopCapHit = (count, cap, detail) => {
|
|
@@ -2763,7 +2843,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2763
2843
|
return true;
|
|
2764
2844
|
};
|
|
2765
2845
|
const suspendableEnv = ownedEnv !== undefined && isSuspendable(ownedEnv) ? ownedEnv : undefined;
|
|
2766
|
-
const parkOnlyRemoteEnv = suspendableEnv === undefined && ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv)
|
|
2846
|
+
const parkOnlyRemoteEnv = suspendableEnv === undefined && ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && incompleteSuspendAdapter === undefined
|
|
2847
|
+
? ownedEnv
|
|
2848
|
+
: undefined;
|
|
2767
2849
|
const parkOnlyHandle = (env) => {
|
|
2768
2850
|
const { snapshotId: _lineage, ...identity } = env.workspaceHandle();
|
|
2769
2851
|
return { ...identity, restoreMode: "park_only" };
|
|
@@ -2788,6 +2870,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2788
2870
|
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
2789
2871
|
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2790
2872
|
if (!snap.ok) {
|
|
2873
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2791
2874
|
deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
2792
2875
|
return false;
|
|
2793
2876
|
}
|
|
@@ -2822,7 +2905,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2822
2905
|
};
|
|
2823
2906
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
|
|
2824
2907
|
return false;
|
|
2825
|
-
publishCommittedSuspend(token, gate, rs.scope);
|
|
2908
|
+
publishCommittedSuspend(token, gate, rs.scope, remoteHandle);
|
|
2826
2909
|
try {
|
|
2827
2910
|
await sessions.pin?.(sessionId);
|
|
2828
2911
|
}
|
|
@@ -2839,6 +2922,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2839
2922
|
return false;
|
|
2840
2923
|
if (abortController.signal.aborted)
|
|
2841
2924
|
return false;
|
|
2925
|
+
if (incompleteSuspendAdapter !== undefined) {
|
|
2926
|
+
try {
|
|
2927
|
+
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 });
|
|
2928
|
+
}
|
|
2929
|
+
catch {
|
|
2930
|
+
}
|
|
2931
|
+
return false;
|
|
2932
|
+
}
|
|
2842
2933
|
if (suspendLoopCapHit(priorSuspendCount, maxSuspends, " for a plan_review (likely a resume/restart loop)."))
|
|
2843
2934
|
return false;
|
|
2844
2935
|
const leafId = await session.getLeafId();
|
|
@@ -2858,6 +2949,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2858
2949
|
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
2859
2950
|
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2860
2951
|
if (!snap.ok) {
|
|
2952
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2861
2953
|
deps.onError?.(new Error(`plan_review suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
2862
2954
|
return false;
|
|
2863
2955
|
}
|
|
@@ -2896,7 +2988,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2896
2988
|
};
|
|
2897
2989
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
|
|
2898
2990
|
return false;
|
|
2899
|
-
publishCommittedSuspend(token, gate, scope);
|
|
2991
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
2900
2992
|
try {
|
|
2901
2993
|
await sessions.pin?.(sessionId);
|
|
2902
2994
|
}
|
|
@@ -2930,10 +3022,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2930
3022
|
"RunnerDeps.toolResultStore or disable offload.");
|
|
2931
3023
|
}
|
|
2932
3024
|
if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
|
|
2933
|
-
throw new Error(
|
|
2934
|
-
|
|
2935
|
-
"
|
|
2936
|
-
|
|
3025
|
+
throw new Error(incompleteSuspendAdapter !== undefined
|
|
3026
|
+
? `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).`
|
|
3027
|
+
: "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
|
|
3028
|
+
"minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
|
|
3029
|
+
"(empty) env. Use a RemoteExecutionEnv factory (it is paused, not destroyed) or a static, " +
|
|
3030
|
+
"caller-owned RunnerDeps.executionEnv.");
|
|
2937
3031
|
}
|
|
2938
3032
|
const leafId = await session.getLeafId();
|
|
2939
3033
|
if (!leafId) {
|
|
@@ -2951,6 +3045,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2951
3045
|
await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
|
|
2952
3046
|
const snap = await remoteEnv.suspendVM({ abortSignal: abortController.signal });
|
|
2953
3047
|
if (!snap.ok) {
|
|
3048
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
2954
3049
|
throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
|
|
2955
3050
|
}
|
|
2956
3051
|
remoteHandle = { ...remoteEnv.workspaceHandle(), snapshotId: snap.value };
|
|
@@ -3039,7 +3134,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3039
3134
|
if (!(await commitSuspendSaga(token, cp, remoteEnv, remoteHandle))) {
|
|
3040
3135
|
return undefined;
|
|
3041
3136
|
}
|
|
3042
|
-
publishCommittedSuspend(token, gate, cp.scope);
|
|
3137
|
+
publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
|
|
3043
3138
|
try {
|
|
3044
3139
|
await sessions.pin?.(sessionId);
|
|
3045
3140
|
}
|
|
@@ -3388,7 +3483,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3388
3483
|
: undefined;
|
|
3389
3484
|
overheadState.promptChars = systemPrompt.length;
|
|
3390
3485
|
const preparedHolder = {};
|
|
3391
|
-
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 } : {}) });
|
|
3486
|
+
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 } : {}) });
|
|
3392
3487
|
const prepared = buildPrepared();
|
|
3393
3488
|
preparedHolder.current = prepared;
|
|
3394
3489
|
return prepared;
|
|
@@ -222,6 +222,7 @@ function resumeContinuation(resume) {
|
|
|
222
222
|
}
|
|
223
223
|
function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
224
224
|
const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, turnToolSpan, walltimeDeadlineMs, walltimeMonotonicDeadline, nudgeSchedule, buildFinalizeText, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
|
|
225
|
+
let finalVerifySeenAtLastBoundary = 0;
|
|
225
226
|
const onTurnBoundary = async (event) => {
|
|
226
227
|
if (prepared.callCapRef && turnToolSpan.startMin !== undefined && turnToolSpan.endMax !== undefined) {
|
|
227
228
|
recordToolCycleSample(prepared.callCapRef.state, turnToolSpan.endMax - turnToolSpan.startMin);
|
|
@@ -499,11 +500,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
499
500
|
}
|
|
500
501
|
}
|
|
501
502
|
let batchContextBlock;
|
|
503
|
+
const finalVerifyInjectedThisTurn = rs.counters.finalVerifyInjections !== finalVerifySeenAtLastBoundary;
|
|
504
|
+
finalVerifySeenAtLastBoundary = rs.counters.finalVerifyInjections;
|
|
502
505
|
if (postToolBatchHook !== undefined && rs.turn.toolBatch.length > 0) {
|
|
503
506
|
const batch = rs.turn.toolBatch;
|
|
504
507
|
rs.turn.toolBatch = [];
|
|
505
508
|
batchArgs?.clear();
|
|
506
|
-
if (!boundarySteered && !rs.counters.finalizeInjected &&
|
|
509
|
+
if (!boundarySteered && !rs.counters.finalizeInjected && !finalVerifyInjectedThisTurn && !prepared.abortController.signal.aborted) {
|
|
507
510
|
try {
|
|
508
511
|
const r = await postToolBatchHook(batch);
|
|
509
512
|
if (r?.additionalContext) {
|
|
@@ -1345,6 +1348,16 @@ export class Runner {
|
|
|
1345
1348
|
queue.close();
|
|
1346
1349
|
return;
|
|
1347
1350
|
}
|
|
1351
|
+
const remoteEnvFailure = (() => {
|
|
1352
|
+
let cur = err;
|
|
1353
|
+
for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
|
|
1354
|
+
const note = cur.remoteEnvFailure;
|
|
1355
|
+
if (note !== undefined)
|
|
1356
|
+
return [note];
|
|
1357
|
+
cur = cur.cause;
|
|
1358
|
+
}
|
|
1359
|
+
return undefined;
|
|
1360
|
+
})();
|
|
1348
1361
|
resultValue = {
|
|
1349
1362
|
taskId: taskIdRef.current ?? "unknown",
|
|
1350
1363
|
sessionId: taskIdRef.sessionId ?? "unknown",
|
|
@@ -1352,6 +1365,7 @@ export class Runner {
|
|
|
1352
1365
|
result: "",
|
|
1353
1366
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1354
1367
|
errorCode: code,
|
|
1368
|
+
...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
|
|
1355
1369
|
stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
|
|
1356
1370
|
};
|
|
1357
1371
|
emitTrace(spec.tracer ?? this.deps.tracer, () => ({
|
|
@@ -1651,6 +1665,9 @@ export class Runner {
|
|
|
1651
1665
|
const gateToolName = "toolName" in resume.cp.gate ? resume.cp.gate.toolName : undefined;
|
|
1652
1666
|
prepared.humanReviewRef.gates.push({ kind: resume.cp.gate.kind, waitMs, ...(decision !== undefined ? { decision } : {}), ...(gateToolName !== undefined ? { toolName: gateToolName } : {}) });
|
|
1653
1667
|
}
|
|
1668
|
+
if (resume !== undefined && resume.outcome.gate === "plan_review" && resume.outcome.decision === "reject") {
|
|
1669
|
+
prepared.planModeRef.active = true;
|
|
1670
|
+
}
|
|
1654
1671
|
const rs = createRunState();
|
|
1655
1672
|
rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
|
|
1656
1673
|
rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
|
|
@@ -2520,13 +2537,26 @@ export class Runner {
|
|
|
2520
2537
|
let promptBlocked = false;
|
|
2521
2538
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
2522
2539
|
if (userPromptSubmit) {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2540
|
+
try {
|
|
2541
|
+
const decision = await userPromptSubmit(spec.objective);
|
|
2542
|
+
if (decision?.block) {
|
|
2543
|
+
prepared.blockedRef.reason = formatHookFeedback(decision.block);
|
|
2544
|
+
promptBlocked = true;
|
|
2545
|
+
}
|
|
2546
|
+
else if (decision?.additionalContext) {
|
|
2547
|
+
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
|
|
2548
|
+
}
|
|
2527
2549
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2550
|
+
catch (hookErr) {
|
|
2551
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
2552
|
+
try {
|
|
2553
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: prepared.sessionId });
|
|
2554
|
+
}
|
|
2555
|
+
catch {
|
|
2556
|
+
}
|
|
2557
|
+
prepared.blockedRef.reason = formatHookFeedback(`the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` +
|
|
2558
|
+
`the prompt was NOT submitted (fail-closed)`);
|
|
2559
|
+
promptBlocked = true;
|
|
2530
2560
|
}
|
|
2531
2561
|
}
|
|
2532
2562
|
const firstFrames = [];
|
|
@@ -2765,6 +2795,7 @@ export class Runner {
|
|
|
2765
2795
|
model: prepared.model.id,
|
|
2766
2796
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
2767
2797
|
rewindNotes: prepared.rewindNotes,
|
|
2798
|
+
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
2768
2799
|
abortedForTimeout: timeout.fired,
|
|
2769
2800
|
abortedForTurns: rs.limits.turnsExceeded,
|
|
2770
2801
|
abortedLive,
|
|
@@ -2774,10 +2805,18 @@ export class Runner {
|
|
|
2774
2805
|
outputInvalid: rs.degrade.outputInvalid,
|
|
2775
2806
|
suspendLoop: prepared.suspendLoopRef.hit,
|
|
2776
2807
|
suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
|
|
2777
|
-
? {
|
|
2808
|
+
? {
|
|
2809
|
+
token: prepared.suspendRef.token,
|
|
2810
|
+
gate: prepared.suspendRef.gate,
|
|
2811
|
+
...(prepared.suspendRef.restoreMode !== undefined ? { restoreMode: prepared.suspendRef.restoreMode } : {}),
|
|
2812
|
+
}
|
|
2778
2813
|
: undefined,
|
|
2779
2814
|
reviewRef: prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined
|
|
2780
|
-
? {
|
|
2815
|
+
? {
|
|
2816
|
+
token: prepared.reviewRef.token,
|
|
2817
|
+
gate: prepared.reviewRef.gate,
|
|
2818
|
+
...(prepared.reviewRef.restoreMode !== undefined ? { restoreMode: prepared.reviewRef.restoreMode } : {}),
|
|
2819
|
+
}
|
|
2781
2820
|
: undefined,
|
|
2782
2821
|
});
|
|
2783
2822
|
if (resume !== undefined &&
|
|
@@ -3082,7 +3121,7 @@ export class Runner {
|
|
|
3082
3121
|
if (!r.ok) {
|
|
3083
3122
|
const tooLarge = r.error.code === "too_large";
|
|
3084
3123
|
const refusedAt = Date.now();
|
|
3085
|
-
if (tooLarge)
|
|
3124
|
+
if (tooLarge && !this.snapshotTooLargeRoots.has(root))
|
|
3086
3125
|
this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
|
|
3087
3126
|
try {
|
|
3088
3127
|
this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
|
|
@@ -57,4 +57,5 @@ export declare function createToolSearchTool(opts: {
|
|
|
57
57
|
listingRide?: (newlyActivated: readonly string[]) => string | undefined;
|
|
58
58
|
mountedNames?: () => ReadonlySet<string>;
|
|
59
59
|
directCallEnabled?: boolean;
|
|
60
|
+
serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
|
|
60
61
|
}): AgentTool;
|
|
@@ -172,11 +172,22 @@ export function resolveToolSearch(args, registry) {
|
|
|
172
172
|
export function extractDiscoveredToolNames(messages, registry) {
|
|
173
173
|
const names = new Set();
|
|
174
174
|
const pendingDirect = new Map();
|
|
175
|
+
const pendingSearch = new Map();
|
|
175
176
|
for (const m of messages) {
|
|
176
177
|
if (m.role === "toolResult") {
|
|
177
178
|
const name = pendingDirect.get(m.toolCallId);
|
|
178
|
-
if (name !== undefined
|
|
179
|
-
|
|
179
|
+
if (name !== undefined) {
|
|
180
|
+
pendingDirect.delete(m.toolCallId);
|
|
181
|
+
if (m.isError !== true)
|
|
182
|
+
names.add(name);
|
|
183
|
+
}
|
|
184
|
+
const searched = pendingSearch.get(m.toolCallId);
|
|
185
|
+
if (searched !== undefined) {
|
|
186
|
+
pendingSearch.delete(m.toolCallId);
|
|
187
|
+
if (m.isError !== true)
|
|
188
|
+
for (const n of searched)
|
|
189
|
+
names.add(n);
|
|
190
|
+
}
|
|
180
191
|
continue;
|
|
181
192
|
}
|
|
182
193
|
if (m.role !== "assistant")
|
|
@@ -185,15 +196,18 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
185
196
|
if (part.type !== "toolCall")
|
|
186
197
|
continue;
|
|
187
198
|
if (part.name === TOOL_SEARCH_NAME) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
const prior = pendingSearch.get(part.id);
|
|
200
|
+
const resolved = resolveToolSearch(part.arguments, registry);
|
|
201
|
+
pendingSearch.set(part.id, prior === undefined ? resolved : [...prior, ...resolved]);
|
|
191
202
|
}
|
|
192
203
|
else if (registry.has(part.name)) {
|
|
193
204
|
pendingDirect.set(part.id, part.name);
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
}
|
|
208
|
+
for (const list of pendingSearch.values())
|
|
209
|
+
for (const n of list)
|
|
210
|
+
names.add(n);
|
|
197
211
|
return [...names];
|
|
198
212
|
}
|
|
199
213
|
export function createToolSearchTool(opts) {
|
|
@@ -209,6 +223,12 @@ export function createToolSearchTool(opts) {
|
|
|
209
223
|
"first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
|
|
210
224
|
'with query "select:<name>" before calling it. ';
|
|
211
225
|
let activationChain = Promise.resolve();
|
|
226
|
+
const serializeActivation = opts.serializeActivation ??
|
|
227
|
+
((section) => {
|
|
228
|
+
const p = activationChain.then(section);
|
|
229
|
+
activationChain = p.then(() => undefined, () => undefined);
|
|
230
|
+
return p;
|
|
231
|
+
});
|
|
212
232
|
return defineTool({
|
|
213
233
|
name: TOOL_SEARCH_NAME,
|
|
214
234
|
contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
|
|
@@ -259,7 +279,7 @@ export function createToolSearchTool(opts) {
|
|
|
259
279
|
},
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
|
-
const section =
|
|
282
|
+
const section = serializeActivation(async () => {
|
|
263
283
|
const newly = matched.filter((n) => !active.has(n));
|
|
264
284
|
if (newly.length > 0) {
|
|
265
285
|
for (const n of newly)
|
|
@@ -275,7 +295,6 @@ export function createToolSearchTool(opts) {
|
|
|
275
295
|
}
|
|
276
296
|
return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
|
|
277
297
|
});
|
|
278
|
-
activationChain = section.then(() => undefined, () => undefined);
|
|
279
298
|
const { newly, ride } = await section;
|
|
280
299
|
const lines = matched.map((n) => {
|
|
281
300
|
const info = registry.get(n);
|
|
@@ -114,6 +114,11 @@ export class TtlSessionStore {
|
|
|
114
114
|
}
|
|
115
115
|
const forked = await this.repo.fork(await source.getMetadata(), forkParams);
|
|
116
116
|
const meta = await forked.getMetadata();
|
|
117
|
+
const inherited = this.owners.has(sourceId) ? this.owners.get(sourceId) : undefined;
|
|
118
|
+
const forkOwner = owner ?? inherited;
|
|
119
|
+
if (forkOwner !== undefined && !this.owners.has(meta.id)) {
|
|
120
|
+
this.owners.set(meta.id, forkOwner);
|
|
121
|
+
}
|
|
117
122
|
this.entries.set(meta.id, {
|
|
118
123
|
session: forked,
|
|
119
124
|
lastActiveAt: Date.now(),
|
|
@@ -149,8 +154,13 @@ export class TtlSessionStore {
|
|
|
149
154
|
return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined;
|
|
150
155
|
}
|
|
151
156
|
async register(sessionId, owner) {
|
|
152
|
-
if (
|
|
153
|
-
|
|
157
|
+
if (this.owners.has(sessionId)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (owner !== null && typeof owner !== "string") {
|
|
161
|
+
throw new TypeError(`SessionStore.register: owner must be a string (owned) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner}`);
|
|
162
|
+
}
|
|
163
|
+
this.owners.set(sessionId, owner);
|
|
154
164
|
}
|
|
155
165
|
sweep(now = Date.now()) {
|
|
156
166
|
for (const [id, e] of this.entries) {
|
|
@@ -160,8 +170,9 @@ export class TtlSessionStore {
|
|
|
160
170
|
if (now - e.lastActiveAt > this.defaultTtlMs) {
|
|
161
171
|
this.entries.delete(id);
|
|
162
172
|
if (this.evictPolicy === "delete") {
|
|
163
|
-
void this.repo.delete({ id, createdAt: "" })
|
|
164
|
-
|
|
173
|
+
void this.repo.delete({ id, createdAt: "" }).then(() => {
|
|
174
|
+
this.owners.delete(id);
|
|
175
|
+
}, () => { });
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SkillToolEntry {
|
|
2
|
+
readonly raw: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly specifier?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSkillToolEntry(entry: string): SkillToolEntry;
|
|
7
|
+
export declare function isSkillSpecifierEnforced(canonicalName: string): boolean;
|
|
8
|
+
export declare function skillSpecifierRejection(entry: SkillToolEntry, args: unknown): string | undefined;
|