@sema-agent/core 5.55.0 → 5.56.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/CHANGELOG.md +66 -0
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +34 -23
- package/dist/core/runner/runtask.js +158 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +37 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +10 -4
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ import { isValidReminderMark, mintReminderMark, reminderMarkDeclaration } from "
|
|
|
30
30
|
import { policyAskClassOf } from "../ask-class.js";
|
|
31
31
|
import { emitTrace } from "../trace.js";
|
|
32
32
|
import { createSessionRulePolicy, PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
|
|
33
|
-
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, mintHookInvocationIdentity, persistedRuleMandateOf, runToolGate } from "../hooks.js";
|
|
33
|
+
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, mintHookInvocationIdentity, persistedRuleMandateOf, resolveHookTimeoutMs, runHookSeat, hookSeatExpiredError, runToolGate } from "../hooks.js";
|
|
34
34
|
import { createWriteProtectionCheck } from "../write-protect.js";
|
|
35
35
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
36
36
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
@@ -39,7 +39,7 @@ import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGua
|
|
|
39
39
|
import { defineTool, isDefineToolProduct } from "../tools.js";
|
|
40
40
|
import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
|
|
41
41
|
import { protocolOf } from "../protocol-table.js";
|
|
42
|
-
import {
|
|
42
|
+
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
|
|
43
43
|
import { pathToUri } from "../lsp-protocol.js";
|
|
44
44
|
import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
|
|
45
45
|
import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
|
|
@@ -302,6 +302,20 @@ function raceAbort(p, signal, onAbort) {
|
|
|
302
302
|
p.then(finish, () => finish(onAbort()));
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
|
+
function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
|
|
306
|
+
if (hooks?.permissionDenied === undefined)
|
|
307
|
+
return undefined;
|
|
308
|
+
return async (payload) => {
|
|
309
|
+
try {
|
|
310
|
+
const seat = await runHookSeat("permissionDenied", { timeoutMs: hookTimeoutMs, signal }, (sig) => hooks.permissionDenied({ ...payload, signal: sig }));
|
|
311
|
+
if (seat.expired)
|
|
312
|
+
report(hookSeatExpiredError("permissionDenied", hookTimeoutMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged"));
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
report(err);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
305
319
|
function mcpRevocationWiring(deps) {
|
|
306
320
|
if (deps.mcpRevocations === undefined)
|
|
307
321
|
return undefined;
|
|
@@ -859,7 +873,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
859
873
|
pc.autoMode?.decider === autoModeDecider)
|
|
860
874
|
? [
|
|
861
875
|
{
|
|
862
|
-
policy: createPreToolUseConstraintPolicy(ownPreToolUse, hookEnvFace, notifyOwnHookCrash),
|
|
876
|
+
policy: createPreToolUseConstraintPolicy(ownPreToolUse, hookEnvFace, notifyOwnHookCrash, hookTimeoutMs),
|
|
863
877
|
preToolUse: ownPreToolUse,
|
|
864
878
|
...(hookEnvSource !== undefined ? { hookEnv: hookEnvSource } : {}),
|
|
865
879
|
...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
|
|
@@ -2711,7 +2725,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2711
2725
|
for (const n of list ?? []) {
|
|
2712
2726
|
if (known.has(n))
|
|
2713
2727
|
continue;
|
|
2714
|
-
if (
|
|
2728
|
+
if (isNamespacedCoveringRuleName(n) && [...known].some((k) => namespacedRuleNameCovers(n, k)))
|
|
2715
2729
|
continue;
|
|
2716
2730
|
const retired = RETIRED_TOOL_NAMES.get(n);
|
|
2717
2731
|
if (retired !== undefined) {
|
|
@@ -2795,6 +2809,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2795
2809
|
const denyNarrowingPolicy = narrowingLayers.length === 0 ? undefined : narrowingLayers.length === 1 ? narrowingLayers[0] : combinePolicies(...narrowingLayers);
|
|
2796
2810
|
const basePolicyForResumeEdit = lockedPreflight.basePolicyForResumeEdit ?? lockedPreflight.toolPolicy;
|
|
2797
2811
|
const hooks = spec.hooks ?? deps.hooks;
|
|
2812
|
+
const hookTimeoutMs = resolveHookTimeoutMs(hooks?.timeoutMs, notifyOwnHookCrash, hooks);
|
|
2798
2813
|
const preToolUseObservational = hooks?.preToolUse !== undefined && hooks.preToolUseObservational === true;
|
|
2799
2814
|
const ownGatePreToolUse = !preToolUseObservational
|
|
2800
2815
|
? hooks?.preToolUse
|
|
@@ -3571,21 +3586,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3571
3586
|
}
|
|
3572
3587
|
return resolved;
|
|
3573
3588
|
};
|
|
3574
|
-
const
|
|
3575
|
-
const notifyPermissionDenied = hooksWithPermissionDenied
|
|
3576
|
-
? async (payload) => {
|
|
3577
|
-
try {
|
|
3578
|
-
await hooksWithPermissionDenied.permissionDenied(payload);
|
|
3579
|
-
}
|
|
3580
|
-
catch (err) {
|
|
3581
|
-
try {
|
|
3582
|
-
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
|
|
3583
|
-
}
|
|
3584
|
-
catch {
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
: undefined;
|
|
3589
|
+
const notifyPermissionDenied = createDenyObserverNotifier(hooks, hookTimeoutMs, abortController.signal, notifyOwnHookCrash);
|
|
3589
3590
|
const notifyHookError = (err) => {
|
|
3590
3591
|
try {
|
|
3591
3592
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
|
|
@@ -4302,6 +4303,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4302
4303
|
identity: hookIdentity,
|
|
4303
4304
|
reminderMark,
|
|
4304
4305
|
preToolUse: ownGatePreToolUse,
|
|
4306
|
+
hookTimeoutMs,
|
|
4307
|
+
...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
|
|
4305
4308
|
...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
|
|
4306
4309
|
adjudicate,
|
|
4307
4310
|
resolveAsk: resolveAskBound,
|
|
@@ -4418,7 +4421,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4418
4421
|
}
|
|
4419
4422
|
catch {
|
|
4420
4423
|
}
|
|
4421
|
-
const
|
|
4424
|
+
const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(e.toolName, e.input, {
|
|
4422
4425
|
error: e.content
|
|
4423
4426
|
.filter((c) => c.type === "text")
|
|
4424
4427
|
.map((c) => c.text)
|
|
@@ -4426,7 +4429,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4426
4429
|
isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
|
|
4427
4430
|
content: e.content.map((c) => ({ ...c })),
|
|
4428
4431
|
details: clonedDetails,
|
|
4429
|
-
}, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
|
|
4432
|
+
}, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
|
|
4433
|
+
if (failureSeat.expired) {
|
|
4434
|
+
notifyOwnHookCrash(hookSeatExpiredError("postToolUseFailure", hookTimeoutMs, failureSeat.cause, "the observation was abandoned and its additionalContext dropped; the failure result itself is unchanged"));
|
|
4435
|
+
}
|
|
4436
|
+
const patch = failureSeat.expired ? undefined : failureSeat.value;
|
|
4430
4437
|
if (patch?.additionalContext) {
|
|
4431
4438
|
content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext, reminderMark) }];
|
|
4432
4439
|
changed = true;
|
|
@@ -4434,7 +4441,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4434
4441
|
}
|
|
4435
4442
|
}
|
|
4436
4443
|
else if (hooks?.postToolUse) {
|
|
4437
|
-
const
|
|
4444
|
+
const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
|
|
4445
|
+
if (successSeat.expired) {
|
|
4446
|
+
notifyOwnHookCrash(hookSeatExpiredError("postToolUse", hookTimeoutMs, successSeat.cause, "the seat was abandoned; the tool result is delivered to the model UNMODIFIED (no updatedOutput, no additionalContext)"));
|
|
4447
|
+
}
|
|
4448
|
+
const patch = successSeat.expired ? undefined : successSeat.value;
|
|
4438
4449
|
if (patch?.updatedOutput) {
|
|
4439
4450
|
content = patch.updatedOutput;
|
|
4440
4451
|
changed = true;
|
|
@@ -4722,7 +4733,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4722
4733
|
const effectiveReadFaceObserved = carrierReadFace();
|
|
4723
4734
|
const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
|
|
4724
4735
|
const preparedHolder = {};
|
|
4725
|
-
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4736
|
+
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4726
4737
|
const prepared = buildPrepared();
|
|
4727
4738
|
preparedHolder.current = prepared;
|
|
4728
4739
|
return prepared;
|
|
@@ -39,15 +39,15 @@ import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
|
39
39
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
40
40
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
41
41
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
42
|
-
import { cloneObserverInput, formatHookFeedback } from "../hooks.js";
|
|
43
|
-
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
42
|
+
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
43
|
+
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
44
44
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
45
|
-
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
45
|
+
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
46
46
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
47
47
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
48
48
|
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
49
49
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
50
|
-
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
50
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
51
51
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
52
52
|
import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
|
|
53
53
|
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
@@ -767,7 +767,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
767
767
|
const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
|
|
768
768
|
if (!boundarySteered && !prepared.abortController.signal.aborted) {
|
|
769
769
|
try {
|
|
770
|
-
const
|
|
770
|
+
const batchSeat = await runHookSeat("postToolBatch", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }, (sig) => postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity, signal: sig }));
|
|
771
|
+
if (batchSeat.expired) {
|
|
772
|
+
runnerHooks.onError?.(hookSeatExpiredError("postToolBatch", prepared.hookTimeoutMs, batchSeat.cause, "the boundary observation was abandoned and its additionalContext dropped; the turn boundary itself is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
|
|
773
|
+
}
|
|
774
|
+
const r = batchSeat.expired ? undefined : batchSeat.value;
|
|
771
775
|
if (r?.additionalContext && injectedThisTurn === undefined) {
|
|
772
776
|
const body = sanitizeUntrustedText(r.additionalContext, SHELLED_BODY_ENVELOPE_TAGS);
|
|
773
777
|
const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
|
|
@@ -1504,6 +1508,7 @@ export class Runner {
|
|
|
1504
1508
|
...(typeof h.postCompact === "function" ? { postCompact: (c) => h.postCompact(c) } : {}),
|
|
1505
1509
|
...(typeof h.stopFailure === "function" ? { stopFailure: (c) => h.stopFailure(c) } : {}),
|
|
1506
1510
|
...(typeof h.permissionDenied === "function" ? { permissionDenied: (p) => h.permissionDenied(p) } : {}),
|
|
1511
|
+
...(h.timeoutMs !== undefined ? { timeoutMs: h.timeoutMs } : {}),
|
|
1507
1512
|
}
|
|
1508
1513
|
: undefined;
|
|
1509
1514
|
return {
|
|
@@ -1783,6 +1788,13 @@ export class Runner {
|
|
|
1783
1788
|
const actorIn = options?.actor;
|
|
1784
1789
|
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
1785
1790
|
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1791
|
+
const effectiveInputId = typeof inputId === "string" ? inputId : uuidv7();
|
|
1792
|
+
const parkRecord = {
|
|
1793
|
+
text,
|
|
1794
|
+
trusted,
|
|
1795
|
+
inputId: effectiveInputId,
|
|
1796
|
+
...(actor !== undefined ? { actor } : {}),
|
|
1797
|
+
};
|
|
1786
1798
|
let payload;
|
|
1787
1799
|
let mintsAFrame;
|
|
1788
1800
|
let replay;
|
|
@@ -1797,7 +1809,7 @@ export class Runner {
|
|
|
1797
1809
|
source: "steer",
|
|
1798
1810
|
delivery: "queued",
|
|
1799
1811
|
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1800
|
-
|
|
1812
|
+
inputId: effectiveInputId,
|
|
1801
1813
|
...(actor !== undefined ? { actor } : {}),
|
|
1802
1814
|
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1803
1815
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
@@ -1814,7 +1826,7 @@ export class Runner {
|
|
|
1814
1826
|
const h = handle ?? (await orTimeout(ready));
|
|
1815
1827
|
if (!h)
|
|
1816
1828
|
throw steeringError("the task is not running");
|
|
1817
|
-
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : projected;
|
|
1829
|
+
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
|
|
1818
1830
|
mintsAFrame = payload.trim().length !== 0;
|
|
1819
1831
|
replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
|
|
1820
1832
|
if (typeof inputId === "string") {
|
|
@@ -1830,7 +1842,7 @@ export class Runner {
|
|
|
1830
1842
|
}
|
|
1831
1843
|
}
|
|
1832
1844
|
try {
|
|
1833
|
-
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, ...(actor !== undefined ? { actor } : {}) });
|
|
1845
|
+
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
|
|
1834
1846
|
noteAccepted(h);
|
|
1835
1847
|
return;
|
|
1836
1848
|
}
|
|
@@ -1841,7 +1853,7 @@ export class Runner {
|
|
|
1841
1853
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1842
1854
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1843
1855
|
try {
|
|
1844
|
-
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, ...(actor !== undefined ? { actor } : {}) });
|
|
1856
|
+
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
|
|
1845
1857
|
noteAccepted(h);
|
|
1846
1858
|
return;
|
|
1847
1859
|
}
|
|
@@ -1882,6 +1894,9 @@ export class Runner {
|
|
|
1882
1894
|
if (input.source !== undefined && typeof input.source !== "string") {
|
|
1883
1895
|
throw notifyError("input.source must be a string when present", "notify.invalid_payload");
|
|
1884
1896
|
}
|
|
1897
|
+
if (opts?.priority !== undefined && !isSystemInjectionPriority(opts.priority)) {
|
|
1898
|
+
throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
|
|
1899
|
+
}
|
|
1885
1900
|
const payload = {
|
|
1886
1901
|
task_id: input.task_id,
|
|
1887
1902
|
task_type: "external",
|
|
@@ -1945,6 +1960,8 @@ export class Runner {
|
|
|
1945
1960
|
const h = handle ?? (await orTimeout(ready));
|
|
1946
1961
|
if (!h)
|
|
1947
1962
|
return;
|
|
1963
|
+
if (!h.abortController.signal.aborted)
|
|
1964
|
+
h.loop.userInterrupted = true;
|
|
1948
1965
|
h.abortController.abort();
|
|
1949
1966
|
void h.harness.abort();
|
|
1950
1967
|
},
|
|
@@ -2029,7 +2046,18 @@ export class Runner {
|
|
|
2029
2046
|
});
|
|
2030
2047
|
const upstreamTaskNotification = internals?.onTaskNotification;
|
|
2031
2048
|
const deliveredAtTurnOpen = new Set();
|
|
2049
|
+
let priorityNowDisclosed = false;
|
|
2032
2050
|
const injectTaskNotification = (notification, opts) => {
|
|
2051
|
+
if (opts?.priority === "now" && !priorityNowDisclosed) {
|
|
2052
|
+
priorityNowDisclosed = true;
|
|
2053
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
2054
|
+
code: "task.injection_priority_unimplemented",
|
|
2055
|
+
message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
|
|
2056
|
+
`priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
|
|
2057
|
+
`delivered — only the interrupting semantics are absent.`,
|
|
2058
|
+
detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}) },
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2033
2061
|
if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
|
|
2034
2062
|
return Promise.resolve("dropped_duplicate");
|
|
2035
2063
|
if (!notificationLaneLive) {
|
|
@@ -2098,7 +2126,12 @@ export class Runner {
|
|
|
2098
2126
|
this.pendingSessionNotifications.pend(notificationSessionId, p);
|
|
2099
2127
|
};
|
|
2100
2128
|
prepared.harness.engineInjectionsHeld = () => prepared.batchHaltRef.current !== undefined;
|
|
2129
|
+
let undrainedUserAtEnd;
|
|
2101
2130
|
prepared.harness.onUndrainedUserInputs = (counts) => {
|
|
2131
|
+
if (prepared.suspendRef.token !== undefined || prepared.reviewRef.token !== undefined) {
|
|
2132
|
+
undrainedUserAtEnd = counts;
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2102
2135
|
for (const notice of undrainedUserInputNotices(counts, spec.taskId)) {
|
|
2103
2136
|
deliverEngineNotice(this.deps.onNotice, notice);
|
|
2104
2137
|
}
|
|
@@ -2127,7 +2160,7 @@ export class Runner {
|
|
|
2127
2160
|
}
|
|
2128
2161
|
}
|
|
2129
2162
|
}
|
|
2130
|
-
const loopLatch = { ended: false };
|
|
2163
|
+
const loopLatch = { ended: false, userInterrupted: false };
|
|
2131
2164
|
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
|
|
2132
2165
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
2133
2166
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
@@ -2809,12 +2842,21 @@ export class Runner {
|
|
|
2809
2842
|
return [];
|
|
2810
2843
|
let result;
|
|
2811
2844
|
try {
|
|
2812
|
-
|
|
2845
|
+
const stopSeat = await runHookSeat("stop", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => stopHook({
|
|
2813
2846
|
stopHookActive: consecutiveBlocks > 0,
|
|
2814
2847
|
consecutiveBlocks,
|
|
2815
2848
|
getBranch: () => prepared.session.getBranch(),
|
|
2816
2849
|
identity: prepared.hookIdentity,
|
|
2817
|
-
|
|
2850
|
+
signal: sig,
|
|
2851
|
+
}));
|
|
2852
|
+
if (stopSeat.expired) {
|
|
2853
|
+
if (stopSeat.cause === "timeout") {
|
|
2854
|
+
this.deps.onError?.(hookSeatExpiredError("stop", prepared.hookTimeoutMs, stopSeat.cause, "the run was allowed to END (the seat's own no-opinion answer); no pushback and no additional context were injected"), { phase: "hook", sessionId: prepared.sessionId });
|
|
2855
|
+
}
|
|
2856
|
+
consecutiveBlocks = 0;
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
result = stopSeat.value;
|
|
2818
2860
|
}
|
|
2819
2861
|
catch (err) {
|
|
2820
2862
|
this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
|
|
@@ -2889,7 +2931,7 @@ export class Runner {
|
|
|
2889
2931
|
...this.seamCCompactionOptions(prepared),
|
|
2890
2932
|
...gitRestateOption(prepared),
|
|
2891
2933
|
...windowSafetyOptions(prepared.harness.getModel()),
|
|
2892
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity),
|
|
2934
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
|
|
2893
2935
|
});
|
|
2894
2936
|
if (comp.compacted) {
|
|
2895
2937
|
compactionBreaker.failures = 0;
|
|
@@ -2954,7 +2996,7 @@ export class Runner {
|
|
|
2954
2996
|
runnerHooks: {
|
|
2955
2997
|
onError: this.deps.onError,
|
|
2956
2998
|
seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
|
|
2957
|
-
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity),
|
|
2999
|
+
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
|
|
2958
3000
|
recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
|
|
2959
3001
|
},
|
|
2960
3002
|
});
|
|
@@ -3057,6 +3099,7 @@ export class Runner {
|
|
|
3057
3099
|
let final;
|
|
3058
3100
|
let threw;
|
|
3059
3101
|
let abortedLive = false;
|
|
3102
|
+
let userInterruptedLive = false;
|
|
3060
3103
|
let strandedHumanAnswers = [];
|
|
3061
3104
|
try {
|
|
3062
3105
|
if (prepared.abortController.signal.aborted) {
|
|
@@ -3184,7 +3227,23 @@ export class Runner {
|
|
|
3184
3227
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
3185
3228
|
if (userPromptSubmit) {
|
|
3186
3229
|
try {
|
|
3187
|
-
const
|
|
3230
|
+
const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, signal: sig }));
|
|
3231
|
+
if (promptSeat.expired) {
|
|
3232
|
+
if (promptSeat.cause === "timeout") {
|
|
3233
|
+
try {
|
|
3234
|
+
this.deps.onError?.(hookSeatExpiredError("userPromptSubmit", prepared.hookTimeoutMs, promptSeat.cause, "the prompt was NOT submitted (fail-closed) and the task ends blocked"), { phase: "hook", sessionId: prepared.sessionId });
|
|
3235
|
+
}
|
|
3236
|
+
catch {
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
prepared.blockedRef.reason = formatHookFeedback(promptSeat.cause === "timeout"
|
|
3240
|
+
? `the deployment's userPromptSubmit hook did not answer within its ${prepared.hookTimeoutMs}ms bound while screening this prompt; ` +
|
|
3241
|
+
`the prompt was NOT submitted (fail-closed)`
|
|
3242
|
+
: `the task was cancelled while the deployment's userPromptSubmit hook was still screening this prompt; ` +
|
|
3243
|
+
`the prompt was NOT submitted (fail-closed)`, prepared.reminderMark);
|
|
3244
|
+
promptBlocked = true;
|
|
3245
|
+
}
|
|
3246
|
+
const decision = promptSeat.expired ? undefined : promptSeat.value;
|
|
3188
3247
|
if (decision?.block) {
|
|
3189
3248
|
prepared.blockedRef.reason = formatHookFeedback(decision.block, prepared.reminderMark);
|
|
3190
3249
|
promptBlocked = true;
|
|
@@ -3353,6 +3412,7 @@ export class Runner {
|
|
|
3353
3412
|
}
|
|
3354
3413
|
loopLatch.ended = true;
|
|
3355
3414
|
abortedLive = prepared.abortController.signal.aborted;
|
|
3415
|
+
userInterruptedLive = loopLatch.userInterrupted;
|
|
3356
3416
|
}
|
|
3357
3417
|
catch (err) {
|
|
3358
3418
|
loopLatch.ended = true;
|
|
@@ -3364,6 +3424,7 @@ export class Runner {
|
|
|
3364
3424
|
}
|
|
3365
3425
|
threw = err;
|
|
3366
3426
|
abortedLive = prepared.abortController.signal.aborted;
|
|
3427
|
+
userInterruptedLive = loopLatch.userInterrupted;
|
|
3367
3428
|
}
|
|
3368
3429
|
finally {
|
|
3369
3430
|
timeout.clear();
|
|
@@ -3401,13 +3462,17 @@ export class Runner {
|
|
|
3401
3462
|
const durablyPaused = suspended || reviewPaused;
|
|
3402
3463
|
const interrupted = !durablyPaused &&
|
|
3403
3464
|
(threw !== undefined || timeout.fired || rs.limits.turnsExceeded || rs.limits.budgetHit !== undefined || abortedLive || final?.stopReason === "aborted");
|
|
3465
|
+
let orphansClosed = 0;
|
|
3466
|
+
let reconcileComplete = false;
|
|
3404
3467
|
if (interrupted) {
|
|
3405
3468
|
try {
|
|
3406
3469
|
const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
|
|
3470
|
+
orphansClosed = report.recovered.length;
|
|
3407
3471
|
for (const orphan of report.recovered) {
|
|
3408
3472
|
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
|
|
3409
3473
|
emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
|
|
3410
3474
|
}
|
|
3475
|
+
reconcileComplete = true;
|
|
3411
3476
|
}
|
|
3412
3477
|
catch (reconcileErr) {
|
|
3413
3478
|
this.deps.onError?.(reconcileErr instanceof Error ? reconcileErr : new Error(String(reconcileErr)), {
|
|
@@ -3425,6 +3490,52 @@ export class Runner {
|
|
|
3425
3490
|
});
|
|
3426
3491
|
}
|
|
3427
3492
|
}
|
|
3493
|
+
if (interrupted && userInterruptedLive && reconcileComplete) {
|
|
3494
|
+
try {
|
|
3495
|
+
const entryId = await appendInterruptionMarker(prepared.session, { toolUseInFlight: orphansClosed > 0 });
|
|
3496
|
+
emitCommitted(entryId, "user");
|
|
3497
|
+
}
|
|
3498
|
+
catch (markerErr) {
|
|
3499
|
+
this.deps.onError?.(markerErr instanceof Error ? markerErr : new Error(String(markerErr)), {
|
|
3500
|
+
phase: "interrupt-reconcile",
|
|
3501
|
+
sessionId: prepared.sessionId,
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
let migratedParked = { steer: 0, followUp: 0 };
|
|
3506
|
+
const parkToken = prepared.suspendRef.token ?? prepared.reviewRef.token;
|
|
3507
|
+
const parkScope = prepared.suspendRef.token !== undefined ? prepared.suspendRef.scope : prepared.reviewRef.scope;
|
|
3508
|
+
if (durablyPaused && parkToken !== undefined && parkScope !== undefined && this.deps.checkpointStore !== undefined) {
|
|
3509
|
+
const store = this.deps.checkpointStore;
|
|
3510
|
+
const carried = [];
|
|
3511
|
+
for (const record of prepared.harness.readParkableUserInputs()) {
|
|
3512
|
+
try {
|
|
3513
|
+
if (!(await store.setPendingSteer(parkToken, parkScope, record)))
|
|
3514
|
+
break;
|
|
3515
|
+
carried.push(record);
|
|
3516
|
+
}
|
|
3517
|
+
catch (parkErr) {
|
|
3518
|
+
this.deps.onError?.(parkErr instanceof Error ? parkErr : new Error(String(parkErr)), {
|
|
3519
|
+
phase: "config",
|
|
3520
|
+
sessionId: prepared.sessionId,
|
|
3521
|
+
});
|
|
3522
|
+
const parkCode = errorCodeOf(parkErr);
|
|
3523
|
+
if (parkCode === "steering.invalid_content" || parkCode === "steering.queue_full")
|
|
3524
|
+
continue;
|
|
3525
|
+
break;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
migratedParked = prepared.harness.dropParkedUserInputs(carried);
|
|
3529
|
+
}
|
|
3530
|
+
if (undrainedUserAtEnd !== undefined) {
|
|
3531
|
+
const remaining = {
|
|
3532
|
+
steer: Math.max(0, undrainedUserAtEnd.steer - migratedParked.steer),
|
|
3533
|
+
followUp: Math.max(0, undrainedUserAtEnd.followUp - migratedParked.followUp),
|
|
3534
|
+
};
|
|
3535
|
+
for (const notice of undrainedUserInputNotices(remaining, spec.taskId)) {
|
|
3536
|
+
deliverEngineNotice(this.deps.onNotice, notice);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3428
3539
|
if (rs.attach.attachState?.postCompactPending === true && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
|
|
3429
3540
|
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
|
|
3430
3541
|
}
|
|
@@ -3434,6 +3545,7 @@ export class Runner {
|
|
|
3434
3545
|
brain: compactionBrain,
|
|
3435
3546
|
windowSafety: windowSafetyOptions(prepared.model),
|
|
3436
3547
|
onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
|
|
3548
|
+
cancelled: abortedLive,
|
|
3437
3549
|
});
|
|
3438
3550
|
try {
|
|
3439
3551
|
if (comp?.compacted) {
|
|
@@ -3596,12 +3708,20 @@ export class Runner {
|
|
|
3596
3708
|
!abortedLive &&
|
|
3597
3709
|
result.errorCode !== "conflict") {
|
|
3598
3710
|
try {
|
|
3599
|
-
await stopFailureHook({
|
|
3711
|
+
const failureSeat = await runHookSeat("stopFailure", { timeoutMs: prepared.hookTimeoutMs }, (sig) => stopFailureHook({
|
|
3600
3712
|
identity: prepared.hookIdentity,
|
|
3601
3713
|
error: result.errorMessage ?? "model error",
|
|
3602
3714
|
...(result.errorCode !== undefined ? { errorKind: result.errorCode } : {}),
|
|
3603
3715
|
turns: stats.turns,
|
|
3604
|
-
|
|
3716
|
+
signal: sig,
|
|
3717
|
+
}));
|
|
3718
|
+
if (failureSeat.expired) {
|
|
3719
|
+
try {
|
|
3720
|
+
this.deps.onError?.(hookSeatExpiredError("stopFailure", prepared.hookTimeoutMs, failureSeat.cause, "the terminal observation was abandoned; the assembled TaskResult is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
|
|
3721
|
+
}
|
|
3722
|
+
catch {
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3605
3725
|
}
|
|
3606
3726
|
catch (err) {
|
|
3607
3727
|
try {
|
|
@@ -4612,10 +4732,12 @@ export class Runner {
|
|
|
4612
4732
|
consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
|
|
4613
4733
|
};
|
|
4614
4734
|
}
|
|
4615
|
-
compactionHookOptions(spec, sessionId, trigger, identity) {
|
|
4735
|
+
compactionHookOptions(spec, sessionId, trigger, identity, seatBound) {
|
|
4616
4736
|
const hooks = spec.hooks ?? this.deps.hooks;
|
|
4617
4737
|
const pre = hooks?.preCompact;
|
|
4618
4738
|
const post = hooks?.postCompact;
|
|
4739
|
+
const seatMs = seatBound?.timeoutMs ?? resolveHookTimeoutMs(hooks?.timeoutMs);
|
|
4740
|
+
const seatSignal = seatBound?.signal;
|
|
4619
4741
|
const withIdentity = (ctx) => identity !== undefined ? { ...ctx, identity } : ctx;
|
|
4620
4742
|
return {
|
|
4621
4743
|
trigger,
|
|
@@ -4631,7 +4753,12 @@ export class Runner {
|
|
|
4631
4753
|
}
|
|
4632
4754
|
};
|
|
4633
4755
|
try {
|
|
4634
|
-
const
|
|
4756
|
+
const seat = await runHookSeat("preCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => pre.call(hooks, { ...ctx, signal: sig }));
|
|
4757
|
+
if (seat.expired) {
|
|
4758
|
+
report(hookSeatExpiredError("preCompact", seatMs, seat.cause, `the "${ctx.trigger}" compaction PROCEEDED unblocked and with no additional instructions`));
|
|
4759
|
+
return undefined;
|
|
4760
|
+
}
|
|
4761
|
+
const r = seat.value;
|
|
4635
4762
|
if (r?.block && ctx.trigger === "forced") {
|
|
4636
4763
|
report(new Error(`a preCompact callback blocked a "forced" compaction — ignored (PTL/trim-pressure compaction is not optional): ${r.block}`));
|
|
4637
4764
|
}
|
|
@@ -4648,7 +4775,14 @@ export class Runner {
|
|
|
4648
4775
|
? {
|
|
4649
4776
|
postCompact: async (rawCtx) => {
|
|
4650
4777
|
try {
|
|
4651
|
-
await post.call(hooks, withIdentity(rawCtx));
|
|
4778
|
+
const seat = await runHookSeat("postCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => post.call(hooks, { ...withIdentity(rawCtx), signal: sig }));
|
|
4779
|
+
if (seat.expired) {
|
|
4780
|
+
try {
|
|
4781
|
+
this.deps.onError?.(hookSeatExpiredError("postCompact", seatMs, seat.cause, "the observation was abandoned; the compaction that already landed is unchanged"), { phase: "hook", sessionId });
|
|
4782
|
+
}
|
|
4783
|
+
catch {
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4652
4786
|
}
|
|
4653
4787
|
catch (err) {
|
|
4654
4788
|
try {
|
|
@@ -4692,7 +4826,10 @@ export class Runner {
|
|
|
4692
4826
|
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
4693
4827
|
...this.seamCCompactionOptions(prepared),
|
|
4694
4828
|
...gitRestateOption(prepared),
|
|
4695
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity
|
|
4829
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity, {
|
|
4830
|
+
timeoutMs: prepared.hookTimeoutMs,
|
|
4831
|
+
...(opts?.cancelled === true ? { signal: prepared.abortController.signal } : {}),
|
|
4832
|
+
}),
|
|
4696
4833
|
});
|
|
4697
4834
|
this.recordCompactionReuse(prepared, finishComp);
|
|
4698
4835
|
if (finishComp.unevaluableWindow) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
|
-
import { createCoarseCommandNamePolicy,
|
|
3
|
+
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
4
4
|
export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
|
|
5
5
|
export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
|
|
6
6
|
export function isWithin(root, p) {
|
|
@@ -24,8 +24,8 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
24
24
|
const { env, rootPath, toolEffects } = opts;
|
|
25
25
|
const toolDeny = new Set(rules.toolDeny ?? []);
|
|
26
26
|
const toolAllow = rules.toolAllow ? new Set(rules.toolAllow) : undefined;
|
|
27
|
-
const toolDenyCovering =
|
|
28
|
-
const toolAllowCovering =
|
|
27
|
+
const toolDenyCovering = namespacedCoveringEntries(rules.toolDeny);
|
|
28
|
+
const toolAllowCovering = namespacedCoveringEntries(rules.toolAllow);
|
|
29
29
|
const cmdPolicy = rules.commandAllow || rules.commandDeny
|
|
30
30
|
? createCoarseCommandNamePolicy({
|
|
31
31
|
...(rules.commandAllow ? { allow: rules.commandAllow } : {}),
|
|
@@ -38,9 +38,9 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
38
38
|
nameSets: [{ ...(rules.toolDeny?.length ? { deny: [...rules.toolDeny] } : {}), ...(rules.toolAllow?.length ? { allow: [...rules.toolAllow] } : {}) }],
|
|
39
39
|
async check(req, signal) {
|
|
40
40
|
const toolName = req.toolName;
|
|
41
|
-
if (toolDeny.has(toolName) ||
|
|
41
|
+
if (toolDeny.has(toolName) || namespacedCoveringHit(toolDenyCovering, toolName))
|
|
42
42
|
return deny(`tool "${req.toolName}" is denied by a session rule`);
|
|
43
|
-
if (toolAllow && !toolAllow.has(toolName) && !
|
|
43
|
+
if (toolAllow && !toolAllow.has(toolName) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
|
|
44
44
|
return deny(`tool "${req.toolName}" is not in the session-rule allowlist`);
|
|
45
45
|
}
|
|
46
46
|
if (cmdPolicy) {
|
|
@@ -84,6 +84,38 @@ export interface ReconcileReport {
|
|
|
84
84
|
* `runner.resume()` uses a checkpoint-aware entry that bypasses this reconcile entirely for those calls.
|
|
85
85
|
*/
|
|
86
86
|
export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
|
|
87
|
+
/**
|
|
88
|
+
* backlog #389 伴生 (D-2) — CC 2.1.223's interruption markers, VERBATIM (`$U`/`CR` @ `CC:151120-151121`,
|
|
89
|
+
* minted as a USER message by `Jce` @ `CC:640141-640154`). CC mints one on every abort whose reason is
|
|
90
|
+
* outside `{"interrupt","refusal-fallback-edit"}` — and the interactive Esc / remote cancel, which is
|
|
91
|
+
* what `TaskStream.interrupt()` corresponds to, is precisely on the minting side (`CC:1033698-1033733`).
|
|
92
|
+
* The suppressed reason is the one case where the user's own replacement message is already the context.
|
|
93
|
+
*
|
|
94
|
+
* Taken verbatim rather than reworded: this string is an INPUT to later reasoning in CC (its own
|
|
95
|
+
* "interrupted then immediately retried the same action" rule reads it back), and the constitution's
|
|
96
|
+
* standing rule is that a question CC has answered is answered in CC's form.
|
|
97
|
+
*/
|
|
98
|
+
export declare const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
99
|
+
/** backlog #389 伴生 — the tool-use variant (`CR`): the run was cut while a tool batch was in flight. */
|
|
100
|
+
export declare const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
101
|
+
/**
|
|
102
|
+
* backlog #389 伴生 (D-2) — append the interruption marker so the SESSION records that a person stopped
|
|
103
|
+
* this run.
|
|
104
|
+
*
|
|
105
|
+
* Without it, an interrupt that lands on the model stream (no tool call in flight) leaves literally no
|
|
106
|
+
* trace: the orphan reconcile has nothing to close, and the empty aborted assistant is deliberately not
|
|
107
|
+
* persisted (`isEmptyFailureAssistant`). The next run on that session then reads a transcript in which
|
|
108
|
+
* the half-finished work simply stops, and continues as if it had ended by itself.
|
|
109
|
+
*
|
|
110
|
+
* MUST be called AFTER {@link reconcileInterruptedSession} on the same interruption: a user message
|
|
111
|
+
* placed between an assistant's tool calls and their results is exactly the invalid sequence the
|
|
112
|
+
* reconcile exists to prevent.
|
|
113
|
+
*
|
|
114
|
+
* Returns the persisted entry id so the caller can mint its `message_committed` frame.
|
|
115
|
+
*/
|
|
116
|
+
export declare function appendInterruptionMarker(session: Session, opts: {
|
|
117
|
+
toolUseInFlight: boolean;
|
|
118
|
+
}): Promise<string>;
|
|
87
119
|
/**
|
|
88
120
|
* Reconcile a resumed session's active branch: close any orphan tool calls with a synthetic
|
|
89
121
|
* interrupted `toolResult` (never re-running the tool). Returns what was recovered.
|
|
@@ -81,6 +81,21 @@ export function findOrphanToolCalls(messages, suspendedBatch) {
|
|
|
81
81
|
});
|
|
82
82
|
return orphans;
|
|
83
83
|
}
|
|
84
|
+
export const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
85
|
+
export const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
86
|
+
export async function appendInterruptionMarker(session, opts) {
|
|
87
|
+
return await session.appendMessage({
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: opts.toolUseInFlight ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
provenance: "engine-note",
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
|
|
85
100
|
const { messages } = await session.buildContext();
|
|
86
101
|
const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
|