@sema-agent/core 5.62.0 → 5.64.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 (55) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/agents/cascade.d.ts +5 -1
  3. package/dist/agents/cascade.js +6 -1
  4. package/dist/agents/subagent.d.ts +12 -2
  5. package/dist/agents/subagent.js +4 -2
  6. package/dist/agents/verify.d.ts +5 -1
  7. package/dist/agents/verify.js +5 -2
  8. package/dist/core/auto-compaction.d.ts +6 -4
  9. package/dist/core/auto-compaction.js +3 -0
  10. package/dist/core/checkpoint-store.d.ts +5 -1
  11. package/dist/core/context-edit.d.ts +36 -29
  12. package/dist/core/context-edit.js +3 -3
  13. package/dist/core/fs-write-gate-policy.d.ts +21 -0
  14. package/dist/core/fs-write-gate-policy.js +14 -3
  15. package/dist/core/hooks.d.ts +8 -5
  16. package/dist/core/memory-engine/engine.d.ts +11 -0
  17. package/dist/core/memory-engine/engine.js +29 -3
  18. package/dist/core/memory-engine/index.d.ts +1 -1
  19. package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
  20. package/dist/core/remote-env.d.ts +34 -2
  21. package/dist/core/runner/prepare-config-doors.d.ts +2 -2
  22. package/dist/core/runner/prepare-config-doors.js +11 -8
  23. package/dist/core/runner/prepare-task.d.ts +87 -5
  24. package/dist/core/runner/prepare-task.js +174 -92
  25. package/dist/core/runner/prepare-workspace-restore.js +13 -0
  26. package/dist/core/runner/runtask.js +203 -152
  27. package/dist/core/trace.d.ts +5 -4
  28. package/dist/core/types.d.ts +38 -20
  29. package/dist/core/usage-window-store.d.ts +44 -12
  30. package/dist/core/usage-window-store.js +11 -3
  31. package/dist/core/workflow-run-store-contract.js +17 -0
  32. package/dist/core/workflow-run-store.d.ts +22 -1
  33. package/dist/core/workflow-run-store.js +1 -0
  34. package/dist/engine/harness/agent-harness.d.ts +8 -3
  35. package/dist/engine/harness/agent-harness.js +29 -9
  36. package/dist/engine/harness/types.d.ts +127 -3
  37. package/dist/engine/loop/agent-loop.js +47 -4
  38. package/dist/engine/loop/types.d.ts +67 -6
  39. package/dist/index.d.ts +2 -2
  40. package/dist/internal/harness-types.d.ts +1 -1
  41. package/dist/orchestration/run-workflow-tool.d.ts +6 -0
  42. package/dist/orchestration/run-workflow-tool.js +1 -0
  43. package/dist/orchestration/workflow-types.d.ts +48 -1
  44. package/dist/orchestration/workflow-types.js +12 -4
  45. package/dist/orchestration/workflow.d.ts +18 -1
  46. package/dist/orchestration/workflow.js +45 -19
  47. package/dist/prompts/default.js +1 -1
  48. package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
  49. package/dist/tools/fs/bash-readonly-classifier.js +132 -5
  50. package/dist/tools/fs/fs-bash.js +9 -2
  51. package/dist/tools/fs/fs-write.js +19 -8
  52. package/dist/tools/fs/index.d.ts +1 -0
  53. package/dist/tools/fs/index.js +1 -0
  54. package/package.json +1 -1
  55. package/test/export-surface.snapshot.json +7 -1
@@ -3030,15 +3030,119 @@ export class Runner {
3030
3030
  return messages;
3031
3031
  });
3032
3032
  }
3033
+ const runForcedCompactionPass = async (lane, turnSignal) => {
3034
+ if (!(spec.compaction?.enabled ?? true))
3035
+ return false;
3036
+ if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
3037
+ return false;
3038
+ const passSignal = turnSignal !== undefined ? AbortSignal.any([prepared.abortController.signal, turnSignal]) : prepared.abortController.signal;
3039
+ try {
3040
+ const comp = await maybeCompact({
3041
+ session: prepared.session,
3042
+ epochDeclaredSections: prepared.epochDeclaredSections,
3043
+ ...centerAdoptionOption(prepared),
3044
+ model: prepared.harness.getModel(),
3045
+ compactionModel: prepared.compModel,
3046
+ ...forkContextOption(prepared, true),
3047
+ brain: compactionBrain,
3048
+ getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
3049
+ thinking: prepared.thinking,
3050
+ settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
3051
+ customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
3052
+ signal: passSignal,
3053
+ minTokens: 0,
3054
+ force: true,
3055
+ overheadTokens: prepared.promptOverheadTokens,
3056
+ ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3057
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
3058
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3059
+ ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3060
+ ...this.seamCCompactionOptions(prepared),
3061
+ ...gitRestateOption(prepared),
3062
+ ...windowSafetyOptions(prepared.harness.getModel()),
3063
+ ...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: passSignal }),
3064
+ });
3065
+ if (comp.compacted) {
3066
+ compactionBreaker.failures = 0;
3067
+ this.recordCompactionReuse(prepared, comp);
3068
+ if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
3069
+ const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
3070
+ rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
3071
+ }
3072
+ queue.push({
3073
+ type: "compacted",
3074
+ trigger: "forced",
3075
+ tokensBefore: comp.tokensBefore ?? 0,
3076
+ ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
3077
+ ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
3078
+ ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
3079
+ ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
3080
+ ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
3081
+ ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
3082
+ ...(comp.modelFallback ? { modelFallback: true } : {}),
3083
+ ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
3084
+ ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
3085
+ ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
3086
+ ...ident(),
3087
+ });
3088
+ if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
3089
+ const pd = comp.phaseDurations;
3090
+ const pdDur = comp.durationMs;
3091
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
3092
+ }
3093
+ prepared.cacheBreakDetector?.notifyCompaction();
3094
+ if (rs.attach.attachState !== undefined) {
3095
+ rs.attach.attachState.postCompactPending = true;
3096
+ rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
3097
+ }
3098
+ }
3099
+ else {
3100
+ if (comp.noop) {
3101
+ compactionBreaker.failures = 0;
3102
+ }
3103
+ else {
3104
+ compactionBreaker.failures += 1;
3105
+ }
3106
+ const declineReason = lane === "rejection" ? "prompt-too-long recovery pass did not land" : "guard-chain forced compaction pass did not land";
3107
+ if (!comp.noop) {
3108
+ this.deps.onError?.(new Error(declineReason), { phase: "compaction", sessionId: prepared.sessionId });
3109
+ }
3110
+ queue.push({
3111
+ type: "compaction_outcome",
3112
+ outcome: comp.noop ? "noop" : "failed",
3113
+ trigger: "forced",
3114
+ reason: declineReason,
3115
+ ...ident(),
3116
+ });
3117
+ }
3118
+ return comp.compacted === true;
3119
+ }
3120
+ catch (err) {
3121
+ if (turnSignal?.aborted === true && !prepared.abortController.signal.aborted) {
3122
+ const interruptReason = lane === "rejection"
3123
+ ? "prompt-too-long recovery pass cut short by a turn interrupt"
3124
+ : "guard-chain forced compaction cut short by a turn interrupt";
3125
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: interruptReason, ...ident() });
3126
+ return false;
3127
+ }
3128
+ compactionBreaker.failures += 1;
3129
+ this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "compaction", sessionId: prepared.sessionId });
3130
+ const msg = String(err instanceof Error ? err.message : err);
3131
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
3132
+ return false;
3133
+ }
3134
+ };
3033
3135
  prepared.harness.setLoopRecovery({
3034
3136
  truncatedOutput: {},
3035
3137
  malformedToolUse: {},
3036
3138
  thinkingOnly: {},
3037
3139
  degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
3038
3140
  promptTooLong: {
3039
- recover: async (attempt) => {
3141
+ recover: async (attempt, turnSignal) => {
3040
3142
  if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
3041
3143
  return false;
3144
+ if (turnSignal?.aborted === true)
3145
+ return false;
3042
3146
  if (prepared.microCompact.clearOnRejection && attempt === 1) {
3043
3147
  const proj = prepared.microCompact.projectionRef.current;
3044
3148
  const compactionAvailable = (spec.compaction?.enabled ?? true) && compactionBreaker.failures < MAX_CONSECUTIVE_COMPACTION_FAILURES;
@@ -3075,85 +3179,23 @@ export class Runner {
3075
3179
  return true;
3076
3180
  }
3077
3181
  }
3078
- if (!(spec.compaction?.enabled ?? true))
3079
- return false;
3080
- if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
3081
- return false;
3082
- try {
3083
- const comp = await maybeCompact({
3084
- session: prepared.session,
3085
- epochDeclaredSections: prepared.epochDeclaredSections,
3086
- ...centerAdoptionOption(prepared),
3087
- model: prepared.harness.getModel(),
3088
- compactionModel: prepared.compModel,
3089
- ...forkContextOption(prepared, true),
3090
- brain: compactionBrain,
3091
- getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
3092
- thinking: prepared.thinking,
3093
- settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
3094
- customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
3095
- signal: prepared.abortController.signal,
3096
- minTokens: 0,
3097
- force: true,
3098
- overheadTokens: prepared.promptOverheadTokens,
3099
- ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3100
- onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
3101
- workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3102
- ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3103
- ...this.seamCCompactionOptions(prepared),
3104
- ...gitRestateOption(prepared),
3105
- ...windowSafetyOptions(prepared.harness.getModel()),
3106
- ...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
3107
- });
3108
- if (comp.compacted) {
3109
- compactionBreaker.failures = 0;
3110
- this.recordCompactionReuse(prepared, comp);
3111
- if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
3112
- const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
3113
- rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
3114
- }
3115
- queue.push({
3116
- type: "compacted",
3117
- trigger: "forced",
3118
- tokensBefore: comp.tokensBefore ?? 0,
3119
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
3120
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
3121
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
3122
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
3123
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
3124
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
3125
- ...(comp.modelFallback ? { modelFallback: true } : {}),
3126
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
3127
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
3128
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
3129
- ...ident(),
3130
- });
3131
- if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
3132
- const pd = comp.phaseDurations;
3133
- const pdDur = comp.durationMs;
3134
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
3135
- }
3136
- prepared.cacheBreakDetector?.notifyCompaction();
3137
- if (rs.attach.attachState !== undefined) {
3138
- rs.attach.attachState.postCompactPending = true;
3139
- rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
3140
- }
3141
- }
3142
- else {
3143
- compactionBreaker.failures += 1;
3144
- queue.push({ type: "compaction_outcome", outcome: comp.noop ? "noop" : "failed", trigger: "forced", reason: "prompt-too-long recovery pass did not land", ...ident() });
3145
- }
3146
- return comp.compacted === true;
3147
- }
3148
- catch (err) {
3149
- compactionBreaker.failures += 1;
3150
- const msg = String(err instanceof Error ? err.message : err);
3151
- queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
3152
- return false;
3153
- }
3182
+ return runForcedCompactionPass("rejection", turnSignal);
3154
3183
  },
3155
3184
  },
3156
3185
  });
3186
+ prepared.microCompact.inTurnCompactionRef.current = async (turnSignal, anchoredEstimate) => {
3187
+ if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
3188
+ return false;
3189
+ if (turnSignal?.aborted === true)
3190
+ return false;
3191
+ if (anchoredEstimate !== undefined && rs.counters.compactionFloor > 0 && anchoredEstimate < rs.counters.compactionFloor) {
3192
+ const floor = rs.counters.compactionFloor;
3193
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.suppressed", version: 1, taskId: rs.telemetry.taskId, estTokens: anchoredEstimate, floor, ts: Date.now() }));
3194
+ queue.push({ type: "compaction_outcome", outcome: "suppressed", trigger: "forced", ...ident() });
3195
+ return false;
3196
+ }
3197
+ return runForcedCompactionPass("guard", turnSignal);
3198
+ };
3157
3199
  const rapidRefill = createRapidRefillState();
3158
3200
  const drainManualCompact = (outcome) => {
3159
3201
  manualCompactRef.requested = false;
@@ -4271,7 +4313,8 @@ export class Runner {
4271
4313
  if (outcome === null || typeof outcome !== "object") {
4272
4314
  throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
4273
4315
  }
4274
- const store = resolveCheckpointStore(taskConfig, this.deps);
4316
+ const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
4317
+ const store = resolveCheckpointStore(config, this.deps);
4275
4318
  if (!store) {
4276
4319
  throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
4277
4320
  }
@@ -4298,7 +4341,7 @@ export class Runner {
4298
4341
  let wakeMessage;
4299
4342
  let wakeHookContext;
4300
4343
  const outcomeGate = outcome.gate;
4301
- const resumeSignal = taskConfig.signal;
4344
+ const resumeSignal = config.signal;
4302
4345
  let suppliedMessage;
4303
4346
  if (outcomeGate === "wake") {
4304
4347
  suppliedMessage = outcome.message;
@@ -4336,62 +4379,6 @@ export class Runner {
4336
4379
  }
4337
4380
  if (suppliedMessage !== undefined) {
4338
4381
  wakeMessage = validatePendingSteer(suppliedMessage);
4339
- {
4340
- const resumeScreenHooks = taskConfig.hooks ?? this.deps.hooks;
4341
- const screen = resumeScreenHooks?.userPromptSubmit;
4342
- if (screen !== undefined && resumeSignal?.aborted !== true) {
4343
- const screenedMessage = wakeMessage;
4344
- const identity = mintHookInvocationIdentity({
4345
- sessionId: cp.sessionId,
4346
- taskId: taskConfig.taskId ?? cp.sessionId,
4347
- legKind: "resume",
4348
- isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
4349
- ...(internals?.insideFork === true ? { insideFork: true } : {}),
4350
- ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
4351
- ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
4352
- });
4353
- const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
4354
- try {
4355
- this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4356
- }
4357
- catch {
4358
- }
4359
- }, resumeScreenHooks);
4360
- let decision;
4361
- try {
4362
- const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
4363
- identity,
4364
- signal: sig,
4365
- source: "resume_message",
4366
- ...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
4367
- ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
4368
- }));
4369
- if (seat.expired) {
4370
- throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
4371
- ? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
4372
- : `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
4373
- }
4374
- decision = seat.value;
4375
- }
4376
- catch (hookErr) {
4377
- if (hookErr instanceof CheckpointError)
4378
- throw hookErr;
4379
- const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
4380
- try {
4381
- this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4382
- }
4383
- catch {
4384
- }
4385
- throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
4386
- }
4387
- if (decision?.block !== undefined && decision.block !== "") {
4388
- throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
4389
- }
4390
- if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
4391
- wakeHookContext = decision.additionalContext;
4392
- }
4393
- }
4394
- }
4395
4382
  }
4396
4383
  else if (readPendingSteerQueue(cp.state).length === 0) {
4397
4384
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
@@ -4583,7 +4570,7 @@ export class Runner {
4583
4570
  : "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
4584
4571
  }
4585
4572
  if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
4586
- if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
4573
+ if (!isLiveQuestionFace(config.onQuestion ?? this.deps.onQuestion)) {
4587
4574
  throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
4588
4575
  'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
4589
4576
  "re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
@@ -4694,13 +4681,14 @@ export class Runner {
4694
4681
  },
4695
4682
  }
4696
4683
  : internals;
4697
- if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && taskConfig.basePolicyForResumeEdit !== undefined) {
4684
+ if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && config.basePolicyForResumeEdit !== undefined) {
4698
4685
  throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
4699
4686
  "resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
4700
4687
  }
4688
+ let recheckGovernanceWindow;
4701
4689
  {
4702
4690
  const rowPrincipal = cp.principal || undefined;
4703
- const suppliedPrincipal = taskConfig.principal || undefined;
4691
+ const suppliedPrincipal = config.principal || undefined;
4704
4692
  if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
4705
4693
  throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
4706
4694
  "running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
@@ -4709,16 +4697,79 @@ export class Runner {
4709
4697
  const owesDelivery = outcomeGate !== "resource_limit" ||
4710
4698
  readPendingSteerQueue(cp.state).length > 0 ||
4711
4699
  (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
4712
- if (owesDelivery && this.deps.usageWindowStore !== undefined) {
4700
+ const usageWindowStore = this.deps.usageWindowStore;
4701
+ if (owesDelivery && usageWindowStore !== undefined) {
4713
4702
  const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4714
4703
  if (preCasWindows !== undefined && preCasWindows.length > 0) {
4715
4704
  const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
4716
- const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
4717
- if (wait !== undefined) {
4718
- throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
4719
- `refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
4705
+ const assertWindowOpen = async () => {
4706
+ const wait = usageRetryAfterMs(await usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
4707
+ if (wait !== undefined) {
4708
+ throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
4709
+ `refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
4710
+ }
4711
+ };
4712
+ recheckGovernanceWindow = assertWindowOpen;
4713
+ await assertWindowOpen();
4714
+ }
4715
+ }
4716
+ }
4717
+ if (outcomeGate === "wake" && wakeMessage !== undefined) {
4718
+ const resumeScreenHooks = config.hooks ?? this.deps.hooks;
4719
+ const screen = resumeScreenHooks?.userPromptSubmit;
4720
+ if (screen !== undefined && resumeSignal?.aborted !== true) {
4721
+ const screenedMessage = wakeMessage;
4722
+ const identity = mintHookInvocationIdentity({
4723
+ sessionId: cp.sessionId,
4724
+ taskId: config.taskId ?? cp.sessionId,
4725
+ legKind: "resume",
4726
+ isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
4727
+ ...(internals?.insideFork === true ? { insideFork: true } : {}),
4728
+ ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
4729
+ ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
4730
+ });
4731
+ const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
4732
+ try {
4733
+ this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4720
4734
  }
4735
+ catch {
4736
+ }
4737
+ }, resumeScreenHooks);
4738
+ let decision;
4739
+ try {
4740
+ const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
4741
+ identity,
4742
+ signal: sig,
4743
+ source: "resume_message",
4744
+ ...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
4745
+ ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
4746
+ }));
4747
+ if (seat.expired) {
4748
+ throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
4749
+ ? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
4750
+ : `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
4751
+ }
4752
+ decision = seat.value;
4753
+ }
4754
+ catch (hookErr) {
4755
+ if (hookErr instanceof CheckpointError)
4756
+ throw hookErr;
4757
+ const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
4758
+ try {
4759
+ this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4760
+ }
4761
+ catch {
4762
+ }
4763
+ throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
4764
+ }
4765
+ if (decision?.block !== undefined && decision.block !== "") {
4766
+ throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
4767
+ }
4768
+ if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
4769
+ wakeHookContext = decision.additionalContext;
4721
4770
  }
4771
+ if (recheckGovernanceWindow !== undefined)
4772
+ await recheckGovernanceWindow();
4722
4773
  }
4723
4774
  }
4724
4775
  if (plainPolicyOutcome !== undefined &&
@@ -4739,7 +4790,7 @@ export class Runner {
4739
4790
  budget: editBudget,
4740
4791
  ...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
4741
4792
  };
4742
- const editSignal = taskConfig.signal;
4793
+ const editSignal = config.signal;
4743
4794
  const raceEditAbort = async (p) => {
4744
4795
  if (editSignal === undefined)
4745
4796
  return p;
@@ -4778,7 +4829,7 @@ export class Runner {
4778
4829
  const toolPolicyLocked = this.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
4779
4830
  const editBasePolicy = toolPolicyLocked
4780
4831
  ? (this.deps.basePolicyForResumeEdit ?? this.deps.toolPolicy)
4781
- : (taskConfig.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? taskConfig.toolPolicy ?? this.deps.toolPolicy);
4832
+ : (config.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? this.deps.toolPolicy);
4782
4833
  if (editBasePolicy !== undefined) {
4783
4834
  let rechecked;
4784
4835
  try {
@@ -4798,7 +4849,7 @@ export class Runner {
4798
4849
  `refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
4799
4850
  }
4800
4851
  }
4801
- const resumeHooks = taskConfig.hooks ?? this.deps.hooks;
4852
+ const resumeHooks = config.hooks ?? this.deps.hooks;
4802
4853
  if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
4803
4854
  try {
4804
4855
  await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
@@ -4833,15 +4884,15 @@ export class Runner {
4833
4884
  consumeFlipDone = true;
4834
4885
  }
4835
4886
  const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
4836
- ? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
4887
+ ? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, config.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
4837
4888
  : undefined;
4838
4889
  const spec = {
4839
- ...taskConfig,
4840
- ...((taskConfig.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
4890
+ ...config,
4891
+ ...((config.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
4841
4892
  objective: "",
4842
4893
  sessionId: cp.sessionId,
4843
4894
  requireExistingSession: true,
4844
- preemptSignal: taskConfig.preemptSignal?.aborted ? undefined : taskConfig.preemptSignal,
4895
+ preemptSignal: config.preemptSignal?.aborted ? undefined : config.preemptSignal,
4845
4896
  ...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
4846
4897
  };
4847
4898
  const reopenFn = store.reopen?.bind(store);
@@ -4853,10 +4904,10 @@ export class Runner {
4853
4904
  }
4854
4905
  }
4855
4906
  : undefined;
4856
- if (taskConfig.signal?.aborted === true && consumeFlipDone) {
4907
+ if (resumeSignal?.aborted === true && consumeFlipDone) {
4857
4908
  throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted after its row was already claimed — the checkpoint stays consumed and the caller's rollback settles the outcome-unknown terminal (reopening here would strand a pending approval no redemption path can reach)");
4858
4909
  }
4859
- if (taskConfig.signal?.aborted === true) {
4910
+ if (resumeSignal?.aborted === true) {
4860
4911
  let reopenOutcome = "refused";
4861
4912
  if (onEnvRestoreFailed !== undefined) {
4862
4913
  try {
@@ -734,10 +734,11 @@ export type TraceEvent = {
734
734
  * occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
735
735
  * = the proactive request-build pass (anchored estimate crossed the edit budget),
736
736
  * `"refusal"` = the MC-R rejection-recovery arm (provider said input-too-long), `"blocking"`
737
- * = the slice-3 pre-guard arm (reserved; not emitted before slice 3). SCOPE (X1): emitted
738
- * only when the design/374 machinery is enabled the `"frontier"` arm requires
739
- * `microCompact.machine: "cc"`, the `"refusal"` arm requires `microCompact.clearOnRejection`;
740
- * the legacy DEFAULT machine clears silently exactly as pre-374 (its clears are ledger-less
737
+ * = the slice-3 guard-chain arm A (the machine's one pre-guard shot when the frontier pass
738
+ * is off, `microCompact.machine: "off"`). SCOPE (X1): emitted only by the cc machine — the
739
+ * `"frontier"` arm requires `microCompact.machine: "cc"` (the default since the slice-3
740
+ * flip), the `"refusal"` arm requires `microCompact.clearOnRejection` (also default-on);
741
+ * the legacy OPT-OUT machine clears silently exactly as pre-374 (its clears are ledger-less
741
742
  * and re-fire per request, so a frame there would re-count the same occurrences and break
742
743
  * this frame's cardinality clause). Cardinality: exactly ONE frame per firing pass — a retry
743
744
  * chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
@@ -552,6 +552,13 @@ export interface ToolExecuteContext {
552
552
  * fixed point `ctx.rootSessionId ?? ctx.sessionId` at every spawn). Absent for a top-level run
553
553
  * (its OWN sessionId is the root). */
554
554
  rootSessionId?: string;
555
+ /** design/380 O1② (C12) — the spawning run's EXPLICIT placement fixed point
556
+ * (`RunInternals.placementRoot`), present only when its internals carried one (cascade rungs /
557
+ * verification legs after the first, or a caller-declared root). A delegation tool threads it
558
+ * VERBATIM into child RunInternals so every descendant keeps the ladder/gate placement; absent ⇒
559
+ * the `rootSessionId` chain above IS the placement root through prepare's mint middle segment
560
+ * (`placementRoot ?? rootSessionId ?? sessionId`) and nothing extra is threaded. */
561
+ placementRoot?: string;
555
562
  /**
556
563
  * design/151 §7 S3c — TRUSTED tier-3 revival claim (same trusted-internals seat as
557
564
  * {@link parentTaskId}; never a model argument). Filled ONLY by the auto-mounted SendMessage's
@@ -2658,9 +2665,12 @@ export interface TaskSpec {
2658
2665
  * Scope caveat (design/141 examples 批实测): `instructions` rides the WHOLE-TURN summarization
2659
2666
  * request only; a split-turn cut point (mid-turn prefix summarization) uses the engine-owned
2660
2667
  * turn-prefix prompt and does not carry it (same A2 boundary as `RunnerDeps.summaryProvider`).
2661
- * `withinTask: false` keeps end-of-task compaction but disables the within-task (turn-boundary)
2662
- * trigger — the escape hatch for the §25 (A) control-flow change; the system prompt then stops
2663
- * claiming mid-task summarization (§6.3 honesty).
2668
+ * `withinTask: false` keeps end-of-task compaction but disables the ROUTINE within-task
2669
+ * (turn-boundary) trigger — the escape hatch for the §25 (A) control-flow change; the system
2670
+ * prompt then stops claiming ROUTINE mid-task summarization (§6.3 honesty). It does NOT silence
2671
+ * the recovery-class forced lanes (design/374 slice 3): a prompt-too-long recovery or the guard
2672
+ * chain's arm B may still legitimately compact WITHIN the task — error/pressure recovery is not
2673
+ * a routine boundary pass, so a within-task compaction under this flag is contract-conforming.
2664
2674
  * `attachWorkingFiles` (LONGRUN-2; **default ON since 2026-07-03** — CC 198 hard-codes its
2665
2675
  * post-compact file restore, and LONGRUN-2 measured ≈2.3 extra read round-trips per compaction
2666
2676
  * without it): after each compaction, re-read the task's most recently READ files (CC
@@ -5929,9 +5939,12 @@ export interface RunnerDeps {
5929
5939
  * `TaskSpec.limits`, which is the allowance ONE task asked for — an operator granting "N tokens per 5
5930
5940
  * hours" cannot express it as a task limit, because nothing stops the next task from asking again.
5931
5941
  *
5932
- * A window carries a TOKEN ceiling and, optionally, a MONEY ceiling (`UsageWindow.maxCostUsd`, absolute
5933
- * USD — the `TaskLimits.maxCostUsd` quantity one governance level up). The two are independent and
5934
- * either one binds. A $ ceiling requires a PRICED run: a task whose model has neither a {@link pricing}
5942
+ * A window carries a TOKEN ceiling (`UsageWindow.maxTokens`), a MONEY ceiling (`UsageWindow.maxCostUsd`,
5943
+ * absolute USD — the `TaskLimits.maxCostUsd` quantity one governance level up), or both: each is
5944
+ * optional on its own and at least one is required, so a deployment governed by spend alone declares
5945
+ * only `maxCostUsd` (a window with neither ceiling is refused, `config.usage_window_invalid`). The two
5946
+ * are independent and every declared one binds. A $ ceiling requires a PRICED run: a task whose model
5947
+ * has neither a {@link pricing}
5935
5948
  * entry nor a `Model.cost` declaration is refused at the door (`config.usage_window_unpriced`) rather
5936
5949
  * than charged the fabricated 0 an unpriced run would otherwise file into an operator's ceiling.
5937
5950
  *
@@ -6345,22 +6358,26 @@ export interface RunnerDeps {
6345
6358
  */
6346
6359
  toolResultThresholdChars?: number;
6347
6360
  /**
6348
- * design/374 — microCompact machine-alignment knobs (EXPERIMENTAL until the slice-3 default
6349
- * flip; both default OFF so a deployment that never touches this bag runs the pre-374 machine
6350
- * byte for byte).
6361
+ * design/374 — microCompact machine-alignment knobs. Since the slice-3 default flip BOTH knobs
6362
+ * default ON (`machine: "cc"`, `clearOnRejection: true`); the pre-374 behavior is the explicit
6363
+ * opt-out `{ machine: "legacy", clearOnRejection: false }`.
6351
6364
  *
6352
6365
  * - `machine`: which stale-tool-result clearing machine the request pipeline runs —
6353
- * `"legacy"` (default; the historical keep-3 / clear-to-budget machine) or `"cc"` (the CC
6354
- * 2.1.223 rejection-leg form: keep 5, ≥20k minimum-savings gate, one deep clear beyond the
6355
- * keep window, CC marker bytes). ⚠️ Read the P-form warning on
6356
- * {@link import("./context-edit.js").ContextEditMachine} before selecting `"cc"`: until the
6357
- * slice-3 fallback re-ordering ships, the 20k gate sits in front of the only reduction while
6358
- * the message-dropping guard trim still backstops opting in is accepting that trade.
6366
+ * `"cc"` (default: the CC 2.1.223 rejection-leg form keep 5, ≥20k minimum-savings gate, one
6367
+ * deep clear beyond the keep window, CC marker bytes), `"legacy"` (the historical keep-3 /
6368
+ * clear-to-budget machine, kept as the compatibility opt-out; it also keeps the pre-374
6369
+ * backstop order — guard trim as the ordinary second line), or `"off"` (no proactive frontier
6370
+ * clearing at all — the D-2 off switch; the unified machine then gets its ONE shot at the
6371
+ * blocking point instead, the slice-3 arm A, trace trigger `"blocking"`). The pre-flip P-form
6372
+ * warning on {@link import("./context-edit.js").ContextEditMachine} is resolved: the slice-3
6373
+ * fallback re-ordering shipped with this default (blocking-point re-run → in-turn forced
6374
+ * compaction behind the adopt seam → trim demoted to the disaster-only last resort), so an
6375
+ * under-20k refusal no longer falls straight into a message-dropping trim.
6359
6376
  * - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
6360
6377
  * deterministic clear over the rejected projection (same cc machine, savings ≥20k or nothing)
6361
- * and retry inside the turn BEFORE the forced-compaction recovery. Default false (X2: the
6362
- * machinery lands dark; the default flips together with the machine in slice 3). Independent
6363
- * of `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
6378
+ * and retry inside the turn BEFORE the forced-compaction recovery. Default true (flipped with
6379
+ * the machine default in slice 3; X2's dark-landing clause is spent). Independent of
6380
+ * `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
6364
6381
  * budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
6365
6382
  * §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
6366
6383
  * clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
@@ -6372,10 +6389,11 @@ export interface RunnerDeps {
6372
6389
  * A declaration outside the closed vocabulary (a `machine` string not in the union, a
6373
6390
  * non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
6374
6391
  * whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
6375
- * would run the pre-374 machine while the deployment believes it opted in.
6392
+ * would silently run the DEFAULT machine while the deployment believes its declaration took
6393
+ * effect.
6376
6394
  */
6377
6395
  microCompact?: {
6378
- machine?: "legacy" | "cc";
6396
+ machine?: "off" | "legacy" | "cc";
6379
6397
  clearOnRejection?: boolean;
6380
6398
  };
6381
6399
  /**