@sema-agent/core 5.62.0 → 5.63.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.
@@ -3030,6 +3030,105 @@ 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
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: "guard-chain forced compaction cut short by a turn interrupt", ...ident() });
3123
+ return false;
3124
+ }
3125
+ compactionBreaker.failures += 1;
3126
+ this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "compaction", sessionId: prepared.sessionId });
3127
+ const msg = String(err instanceof Error ? err.message : err);
3128
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
3129
+ return false;
3130
+ }
3131
+ };
3033
3132
  prepared.harness.setLoopRecovery({
3034
3133
  truncatedOutput: {},
3035
3134
  malformedToolUse: {},
@@ -3075,85 +3174,23 @@ export class Runner {
3075
3174
  return true;
3076
3175
  }
3077
3176
  }
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
- }
3177
+ return runForcedCompactionPass("rejection");
3154
3178
  },
3155
3179
  },
3156
3180
  });
3181
+ prepared.microCompact.inTurnCompactionRef.current = async (turnSignal, anchoredEstimate) => {
3182
+ if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
3183
+ return false;
3184
+ if (turnSignal?.aborted === true)
3185
+ return false;
3186
+ if (anchoredEstimate !== undefined && rs.counters.compactionFloor > 0 && anchoredEstimate < rs.counters.compactionFloor) {
3187
+ const floor = rs.counters.compactionFloor;
3188
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.suppressed", version: 1, taskId: rs.telemetry.taskId, estTokens: anchoredEstimate, floor, ts: Date.now() }));
3189
+ queue.push({ type: "compaction_outcome", outcome: "suppressed", trigger: "forced", ...ident() });
3190
+ return false;
3191
+ }
3192
+ return runForcedCompactionPass("guard", turnSignal);
3193
+ };
3157
3194
  const rapidRefill = createRapidRefillState();
3158
3195
  const drainManualCompact = (outcome) => {
3159
3196
  manualCompactRef.requested = false;
@@ -4336,62 +4373,6 @@ export class Runner {
4336
4373
  }
4337
4374
  if (suppliedMessage !== undefined) {
4338
4375
  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
4376
  }
4396
4377
  else if (readPendingSteerQueue(cp.state).length === 0) {
4397
4378
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
@@ -4698,6 +4679,7 @@ export class Runner {
4698
4679
  throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
4699
4680
  "resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
4700
4681
  }
4682
+ let recheckGovernanceWindow;
4701
4683
  {
4702
4684
  const rowPrincipal = cp.principal || undefined;
4703
4685
  const suppliedPrincipal = taskConfig.principal || undefined;
@@ -4709,16 +4691,79 @@ export class Runner {
4709
4691
  const owesDelivery = outcomeGate !== "resource_limit" ||
4710
4692
  readPendingSteerQueue(cp.state).length > 0 ||
4711
4693
  (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
4712
- if (owesDelivery && this.deps.usageWindowStore !== undefined) {
4694
+ const usageWindowStore = this.deps.usageWindowStore;
4695
+ if (owesDelivery && usageWindowStore !== undefined) {
4713
4696
  const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4714
4697
  if (preCasWindows !== undefined && preCasWindows.length > 0) {
4715
4698
  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 });
4699
+ const assertWindowOpen = async () => {
4700
+ const wait = usageRetryAfterMs(await usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
4701
+ if (wait !== undefined) {
4702
+ 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 — ` +
4703
+ `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 });
4704
+ }
4705
+ };
4706
+ recheckGovernanceWindow = assertWindowOpen;
4707
+ await assertWindowOpen();
4708
+ }
4709
+ }
4710
+ }
4711
+ if (outcomeGate === "wake" && wakeMessage !== undefined) {
4712
+ const resumeScreenHooks = taskConfig.hooks ?? this.deps.hooks;
4713
+ const screen = resumeScreenHooks?.userPromptSubmit;
4714
+ if (screen !== undefined && resumeSignal?.aborted !== true) {
4715
+ const screenedMessage = wakeMessage;
4716
+ const identity = mintHookInvocationIdentity({
4717
+ sessionId: cp.sessionId,
4718
+ taskId: taskConfig.taskId ?? cp.sessionId,
4719
+ legKind: "resume",
4720
+ isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
4721
+ ...(internals?.insideFork === true ? { insideFork: true } : {}),
4722
+ ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
4723
+ ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
4724
+ });
4725
+ const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
4726
+ try {
4727
+ this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4720
4728
  }
4729
+ catch {
4730
+ }
4731
+ }, resumeScreenHooks);
4732
+ let decision;
4733
+ try {
4734
+ const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
4735
+ identity,
4736
+ signal: sig,
4737
+ source: "resume_message",
4738
+ ...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
4739
+ ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
4740
+ }));
4741
+ if (seat.expired) {
4742
+ throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
4743
+ ? `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`
4744
+ : `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
4745
+ }
4746
+ decision = seat.value;
4747
+ }
4748
+ catch (hookErr) {
4749
+ if (hookErr instanceof CheckpointError)
4750
+ throw hookErr;
4751
+ const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
4752
+ try {
4753
+ this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4754
+ }
4755
+ catch {
4756
+ }
4757
+ 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`);
4758
+ }
4759
+ if (decision?.block !== undefined && decision.block !== "") {
4760
+ throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
4761
+ }
4762
+ if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
4763
+ wakeHookContext = decision.additionalContext;
4721
4764
  }
4765
+ if (recheckGovernanceWindow !== undefined)
4766
+ await recheckGovernanceWindow();
4722
4767
  }
4723
4768
  }
4724
4769
  if (plainPolicyOutcome !== undefined &&
@@ -4853,10 +4898,10 @@ export class Runner {
4853
4898
  }
4854
4899
  }
4855
4900
  : undefined;
4856
- if (taskConfig.signal?.aborted === true && consumeFlipDone) {
4901
+ if (resumeSignal?.aborted === true && consumeFlipDone) {
4857
4902
  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
4903
  }
4859
- if (taskConfig.signal?.aborted === true) {
4904
+ if (resumeSignal?.aborted === true) {
4860
4905
  let reopenOutcome = "refused";
4861
4906
  if (onEnvRestoreFailed !== undefined) {
4862
4907
  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
@@ -2658,9 +2658,12 @@ export interface TaskSpec {
2658
2658
  * Scope caveat (design/141 examples 批实测): `instructions` rides the WHOLE-TURN summarization
2659
2659
  * request only; a split-turn cut point (mid-turn prefix summarization) uses the engine-owned
2660
2660
  * 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).
2661
+ * `withinTask: false` keeps end-of-task compaction but disables the ROUTINE within-task
2662
+ * (turn-boundary) trigger — the escape hatch for the §25 (A) control-flow change; the system
2663
+ * prompt then stops claiming ROUTINE mid-task summarization (§6.3 honesty). It does NOT silence
2664
+ * the recovery-class forced lanes (design/374 slice 3): a prompt-too-long recovery or the guard
2665
+ * chain's arm B may still legitimately compact WITHIN the task — error/pressure recovery is not
2666
+ * a routine boundary pass, so a within-task compaction under this flag is contract-conforming.
2664
2667
  * `attachWorkingFiles` (LONGRUN-2; **default ON since 2026-07-03** — CC 198 hard-codes its
2665
2668
  * post-compact file restore, and LONGRUN-2 measured ≈2.3 extra read round-trips per compaction
2666
2669
  * without it): after each compaction, re-read the task's most recently READ files (CC
@@ -6345,22 +6348,26 @@ export interface RunnerDeps {
6345
6348
  */
6346
6349
  toolResultThresholdChars?: number;
6347
6350
  /**
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).
6351
+ * design/374 — microCompact machine-alignment knobs. Since the slice-3 default flip BOTH knobs
6352
+ * default ON (`machine: "cc"`, `clearOnRejection: true`); the pre-374 behavior is the explicit
6353
+ * opt-out `{ machine: "legacy", clearOnRejection: false }`.
6351
6354
  *
6352
6355
  * - `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.
6356
+ * `"cc"` (default: the CC 2.1.223 rejection-leg form keep 5, ≥20k minimum-savings gate, one
6357
+ * deep clear beyond the keep window, CC marker bytes), `"legacy"` (the historical keep-3 /
6358
+ * clear-to-budget machine, kept as the compatibility opt-out; it also keeps the pre-374
6359
+ * backstop order — guard trim as the ordinary second line), or `"off"` (no proactive frontier
6360
+ * clearing at all — the D-2 off switch; the unified machine then gets its ONE shot at the
6361
+ * blocking point instead, the slice-3 arm A, trace trigger `"blocking"`). The pre-flip P-form
6362
+ * warning on {@link import("./context-edit.js").ContextEditMachine} is resolved: the slice-3
6363
+ * fallback re-ordering shipped with this default (blocking-point re-run → in-turn forced
6364
+ * compaction behind the adopt seam → trim demoted to the disaster-only last resort), so an
6365
+ * under-20k refusal no longer falls straight into a message-dropping trim.
6359
6366
  * - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
6360
6367
  * 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
6368
+ * and retry inside the turn BEFORE the forced-compaction recovery. Default true (flipped with
6369
+ * the machine default in slice 3; X2's dark-landing clause is spent). Independent of
6370
+ * `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
6364
6371
  * budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
6365
6372
  * §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
6366
6373
  * clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
@@ -6372,10 +6379,11 @@ export interface RunnerDeps {
6372
6379
  * A declaration outside the closed vocabulary (a `machine` string not in the union, a
6373
6380
  * non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
6374
6381
  * 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.
6382
+ * would silently run the DEFAULT machine while the deployment believes its declaration took
6383
+ * effect.
6376
6384
  */
6377
6385
  microCompact?: {
6378
- machine?: "legacy" | "cc";
6386
+ machine?: "off" | "legacy" | "cc";
6379
6387
  clearOnRejection?: boolean;
6380
6388
  };
6381
6389
  /**
@@ -547,7 +547,7 @@ export class AgentHarness {
547
547
  userInputParkRecords.delete(message);
548
548
  engineNotePayloads.delete(message);
549
549
  }
550
- createLoopConfig(getTurnState, setTurnState) {
550
+ createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
551
551
  const turnState = getTurnState();
552
552
  const self = this;
553
553
  return {
@@ -564,9 +564,24 @@ export class AgentHarness {
564
564
  : {}),
565
565
  convertToLlm: (messages) => stripEngineMetadata(convertToLlm(messages)),
566
566
  shouldStopAfterTurn: () => this._stopAfterTurn,
567
- transformContext: async (messages) => {
568
- const result = await this.emitHook({ type: "context", messages: [...messages] });
569
- return result?.messages ?? messages;
567
+ transformContext: async (messages, signal) => {
568
+ const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
569
+ if (result?.adoptSessionRebuild !== true) {
570
+ return result?.messages ?? messages;
571
+ }
572
+ await this.flushPendingSessionWrites();
573
+ const nextTurnState = await this.createTurnState();
574
+ setTurnState(nextTurnState);
575
+ const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
576
+ const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
577
+ return {
578
+ messages: recheck?.messages ?? rebuiltContext.messages,
579
+ adoptedContext: {
580
+ messages: rebuiltContext.messages,
581
+ systemPrompt: rebuiltContext.systemPrompt,
582
+ ...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
583
+ },
584
+ };
570
585
  },
571
586
  beforeToolCall: async ({ toolCall, args }) => {
572
587
  const result = await this.emitHook({
@@ -780,7 +795,7 @@ export class AgentHarness {
780
795
  this.runAbortController = abortController;
781
796
  const runResultPromise = (async () => {
782
797
  try {
783
- return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
798
+ return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
784
799
  }
785
800
  catch (error) {
786
801
  try {
@@ -919,6 +919,25 @@ export interface BeforeAgentStartEvent<TSkill extends Skill = Skill, TPromptTemp
919
919
  export interface ContextEvent {
920
920
  type: "context";
921
921
  messages: AgentMessage[];
922
+ /**
923
+ * The TURN-scoped abort signal of the request build being transformed (present when the loop
924
+ * handed one — it always does in production; absent only for bare-harness callers). A handler
925
+ * that runs an EXPENSIVE reduction (the slice-3 arm-B in-turn compaction is the standing case)
926
+ * must honor it: a turn interrupt (design/373) fires this signal, and a reduction that only
927
+ * watches the RUN-level signal would make the interrupt wait out a long summary call and commit
928
+ * a session mutation the interrupted turn no longer needs (adversarial review r3).
929
+ */
930
+ signal?: AbortSignal;
931
+ /**
932
+ * design/374 slice 3 — set on the ONE bounded re-dispatch of the adopt seam
933
+ * ({@link ContextResult.adoptSessionRebuild}): `messages` are the just-adopted session rebuild,
934
+ * re-presented so the handler's request pipeline applies to what the provider will actually
935
+ * receive (the "recheck" step of build→reduce→adopt→recheck). A handler must not request a
936
+ * second adoption on this dispatch — the harness does not honor it (see the result member's
937
+ * doc) — so a reduction that is still over budget takes the handler's own next arm (the trim
938
+ * last resort) instead of looping. Absent on every ordinary dispatch.
939
+ */
940
+ recheck?: boolean;
922
941
  }
923
942
  export interface TurnBoundaryEvent {
924
943
  /** Fires between model-request boundaries, after session flush, before the context rebuild. */
@@ -997,6 +1016,25 @@ export interface BeforeAgentStartResult {
997
1016
  /** Hook result for replacing the full context message list before provider conversion. */
998
1017
  export interface ContextResult {
999
1018
  messages: AgentMessage[];
1019
+ /**
1020
+ * design/374 slice 3 (arm B, the adopt seam) — the handler PERSISTED a session-level reduction
1021
+ * during this dispatch (e.g. an in-turn forced compaction appended to the session) and asks the
1022
+ * harness to adopt it MID-BUILD, the same three-step form as the two existing adoption flows
1023
+ * (turn_boundary / prompt-too-long recovery): flush pending session writes, rebuild the turn
1024
+ * context from the session (`createTurnState`), adopt it (`setTurnState` + the loop-context
1025
+ * adoption via {@link import("../loop/types.js").TransformedContext}), then re-dispatch the
1026
+ * context hook ONCE on the rebuilt view (`ContextEvent.recheck`) so the request pipeline applies
1027
+ * to it. Result: the provider request, every subsequent hook dispatch, and the active turn state
1028
+ * all see the SAME reduced transcript — the three-party co-view the seam exists for. `messages`
1029
+ * on this arm are the handler's best unadopted view and are superseded by the rebuild.
1030
+ *
1031
+ * Honored AT MOST ONCE per request build: on the recheck dispatch this member is ignored (its
1032
+ * `messages` are used verbatim) — the bound that keeps a reduction that cannot get under budget
1033
+ * from re-entering forever. Typed on the hook RESULT deliberately (not a closure side channel):
1034
+ * the adoption is part of the hook's answer, and the one prior closure-ref transport in this
1035
+ * area (`requestLossyRef`) is a recorded bypass shape, not a precedent to grow.
1036
+ */
1037
+ adoptSessionRebuild?: boolean;
1000
1038
  }
1001
1039
  /** Hook result for patching provider request options before payload construction. */
1002
1040
  export interface BeforeProviderRequestResult {
@@ -524,7 +524,26 @@ async function settleInterruptedTurn(state, message, executor, committedResults,
524
524
  async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
525
525
  let messages = context.messages;
526
526
  if (config.transformContext) {
527
- messages = await config.transformContext(messages, signal);
527
+ const transformed = await config.transformContext(messages, signal);
528
+ if (Array.isArray(transformed)) {
529
+ messages = transformed;
530
+ }
531
+ else {
532
+ const adopted = transformed.adoptedContext;
533
+ if (adopted !== undefined) {
534
+ context.messages = adopted.messages;
535
+ if (adopted.systemPrompt !== undefined) {
536
+ context.systemPrompt = adopted.systemPrompt;
537
+ if (adopted.systemBlocks !== undefined) {
538
+ context.systemBlocks = adopted.systemBlocks;
539
+ }
540
+ else {
541
+ delete context.systemBlocks;
542
+ }
543
+ }
544
+ }
545
+ messages = transformed.messages;
546
+ }
528
547
  }
529
548
  const llmMessages = await config.convertToLlm(messages);
530
549
  const llmContext = {