@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.
- package/CHANGELOG.md +88 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/errors.d.ts +18 -1
- package/dist/brain/errors.js +7 -1
- package/dist/brain/input-too-long.d.ts +57 -0
- package/dist/brain/input-too-long.js +35 -0
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/brain/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +116 -19
- package/dist/core/checkpoint-store.js +15 -8
- package/dist/core/context-edit.d.ts +243 -41
- package/dist/core/context-edit.js +247 -32
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +57 -1
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.js +10 -6
- package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
- package/dist/core/memory-engine/consolidation-driver.js +54 -5
- package/dist/core/memory-engine/consolidation.d.ts +73 -1
- package/dist/core/memory-engine/consolidation.js +21 -1
- package/dist/core/memory-engine/content-origin.d.ts +24 -2
- package/dist/core/memory-engine/content-origin.js +6 -1
- package/dist/core/memory-engine/engine.d.ts +97 -8
- package/dist/core/memory-engine/engine.js +112 -20
- package/dist/core/memory-engine/file-backend.d.ts +13 -1
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/permission-rule-consent.js +9 -5
- package/dist/core/permission-rule-model.d.ts +42 -1
- package/dist/core/permission-rule-model.js +12 -0
- package/dist/core/runner/prepare-config-doors.d.ts +22 -1
- package/dist/core/runner/prepare-config-doors.js +36 -0
- package/dist/core/runner/prepare-task.d.ts +28 -1
- package/dist/core/runner/prepare-task.js +109 -11
- package/dist/core/runner/runtask.js +45 -8
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +74 -0
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +172 -22
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +11 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/llm/types.d.ts +21 -2
- package/dist/engine/loop/agent-loop.js +7 -1
- package/dist/engine/loop/types.d.ts +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|