@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agents/send-message-tool.js +48 -2
  3. package/dist/agents/subagent.js +250 -89
  4. package/dist/core/auto-compaction.d.ts +17 -4
  5. package/dist/core/auto-compaction.js +3 -0
  6. package/dist/core/context-edit.d.ts +55 -6
  7. package/dist/core/context-edit.js +12 -1
  8. package/dist/core/hooks.d.ts +293 -11
  9. package/dist/core/hooks.js +158 -11
  10. package/dist/core/human-input-projection.d.ts +20 -2
  11. package/dist/core/human-input-projection.js +9 -0
  12. package/dist/core/permission-rules.d.ts +23 -15
  13. package/dist/core/permission-rules.js +40 -31
  14. package/dist/core/runner/prepare-task.d.ts +8 -0
  15. package/dist/core/runner/prepare-task.js +34 -23
  16. package/dist/core/runner/runtask.js +158 -21
  17. package/dist/core/runner/session-rule-policy.js +5 -5
  18. package/dist/core/session-reconcile.d.ts +32 -0
  19. package/dist/core/session-reconcile.js +15 -0
  20. package/dist/core/task-notification.d.ts +34 -7
  21. package/dist/core/task-notification.js +11 -1
  22. package/dist/core/task-registry-agent.d.ts +20 -3
  23. package/dist/core/task-registry-agent.js +31 -2
  24. package/dist/core/tool-policy.d.ts +14 -9
  25. package/dist/core/tool-policy.js +27 -22
  26. package/dist/core/types.d.ts +37 -11
  27. package/dist/core/untrusted-text.js +8 -0
  28. package/dist/engine/compaction/compaction.d.ts +77 -7
  29. package/dist/engine/compaction/compaction.js +98 -9
  30. package/dist/engine/compaction/utils.d.ts +4 -0
  31. package/dist/engine/compaction/utils.js +6 -0
  32. package/dist/engine/harness/agent-harness.d.ts +84 -0
  33. package/dist/engine/harness/agent-harness.js +88 -12
  34. package/dist/engine/harness/messages.d.ts +4 -2
  35. package/dist/engine/harness/messages.js +7 -2
  36. package/dist/engine/harness/types.d.ts +11 -5
  37. package/dist/engine/loop/types.d.ts +7 -0
  38. package/dist/engine/session/import-validate.js +10 -0
  39. package/dist/engine/session/session.js +2 -2
  40. package/dist/orchestration/run-spec.js +8 -1
  41. package/dist/prompts/default.d.ts +10 -4
  42. 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 { isMcpCoveringRuleName, mcpRuleNameCovers } from "../permission-rules.js";
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 (isMcpCoveringRuleName(n) && [...known].some((k) => mcpRuleNameCovers(n, k)))
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 hooksWithPermissionDenied = hooks?.permissionDenied ? hooks : undefined;
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 patch = await hooks.postToolUseFailure(e.toolName, e.input, {
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 patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
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 r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity });
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
- ...(typeof inputId === "string" ? { inputId } : {}),
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
- result = await stopHook({
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 decision = await userPromptSubmit(spec.objective, { identity: prepared.hookIdentity });
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 r = await pre.call(hooks, ctx);
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, mcpCoveringEntries, mcpCoveringHit } from "../tool-policy.js";
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 = mcpCoveringEntries(rules.toolDeny);
28
- const toolAllowCovering = mcpCoveringEntries(rules.toolAllow);
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) || mcpCoveringHit(toolDenyCovering, 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) && !mcpCoveringHit(toolAllowCovering, 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");