@sema-agent/core 5.59.0 → 5.60.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 (40) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/brain/anthropic.js +15 -5
  3. package/dist/brain/errors.d.ts +18 -1
  4. package/dist/brain/errors.js +7 -1
  5. package/dist/brain/input-too-long.d.ts +57 -0
  6. package/dist/brain/input-too-long.js +35 -0
  7. package/dist/brain/stream-engine.js +9 -1
  8. package/dist/core/auto-compaction.js +2 -2
  9. package/dist/core/checkpoint-store.d.ts +116 -19
  10. package/dist/core/checkpoint-store.js +15 -8
  11. package/dist/core/context-edit.d.ts +243 -41
  12. package/dist/core/context-edit.js +247 -32
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +5 -0
  15. package/dist/core/locked-config.d.ts +36 -4
  16. package/dist/core/locked-config.js +34 -1
  17. package/dist/core/mcp.js +10 -6
  18. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  19. package/dist/core/memory-engine/content-origin.js +6 -1
  20. package/dist/core/memory.d.ts +10 -0
  21. package/dist/core/park-selfcheck.js +1 -0
  22. package/dist/core/permission-rule-consent.js +9 -5
  23. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  24. package/dist/core/runner/prepare-config-doors.js +36 -0
  25. package/dist/core/runner/prepare-task.d.ts +28 -1
  26. package/dist/core/runner/prepare-task.js +106 -8
  27. package/dist/core/runner/runtask.js +41 -4
  28. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  29. package/dist/core/tool-policy.d.ts +24 -8
  30. package/dist/core/tool-policy.js +3 -3
  31. package/dist/core/tools.js +1 -1
  32. package/dist/core/trace.d.ts +36 -0
  33. package/dist/core/types.d.ts +127 -2
  34. package/dist/core/untrusted-text.d.ts +11 -0
  35. package/dist/core/untrusted-text.js +1 -0
  36. package/dist/engine/llm/types.d.ts +21 -2
  37. package/dist/engine/loop/agent-loop.js +7 -1
  38. package/dist/engine/loop/types.d.ts +4 -1
  39. package/dist/tools/fs/fs-bash.js +1 -2
  40. package/package.json +1 -1
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial, resolveDelegationEn
20
20
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
21
21
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
22
22
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
23
- import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
23
+ import { askApproverIdentity, carriesBidiControls, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
24
24
  const PERSISTED_RULE_TOOL = "Bash";
25
25
  import { findAdmittingRule, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
26
26
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
@@ -70,7 +70,7 @@ import { resolveEpochAgainstBundled } from "../../prompt-assembly/epoch.js";
70
70
  import { artifactDeclarations } from "../../prompt-assembly/artifact.js";
71
71
  import { buildTurnPromptSnapshot } from "../../prompt-assembly/turn-snapshot.js";
72
72
  import { SEMA_DEFAULT_PACK } from "../../prompt-assembly/packs/sema-default.js";
73
- import { clearStaleToolResults, dropEmptyFailureAssistants, editBudget } from "../context-edit.js";
73
+ import { clearStaleToolResults, createClearedProjectionLedger, dropEmptyFailureAssistants, editBudget, replayClearedProjection, resolveTriggerWindow, } from "../context-edit.js";
74
74
  import { capAggregateToolResults } from "../tool-result-budget.js";
75
75
  import { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "../media-byte-cap.js";
76
76
  import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
@@ -124,7 +124,7 @@ function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId)
124
124
  if (compModel === undefined)
125
125
  return;
126
126
  const compWindow = compModel.contextTokens ?? compModel.contextWindow;
127
- const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
127
+ const mainWindow = resolveTriggerWindow(model).window;
128
128
  if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
129
129
  const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
130
130
  const sanitized = sanitizeCompactionSettings(merged, mainWindow);
@@ -195,6 +195,33 @@ function announceToolModelGate(onNotice, modelId, gate) {
195
195
  }
196
196
  }
197
197
  }
198
+ function announceDeclaredMcpContentClasses(args) {
199
+ for (const entry of args.entries ?? []) {
200
+ const declaredClass = entry.contentOrigin;
201
+ if (declaredClass === undefined)
202
+ continue;
203
+ const server = inlineUntrusted(String(entry.name), 160);
204
+ const toolCount = args.statuses?.find((s) => s.name === entry.name)?.toolNames?.length ?? 0;
205
+ const resourceFaceCaveat = " (Not covered: the cross-server resource faces — ListMcpResourcesTool/ReadMcpResourceTool/ReadMcpResourceDirTool — stay externally classified and still mark.)";
206
+ const posture = declaredClass === "local"
207
+ ? `this server's mounted tools' invocations will not mark this session's memory as externally exposed.${resourceFaceCaveat}`
208
+ : declaredClass === "execution"
209
+ ? `this server's mounted tools' invocations will not mark this session's memory unless the execIsExternalContent strict upgrade is armed (currently: ${args.execIsExternalContent ? "armed" : "off"}).${resourceFaceCaveat}`
210
+ : "its tools stay externally classified and this declaration PINS that — the trustedTools allowlist no longer exempts them, and invocations mark this session's memory.";
211
+ deliverEngineNotice(args.onNotice, {
212
+ code: "memory.content_class_declared",
213
+ message: `MCP server "${server}" (${toolCount} tool(s) mounted) is declared ` +
214
+ `contentOrigin "${declaredClass}" by this deployment's configuration — ${posture}`,
215
+ detail: {
216
+ server,
217
+ contentOrigin: declaredClass,
218
+ toolCount,
219
+ ...(declaredClass === "execution" ? { execIsExternalContent: args.execIsExternalContent } : {}),
220
+ sessionId: args.sessionId,
221
+ },
222
+ });
223
+ }
224
+ }
198
225
  export { __resetReadFaceClampAnnouncement } from "./prepare-hands-readface.js";
199
226
  const DEFAULT_MAX_SUSPENDS = 5;
200
227
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
@@ -242,6 +269,49 @@ class ParkRefusal extends Error {
242
269
  export { resolveCheckpointStore } from "../checkpoint-store.js";
243
270
  export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
244
271
  export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
272
+ function buildMicroCompactState(deps, model, sessionId, offloadStore, knob) {
273
+ const triggerWindow = resolveTriggerWindow(model);
274
+ if (triggerWindow.clamped) {
275
+ deliverEngineNotice(deps.onNotice, {
276
+ code: "config.autocompact_window_clamped",
277
+ message: `model "${model.id}" declares autoCompactTokens=${triggerWindow.declaredAutoCompactTokens} ABOVE its physical window ` +
278
+ `${triggerWindow.physicalWindow}; the autocompact window only ever LOWERS the trigger-side geometry, so the value was ` +
279
+ `clamped to the physical window — fix the model config (declare a value at or below the physical window, or omit it)`,
280
+ detail: {
281
+ modelId: model.id,
282
+ declaredAutoCompactTokens: triggerWindow.declaredAutoCompactTokens,
283
+ physicalWindow: triggerWindow.physicalWindow,
284
+ sessionId,
285
+ },
286
+ });
287
+ }
288
+ return {
289
+ machine: knob.machine,
290
+ clearOnRejection: knob.clearOnRejection,
291
+ ledger: createClearedProjectionLedger(),
292
+ projectionRef: {},
293
+ ...(offloadStore ? { offloadPersist: createOffloadPersist(offloadStore, sessionId, deps.onNotice) } : {}),
294
+ };
295
+ }
296
+ function recordFrontierClears(args) {
297
+ for (const { index } of args.pass.clears) {
298
+ const at = args.index.keyAt(index);
299
+ const clearedMsg = args.edited[index];
300
+ const markerText = clearedMsg.content?.[0]?.text;
301
+ if (at !== undefined && typeof markerText === "string") {
302
+ args.ledger.entries.set(at.key, { marker: markerText, groupCount: at.groupCount, fp: at.fp });
303
+ }
304
+ }
305
+ emitTrace(args.tracer, () => ({
306
+ kind: "context.mc_clear",
307
+ version: 1,
308
+ taskId: args.taskId,
309
+ clearedCount: args.pass.clears.length,
310
+ tokensSavedEstimate: args.pass.tokensSavedEstimate,
311
+ trigger: "frontier",
312
+ ts: Date.now(),
313
+ }));
314
+ }
245
315
  export function gatedCallIdOf(p) {
246
316
  if (p.suspendRef.token !== undefined)
247
317
  return p.suspendRef.gatedCallId;
@@ -492,7 +562,7 @@ async function derivedRouteFallsBack(args) {
492
562
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
493
563
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
494
564
  spec = doors.spec;
495
- const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
565
+ const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
496
566
  announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
497
567
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
498
568
  let shellGatedBash = false;
@@ -2079,6 +2149,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2079
2149
  const trustedTools = effectiveSafety.trustedTools;
2080
2150
  const execIsExternalContent = effectiveSafety.execIsExternalContent;
2081
2151
  provenanceForChildrenRef.current = { trustedTools: [...trustedTools], execIsExternalContent };
2152
+ announceDeclaredMcpContentClasses({
2153
+ entries: lockedPreflight.mcp,
2154
+ statuses: mcp?.statuses,
2155
+ execIsExternalContent,
2156
+ sessionId,
2157
+ onNotice: deps.onNotice,
2158
+ });
2082
2159
  if (memoryEngineSession?.settlement !== undefined) {
2083
2160
  delegationSettlementRef.current = { controlDir: memoryEngineSession.settlement.controlDir, sessionId: memoryEngineSession.settlement.sessionId };
2084
2161
  }
@@ -4229,7 +4306,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4229
4306
  args: parkedArgs,
4230
4307
  ...(() => {
4231
4308
  const preview = approvalPreviewOf(req.toolName, parkedArgs);
4232
- return preview !== undefined ? { preview } : {};
4309
+ const bidi = carriesBidiControls(parkedArgs) || carriesBidiControls(preview);
4310
+ return {
4311
+ ...(preview !== undefined ? { preview } : {}),
4312
+ ...(bidi ? { hasBidiControls: true } : {}),
4313
+ };
4233
4314
  })(),
4234
4315
  ...ruleOffersOf(req.toolName, parkedArgs, {
4235
4316
  ...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
@@ -4502,11 +4583,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4502
4583
  }
4503
4584
  const editAt = editBudget(model);
4504
4585
  const guardAt = guardBudget(model);
4586
+ const microCompact = buildMicroCompactState(deps, model, sessionId, offloadStore, microCompactKnob);
4505
4587
  const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
4506
4588
  harness.on("context", async ({ messages }) => {
4507
4589
  batchHaltRef.current = undefined;
4508
4590
  const healed = dropEmptyFailureAssistants(messages);
4509
- const capped = await capAggregateToolResults(healed, {
4591
+ const replay = replayClearedProjection(healed, microCompact.ledger);
4592
+ const replayed = replay.messages;
4593
+ const capped = await capAggregateToolResults(replayed, {
4510
4594
  store: offloadStore,
4511
4595
  sessionId,
4512
4596
  onCapped: (info) => emitTrace(deps.tracer, () => ({
@@ -4523,8 +4607,17 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4523
4607
  limitBytes: deps.mediaByteCapBytes ?? AGGREGATE_MEDIA_BUDGET_BYTES,
4524
4608
  onStripped: deps.onMediaStripped,
4525
4609
  });
4610
+ let ccPass;
4526
4611
  const edited = clearStaleToolResults(mediaCapped, {
4527
4612
  budgetTokens: editAt,
4613
+ ...(microCompact.machine === "cc" || microCompact.clearOnRejection ? { recognizeCcMarkers: true } : {}),
4614
+ ...(microCompact.machine === "cc"
4615
+ ? {
4616
+ machine: "cc",
4617
+ clearSource: replayed,
4618
+ onCleared: (pass) => { ccPass = pass; },
4619
+ }
4620
+ : {}),
4528
4621
  anchoredTotalTokens: estimateContextTokens(mediaCapped, charsPerToken).tokens,
4529
4622
  charsPerToken,
4530
4623
  ...(offloadStore
@@ -4535,6 +4628,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4535
4628
  }
4536
4629
  : {}),
4537
4630
  });
4631
+ if (ccPass !== undefined) {
4632
+ recordFrontierClears({ pass: ccPass, index: replay.index, edited, ledger: microCompact.ledger, tracer: deps.tracer, taskId: spec.taskId ?? sessionId });
4633
+ }
4538
4634
  let trimmed = trimToBudget(edited, guardAt, estimateContextTokens(edited, charsPerToken).tokens, charsPerToken);
4539
4635
  const trimDroppedMessages = trimmed.length < edited.length;
4540
4636
  if (trimDroppedMessages) {
@@ -4575,12 +4671,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4575
4671
  }
4576
4672
  }
4577
4673
  requestLossyRef.current =
4578
- capped !== healed ||
4674
+ replayed !== healed ||
4675
+ capped !== replayed ||
4579
4676
  mediaCapped !== capped ||
4580
4677
  edited !== mediaCapped ||
4581
4678
  trimDroppedMessages ||
4582
4679
  swept.dropped.length > 0 ||
4583
4680
  gitStatusRef.overBudgetShrunk === true;
4681
+ microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
4584
4682
  return { messages: swept.messages };
4585
4683
  });
4586
4684
  const cacheBreakDetector = deps.cacheBreakDetection === false ? undefined : new CacheBreakDetector();
@@ -4769,7 +4867,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4769
4867
  const effectiveReadFaceObserved = carrierReadFace();
4770
4868
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4771
4869
  const preparedHolder = {};
4772
- const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, 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 } : {}) });
4870
+ const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, 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, microCompact, ...(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 } : {}) });
4773
4871
  const prepared = buildPrepared();
4774
4872
  preparedHolder.current = prepared;
4775
4873
  return prepared;
@@ -2,6 +2,7 @@ import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
4
4
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
5
+ import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
5
6
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
6
7
  import { snapshotActorAssertion } from "../../internal/llm.js";
7
8
  import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
@@ -2734,7 +2735,7 @@ export class Runner {
2734
2735
  const withinTaskCompaction = (spec.compaction?.enabled ?? true) && (spec.compaction?.withinTask ?? true);
2735
2736
  const compactionBreaker = { failures: 0 };
2736
2737
  if (spec.compaction?.enabled ?? true) {
2737
- const prefixWindow = prepared.model.autoCompactTokens ?? prepared.model.contextTokens ?? prepared.model.contextWindow;
2738
+ const prefixWindow = resolveTriggerWindow(prepared.model).window;
2738
2739
  if (Number.isFinite(prefixWindow) && prefixWindow > 0) {
2739
2740
  const prefixSettings = sanitizeCompactionSettings({ ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction }, prefixWindow);
2740
2741
  const prefixCompactAt = prefixWindow - prefixSettings.reserveTokens;
@@ -2940,13 +2941,49 @@ export class Runner {
2940
2941
  thinkingOnly: {},
2941
2942
  degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
2942
2943
  promptTooLong: {
2943
- recover: async () => {
2944
+ recover: async (attempt) => {
2945
+ if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
2946
+ return false;
2947
+ if (prepared.microCompact.clearOnRejection && attempt === 1) {
2948
+ const proj = prepared.microCompact.projectionRef.current;
2949
+ const compactionAvailable = (spec.compaction?.enabled ?? true) && compactionBreaker.failures < MAX_CONSECUTIVE_COMPACTION_FAILURES;
2950
+ const declineArm = compactionAvailable ? "forced_compaction" : "none";
2951
+ const plan = proj === undefined
2952
+ ? { declined: "no_candidates" }
2953
+ : planRejectionClears(proj.messages, {
2954
+ keyOf: proj.keyOf,
2955
+ ...(prepared.microCompact.offloadPersist ? { offload: { persist: prepared.microCompact.offloadPersist } } : {}),
2956
+ });
2957
+ if ("declined" in plan) {
2958
+ emitTrace(rs.telemetry.tracer, () => ({
2959
+ kind: "context.mc_null",
2960
+ version: 1,
2961
+ taskId: rs.telemetry.taskId,
2962
+ reason: plan.declined,
2963
+ nextArm: declineArm,
2964
+ ts: Date.now(),
2965
+ }));
2966
+ }
2967
+ else {
2968
+ for (const e of plan.cleared) {
2969
+ prepared.microCompact.ledger.entries.set(e.key, { marker: e.marker, groupCount: e.groupCount, fp: e.fp });
2970
+ }
2971
+ emitTrace(rs.telemetry.tracer, () => ({
2972
+ kind: "context.mc_clear",
2973
+ version: 1,
2974
+ taskId: rs.telemetry.taskId,
2975
+ clearedCount: plan.cleared.length,
2976
+ tokensSavedEstimate: plan.tokensSavedEstimate,
2977
+ trigger: "refusal",
2978
+ ts: Date.now(),
2979
+ }));
2980
+ return true;
2981
+ }
2982
+ }
2944
2983
  if (!(spec.compaction?.enabled ?? true))
2945
2984
  return false;
2946
2985
  if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
2947
2986
  return false;
2948
- if (prepared.abortController.signal.aborted || prepared.suspendRef.token !== undefined)
2949
- return false;
2950
2987
  try {
2951
2988
  const comp = await maybeCompact({
2952
2989
  session: prepared.session,
@@ -180,6 +180,38 @@ export async function checkpointStoreContract(make, runAssertion) {
180
180
  assert.equal(list[0].token, pending.token);
181
181
  assert.equal((await store.get(pending.token)).status, "pending");
182
182
  });
183
+ run("#438 hasBidiControls three-form matrix: a row's `true` survives + projects; absent stays absent (clean args); an out-of-contract `false` reads as absent; a bit-less marked row backfills at projection", async () => {
184
+ const store = make();
185
+ const RLO = String.fromCodePoint(0x202e);
186
+ const asserted = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "bidi-true" });
187
+ asserted.pendingAction.hasBidiControls = true;
188
+ const absent = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "bidi-absent" });
189
+ const falsed = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "bidi-false" });
190
+ falsed.pendingAction.hasBidiControls = false;
191
+ const bitless = createCheckpointFixture({
192
+ token: mintCheckpointToken(),
193
+ sessionId: "bidi-bitless",
194
+ pendingAction: {
195
+ kind: "tool_approval",
196
+ toolCallId: "call-3",
197
+ toolName: "Write",
198
+ args: { path: `/x/${RLO}txt.exe` },
199
+ boundInputHash: "h0",
200
+ batchToolCallIds: ["call-3"],
201
+ completedCallIds: [],
202
+ },
203
+ });
204
+ for (const cp of [asserted, absent, falsed, bitless])
205
+ await store.put(cp.token, cp);
206
+ const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.sessionId, s]));
207
+ assert.equal(byId.get("bidi-true")?.hasBidiControls, true);
208
+ assert.equal("hasBidiControls" in byId.get("bidi-absent"), false, "clean+absent must project the key OMITTED, not null/false");
209
+ assert.equal("hasBidiControls" in byId.get("bidi-false"), false, "a written false is out-of-contract and must read as absent, never project");
210
+ assert.equal(byId.get("bidi-bitless")?.hasBidiControls, true, "a marked row without the bit must be backfilled at projection time");
211
+ assert.equal(byId.get("bidi-bitless").toolInput.includes(RLO), true);
212
+ const stored = (await store.get(bitless.token)).pendingAction;
213
+ assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
214
+ });
183
215
  await settle();
184
216
  }
185
217
  const sortByToken = (a, b) => a.token.localeCompare(b.token);
@@ -986,10 +986,15 @@ export interface AskRequest {
986
986
  *
987
987
  * SCOPE, stated so absence is not read as a clean bill on the other routes: this is the
988
988
  * SYNCHRONOUS `onAsk` ask, like `principal`/`sourceTaskId` — the closure claim is over AskRequest
989
- * mint sites, not over every approval callback. Two human-decision routes never receive it:
990
- * ① a durable park/suspend never invokes `onAsk`, so a parked approval row carries no twin of
991
- * this bit today an inbox rendering `RiskDescriptor` must run its own screen (or the display
992
- * baseline above) rather than infer "no bit, no problem"; {@link createApprovalPolicy}'s
989
+ * mint sites, not over every approval callback. The other two human-decision routes:
990
+ * ① a durable park/suspend never invokes `onAsk` and never mints an AskRequest its twin of this
991
+ * bit is the PERSISTED `PendingAction.tool_approval.hasBidiControls` on the parked row (judged at
992
+ * the park mint over the row's own fidelity-projected args snapshot + preview, so the two faces
993
+ * can honestly differ on a shape-changing backend), projected to the inbox as
994
+ * `CheckpointSummary.hasBidiControls` — see those fields in `checkpoint-store.ts` for the reading
995
+ * contract; absence there is still "not detected", never a clean bill, and an inbox rendering
996
+ * `RiskDescriptor` still owes its own screen (or the display baseline above);
997
+ * ② {@link createApprovalPolicy}'s
993
998
  * `approve` seat resolves in-place inside the policy and mints no AskRequest at all — its
994
999
  * callback receives a bare `ToolCallRequest` (no bit, no {@link boundInputHash}, none of this
995
1000
  * surface), so a deployment doing HITL through that seat must render through
@@ -1222,18 +1227,29 @@ export type ResolvedAsk = PermissionResult & {
1222
1227
  };
1223
1228
  /**
1224
1229
  * Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
1225
- * and never throwing — the one caller is on the approval path, where a scan that failed must degrade
1226
- * to "not detected" rather than turn an ask into an error (the `deliverEngineNotice` posture: a
1227
- * derived disclosure must never become the failure of the thing it describes).
1230
+ * and never throwing — every caller is on an approval/projection path, where a scan that failed must
1231
+ * degrade to "not detected" rather than turn an ask into an error (the `deliverEngineNotice`
1232
+ * posture: a derived disclosure must never become the failure of the thing it describes).
1228
1233
  *
1229
1234
  * OBJECT KEYS are scanned as well as values: a key is displayed text too, and an argument object
1230
1235
  * `{ "cmd<RLO>": … }` renders its own reordering in any card that prints the shape (the marker
1231
1236
  * is spelled out here on purpose — a literal one in this comment would reorder the comment).
1232
1237
  *
1238
+ * `limits` parameterizes the two budgets WITHOUT opening a second reader: the character class and
1239
+ * the traversal stay defined exactly here, only the allowance varies by caller. Omitted ⇒ the mint
1240
+ * tier above (the resolveAsk chokepoint and the durable park mint, which scan one call's own
1241
+ * payload). A per-row projection caller (the checkpoint summary backfill, which may face
1242
+ * deployment-written rows of arbitrary size on a many-row read) passes its own smaller tier so one
1243
+ * row's work is bounded regardless of the row's provenance. Both members are required when the
1244
+ * object is given — a half-specified tier would silently mix two tiers in one scan.
1245
+ *
1233
1246
  * Not a public export: the contract is the {@link AskRequest.hasBidiControls} bit, and a second
1234
1247
  * spelling of "does this carry bidi" on the public surface would be one more thing to keep in step.
1235
1248
  */
1236
- export declare function carriesBidiControls(value: unknown): boolean;
1249
+ export declare function carriesBidiControls(value: unknown, limits?: {
1250
+ maxNodes: number;
1251
+ maxChars: number;
1252
+ }): boolean;
1237
1253
  /**
1238
1254
  * Resolve an `ask` decision to a terminal `allow`/`deny` via {@link OnAsk}. Centralizes the headless
1239
1255
  * auto-deny default, fail-closed error handling, and stable deny reasons so every ask site is
@@ -903,9 +903,9 @@ export function coreMintedResolutionOf(d, call) {
903
903
  const BIDI_CONTROL_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
904
904
  const BIDI_SCAN_MAX_NODES = 5_000;
905
905
  const BIDI_SCAN_MAX_CHARS = 1_000_000;
906
- export function carriesBidiControls(value) {
907
- let budget = BIDI_SCAN_MAX_NODES;
908
- let charBudget = BIDI_SCAN_MAX_CHARS;
906
+ export function carriesBidiControls(value, limits) {
907
+ let budget = limits?.maxNodes ?? BIDI_SCAN_MAX_NODES;
908
+ let charBudget = limits?.maxChars ?? BIDI_SCAN_MAX_CHARS;
909
909
  const seen = new WeakSet();
910
910
  const scan = (s) => {
911
911
  if (charBudget <= 0)
@@ -45,7 +45,7 @@ export function defineTool(spec, options) {
45
45
  executionMode,
46
46
  ...(spec.isConcurrencySafe ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
47
47
  ...(spec.effect ? { effect: spec.effect } : {}),
48
- ...(spec.contentOrigin ? { contentOrigin: spec.contentOrigin } : {}),
48
+ ...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
49
49
  ...(spec.egress ? { egress: true } : {}),
50
50
  ...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
51
51
  ...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
@@ -729,6 +729,42 @@ export type TraceEvent = {
729
729
  /** Messages dropped from THIS request view. */
730
730
  dropped: number;
731
731
  ts: number;
732
+ } | {
733
+ /** design/374 (G11) — the microCompact clearing machine FIRED: `clearedCount` tool-result
734
+ * occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
735
+ * = the proactive request-build pass (anchored estimate crossed the edit budget),
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
741
+ * and re-fire per request, so a frame there would re-count the same occurrences and break
742
+ * this frame's cardinality clause). Cardinality: exactly ONE frame per firing pass — a retry
743
+ * chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
744
+ * attempt of the shared prompt-too-long recovery budget spent (the MC-R arm's retry rides
745
+ * the same per-chain account the forced-compaction arm draws on — the knob doc on
746
+ * `RunnerDeps.microCompact` carries the full accounting). */
747
+ kind: "context.mc_clear";
748
+ version: 1;
749
+ taskId: string;
750
+ clearedCount: number;
751
+ /** The pass's structural savings estimate (the ≥20k gate's own number). */
752
+ tokensSavedEstimate: number;
753
+ trigger: "frontier" | "refusal" | "blocking";
754
+ ts: number;
755
+ } | {
756
+ /** design/374 (G11) — the MC-R rejection arm ran and DECLINED to clear: `reason` says why
757
+ * (`"no_candidates"` = nothing clearable beyond the keep window on the rejected projection;
758
+ * `"below_min_savings"` = clearable but under the 20k gate), `nextArm` names where the
759
+ * recovery chain goes instead (`"forced_compaction"` when the compaction arm is available,
760
+ * `"none"` when it is disabled/tripped — the turn will surface the provider error). Emitted
761
+ * only on the refusal arm; the frontier pass declines silently every request by design. */
762
+ kind: "context.mc_null";
763
+ version: 1;
764
+ taskId: string;
765
+ reason: "no_candidates" | "below_min_savings";
766
+ nextArm: "forced_compaction" | "none";
767
+ ts: number;
732
768
  } | {
733
769
  /** C8 — the per-turn aggregate tool-result budget capped a result (offloaded to the store, or
734
770
  * degraded to a self-contained truncation preview when the store failed/absent). */
@@ -353,7 +353,14 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
353
353
  * - `"local"` — purely local reads/computation.
354
354
  * Classification is single-sourced with the tool definition: core built-ins declare theirs here;
355
355
  * a HOST tool that omits it is classified fail-closed as `"external"` (unknown = external) unless
356
- * the deployment exempts the name via `TaskSpec.memory.trustedTools`.
356
+ * the deployment exempts the name via `TaskSpec.memory.trustedTools`. A declaration BEATS that
357
+ * allowlist (the allowlist is the channel for UNDECLARED tools, never a lever to re-grade a tool
358
+ * that classified itself), and only a member of the closed vocabulary counts as one — an unreadable
359
+ * value classifies `"external"` rather than exempting anything.
360
+ *
361
+ * Tools core mints on the deployment's behalf get the same seat one level up: an MCP server's whole
362
+ * tool set is declared at its entry ({@link McpServerSpec.contentOrigin}, design/378), because the
363
+ * host never touches those tool objects.
357
364
  */
358
365
  contentOrigin?: ToolContentOrigin;
359
366
  /**
@@ -1299,7 +1306,11 @@ export interface McpServerSpec {
1299
1306
  */
1300
1307
  principalHeader?: string;
1301
1308
  };
1302
- /** Optional allowlist of tool names to expose (others are dropped). */
1309
+ /** Optional allowlist of tool names to expose (others are dropped). ORTHOGONAL to
1310
+ * {@link contentOrigin} and usefully paired with it (design/378 §2): the class declaration follows
1311
+ * the server's roster, so a high-assurance deployment that wants a CLOSED tool set writes both —
1312
+ * a tool a later refresh adds then lands outside this list and is simply not mounted. Existing
1313
+ * semantics, no new mechanism; the refresh receipt still names every added tool either way. */
1303
1314
  allowTools?: string[];
1304
1315
  /**
1305
1316
  * design/99 §E23 — opt in to INBOUND elicitation for THIS server: when `true` AND a {@link RunnerDeps.onElicit}
@@ -1316,12 +1327,66 @@ export interface McpServerSpec {
1316
1327
  * override is AUTHORITATIVE and may LOWER an effect (vouch a tool is `read`/`idempotent`) as well as raise it
1317
1328
  * (`egress` / irreversible). This is the ONLY trusted way to drop an MCP tool below the fail-closed `write`
1318
1329
  * default (design F). Folds over the server hints in prepare-task (caller > server hint > fail-closed write).
1330
+ *
1331
+ * A DIFFERENT AXIS from {@link contentOrigin}: this one is about what a call DOES (repeat-safety,
1332
+ * blast radius, reversibility — it feeds the approval gate); the content class is about what a call
1333
+ * BRINGS BACK (memory-write governance, no gate/policy/roster effect). Neither implies the other —
1334
+ * a read-only tool can return third-party text, and a deployment's own writer brings back nothing
1335
+ * external — so vouching on one axis never quietly vouches on the other.
1319
1336
  */
1320
1337
  toolAxes?: Record<string, {
1321
1338
  effect?: ToolEffect;
1322
1339
  egress?: boolean;
1323
1340
  irreversibility?: "always" | "never";
1324
1341
  }>;
1342
+ /**
1343
+ * design/378 — declare {@link ToolSpec.contentOrigin} on behalf of THIS SERVER'S ENTIRE TOOL SET
1344
+ * (tools a mid-task refresh adds included), with the same authority and the same responsibility a
1345
+ * directly-mounted host tool's own declaration carries.
1346
+ *
1347
+ * WHY THE SEAT EXISTS. Without it the class keys on the MOUNT SHAPE rather than on lineage: a tool
1348
+ * the deployment wrote and runs itself is structurally `"external"` the moment it arrives over the
1349
+ * MCP protocol namespace, so every call marks the session's memory externally exposed. The
1350
+ * per-name channels cannot express the fact either — {@link import("./memory.js").MemorySpecInput.trustedTools}
1351
+ * is a per-REQUEST allowlist keyed on the MINTED name (the host would have to predict the charset
1352
+ * normalization) whose own definition is "the exception channel for UNDECLARED tools", and it says
1353
+ * nothing about tools the server adds later.
1354
+ *
1355
+ * THIS IS A TRUST DECLARATION, not a routing hint. Use it only for servers inside the deployment's
1356
+ * trust boundary — a process, socket or service the deployment itself runs. Declaring a THIRD-PARTY
1357
+ * server means treating its output as content the deployment wrote: the class names are
1358
+ * BOUNDARY-relative, never topological, so neither the transport kind nor the address is evidence
1359
+ * of lineage (a stdio child can be an untrusted package; a loopback URL can be your own service) and
1360
+ * core deliberately does not gate on either. The declaration covers THE PEER THIS ENTRY CONNECTS TO
1361
+ * — authenticating that peer (socket permissions, credentials) is the host's mounting duty.
1362
+ *
1363
+ * SEMANTICS. `"local"` ⇒ invocations no longer mark this session's memory; `"execution"` ⇒ the same,
1364
+ * except that {@link import("./memory.js").MemorySpecInput.execIsExternalContent} can still upgrade
1365
+ * the class for a strict deployment (which is why this seat takes the three-value vocabulary and not
1366
+ * a single "mine" flag — an execution-shaped tool mounted over MCP must stay inside that knob's
1367
+ * reach); `"external"` is an explicit PIN, and pins are not no-ops — a declaration beats the
1368
+ * `trustedTools` allowlist, so writing it forecloses the per-name exemption for this server's tools.
1369
+ * ABSENT ⇒ the pre-378 behavior byte for byte: the protocol namespace classifies the tools
1370
+ * `"external"` (fail-closed). A value outside the vocabulary is refused at the preparation door
1371
+ * (`config.mcp_content_class`), never folded to a class.
1372
+ *
1373
+ * COVERAGE, stated honestly: the class rides this server's own mounted tools. The cross-server
1374
+ * resource faces (ListMcpResourcesTool / ReadMcpResourceTool / ReadMcpResourceDirTool) aggregate
1375
+ * over every connected server in one call, so they stay `"external"` and still mark — over-marking,
1376
+ * the safe direction. A delegated child's pool is a separate static declaration surface
1377
+ * ({@link ToolSpec.agentToolPool} entries carry their own `contentOrigin`): a deployment handing
1378
+ * this server's tools to children mirrors the value there, and not mirroring it over-marks.
1379
+ *
1380
+ * TRUST SOURCE — a DEPLOYMENT-plane key. It redefines where the trust boundary runs, which puts it
1381
+ * on the same authority plane as {@link RunnerDeps} wiring, not on the request plane. Core sees one
1382
+ * `TaskSpec` and cannot tell a deployment-baseline entry from one a request supplied, so any
1383
+ * assembly layer that accepts REQUEST-side MCP entries must reject or strip this key from them:
1384
+ * "allowed to mount a server" is not "allowed to redefine the deployment's trust boundary", and a
1385
+ * caller-supplied `"local"` would otherwise be self-authorization around the session mark. A
1386
+ * single-tenant superuser surface (a host reading its own `--mcp-config` file) IS the deployment
1387
+ * plane and needs no such gate.
1388
+ */
1389
+ contentOrigin?: ToolContentOrigin;
1325
1390
  }
1326
1391
  /**
1327
1392
  * Definition of one A2A (agent-to-agent protocol) PEER to talk to for the duration of one task.
@@ -4830,6 +4895,11 @@ export interface ProjectMemoryLoad {
4830
4895
  */
4831
4896
  export interface EngineNotice {
4832
4897
  /** Stable machine-readable family, dot-namespaced. Current families:
4898
+ * - `"config.autocompact_window_clamped"` (design/374 slice 1b) — a model declared
4899
+ * `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
4900
+ * physical window (an autocompact window only ever lowers the trigger) and the bad value is
4901
+ * announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
4902
+ * physicalWindow, sessionId }`.
4833
4903
  * - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
4834
4904
  * the value in force; `detail: { knob, raw, usedMs }`.
4835
4905
  * - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
@@ -4997,6 +5067,27 @@ export interface EngineNotice {
4997
5067
  * `detail: { reason, subagentType?, sessionId? }` — `reason` is the same sentence the waived
4998
5068
  * mark would have carried, neutralized/length-bounded (tool and agent-type names are
4999
5069
  * host/model-controlled inputs).
5070
+ * - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
5071
+ * {@link McpServerSpec.contentOrigin}: the deployment declared this server's whole tool set to
5072
+ * be inside (or explicitly outside) its trust boundary, so the protocol namespace's structural
5073
+ * `"external"` no longer decides. The AUDIT line for a trust boundary an operator drew by
5074
+ * configuration — which is why it announces on the explicit `"external"` value too: pinning is
5075
+ * not a no-op (a declaration beats the `trustedTools` allowlist, so it forecloses the per-name
5076
+ * exemption for this server's tools). One line per DECLARED ENTRY per prepared task leg, minted
5077
+ * once at preparation and never per call (the declaration's whole effect is that calls stop
5078
+ * marking; narrating each call would trade the saved mark for equal noise). Armed on the SAME
5079
+ * condition as the classification it talks about — a leg with no engine-memory session and no
5080
+ * provenance recorder classifies nothing, so there is no posture to report and no line is
5081
+ * minted. `detail: { server,
5082
+ * contentOrigin, toolCount, execIsExternalContent?, sessionId? }` — `server` is the
5083
+ * host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
5084
+ * mounted (0 for a server that failed to connect — the declaration still stands and is still
5085
+ * disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
5086
+ * knob AS RESOLVED FOR THIS RUN, because that arm's posture depends on it. The message's closing
5087
+ * clause is written per value so it can never describe a posture the run does not have. The
5088
+ * entry's TRANSPORT is deliberately absent: it is a claim about a mutable host-owned object made
5089
+ * long after the dial, and D-10 already rules topology is not evidence of the lineage this line
5090
+ * audits — connection facts live on `MaterializedMcp.statuses` instead.
5000
5091
  *
5001
5092
  * - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
5002
5093
  * settled without reaching the fixpoint: `detail` names the stop reason (the closed
@@ -6070,6 +6161,40 @@ export interface RunnerDeps {
6070
6161
  * ~20000; set `0` or `Infinity` to disable offloading. Per-tool override via `ToolSpec.offloadThresholdChars`.
6071
6162
  */
6072
6163
  toolResultThresholdChars?: number;
6164
+ /**
6165
+ * design/374 — microCompact machine-alignment knobs (EXPERIMENTAL until the slice-3 default
6166
+ * flip; both default OFF so a deployment that never touches this bag runs the pre-374 machine
6167
+ * byte for byte).
6168
+ *
6169
+ * - `machine`: which stale-tool-result clearing machine the request pipeline runs —
6170
+ * `"legacy"` (default; the historical keep-3 / clear-to-budget machine) or `"cc"` (the CC
6171
+ * 2.1.223 rejection-leg form: keep 5, ≥20k minimum-savings gate, one deep clear beyond the
6172
+ * keep window, CC marker bytes). ⚠️ Read the P-form warning on
6173
+ * {@link import("./context-edit.js").ContextEditMachine} before selecting `"cc"`: until the
6174
+ * slice-3 fallback re-ordering ships, the 20k gate sits in front of the only reduction while
6175
+ * the message-dropping guard trim still backstops — opting in is accepting that trade.
6176
+ * - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
6177
+ * deterministic clear over the rejected projection (same cc machine, savings ≥20k or nothing)
6178
+ * and retry inside the turn BEFORE the forced-compaction recovery. Default false (X2: the
6179
+ * machinery lands dark; the default flips together with the machine in slice 3). Independent
6180
+ * of `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
6181
+ * budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
6182
+ * §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
6183
+ * clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
6184
+ * attempts per chain), so a chain that clears and is rejected AGAIN has one forced-compaction
6185
+ * attempt left where the knob-off chain nominally had two — the trade costs no effective
6186
+ * compaction pass, because the second forced-compaction call of the off chain is structurally
6187
+ * a no-op whenever the first one landed (the branch leaf is already a compaction entry).
6188
+ *
6189
+ * A declaration outside the closed vocabulary (a `machine` string not in the union, a
6190
+ * non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
6191
+ * whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
6192
+ * would run the pre-374 machine while the deployment believes it opted in.
6193
+ */
6194
+ microCompact?: {
6195
+ machine?: "legacy" | "cc";
6196
+ clearOnRejection?: boolean;
6197
+ };
6073
6198
  /**
6074
6199
  * Two-phase prefix-cache-break detection (design/31): per turn, fingerprint the prefix and, on a
6075
6200
  * confirmed `cacheRead` drop, emit a root-cause finding via `onError(phase:"prompt-cache")`. Cheap