@sema-agent/core 5.17.0-pre.0 → 5.18.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 (81) hide show
  1. package/CHANGELOG.md +186 -1
  2. package/dist/agents/subagent.d.ts +8 -0
  3. package/dist/agents/subagent.js +93 -2
  4. package/dist/core/ask-question.js +6 -1
  5. package/dist/core/auto-compaction.d.ts +6 -0
  6. package/dist/core/auto-compaction.js +15 -1
  7. package/dist/core/canonical-json.js +176 -14
  8. package/dist/core/checkpoint-store.d.ts +15 -0
  9. package/dist/core/checkpoint-store.js +73 -0
  10. package/dist/core/governance-codes.js +1 -0
  11. package/dist/core/hooks.d.ts +12 -1
  12. package/dist/core/hooks.js +41 -6
  13. package/dist/core/mcp.d.ts +1 -0
  14. package/dist/core/mcp.js +18 -3
  15. package/dist/core/memory-engine/content-origin.d.ts +27 -0
  16. package/dist/core/memory-engine/content-origin.js +38 -0
  17. package/dist/core/memory-engine/engine.d.ts +12 -2
  18. package/dist/core/memory-engine/engine.js +172 -12
  19. package/dist/core/memory-engine/file-backend.d.ts +4 -0
  20. package/dist/core/memory-engine/file-backend.js +25 -3
  21. package/dist/core/memory-engine/index.d.ts +2 -1
  22. package/dist/core/memory-engine/index.js +2 -1
  23. package/dist/core/memory-engine/layout.d.ts +16 -0
  24. package/dist/core/memory-engine/layout.js +90 -2
  25. package/dist/core/memory-engine/sync-client.d.ts +1 -0
  26. package/dist/core/memory-engine/sync-client.js +23 -5
  27. package/dist/core/memory-engine/tools.d.ts +55 -0
  28. package/dist/core/memory-engine/tools.js +307 -0
  29. package/dist/core/memory-engine/types.d.ts +1 -1
  30. package/dist/core/memory.d.ts +4 -0
  31. package/dist/core/memory.js +15 -2
  32. package/dist/core/permission-rule-consent.d.ts +131 -0
  33. package/dist/core/permission-rule-consent.js +307 -0
  34. package/dist/core/permission-rule-model.d.ts +66 -0
  35. package/dist/core/permission-rule-model.js +135 -0
  36. package/dist/core/permission-rule-store.d.ts +89 -0
  37. package/dist/core/permission-rule-store.js +145 -0
  38. package/dist/core/permission-rules.d.ts +3 -2
  39. package/dist/core/permission-rules.js +9 -4
  40. package/dist/core/runner/prepare-memory.d.ts +3 -1
  41. package/dist/core/runner/prepare-memory.js +54 -14
  42. package/dist/core/runner/prepare-task.d.ts +12 -0
  43. package/dist/core/runner/prepare-task.js +270 -32
  44. package/dist/core/runner/runtask.js +27 -1
  45. package/dist/core/runner/tool-output-projection.js +1 -1
  46. package/dist/core/runner/turn-attachments.d.ts +2 -1
  47. package/dist/core/runner/turn-attachments.js +9 -6
  48. package/dist/core/session-reconcile.js +19 -1
  49. package/dist/core/task-registry-agent.js +1 -0
  50. package/dist/core/tool-policy.d.ts +17 -1
  51. package/dist/core/tool-policy.js +64 -8
  52. package/dist/core/tools.js +1 -0
  53. package/dist/core/trace.d.ts +20 -0
  54. package/dist/core/types.d.ts +15 -0
  55. package/dist/core/wiring-manifest.d.ts +5 -1
  56. package/dist/core/wiring-manifest.js +4 -2
  57. package/dist/engine/llm/validation.js +121 -5
  58. package/dist/engine/loop/agent-loop.d.ts +2 -0
  59. package/dist/engine/loop/agent-loop.js +17 -4
  60. package/dist/index.d.ts +7 -2
  61. package/dist/index.js +6 -1
  62. package/dist/prompts/supervisor.d.ts +1 -1
  63. package/dist/prompts/supervisor.js +1 -1
  64. package/dist/stores/file/checkpoint-store.d.ts +1 -0
  65. package/dist/stores/file/checkpoint-store.js +1 -0
  66. package/dist/stores/file/permission-rule-store.d.ts +32 -0
  67. package/dist/stores/file/permission-rule-store.js +213 -0
  68. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  69. package/dist/tools/fs/bash-readonly-classifier.js +11 -10
  70. package/dist/tools/fs/fs-bash.js +17 -10
  71. package/dist/tools/fs/fs-read.d.ts +1 -1
  72. package/dist/tools/fs/fs-read.js +4 -3
  73. package/dist/tools/fs/fs-shared.d.ts +15 -0
  74. package/dist/tools/fs/fs-shared.js +73 -2
  75. package/dist/tools/fs/fs-write.js +8 -8
  76. package/dist/tools/fs/index.d.ts +1 -0
  77. package/dist/tools/fs/index.js +1 -1
  78. package/dist/tools/fs/safety.d.ts +1 -0
  79. package/dist/tools/fs/safety.js +9 -2
  80. package/dist/tools/web.js +2 -0
  81. package/package.json +1 -1
@@ -20,7 +20,9 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
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, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
23
+ import { askApproverIdentity, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
24
+ const PERSISTED_RULE_TOOL = "Bash";
25
+ import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
24
26
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
25
27
  import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
26
28
  import { inlineUntrusted } from "../untrusted-text.js";
@@ -41,6 +43,9 @@ import { compileOutputSchema } from "./strict-output-schema.js";
41
43
  import { TOOL_SEARCH_NAME, buildDeferredRegistry, classifyDeferred, extractDiscoveredToolNames, createPlaceholderTool, createToolSearchTool, staticSchemaRenderable, } from "./tool-disclosure.js";
42
44
  import { createSharedMemoryTools } from "../shared-memory/tools.js";
43
45
  import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
46
+ import { MEMORY_ENGINE_TOOL_NAMES } from "../memory-engine/tools.js";
47
+ import { MEMORY_RECALL_DISCIPLINE } from "../memory-engine/engine.js";
48
+ import { classifyToolContentOrigin, contentOriginPollutes, delegationCallIsExternal } from "../memory-engine/content-origin.js";
44
49
  import { composeMemoryBlock } from "../memory.js";
45
50
  import { preflightLockedConfig } from "../locked-config.js";
46
51
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, complianceCallDenial, resolveComplianceDenies } from "../compliance.js";
@@ -77,7 +82,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
77
82
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
78
83
  import { resolveKey } from "../../tools/fs/safety.js";
79
84
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
80
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, resolveCheckpointStore, } from "../checkpoint-store.js";
85
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
81
86
  import { boundInputHashOf } from "../canonical-json.js";
82
87
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
83
88
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
@@ -168,6 +173,9 @@ export function resolveEnvLifetimeExpiry(env, observedAt) {
168
173
  return { unanchored: true };
169
174
  return { expiresAt: anchor + lifetimeMs };
170
175
  }
176
+ function isLiveApproverSeat(onAsk) {
177
+ return typeof onAsk === "function";
178
+ }
171
179
  const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
172
180
  function sanitizedTtlMs(ttlMs) {
173
181
  if (ttlMs === undefined)
@@ -182,6 +190,12 @@ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
182
190
  export function checkpointScopeOf(spec) {
183
191
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
184
192
  }
193
+ class ParkRefusal extends Error {
194
+ constructor(message, options) {
195
+ super(message, options);
196
+ this.name = "ParkRefusal";
197
+ }
198
+ }
185
199
  export { resolveCheckpointStore } from "../checkpoint-store.js";
186
200
  export function isFableFamilyModelId(id) {
187
201
  const tail = id.toLowerCase().split("/").pop() ?? "";
@@ -1050,7 +1064,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1050
1064
  ...(ownSessionRulesRef.current !== undefined ? [ownSessionRulesRef.current] : []),
1051
1065
  ];
1052
1066
  const ownCallerPolicy = lockedPreflight.toolPolicy;
1053
- const durableMandate = runtimeCaps?.forceDurableGate === true || (spec.durableApproval !== undefined && frozenOnAsk === undefined);
1067
+ const durableMandate = runtimeCaps?.forceDurableGate === true ||
1068
+ (spec.durableApproval !== undefined && !isLiveApproverSeat(frozenOnAsk));
1054
1069
  const parentConstraints = [
1055
1070
  ...(inheritedParentConstraints ?? []),
1056
1071
  ...(ownCallerPolicy !== undefined
@@ -1379,6 +1394,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1379
1394
  }
1380
1395
  tools.push(...mcp.tools.map((t) => remoteToolOffload(t)));
1381
1396
  const rebuildHarnessToolsRef = {};
1397
+ const contentOriginWrapRef = {};
1382
1398
  const toolCallGateArmedRef = { armed: false };
1383
1399
  if (lockedPreflight.mcp?.length) {
1384
1400
  tools.push({
@@ -1595,6 +1611,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1595
1611
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
1596
1612
  ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
1597
1613
  ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
1614
+ ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
1598
1615
  beforeWrite: async (w) => {
1599
1616
  const deploymentGate = deps.hands?.beforeWrite;
1600
1617
  if (deploymentGate) {
@@ -1857,13 +1874,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1857
1874
  .catch(() => undefined);
1858
1875
  }
1859
1876
  : undefined;
1860
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, } = await prepareMemory({
1877
+ const memoryPairNameDomain = new Set();
1878
+ for (const t of tools) {
1879
+ memoryPairNameDomain.add(t.name);
1880
+ for (const alias of t.aliases ?? [])
1881
+ memoryPairNameDomain.add(alias);
1882
+ }
1883
+ const memoryPairExcludedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => (toolFaceSnapshot.exclude ?? []).includes(n));
1884
+ const memoryPairOccupiedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => memoryPairNameDomain.has(n));
1885
+ const memorySearchToolsPlanned = memoryPairExcludedName === undefined && memoryPairOccupiedName === undefined;
1886
+ const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, } = await prepareMemory({
1861
1887
  spec,
1862
1888
  deps,
1863
1889
  sessionId,
1864
1890
  taskRootPath,
1865
1891
  memoryWriteGateRef,
1866
1892
  writeToolsMounted: tools.some((t) => t.name === "Write") && !(toolFaceSnapshot.exclude?.includes("Write") ?? false),
1893
+ memorySearchToolsPlanned,
1867
1894
  admissionCtx: {
1868
1895
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
1869
1896
  complianceDegraded,
@@ -2001,6 +2028,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2001
2028
  sharedMemoryPairMounted = true;
2002
2029
  }
2003
2030
  }
2031
+ let memoryEnginePairMounted = false;
2032
+ if (memoryTools !== undefined && memoryTools.length > 0) {
2033
+ tools.push(...memoryTools.map((s) => defineTool(s)));
2034
+ memoryEnginePairMounted = true;
2035
+ }
2036
+ else if (memoryEngineSession !== undefined && !memorySearchToolsPlanned) {
2037
+ deps.onError?.(new Error(`Memory tools ${MEMORY_ENGINE_TOOL_NAMES.join("/")} were NOT mounted: ` +
2038
+ (memoryPairExcludedName !== undefined
2039
+ ? `excludeTools removes "${memoryPairExcludedName}"`
2040
+ : `this task already declares a tool named "${memoryPairOccupiedName}" (the built-in yields to it)`) +
2041
+ " — the pair mounts together or not at all."), { phase: "config", sessionId, classification: "memory-tools-not-mounted" });
2042
+ }
2004
2043
  const skillsListing = skillSpecs.length > 0
2005
2044
  ? {
2006
2045
  entries: skillSpecs.map((s) => ({
@@ -2205,22 +2244,39 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2205
2244
  }
2206
2245
  return deferredSet;
2207
2246
  };
2208
- const sharedMemoryPair = SHARED_MEMORY_TOOL_NAMES.filter((n) => tools.some((t) => t.name === n));
2247
+ const sharedMemoryPair = sharedMemoryPairMounted ? SHARED_MEMORY_TOOL_NAMES.filter((n) => tools.some((t) => t.name === n)) : [];
2248
+ const memoryEnginePair = memoryEnginePairMounted ? MEMORY_ENGINE_TOOL_NAMES.filter((n) => tools.some((t) => t.name === n)) : [];
2249
+ const builtinDeferPairNames = [...sharedMemoryPair, ...memoryEnginePair];
2209
2250
  let deferred;
2210
- if (sharedMemoryPairMounted && sharedMemoryPair.length > 0) {
2211
- const withoutPair = tools.filter((t) => !sharedMemoryPair.some((n) => n === t.name));
2212
- const d1 = classifyDeferredOverFace(tools, sharedMemoryPair);
2213
- const d0 = classifyDeferredOverFace(withoutPair, []);
2251
+ if (builtinDeferPairNames.length > 0) {
2252
+ const withoutPairs = tools.filter((t) => !builtinDeferPairNames.some((n) => n === t.name));
2253
+ const d1 = classifyDeferredOverFace(tools, builtinDeferPairNames);
2254
+ const d0 = classifyDeferredOverFace(withoutPairs, []);
2214
2255
  const soleCause = d0.size === 0 && d1.size > 0;
2215
2256
  const supportNameTaken = tools.some((t) => t.name === TOOL_SEARCH_NAME || (t.aliases ?? []).includes(TOOL_SEARCH_NAME));
2216
2257
  if (soleCause && supportNameTaken) {
2217
2258
  for (let i = tools.length - 1; i >= 0; i--) {
2218
- if (sharedMemoryPair.some((n) => n === tools[i].name))
2259
+ if (builtinDeferPairNames.some((n) => n === tools[i].name))
2219
2260
  tools.splice(i, 1);
2220
2261
  }
2221
- sharedMemoryPairMounted = false;
2222
- deps.onError?.(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: mounting them would inject ` +
2223
- `the "${TOOL_SEARCH_NAME}" tool, whose name this task already declares the pair mounts together or not at all.`), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
2262
+ if (sharedMemoryPair.length > 0) {
2263
+ sharedMemoryPairMounted = false;
2264
+ deps.onError?.(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: mounting them would inject ` +
2265
+ `the "${TOOL_SEARCH_NAME}" tool, whose name this task already declares — the pair mounts together or not at all.`), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
2266
+ }
2267
+ if (memoryEnginePair.length > 0) {
2268
+ memoryEnginePairMounted = false;
2269
+ if (memoryBlock !== undefined) {
2270
+ if (memoryBlock === MEMORY_RECALL_DISCIPLINE)
2271
+ memoryBlock = undefined;
2272
+ else if (memoryBlock.includes(`\n\n${MEMORY_RECALL_DISCIPLINE}`))
2273
+ memoryBlock = memoryBlock.replace(`\n\n${MEMORY_RECALL_DISCIPLINE}`, "");
2274
+ else if (memoryBlock.startsWith(`${MEMORY_RECALL_DISCIPLINE}\n\n`))
2275
+ memoryBlock = memoryBlock.slice(MEMORY_RECALL_DISCIPLINE.length + 2);
2276
+ }
2277
+ deps.onError?.(new Error(`Memory tools ${MEMORY_ENGINE_TOOL_NAMES.join("/")} were NOT mounted: mounting them would inject ` +
2278
+ `the "${TOOL_SEARCH_NAME}" tool, whose name this task already declares — the pair mounts together or not at all.`), { phase: "config", sessionId, classification: "memory-tools-not-mounted" });
2279
+ }
2224
2280
  deferred = d0;
2225
2281
  }
2226
2282
  else {
@@ -2230,6 +2286,78 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2230
2286
  else {
2231
2287
  deferred = classifyDeferredOverFace(tools, []);
2232
2288
  }
2289
+ if (memoryEngineSession !== undefined) {
2290
+ const { pollution, contentSafety } = memoryEngineSession;
2291
+ const callerToolNames = new Set((spec.tools ?? []).map((t) => t.name));
2292
+ const contentOriginWrapped = new WeakSet();
2293
+ const classOf = (t) => classifyToolContentOrigin({
2294
+ declared: t.contentOrigin,
2295
+ isProtocolTool: protocolOf(t.name) !== undefined,
2296
+ isCallerTool: callerToolNames.has(t.name),
2297
+ trusted: contentSafety.trustedTools.has(t.name),
2298
+ });
2299
+ const delegationByName = new Map();
2300
+ for (const t of spec.tools ?? []) {
2301
+ const withFaces = t;
2302
+ if (withFaces.agentToolFaces === undefined && t.agentListing === undefined)
2303
+ continue;
2304
+ delegationByName.set(t.name, { faces: withFaces.agentToolFaces, pool: withFaces.agentToolPool });
2305
+ }
2306
+ const poolToolPollutes = (t) => contentOriginPollutes(classifyToolContentOrigin({
2307
+ declared: t.contentOrigin,
2308
+ isProtocolTool: protocolOf(t.name) !== undefined,
2309
+ isCallerTool: true,
2310
+ trusted: contentSafety.trustedTools.has(t.name),
2311
+ }), contentSafety.execIsExternalContent);
2312
+ contentOriginWrapRef.current = () => {
2313
+ for (let i = 0; i < tools.length; i++) {
2314
+ const t = tools[i];
2315
+ if (contentOriginWrapped.has(t))
2316
+ continue;
2317
+ const origin = classOf(t);
2318
+ const delegation = delegationByName.get(t.name);
2319
+ const isDelegation = delegation !== undefined;
2320
+ const pollutes = contentOriginPollutes(origin, contentSafety.execIsExternalContent);
2321
+ if (!pollutes && !isDelegation) {
2322
+ contentOriginWrapped.add(t);
2323
+ continue;
2324
+ }
2325
+ const inner = t.execute.bind(t);
2326
+ const mark = (reason) => {
2327
+ try {
2328
+ pollution.markPolluted(reason);
2329
+ }
2330
+ catch {
2331
+ }
2332
+ };
2333
+ const wrapped = {
2334
+ ...t,
2335
+ execute: async (toolCallId, params, signal, onUpdate) => {
2336
+ if (pollutes)
2337
+ mark(`tool "${t.name}" (${origin} content class) was invoked in this session`);
2338
+ if (!isDelegation)
2339
+ return inner(toolCallId, params, signal, onUpdate);
2340
+ const requested = params?.subagent_type;
2341
+ const external = delegationCallIsExternal({
2342
+ requestedType: typeof requested === "string" ? requested : undefined,
2343
+ faces: delegation.faces,
2344
+ pool: delegation.pool,
2345
+ isPolluting: poolToolPollutes,
2346
+ });
2347
+ const result = await inner(toolCallId, params, signal, onUpdate);
2348
+ if (external) {
2349
+ mark(`tool "${t.name}" returned the output of a delegated agent whose tool face can reach external content` +
2350
+ (typeof requested === "string" ? ` (subagent_type "${requested}")` : " (no subagent_type named — classified fail-closed)"));
2351
+ }
2352
+ return result;
2353
+ },
2354
+ };
2355
+ contentOriginWrapped.add(wrapped);
2356
+ tools[i] = wrapped;
2357
+ }
2358
+ };
2359
+ contentOriginWrapRef.current();
2360
+ }
2233
2361
  let centerAdoption;
2234
2362
  let centerAdoptionFresh = false;
2235
2363
  {
@@ -2358,6 +2486,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2358
2486
  .map((s) => ({ name: inlineUntrusted(s.name, 160), ...(s.error !== undefined ? { error: inlineUntrusted(s.error, 240) } : {}) }));
2359
2487
  let toolsDeltaRef;
2360
2488
  let toolMaterializeStatic = false;
2489
+ let deferDirectCall = false;
2361
2490
  const staticFaceForRef = {};
2362
2491
  if (deferred.size > 0 || failedMcpServers.length > 0) {
2363
2492
  toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
@@ -2380,6 +2509,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2380
2509
  const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
2381
2510
  const materializeStatic = requestedStrategy === "static" && !laneDegrade;
2382
2511
  toolMaterializeStatic = materializeStatic;
2512
+ deferDirectCall = spec.deferSelfResolve !== false;
2383
2513
  promptManifest.toolDisclosure = {
2384
2514
  deferredTools: deferred.size,
2385
2515
  strategy: materializeStatic ? "static" : "swap",
@@ -2472,6 +2602,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2472
2602
  const withdrawnTools = new Set();
2473
2603
  const deltaRef = toolsDeltaRef;
2474
2604
  const rematerialize = async (active) => {
2605
+ contentOriginWrapRef.current?.();
2475
2606
  const list = buildToolList(active);
2476
2607
  await harnessRef.current.setTools(list, list.map((t) => t.name));
2477
2608
  if (fpRef.current)
@@ -2515,6 +2646,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2515
2646
  }
2516
2647
  else {
2517
2648
  rebuildHarnessToolsRef.current = async () => {
2649
+ contentOriginWrapRef.current?.();
2518
2650
  const list = [...tools];
2519
2651
  await harnessRef.current.setTools(list, list.map((t) => t.name));
2520
2652
  if (fpRef.current)
@@ -2851,6 +2983,47 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2851
2983
  return {};
2852
2984
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
2853
2985
  };
2986
+ const permissionRuleLane = (() => {
2987
+ const provider = deps.permissionRuleStore;
2988
+ if (provider === undefined)
2989
+ return undefined;
2990
+ const root = taskRootPath;
2991
+ return {
2992
+ admits: async (req) => {
2993
+ if (spec.principal === undefined || spec.principal === "")
2994
+ return undefined;
2995
+ if (req.toolName !== PERSISTED_RULE_TOOL)
2996
+ return undefined;
2997
+ const command = req.args?.command;
2998
+ if (typeof command !== "string")
2999
+ return undefined;
3000
+ let listed;
3001
+ try {
3002
+ listed = await provider.forPrincipal(spec.principal).list();
3003
+ }
3004
+ catch (err) {
3005
+ emitTrace(deps.tracer, () => ({
3006
+ kind: "permission.rule_store_unreadable",
3007
+ version: 1,
3008
+ taskId: spec.taskId ?? sessionId,
3009
+ message: err instanceof Error ? err.message : String(err),
3010
+ ts: Date.now(),
3011
+ }));
3012
+ return undefined;
3013
+ }
3014
+ return findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root })?.rule;
3015
+ },
3016
+ };
3017
+ })();
3018
+ const ruleSuggestionsOf = (toolName, args) => {
3019
+ if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
3020
+ return {};
3021
+ const command = args?.command;
3022
+ if (typeof command !== "string")
3023
+ return {};
3024
+ const suggestions = suggestRulesForCommand(command);
3025
+ return suggestions.length > 0 ? { ruleSuggestions: suggestions } : {};
3026
+ };
2854
3027
  const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal) => {
2855
3028
  let editArgs = edit;
2856
3029
  for (let round = 0;; round++) {
@@ -2881,6 +3054,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2881
3054
  toolName: creq.toolName,
2882
3055
  toolCallId: creq.toolCallId,
2883
3056
  args: editArgs,
3057
+ ...ruleSuggestionsOf(creq.toolName, editArgs),
2884
3058
  message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2885
3059
  ...askSourceIdentity(),
2886
3060
  ...riskAxesOf(creq.toolName),
@@ -2929,6 +3103,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2929
3103
  toolName: creq.toolName,
2930
3104
  toolCallId: creq.toolCallId,
2931
3105
  args: presentedArgs,
3106
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
2932
3107
  message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2933
3108
  ...askSourceIdentity(),
2934
3109
  ...riskAxesOf(creq.toolName),
@@ -2987,6 +3162,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2987
3162
  toolName: creq.toolName,
2988
3163
  toolCallId: creq.toolCallId,
2989
3164
  args: presentedArgs,
3165
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
2990
3166
  message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2991
3167
  ...askSourceIdentity(),
2992
3168
  ...riskAxesOf(creq.toolName),
@@ -3202,6 +3378,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3202
3378
  ...(checkpointStore !== undefined ? { checkpointDurability: resolveDeclaredDurability(checkpointStore, "checkpointStore") } : {}),
3203
3379
  sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
3204
3380
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
3381
+ permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
3205
3382
  hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
3206
3383
  lockedConfigWired: deps.lockedConfig !== undefined,
3207
3384
  complianceWired: deps.compliancePostureResolver !== undefined,
@@ -3290,6 +3467,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3290
3467
  const preview = approvalPreviewOf(req.toolName, req.args);
3291
3468
  return preview !== undefined ? { preview } : {};
3292
3469
  })(),
3470
+ ...ruleSuggestionsOf(req.toolName, req.args),
3293
3471
  message: decision.message ?? `approval required for "${req.toolName}"`,
3294
3472
  ...askSourceIdentity(),
3295
3473
  ...riskAxesOf(req.toolName),
@@ -3403,15 +3581,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3403
3581
  });
3404
3582
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3405
3583
  if (!checkpointStore)
3406
- return false;
3584
+ return { ok: false };
3407
3585
  if (memoryEngineSession)
3408
3586
  await memoryEngineSession.harvest("checkpoint");
3409
3587
  try {
3410
3588
  await checkpointStore.put(token, cp);
3411
- return true;
3589
+ return { ok: true };
3412
3590
  }
3413
3591
  catch (putErr) {
3414
3592
  deps.onError?.(putErr, { phase: "config", sessionId });
3593
+ const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
3415
3594
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
3416
3595
  const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
3417
3596
  abortSignal: abortController.signal,
@@ -3419,7 +3598,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3419
3598
  if (back.ok) {
3420
3599
  const init = await remoteEnv.postResumeInit();
3421
3600
  if (init.ok)
3422
- return false;
3601
+ return { ok: false, reason };
3423
3602
  remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
3424
3603
  deps.onError?.(init.error, { phase: "config", sessionId });
3425
3604
  }
@@ -3429,9 +3608,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3429
3608
  }
3430
3609
  abortController.abort();
3431
3610
  void harness.abort();
3432
- return false;
3611
+ return { ok: false, reason };
3433
3612
  }
3434
- return false;
3613
+ return { ok: false, reason };
3435
3614
  }
3436
3615
  };
3437
3616
  const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
@@ -3519,7 +3698,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3519
3698
  resourceLedger: resourceLedgerOut,
3520
3699
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3521
3700
  };
3522
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3701
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3523
3702
  return false;
3524
3703
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3525
3704
  try {
@@ -3604,7 +3783,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3604
3783
  ...(spec.principal ? { principal: spec.principal } : {}),
3605
3784
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3606
3785
  };
3607
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3786
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3608
3787
  return false;
3609
3788
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3610
3789
  try {
@@ -3700,7 +3879,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3700
3879
  };
3701
3880
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
3702
3881
  ? async (req, postHookArgs, safety, liveFaceUnavailable) => {
3703
- const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : typeof onAsk === "function";
3882
+ const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
3704
3883
  if (syncFirstEligible &&
3705
3884
  runtimeCaps?.forceDurableGate !== true &&
3706
3885
  liveFaceUnavailable !== true &&
@@ -3713,13 +3892,50 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3713
3892
  let remoteHandle;
3714
3893
  const remoteEnv = suspendableEnv;
3715
3894
  try {
3895
+ const parked = tryCloneArgs(postHookArgs);
3896
+ if (!parked.ok) {
3897
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be captured onto an approval checkpoint (unclonable value: ${parked.reason}) — ` +
3898
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: parked.cause });
3899
+ }
3900
+ let backendFidelity;
3901
+ try {
3902
+ backendFidelity = resolveDeclaredFidelity(checkpointStore, "checkpointStore");
3903
+ }
3904
+ catch (err) {
3905
+ throw new ParkRefusal(`the checkpoint backend declares an unrecognized encoding fidelity, so the width of the snapshot this row may hold is unknown (${describeThrown(err)}) — ` +
3906
+ `refusing to park rather than guess which values would survive it`, { cause: err });
3907
+ }
3908
+ const captured = encodeAtFidelity(backendFidelity, parked.value);
3909
+ if (!captured.ok) {
3910
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be encoded by this checkpoint backend (${describeThrown(captured.cause)}) — ` +
3911
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: captured.cause });
3912
+ }
3913
+ const parkedArgs = captured.value;
3914
+ const projectionMovedArgs = backendFidelity !== "structured-clone" && !samePlainValue(parked.value, parkedArgs);
3915
+ if (projectionMovedArgs && basePolicyForResumeEdit !== undefined) {
3916
+ const presented = encodeAtFidelity(backendFidelity, parkedArgs);
3917
+ if (!presented.ok) {
3918
+ throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
3919
+ }
3920
+ const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({ toolName: req.toolName, args: presented.value, toolCallId: req.toolCallId }, abortController.signal));
3921
+ const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
3922
+ const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
3923
+ !(demanded?.ok === true &&
3924
+ samePlainValue(demanded.value, parkedArgs) &&
3925
+ samePlainValue(reprojected.updatedInput, parkedArgs));
3926
+ if (reprojected.action === "deny" || rewroteTheFiledValue) {
3927
+ throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and the deployment's tool policy does not accept the stored shape` +
3928
+ `${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
3929
+ `refusing to file a row whose executable form nothing adjudicated`);
3930
+ }
3931
+ }
3716
3932
  if (offloadStore && isVolatileOffloadStore(offloadStore) && !offloadStore.isEmpty()) {
3717
- throw new Error("durable suspend requires a durable toolResultStore (the default InMemoryToolResultStore " +
3933
+ throw new ParkRefusal("durable suspend requires a durable toolResultStore (the default InMemoryToolResultStore " +
3718
3934
  "holds offloaded results that are lost across a cross-replica resume); inject " +
3719
3935
  "RunnerDeps.toolResultStore or disable offload.");
3720
3936
  }
3721
3937
  if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
3722
- throw new Error(incompleteSuspendAdapter !== undefined
3938
+ throw new ParkRefusal(incompleteSuspendAdapter !== undefined
3723
3939
  ? `durable suspend is not supported with this per-task executionEnvFactory env: it declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so a snapshot taken now could never be restored. Implement the full RemoteExecutionEnv restore surface, or declare capabilities.suspendable:false if the workspace is externally durable (the park-only lane).`
3724
3940
  : "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
3725
3941
  "minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
@@ -3728,7 +3944,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3728
3944
  }
3729
3945
  const leafId = await session.getLeafId();
3730
3946
  if (!leafId) {
3731
- throw new Error("cannot suspend: session has no committed leaf to resume from");
3947
+ throw new ParkRefusal("cannot suspend: session has no committed leaf to resume from");
3732
3948
  }
3733
3949
  const { messages } = await session.buildContext();
3734
3950
  const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
@@ -3755,7 +3971,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3755
3971
  token = mintCheckpointToken();
3756
3972
  const riskDescriptor = buildRiskDescriptor({
3757
3973
  toolName: req.toolName,
3758
- args: postHookArgs,
3974
+ args: parkedArgs,
3759
3975
  safety,
3760
3976
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
3761
3977
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
@@ -3798,12 +4014,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3798
4014
  kind: "tool_approval",
3799
4015
  toolCallId: req.toolCallId,
3800
4016
  toolName: req.toolName,
3801
- args: postHookArgs,
4017
+ args: parkedArgs,
3802
4018
  ...(() => {
3803
- const preview = approvalPreviewOf(req.toolName, postHookArgs);
4019
+ const preview = approvalPreviewOf(req.toolName, parkedArgs);
3804
4020
  return preview !== undefined ? { preview } : {};
3805
4021
  })(),
3806
- boundInputHash: boundInputHashOf(postHookArgs),
4022
+ ...ruleSuggestionsOf(req.toolName, parkedArgs),
4023
+ boundInputHash: boundInputHashOf(parkedArgs),
3807
4024
  batchToolCallIds,
3808
4025
  completedCallIds,
3809
4026
  },
@@ -3828,10 +4045,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3828
4045
  }
3829
4046
  catch (err) {
3830
4047
  deps.onError?.(err, { phase: "config", sessionId });
3831
- return undefined;
4048
+ return {
4049
+ parkFailed: err instanceof ParkRefusal
4050
+ ? err.message
4051
+ : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
4052
+ };
3832
4053
  }
3833
- if (!(await commitSuspendSaga(token, cp, remoteEnv, remoteHandle))) {
3834
- return undefined;
4054
+ const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
4055
+ if (!committed.ok) {
4056
+ return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
3835
4057
  }
3836
4058
  publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
3837
4059
  try {
@@ -3905,6 +4127,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3905
4127
  onHookError: notifyHookError,
3906
4128
  shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
3907
4129
  ...(autoModeDecider ? { autoMode: { decider: autoModeDecider } } : {}),
4130
+ ...(permissionRuleLane
4131
+ ? {
4132
+ persistedRules: {
4133
+ admits: permissionRuleLane.admits,
4134
+ onResolved: (info) => emitTrace(deps.tracer, () => ({
4135
+ kind: "permission.persisted_rule_allowed",
4136
+ version: 1,
4137
+ taskId: spec.taskId ?? sessionId,
4138
+ toolName: info.toolName,
4139
+ toolCallId: info.toolCallId,
4140
+ rule: info.rule,
4141
+ ts: Date.now(),
4142
+ })),
4143
+ },
4144
+ }
4145
+ : {}),
3908
4146
  isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
3909
4147
  });
3910
4148
  }
@@ -4206,7 +4444,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4206
4444
  : undefined;
4207
4445
  overheadState.promptChars = systemPrompt.length;
4208
4446
  const preparedHolder = {};
4209
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, ...(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, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4447
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), 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, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4210
4448
  const prepared = buildPrepared();
4211
4449
  preparedHolder.current = prepared;
4212
4450
  return prepared;
@@ -328,6 +328,24 @@ function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
328
328
  e.retryAfterMs = retryAfterMs;
329
329
  return e;
330
330
  }
331
+ const disclosedUnevaluableWindow = new WeakSet();
332
+ function discloseUnevaluableWindow(u, prepared, tracer, taskId, onError) {
333
+ if (disclosedUnevaluableWindow.has(prepared))
334
+ return;
335
+ disclosedUnevaluableWindow.add(prepared);
336
+ emitTrace(tracer, () => ({ kind: "compaction.unevaluable", version: 1, taskId, estTokens: u.estTokens, ts: Date.now() }));
337
+ const modelClause = u.modelId !== undefined ? ` "${u.modelId}"` : "";
338
+ const cause = u.windowValue !== undefined
339
+ ? `its \`${u.windowField}\` is ${u.windowValue}, which is not a positive number of tokens — that field takes precedence over the ` +
340
+ `others, so fixing or removing it is what restores the threshold`
341
+ : `it declares no context window at all (\`autoCompactTokens\` / \`contextTokens\` / \`contextWindow\` are all absent) — declare one to restore the threshold`;
342
+ try {
343
+ onError?.(new Error(`auto-compaction cannot run for model${modelClause}: ${cause}. Until then there is no threshold to compare against, so the ` +
344
+ `context will grow until the provider refuses the request. The conversation is already about ${u.estTokens} estimated tokens.`), { phase: "config", sessionId: prepared.sessionId });
345
+ }
346
+ catch {
347
+ }
348
+ }
331
349
  function makeTurnBoundary(prepared, stats, rs, deps) {
332
350
  const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, walltimeMonotonicDeadline, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
333
351
  let finalVerifySeenAtLastBoundary = 0;
@@ -577,7 +595,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
577
595
  skillsListing: rs.attach.skillsListingOn,
578
596
  mcpInstructions: mcpInstructionsOn,
579
597
  },
580
- ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0 ? { undiscoveredTools } : {}),
598
+ ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0
599
+ ? { undiscoveredTools, deferDirectCall: prepared.deferDirectCall }
600
+ : {}),
581
601
  ...(changed !== undefined ? { changedFiles: changed } : {}),
582
602
  ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
583
603
  ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
@@ -851,6 +871,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
851
871
  if (comp.contextUsage) {
852
872
  queue.push({ type: "context_usage", ...comp.contextUsage, ...ident() });
853
873
  }
874
+ if (comp.unevaluableWindow) {
875
+ discloseUnevaluableWindow(comp.unevaluableWindow, prepared, rs.telemetry.tracer, rs.telemetry.taskId, runnerHooks.onError);
876
+ }
854
877
  if (comp.suppressedByFloor) {
855
878
  const s = comp.suppressedByFloor;
856
879
  emitTrace(rs.telemetry.tracer, () => ({
@@ -3967,6 +3990,9 @@ export class Runner {
3967
3990
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
3968
3991
  });
3969
3992
  this.recordCompactionReuse(prepared, finishComp);
3993
+ if (finishComp.unevaluableWindow) {
3994
+ discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId, this.deps.onError);
3995
+ }
3970
3996
  comp = finishComp;
3971
3997
  }
3972
3998
  catch (err) {