@sema-agent/core 5.58.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 (59) hide show
  1. package/CHANGELOG.md +88 -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/route-adjudicator.d.ts +8 -1
  8. package/dist/brain/route-adjudicator.js +8 -1
  9. package/dist/brain/stream-engine.js +9 -1
  10. package/dist/core/auto-compaction.js +2 -2
  11. package/dist/core/checkpoint-store.d.ts +116 -19
  12. package/dist/core/checkpoint-store.js +15 -8
  13. package/dist/core/context-edit.d.ts +243 -41
  14. package/dist/core/context-edit.js +247 -32
  15. package/dist/core/governance-codes.d.ts +37 -10
  16. package/dist/core/governance-codes.js +57 -1
  17. package/dist/core/locked-config.d.ts +36 -4
  18. package/dist/core/locked-config.js +34 -1
  19. package/dist/core/mcp.js +10 -6
  20. package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
  21. package/dist/core/memory-engine/consolidation-driver.js +54 -5
  22. package/dist/core/memory-engine/consolidation.d.ts +73 -1
  23. package/dist/core/memory-engine/consolidation.js +21 -1
  24. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  25. package/dist/core/memory-engine/content-origin.js +6 -1
  26. package/dist/core/memory-engine/engine.d.ts +97 -8
  27. package/dist/core/memory-engine/engine.js +112 -20
  28. package/dist/core/memory-engine/file-backend.d.ts +13 -1
  29. package/dist/core/memory-engine/file-backend.js +3 -0
  30. package/dist/core/memory-engine/index.d.ts +4 -3
  31. package/dist/core/memory-engine/layout.js +20 -6
  32. package/dist/core/memory-engine/types.d.ts +17 -0
  33. package/dist/core/memory.d.ts +10 -0
  34. package/dist/core/park-selfcheck.js +1 -0
  35. package/dist/core/permission-rule-consent.js +9 -5
  36. package/dist/core/permission-rule-model.d.ts +42 -1
  37. package/dist/core/permission-rule-model.js +12 -0
  38. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  39. package/dist/core/runner/prepare-config-doors.js +36 -0
  40. package/dist/core/runner/prepare-task.d.ts +28 -1
  41. package/dist/core/runner/prepare-task.js +109 -11
  42. package/dist/core/runner/runtask.js +45 -8
  43. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  44. package/dist/core/tool-policy.d.ts +74 -0
  45. package/dist/core/tool-policy.js +80 -1
  46. package/dist/core/tools.js +1 -1
  47. package/dist/core/trace.d.ts +36 -0
  48. package/dist/core/types.d.ts +172 -22
  49. package/dist/core/types.js +4 -3
  50. package/dist/core/untrusted-text.d.ts +11 -0
  51. package/dist/core/untrusted-text.js +1 -0
  52. package/dist/engine/llm/types.d.ts +21 -2
  53. package/dist/engine/loop/agent-loop.js +7 -1
  54. package/dist/engine/loop/types.d.ts +4 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +2 -2
  57. package/dist/tools/fs/fs-bash.js +1 -2
  58. package/package.json +1 -1
  59. package/test/export-surface.snapshot.json +1771 -1
@@ -83,10 +83,45 @@ export function resolveModelPromptTraits(model, spec, internals) {
83
83
  fableMitigations: isFableFamilyModelId(model.id),
84
84
  };
85
85
  }
86
+ function microCompactConfigError(field, value, legal) {
87
+ let shown;
88
+ try {
89
+ shown = JSON.stringify(value) ?? String(value);
90
+ }
91
+ catch {
92
+ try {
93
+ shown = String(value);
94
+ }
95
+ catch {
96
+ shown = `[unrepresentable ${typeof value}]`;
97
+ }
98
+ }
99
+ const seat = field === undefined ? "microCompact" : `microCompact.${field}`;
100
+ const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` +
101
+ `never folded to the default: a silently-ignored opt-in would run the pre-374 machine while the ` +
102
+ `deployment believes it opted in.`);
103
+ e.code = "config.microcompact_invalid";
104
+ return e;
105
+ }
106
+ export function resolveMicroCompactKnob(bag) {
107
+ if (bag !== undefined && (typeof bag !== "object" || bag === null || Array.isArray(bag))) {
108
+ throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys");
109
+ }
110
+ const declaredMachine = bag?.machine;
111
+ if (declaredMachine !== undefined && declaredMachine !== "legacy" && declaredMachine !== "cc") {
112
+ throw microCompactConfigError("machine", declaredMachine, `"legacy" | "cc"`);
113
+ }
114
+ const declaredClearOnRejection = bag?.clearOnRejection;
115
+ if (declaredClearOnRejection !== undefined && typeof declaredClearOnRejection !== "boolean") {
116
+ throw microCompactConfigError("clearOnRejection", declaredClearOnRejection, "a boolean");
117
+ }
118
+ return { machine: declaredMachine ?? "legacy", clearOnRejection: declaredClearOnRejection === true };
119
+ }
86
120
  export function prepareConfigDoors(input) {
87
121
  const { deps, sessions, resume, internals } = input;
88
122
  let spec = input.spec;
89
123
  assertRestoreGatedToolsValue(spec.restoreGatedTools);
124
+ const microCompactKnob = resolveMicroCompactKnob(deps.microCompact);
90
125
  const assertToolNameListValue = (value, seat) => {
91
126
  if (value === undefined)
92
127
  return;
@@ -318,6 +353,7 @@ export function prepareConfigDoors(input) {
318
353
  promptProfile,
319
354
  lockedPreflight,
320
355
  resolvedInteractionPosture,
356
+ microCompactKnob,
321
357
  resolvedRole,
322
358
  model,
323
359
  thinking,
@@ -20,6 +20,7 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
20
20
  import { type GitStatusLaneRef } from "./git-status-frame.js";
21
21
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
22
22
  import type { ToolDisclosureManifest } from "../trace.js";
23
+ import { type ClearedProjectionLedger, type ContextEditMachine, type OccurrenceIndex } from "../context-edit.js";
23
24
  import type { TaskNotificationPayload } from "../task-notification.js";
24
25
  import { type CwdRef, type ReadFace } from "../../tools/fs/index.js";
25
26
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
@@ -134,7 +135,6 @@ export declare function checkpointScopeOf(spec: {
134
135
  export { resolveCheckpointStore } from "../checkpoint-store.js";
135
136
  export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
136
137
  export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
137
- /** Everything the run loop needs, built once by {@link prepareTask} (task setup, isolated from the loop). */
138
138
  export interface Prepared {
139
139
  harness: AgentHarness;
140
140
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -876,6 +876,33 @@ export interface Prepared {
876
876
  trimPressureRef: {
877
877
  droppedMessages: boolean;
878
878
  };
879
+ /** design/374 slices 1b/2 — the microCompact machine state this run: the selected clearing
880
+ * machine, the cleared-projection ledger (request-view application, durable decisions — see
881
+ * `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
882
+ * start EMPTY by construction), the last request's projection seat (what the provider actually
883
+ * saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
884
+ * raw session rebuild), and the MC-R knob. Default machine "legacy" + MC-R off ⇒ the ledger
885
+ * never gains an entry and every replay is a same-reference no-op (default bytes unchanged). */
886
+ microCompact: PreparedMicroCompact;
887
+ }
888
+ /** See {@link Prepared.microCompact}. */
889
+ export interface PreparedMicroCompact {
890
+ machine: ContextEditMachine;
891
+ /** MC-R (design/374 §3.2): one-shot clear-and-retry on a provider input-too-long rejection.
892
+ * Default false (X2: lands with the machinery, flips with slice 3). */
893
+ clearOnRejection: boolean;
894
+ ledger: ClearedProjectionLedger;
895
+ projectionRef: {
896
+ current?: {
897
+ /** The FINAL projected view of the last provider request (post trim/sweep). */
898
+ messages: AgentMessage[];
899
+ /** Occurrence coordinates of that view (object-identity first, unambiguous-group fallback). */
900
+ keyOf: OccurrenceIndex["keyOf"];
901
+ };
902
+ };
903
+ /** The same offload persist seat the frontier machine uses (write-once, idempotent), so MC-R
904
+ * clears compose identical markers. Absent when no offload store is configured. */
905
+ offloadPersist?: (toolCallId: string, fullText: string) => string;
879
906
  }
880
907
  /**
881
908
  * WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
@@ -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;
@@ -482,7 +552,7 @@ async function derivedRouteFallsBack(args) {
482
552
  const verdict = await adjudicateDerivedRoute({ brain: args.brain, model: args.derived, getApiKeyAndHeaders: args.getApiKeyAndHeaders });
483
553
  if (verdict === undefined || verdict.ok)
484
554
  return false;
485
- deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict }));
555
+ deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}) }));
486
556
  return true;
487
557
  }
488
558
  catch {
@@ -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;
@@ -509,7 +579,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
509
579
  let effectiveCompModel = compModel;
510
580
  if (spec.compactionModel === undefined &&
511
581
  compModel !== undefined &&
512
- (await derivedRouteFallsBack({ seat: "compaction-summary", derived: compModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice }))) {
582
+ (await derivedRouteFallsBack({ seat: "compaction-summary", derived: compModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice, sessionId }))) {
513
583
  effectiveCompModel = undefined;
514
584
  }
515
585
  warnCompactionWindowHazard(deps.tracer, spec, model, effectiveCompModel, hostTaskId);
@@ -1096,7 +1166,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1096
1166
  catch {
1097
1167
  classifierModel = model;
1098
1168
  }
1099
- if (await derivedRouteFallsBack({ seat: "auto-mode-classifier", derived: classifierModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice })) {
1169
+ if (await derivedRouteFallsBack({ seat: "auto-mode-classifier", derived: classifierModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice, sessionId })) {
1100
1170
  classifierModel = model;
1101
1171
  }
1102
1172
  const classifierSystemPrompt = buildAutoModePrompt(am);
@@ -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";
@@ -2095,7 +2096,7 @@ export class Runner {
2095
2096
  message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
2096
2097
  `priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
2097
2098
  `delivered — only the interrupting semantics are absent.`,
2098
- detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}) },
2099
+ detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), sessionId: prepared.sessionId },
2099
2100
  });
2100
2101
  }
2101
2102
  if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
@@ -2172,7 +2173,7 @@ export class Runner {
2172
2173
  undrainedUserAtEnd = counts;
2173
2174
  return;
2174
2175
  }
2175
- for (const notice of undrainedUserInputNotices(counts, spec.taskId)) {
2176
+ for (const notice of undrainedUserInputNotices(counts, spec.taskId, prepared.sessionId)) {
2176
2177
  deliverEngineNotice(this.deps.onNotice, notice);
2177
2178
  }
2178
2179
  };
@@ -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,
@@ -3572,7 +3609,7 @@ export class Runner {
3572
3609
  steer: Math.max(0, undrainedUserAtEnd.steer - migratedParked.steer),
3573
3610
  followUp: Math.max(0, undrainedUserAtEnd.followUp - migratedParked.followUp),
3574
3611
  };
3575
- for (const notice of undrainedUserInputNotices(remaining, spec.taskId)) {
3612
+ for (const notice of undrainedUserInputNotices(remaining, spec.taskId, prepared.sessionId)) {
3576
3613
  deliverEngineNotice(this.deps.onNotice, notice);
3577
3614
  }
3578
3615
  }
@@ -3933,7 +3970,7 @@ export class Runner {
3933
3970
  if (!sameRouteIdentity(model, prepared.model)) {
3934
3971
  const verdict = await adjudicateDerivedRoute({ brain: this.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
3935
3972
  if (verdict !== undefined && !verdict.ok) {
3936
- deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict }));
3973
+ deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId }));
3937
3974
  model = prepared.model;
3938
3975
  thinking = prepared.thinking;
3939
3976
  }
@@ -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);
@@ -576,6 +576,13 @@ export declare function createAllowDenyPolicy(opts: {
576
576
  * Human-in-the-loop approval for selected tools. Tools in `requireApproval` call `approve(req)` and
577
577
  * are allowed only if it resolves true; tools in `deny` are always blocked; everything else is allowed
578
578
  * (override with `denyByDefault: true` to allow only `requireApproval` + an explicit `autoAllow`).
579
+ *
580
+ * This seat receives a bare `ToolCallRequest`, NOT the ask surface: the decision settles in-place
581
+ * inside the policy, no AskRequest is minted and `resolveAsk` is never entered — so none of the
582
+ * ask-side approval members (`hasBidiControls`, `boundInputHash`, `preview`, `riskAxes`, …) exist
583
+ * here. A surface rendering the command text for a human on this seat must render through
584
+ * {@link import("./permission-rule-model.js").renderUntrustedCommandText} (the display baseline)
585
+ * or run its own screen — absence of the warning bit is "not on this surface", never "clean".
579
586
  */
580
587
  export declare function createApprovalPolicy(opts: {
581
588
  /** Tools that need an approval decision. */
@@ -952,6 +959,48 @@ export interface AskRequest {
952
959
  * so this — like `principal`/`sourceTaskId` — exists only on the synchronous resolution path.
953
960
  * See {@link AskDelegationProvenance} for the trust posture of each member. */
954
961
  readonly delegation?: AskDelegationProvenance;
962
+ /**
963
+ * PRESENCE ONLY: the command/argument text this ask is about carries at least one DIRECTIONAL
964
+ * format control ({@link BIDI_CONTROL_RE} — the closed set: U+061C, U+200E/U+200F, the embeddings
965
+ * and overrides U+202A–U+202E, the isolates U+2066–U+2069). Those characters change nothing about
966
+ * what EXECUTES and everything about what a terminal or a card SHOWS: bytes that run
967
+ * `rm -rf /` can display as a benign line, which is precisely the deception an approval surface
968
+ * cannot afford to render unannotated.
969
+ *
970
+ * A WARNING BIT, not a verdict and not a transform. Core does NOT strip, reorder, refuse or rewrite
971
+ * anything on account of it — {@link args} is delivered byte-identical either way, and the decision
972
+ * stays the approver's. What a surface owes the person is a visible "the text below can display
973
+ * differently from what it runs" and a rendering that neutralizes the controls (see
974
+ * {@link import("./permission-rule-model.js").renderUntrustedCommandText} for the display baseline).
975
+ *
976
+ * Read it as PRESENCE-or-nothing: the field is either `true` or ABSENT. It is never written `false`,
977
+ * because absence means "not detected", which honestly covers both "clean" and "the bounded scan did
978
+ * not reach it" (a pathologically deep or huge argument graph stops at the scan budget) — a `false`
979
+ * would claim a proof the scan does not offer. Judged over the EXECUTING argument snapshot and the
980
+ * tool's own {@link preview} projection, i.e. the payload; {@link message} is deliberately out of
981
+ * scope (engine/policy-composed prose, not the thing that runs).
982
+ *
983
+ * Filled at the `resolveAsk` chokepoint, beside {@link boundInputHash} — every wired `onAsk`
984
+ * approver call crosses it, so an ask mint site added later is covered by construction. Optional
985
+ * on the type because a deployment may invoke its approver function directly.
986
+ *
987
+ * SCOPE, stated so absence is not read as a clean bill on the other routes: this is the
988
+ * SYNCHRONOUS `onAsk` ask, like `principal`/`sourceTaskId` — the closure claim is over AskRequest
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
998
+ * `approve` seat resolves in-place inside the policy and mints no AskRequest at all — its
999
+ * callback receives a bare `ToolCallRequest` (no bit, no {@link boundInputHash}, none of this
1000
+ * surface), so a deployment doing HITL through that seat must render through
1001
+ * {@link import("./permission-rule-model.js").renderUntrustedCommandText} or run its own screen.
1002
+ */
1003
+ readonly hasBidiControls?: true;
955
1004
  }
956
1005
  /**
957
1006
  * How an `ask` decision is resolved when a policy/hook requests human confirmation (design/37):
@@ -1176,6 +1225,31 @@ export type ResolvedAsk = PermissionResult & {
1176
1225
  * frame), so a consumer classifies a refusal by code instead of parsing its text. */
1177
1226
  resolution?: AskDenyResolution;
1178
1227
  };
1228
+ /**
1229
+ * Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
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).
1233
+ *
1234
+ * OBJECT KEYS are scanned as well as values: a key is displayed text too, and an argument object
1235
+ * `{ "cmd<RLO>": … }` renders its own reordering in any card that prints the shape (the marker
1236
+ * is spelled out here on purpose — a literal one in this comment would reorder the comment).
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
+ *
1246
+ * Not a public export: the contract is the {@link AskRequest.hasBidiControls} bit, and a second
1247
+ * spelling of "does this carry bidi" on the public surface would be one more thing to keep in step.
1248
+ */
1249
+ export declare function carriesBidiControls(value: unknown, limits?: {
1250
+ maxNodes: number;
1251
+ maxChars: number;
1252
+ }): boolean;
1179
1253
  /**
1180
1254
  * Resolve an `ask` decision to a terminal `allow`/`deny` via {@link OnAsk}. Centralizes the headless
1181
1255
  * auto-deny default, fail-closed error handling, and stable deny reasons so every ask site is