@sema-agent/core 5.32.0 → 5.33.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 (41) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/agents/cascade.d.ts +49 -1
  3. package/dist/agents/cascade.js +2 -2
  4. package/dist/agents/verify.d.ts +70 -4
  5. package/dist/agents/verify.js +62 -16
  6. package/dist/core/checkpoint-store.d.ts +95 -0
  7. package/dist/core/checkpoint-store.js +40 -0
  8. package/dist/core/hooks.d.ts +14 -6
  9. package/dist/core/hooks.js +14 -3
  10. package/dist/core/memory-engine/file-backend.d.ts +172 -22
  11. package/dist/core/memory-engine/file-backend.js +877 -79
  12. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  13. package/dist/core/runner/assemble-result.d.ts +7 -0
  14. package/dist/core/runner/assemble-result.js +1 -1
  15. package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
  17. package/dist/core/runner/prepare-config-doors.d.ts +140 -0
  18. package/dist/core/runner/prepare-config-doors.js +250 -0
  19. package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
  20. package/dist/core/runner/prepare-safety-scan.js +80 -0
  21. package/dist/core/runner/prepare-task.d.ts +27 -81
  22. package/dist/core/runner/prepare-task.js +83 -586
  23. package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
  24. package/dist/core/runner/prepare-workspace-restore.js +144 -0
  25. package/dist/core/runner/runtask.js +8 -2
  26. package/dist/core/tool-policy.d.ts +25 -0
  27. package/dist/core/types.d.ts +140 -13
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +1 -1
  30. package/dist/orchestration/workflow-governance.d.ts +6 -4
  31. package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
  32. package/dist/tools/fs/bash-readonly-classifier.js +4 -1
  33. package/dist/tools/fs/fs-bash.d.ts +19 -3
  34. package/dist/tools/fs/fs-bash.js +26 -1
  35. package/dist/tools/fs/index.d.ts +20 -4
  36. package/dist/tools/fs/index.js +4 -1
  37. package/dist/tools/fs/read-deny.d.ts +66 -8
  38. package/dist/tools/fs/read-deny.js +75 -39
  39. package/dist/tools/fs/read-face.d.ts +3 -2
  40. package/dist/tools/fs/search.js +2 -0
  41. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { sanitizeCompactionSettings } from "../auto-compaction.js";
7
7
  import { projectStaleToolResults, resolveStaleToolResultOffload } from "./compaction-call-options.js";
8
8
  import { createAutoModeDecider } from "../auto-mode.js";
9
9
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
10
- import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
10
+ import { resolveTaskModel } from "../roles.js";
11
11
  import { primaryActivityArg } from "../arg-summary.js";
12
12
  import { materializeMcpTools } from "../mcp.js";
13
13
  import { materializeA2aTools } from "../a2a.js";
@@ -15,7 +15,7 @@ import { Type } from "typebox";
15
15
  import { Value } from "typebox/value";
16
16
  import { uuidv7 } from "../../engine/session/uuid.js";
17
17
  import { brainToRuntime } from "../runtime.js";
18
- import { StoredSession, isSessionConflict, hasSessionFork } from "../session.js";
18
+ import { hasSessionFork } from "../session.js";
19
19
  import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents/subagent.js";
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";
@@ -31,13 +31,12 @@ import { emitTrace } from "../trace.js";
31
31
  import { createSessionRulePolicy } from "./session-rule-policy.js";
32
32
  import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
33
33
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
34
- import { reconcileInterruptedSession } from "../session-reconcile.js";
35
34
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
36
35
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
37
- import { resolveBrainCallGuardrailMs, STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGuardrail } from "../../brain/timeout.js";
36
+ import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGuardrail } from "../../brain/timeout.js";
38
37
  import { defineTool, isDefineToolProduct } from "../tools.js";
39
38
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
40
- import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
39
+ import { protocolOf } from "../protocol-table.js";
41
40
  import { pathToUri } from "../lsp-protocol.js";
42
41
  import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
43
42
  import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
@@ -50,11 +49,13 @@ import { MEMORY_RECALL_DISCIPLINE } from "../memory-engine/engine.js";
50
49
  import { classifyToolContentOrigin, contentOriginPollutes, delegationCallIsExternal } from "../memory-engine/content-origin.js";
51
50
  import { narrowContentSafety, readCardAttestation } from "../memory-engine/delegation-provenance.js";
52
51
  import { composeMemoryBlock } from "../memory.js";
53
- import { preflightLockedConfig } from "../locked-config.js";
54
52
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, complianceCallDenial, resolveComplianceDenies } from "../compliance.js";
55
- import { assertRetentionCapability } from "../retention.js";
56
53
  import { foldAdmissionFreeze } from "../memory-admission.js";
57
54
  import { prepareMemory } from "./prepare-memory.js";
55
+ import { limitConfigError, prepareConfigDoors } from "./prepare-config-doors.js";
56
+ import { NAMESPACED_NAME_SHAPES, prepareSafetyScan } from "./prepare-safety-scan.js";
57
+ import { prepareAcquireReconcile } from "./prepare-acquire-reconcile.js";
58
+ import { prepareWorkspaceRestore, rebaseWorkspacePath, remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./prepare-workspace-restore.js";
58
59
  import { defaultPromptProvider, buildEnvironmentContext, buildGitSnapshot, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
59
60
  import { assemblePrompt } from "../../prompt-assembly/assemble.js";
60
61
  import { auditToolCollisions, getToolContract, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
@@ -67,18 +68,17 @@ import { capAggregateToolResults } from "../tool-result-budget.js";
67
68
  import { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "../media-byte-cap.js";
68
69
  import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
69
70
  import { StubExecutionEnv } from "../stub-env.js";
70
- import { hasDestroy, isIsolated, isRemoteExecutionEnv, isRetryableRemoteErrorCode, isSuspendable, missingRestoreSurface, RETRYABLE_REMOTE_ERROR_CODES } from "../remote-env.js";
71
- import { withRetry } from "../with-retry.js";
71
+ import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable, missingRestoreSurface } from "../remote-env.js";
72
72
  import { settleTeardownLeg } from "./teardown-bounded.js";
73
73
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
74
74
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
75
75
  import { createMonitorTool } from "../../tools/monitor.js";
76
76
  import { createWorktreeTools } from "../../tools/worktree.js";
77
- import { applyCompactionToReadFileState, assertReadFaceValue, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, isReadDedupStubResult, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
77
+ import { applyCompactionToReadFileState, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, isReadDedupStubResult, resolveReadDenyBuiltins, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
78
78
  import { decodeTextBytes } from "../../tools/fs/encoding.js";
79
79
  import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, classifyQuestionOutcome, isLiveQuestionFace, validateAskQuestions, } from "../ask-question.js";
80
80
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
81
- import { createPresentPlanTool, createEnterPlanModeTool, PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
81
+ import { createPresentPlanTool, createEnterPlanModeTool, PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
82
82
  import { isSelfOrchestrationActive, selfOrchestrationFailClosedReason } from "../../orchestration/workflow-script-runner.js";
83
83
  import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
84
84
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
@@ -87,8 +87,8 @@ import { resolveKey } from "../../tools/fs/safety.js";
87
87
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
88
88
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
- import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
- import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
90
+ import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
+ import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
92
92
  import { deliverEngineNotice } from "../types.js";
93
93
  const announcedMaterializeEnv = new Set();
94
94
  export function __resetMaterializeEnvAnnouncements() {
@@ -102,74 +102,7 @@ let readFaceClampConsoleAnnounced = false;
102
102
  export function __resetReadFaceClampAnnouncement() {
103
103
  readFaceClampConsoleAnnounced = false;
104
104
  }
105
- const RECONCILE_MAX_RETRIES = 3;
106
105
  const DEFAULT_MAX_SUSPENDS = 5;
107
- const TASK_LIMIT_KEY_DICT = {
108
- maxTokens: true,
109
- maxCostUsd: true,
110
- maxTurns: true,
111
- maxWalltimeMs: true,
112
- maxOutputTokens: true,
113
- approachNotice: true,
114
- budgetStreamCancel: true,
115
- degrade: true,
116
- brainCallGuardrailMs: true,
117
- };
118
- const TASK_LIMIT_KEYS = Object.keys(TASK_LIMIT_KEY_DICT);
119
- const NUMERIC_TASK_LIMIT_KEYS = ["maxTokens", "maxCostUsd", "maxTurns", "maxWalltimeMs", "maxOutputTokens"];
120
- const RETIRED_TASK_LIMIT_KEYS = {
121
- timeoutSec: "maxWalltimeMs (milliseconds, not seconds)",
122
- deadlineNudge: "approachNotice",
123
- callCapByDeadline: "(retired — no replacement)",
124
- gracefulFinalize: "(retired — the approach notice is the only end-of-budget prompt)",
125
- };
126
- function limitConfigError(code, message) {
127
- const e = new Error(message);
128
- e.code = code;
129
- return e;
130
- }
131
- export function resolveTaskLimits(limits) {
132
- if (limits === undefined)
133
- return undefined;
134
- if (typeof limits !== "object" || Array.isArray(limits)) {
135
- throw limitConfigError("config.limit_invalid", `TaskSpec.limits must be an object (got ${Array.isArray(limits) ? "an array" : typeof limits})`);
136
- }
137
- const raw = limits;
138
- const legal = TASK_LIMIT_KEYS;
139
- for (const key of Object.keys(raw)) {
140
- if (legal.includes(key))
141
- continue;
142
- const replacement = RETIRED_TASK_LIMIT_KEYS[key];
143
- throw limitConfigError("config.limit_unknown_key", `TaskSpec.limits.${key} is not a limit this engine reads` +
144
- (replacement !== undefined ? ` — it was replaced by \`${replacement}\`` : "") +
145
- `. Legal keys: ${TASK_LIMIT_KEYS.join(", ")}. Refused rather than ignored: a limit that is silently dropped reads to the caller as an armed ceiling.`);
146
- }
147
- for (const key of NUMERIC_TASK_LIMIT_KEYS) {
148
- const value = raw[key];
149
- if (value === undefined)
150
- continue;
151
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
152
- throw limitConfigError("config.limit_invalid", `TaskSpec.limits.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and folding it to a default would run the task under a limit nobody chose.`);
153
- }
154
- }
155
- const notice = raw.approachNotice;
156
- if (notice !== undefined && notice !== false) {
157
- if (typeof notice !== "object" || notice === null || Array.isArray(notice)) {
158
- throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice must be \`false\` or { at?: [first, second] } (got ${String(notice)})`);
159
- }
160
- const at = notice.at;
161
- if (at !== undefined) {
162
- const ok = Array.isArray(at) &&
163
- at.length === 2 &&
164
- at.every((n) => typeof n === "number" && Number.isFinite(n) && n > 0 && n <= 1) &&
165
- at[0] <= at[1];
166
- if (!ok) {
167
- throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice.at must be two fractions in (0, 1] with first <= second (got ${JSON.stringify(at)})`);
168
- }
169
- }
170
- }
171
- return limits;
172
- }
173
106
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
174
107
  const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
175
108
  export const ENV_LIFETIME_SUSPEND_MARGIN_MS = 60_000;
@@ -202,7 +135,6 @@ function sanitizedTtlMs(ttlMs) {
202
135
  const ungatedWarnedShapes = new WeakMap();
203
136
  const advisedPolicyNames = new WeakMap();
204
137
  const ADVISED_KEYS_CAP = 64;
205
- const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
206
138
  export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
207
139
  export function checkpointScopeOf(spec) {
208
140
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
@@ -214,16 +146,8 @@ class ParkRefusal extends Error {
214
146
  }
215
147
  }
216
148
  export { resolveCheckpointStore } from "../checkpoint-store.js";
217
- export function isFableFamilyModelId(id) {
218
- const tail = id.toLowerCase().split("/").pop() ?? "";
219
- return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail);
220
- }
221
- export function resolveModelPromptTraits(model, spec, internals) {
222
- return {
223
- promptProfile: spec.promptProfile ?? internals?.promptProfile ?? "simple",
224
- fableMitigations: isFableFamilyModelId(model.id),
225
- };
226
- }
149
+ export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
150
+ export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
227
151
  function hasConversationContent(branch) {
228
152
  return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
229
153
  }
@@ -243,31 +167,6 @@ export function batchContextAt(messages, currentId) {
243
167
  const completedCallIds = batch.filter((id) => id !== currentId && resolved.has(id));
244
168
  return { batchToolCallIds: batch, completedCallIds };
245
169
  }
246
- function detectConflicts(storage, ref) {
247
- const guard = (fn) => async (...args) => {
248
- try {
249
- return await fn(...args);
250
- }
251
- catch (err) {
252
- if (isSessionConflict(err)) {
253
- ref.hit = true;
254
- }
255
- throw err;
256
- }
257
- };
258
- return new Proxy(storage, {
259
- get(target, prop, receiver) {
260
- const value = Reflect.get(target, prop, receiver);
261
- if (typeof value !== "function") {
262
- return value;
263
- }
264
- const bound = value.bind(target);
265
- return prop === "appendEntry" || prop === "setLeafId"
266
- ? guard(bound)
267
- : bound;
268
- },
269
- });
270
- }
271
170
  function raceAbort(p, signal, onAbort) {
272
171
  if (signal.aborted)
273
172
  return Promise.resolve(onAbort());
@@ -293,353 +192,19 @@ async function forgetQuietly(sessions, sessionId) {
293
192
  catch {
294
193
  }
295
194
  }
296
- export function rebaseWorkspacePath(p, fromRaw, toRaw) {
297
- if (p.includes("\\") || fromRaw.includes("\\") || toRaw.includes("\\"))
298
- return p;
299
- const stripTrail = (s) => (s.length > 1 && s.endsWith("/") ? stripTrail(s.slice(0, -1)) : s);
300
- const from = stripTrail(fromRaw);
301
- const to = stripTrail(toRaw);
302
- if (p === from || stripTrail(p) === from)
303
- return to;
304
- const fromPrefix = from === "/" ? from : `${from}/`;
305
- if (!p.startsWith(fromPrefix))
306
- return p;
307
- const suffix = p.slice(fromPrefix.length).replace(/^\/+/, "");
308
- return to === "/" ? `${to}${suffix}` : `${to}/${suffix}`;
309
- }
310
- function remoteEnvFailureNote(op, error, attempts) {
311
- return { op, code: error.code, retryable: isRetryableRemoteErrorCode(error.code), attempts, message: error.message };
312
- }
313
- const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
314
- const REMOTE_RESTORE_BACKOFF_MS = 200;
315
- async function restoreWorkspaceWithRetry(env, snapshotId, options) {
316
- let attempts = 0;
317
- const outcome = await withRetry(async (attempt) => {
318
- attempts = attempt;
319
- return env.resumeVM(snapshotId, options);
320
- }, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
321
- return { outcome, attempts };
322
- }
323
- export function rebaseWorkspacePathAcross(p, froms, to) {
324
- for (const from of froms) {
325
- const out = rebaseWorkspacePath(p, from, to);
326
- if (out !== p)
327
- return out;
328
- }
329
- return p;
330
- }
331
195
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
332
- const toolFaceSnapshot = {
333
- exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
334
- defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
335
- alwaysLoad: spec.alwaysLoadTools ? Object.freeze([...spec.alwaysLoadTools]) : undefined,
336
- };
337
- const promptProfile = resolveModelPromptTraits({ id: "" }, spec, internals).promptProfile;
338
- if (spec.resumeAt !== undefined) {
339
- if (resume) {
340
- const e = new Error("resumeAt cannot be combined with a durable resume");
341
- e.code = "resume_at.conflicts_resume";
342
- throw e;
343
- }
344
- if (!spec.sessionId) {
345
- const e = new Error("resumeAt requires a sessionId (the session to branch)");
346
- e.code = "resume_at.no_session";
347
- throw e;
348
- }
349
- }
350
- assertReadFaceValue(spec.readFace, "TaskSpec.readFace");
351
- assertReadFaceValue(deps.readFace, "readFace (deployment seat)");
352
- if (spec.resumeAtMode !== undefined) {
353
- if (spec.resumeAt === undefined) {
354
- const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
355
- e.code = "resume_at.mode_without_target";
356
- throw e;
357
- }
358
- if (spec.resumeAtMode !== "at" && spec.resumeAtMode !== "before") {
359
- const e = new Error(`resumeAtMode "${String(spec.resumeAtMode)}" is invalid — expected "at" (inclusive branch, the default) or "before" (exclusive branch)`);
360
- e.code = "resume_at.invalid_mode";
361
- throw e;
362
- }
363
- }
364
- if (spec.toolMaterializeStrategy !== undefined && spec.toolMaterializeStrategy !== "swap" && spec.toolMaterializeStrategy !== "static") {
365
- const e = new Error(`toolMaterializeStrategy must be "swap" or "static" (got ${JSON.stringify(spec.toolMaterializeStrategy)}).`);
366
- e.code = "config.tool_materialize_invalid";
367
- throw e;
368
- }
369
- if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
370
- const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
371
- e.code = "config.memory_persistence_invalid";
372
- throw e;
373
- }
374
- if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
375
- const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
376
- `lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
377
- `called. Use "swap", or leave deferSelfResolve on.`);
378
- e.code = "config.tool_materialize_unreachable";
379
- throw e;
380
- }
381
- if (resume === undefined && spec.objective.trim().length === 0) {
382
- const e = new Error("TaskSpec.objective is empty — a task needs an instruction (an empty user message is rejected by strict model endpoints and would fail every later request of the session).");
383
- e.code = "config.empty_objective";
384
- throw e;
385
- }
386
- const lockedPreflight = preflightLockedConfig(spec, deps);
387
- assertRetentionCapability({
388
- policy: deps.retentionPolicy,
389
- locked: lockedPreflight.lockedKeys.has("retentionPolicy"),
390
- stores: [
391
- { name: "sessionStore", store: sessions },
392
- { name: "checkpointStore", store: resolveCheckpointStore(spec, deps) },
393
- { name: "toolResultStore", store: deps.toolResultStore },
394
- ],
395
- });
396
- const resolvedInteractionPosture = spec.interactionPosture ?? internals?.parentInteractionPosture ?? deps.interactionPosture;
397
- {
398
- const interactionPosture = resolvedInteractionPosture;
399
- const discloseInteractionPostureRefusal = (err) => {
400
- try {
401
- deps.onError?.(err, { phase: "config", sessionId: spec.sessionId ?? "(pre-session)", classification: "interaction-posture-refused" });
402
- }
403
- catch {
404
- }
405
- };
406
- if (interactionPosture !== undefined && interactionPosture !== "interactive" && interactionPosture !== "headless") {
407
- const e = new Error(`interactionPosture ${JSON.stringify(interactionPosture)} is not a recognized posture ("interactive" | "headless") — ` +
408
- `an unevaluable declaration is refused loudly, never folded to either posture.`);
409
- e.code = "config.interaction_posture";
410
- discloseInteractionPostureRefusal(e);
411
- throw e;
412
- }
413
- if (interactionPosture === "interactive") {
414
- const askDoor = resolveAskSeamForm(spec, deps);
415
- const humanReachable = deriveAskEffective(askDoor.form, "unresolved") === "human_reachable";
416
- const questionDoor = resolveQuestionSeam(spec, deps);
417
- const strippedByEngine = internals?.questionFaceStripped === true;
418
- if (spec.interactiveTools === false) {
419
- const e = new Error(`interaction posture "interactive" declared together with interactiveTools: false — the hard-headless ` +
420
- `clamp removes the AskUserQuestion mount, so no content question can ever reach the human this posture ` +
421
- `promises. Drop one of the two declarations.`);
422
- e.code = "config.interaction_posture";
423
- discloseInteractionPostureRefusal(e);
424
- throw e;
425
- }
426
- if (!humanReachable || !(questionDoor.wired || strippedByEngine)) {
427
- const missing = [];
428
- if (!humanReachable) {
429
- missing.push(askDoor.form === "absent"
430
- ? "no onAsk approver is wired (spec.onAsk ?? deps.onAsk is absent — asks would auto-deny or park)"
431
- : `the resolved onAsk seat is the blanket policy "${askDoor.form}" (${askDoor.provenance ?? "?"}) — a policy setting is not a reachable human`);
432
- }
433
- if (!questionDoor.wired && !strippedByEngine) {
434
- missing.push("no content-question channel (spec.onQuestion ?? deps.onQuestion is absent, and this leg is not an engine-stripped background lane)");
435
- }
436
- const e = new Error(`interaction posture "interactive" declared, but this assembly cannot reach a human: ${missing.join("; ")}. ` +
437
- `Wire the missing seam(s), or drop the posture declaration (absent = no check).`);
438
- e.code = "config.interaction_posture";
439
- discloseInteractionPostureRefusal(e);
440
- throw e;
441
- }
442
- }
443
- }
444
- resolveTaskLimits(spec.limits);
445
- if (spec.resourceSuspend !== undefined) {
446
- const rsus = spec.resourceSuspend;
447
- if (typeof rsus.scope !== "string" || rsus.scope === "") {
448
- throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.scope must be a non-empty string (got ${String(rsus.scope)}) — it is the multi-tenant isolation key every resource checkpoint is filed under.`);
449
- }
450
- for (const key of ["totalBudgetUsd", "totalTokens", "maxSlices", "ttlMs"]) {
451
- const value = rsus[key];
452
- if (value === undefined)
453
- continue;
454
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
455
- throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable allocation is not an allocation, and a NaN here blinds even the validated per-slice window.`);
456
- }
457
- }
458
- }
459
- const usageWindows = resolveUsageWindows(deps.usageWindows);
460
- const brainCallGuardrailRef = {};
461
- const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
462
- if (spec.agents !== undefined && spec.agents.length > 0) {
463
- const pool = spec.tools ?? [];
464
- if (!pool.some((t) => typeof t.withAgents === "function")) {
465
- const e = new Error(`TaskSpec.agents was provided but no delegation tool (createSubagentTool) is mounted in spec.tools — the per-task agents could never be offered. Mount the delegation tool, or drop spec.agents.`);
466
- e.code = "config.agents.no_delegation_tool";
467
- throw e;
468
- }
469
- const perTask = spec.agents;
470
- spec = { ...spec, tools: pool.map((t) => (typeof t.withAgents === "function" ? t.withAgents(perTask) : t)) };
471
- }
472
- const resolvedRole = resolveTaskModel(spec, deps);
473
- const model = resolvedRole.model;
474
- const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations;
475
- const thinking = spec.thinking ?? resolvedRole.thinking ?? model.defaultThinking;
476
- const compModel = spec.compactionModel
477
- ? resolveModel(spec.compactionModel, deps.models)
478
- : roleModelIfSet("summarize", spec, deps);
479
- const toolEffects = new Map();
480
- const ownToolNames = new Set((spec.tools ?? []).map((t) => t.name));
481
- const claimedAliases = new Set();
482
- const egressTools = new Set();
483
- const irreversibleTools = new Set();
484
- const irreversibilityTier = new Map();
485
- const axisExplicitNegatives = new Map();
486
- const reversibilityProbes = new Map();
196
+ const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
197
+ spec = doors.spec;
198
+ const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
199
+ const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
487
200
  let shellGatedBash = false;
488
201
  let shellGatedMonitor = false;
489
- for (const t of spec.tools ?? []) {
490
- if (t.name.includes("__")) {
491
- const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`);
492
- e.code = "config.tool_name_invalid";
493
- throw e;
494
- }
495
- if (t.effect) {
496
- toolEffects.set(t.name, t.effect);
497
- }
498
- const tier = t.irreversibility ?? (t.reversibilityProbe ? "maybe" : undefined);
499
- if (tier !== undefined) {
500
- irreversibilityTier.set(t.name, tier);
501
- if (tier === "always" || tier === "maybe")
502
- irreversibleTools.add(t.name);
503
- }
504
- if (t.reversibilityProbe)
505
- reversibilityProbes.set(t.name, t.reversibilityProbe);
506
- if (t.egress) {
507
- if (t.effect !== undefined && t.effect !== "write") {
508
- const e = new Error(`Tool "${t.name}" declares egress:true with effect:"${t.effect}" — an egress tool (external write) must have effect:"write" (or omit effect; write is the default).`);
509
- e.code = "config.egress_requires_write_effect";
510
- throw e;
511
- }
512
- egressTools.add(t.name);
513
- }
514
- for (const alias of t.aliases ?? []) {
515
- if (alias === t.name || ownToolNames.has(alias))
516
- continue;
517
- if (claimedAliases.has(alias))
518
- continue;
519
- claimedAliases.add(alias);
520
- if (t.effect)
521
- toolEffects.set(alias, t.effect);
522
- if (tier !== undefined) {
523
- irreversibilityTier.set(alias, tier);
524
- if (tier === "always" || tier === "maybe")
525
- irreversibleTools.add(alias);
526
- }
527
- if (t.reversibilityProbe)
528
- reversibilityProbes.set(alias, t.reversibilityProbe);
529
- if (t.egress)
530
- egressTools.add(alias);
531
- }
532
- }
533
- toolEffects.set(OFFLOAD_TOOL_NAME, "read");
534
- if (deps.executionEnvFactory || deps.executionEnv) {
535
- for (const [name, effect] of Object.entries(HAND_TOOL_EFFECTS))
536
- toolEffects.set(name, effect);
537
- if (spec.handsReadOnly === true)
538
- toolEffects.set("Bash", "read");
539
- }
540
- if (spec.enablePlanMode === true) {
541
- toolEffects.set(PRESENT_PLAN_TOOL_NAME, "read");
542
- toolEffects.set(ENTER_PLAN_MODE_TOOL_NAME, "read");
543
- }
544
202
  if (spec.requireExistingSession && !spec.sessionId) {
545
203
  const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
546
204
  e.code = "resume.session_not_found";
547
205
  throw e;
548
206
  }
549
- let acquired;
550
- let session;
551
- let conflictRef;
552
- let wakeRecovered = [];
553
- let resumeAtBeforeParentId = null;
554
- for (let attempt = 0;; attempt++) {
555
- try {
556
- acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : undefined);
557
- }
558
- catch (err) {
559
- if (spec.requireExistingSession && err?.code === "not_found") {
560
- const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)`);
561
- e.code = "resume.session_not_found";
562
- throw e;
563
- }
564
- throw err;
565
- }
566
- conflictRef = { hit: false };
567
- session = new StoredSession(detectConflicts(acquired.session.getStorage(), conflictRef));
568
- if (!spec.sessionId) {
569
- break;
570
- }
571
- try {
572
- if (resume) {
573
- await session.getStorage().setLeafId(resume.leafId);
574
- }
575
- else if (spec.resumeAt !== undefined) {
576
- const entry = await session.getEntry(spec.resumeAt);
577
- if (!entry) {
578
- const e = new Error(`resumeAt entry "${spec.resumeAt}" not found in session "${spec.sessionId}"`);
579
- e.code = "resume_at.not_found";
580
- throw e;
581
- }
582
- if (entry.type !== "message" && entry.type !== "custom_message") {
583
- const e = new Error(`resumeAt entry "${spec.resumeAt}" is a "${entry.type}" entry; resume-at requires a message boundary`);
584
- e.code = "resume_at.not_a_message";
585
- throw e;
586
- }
587
- if (entry.type === "message") {
588
- const msg = entry.message;
589
- if (msg?.role === "toolResult") {
590
- const e = new Error(`resumeAt entry "${spec.resumeAt}" is a tool-result (mid-turn); resume-at requires a settled turn boundary (a user message or a finished assistant turn)`);
591
- e.code = "resume_at.not_a_message";
592
- throw e;
593
- }
594
- if (msg?.role === "assistant" && Array.isArray(msg.content) && msg.content.some((p) => p?.type === "toolCall")) {
595
- const e = new Error(`resumeAt entry "${spec.resumeAt}" ends a turn mid-tool-call; resume-at requires a settled turn boundary`);
596
- e.code = "resume_at.not_a_message";
597
- throw e;
598
- }
599
- }
600
- if (spec.resumeAtMode === "before") {
601
- const role = entry.type === "message" ? entry.message.role : undefined;
602
- if (role !== "user") {
603
- const kind = entry.type === "message" ? `role "${String(role)}" message` : `"${entry.type}" entry`;
604
- const e = new Error(`resumeAt entry "${spec.resumeAt}" is a ${kind}; resumeAtMode "before" supports only USER-message targets (the exclusive-rewind shape: the removed message is a user message)`);
605
- e.code = "resume_at.before_target_not_user";
606
- throw e;
607
- }
608
- let ancestorId = entry.parentId;
609
- let sawConversationAbove = false;
610
- while (ancestorId !== null) {
611
- const ancestor = await session.getEntry(ancestorId);
612
- if (!ancestor)
613
- break;
614
- if (ancestor.type === "message" || ancestor.type === "custom_message") {
615
- sawConversationAbove = true;
616
- break;
617
- }
618
- ancestorId = ancestor.parentId;
619
- }
620
- if (!sawConversationAbove) {
621
- const e = new Error(`resumeAt entry "${spec.resumeAt}" is the session's first message; resumeAtMode "before" cannot rewind past the session root — start a NEW session instead`);
622
- e.code = "resume_at.before_root_unsupported";
623
- throw e;
624
- }
625
- resumeAtBeforeParentId = entry.parentId;
626
- }
627
- await session.getStorage().setLeafId(spec.resumeAtMode === "before" ? entry.parentId : spec.resumeAt);
628
- }
629
- wakeRecovered = (await reconcileInterruptedSession(session, toolEffects, resume?.suspendedBatch)).recovered;
630
- break;
631
- }
632
- catch (err) {
633
- if (isSessionConflict(err) && attempt < RECONCILE_MAX_RETRIES) {
634
- if (sessions.forget) {
635
- await Promise.resolve(sessions.forget(spec.sessionId)).catch(() => undefined);
636
- }
637
- continue;
638
- }
639
- await Promise.resolve(sessions.forget ? sessions.forget(acquired.sessionId) : undefined).catch(() => undefined);
640
- throw err;
641
- }
642
- }
207
+ const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects });
643
208
  const sessionId = acquired.sessionId;
644
209
  const hostTaskId = spec.taskId ?? sessionId;
645
210
  if (compModel !== undefined) {
@@ -781,7 +346,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
781
346
  }
782
347
  }
783
348
  const handsEnabled = ownedEnv !== undefined || deps.executionEnv !== undefined;
784
- let taskRootPath = internals?.isolation === "worktree" || internals?.requestedCwd !== undefined || !deps.rootPath ? executionEnv.cwd : deps.rootPath;
349
+ const taskRootInitial = internals?.isolation === "worktree" || internals?.requestedCwd !== undefined || !deps.rootPath ? executionEnv.cwd : deps.rootPath;
785
350
  const abortController = new AbortController();
786
351
  if (spec.signal?.aborted)
787
352
  abortController.abort();
@@ -829,108 +394,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
829
394
  }
830
395
  }
831
396
  }
832
- let restoredRootRebase;
833
- const rebaseRestoredPath = (p) => restoredRootRebase === undefined ? p : rebaseWorkspacePathAcross(p, restoredRootRebase.from, restoredRootRebase.to);
834
- if (resume?.workspaceHandle !== undefined) {
835
- const failResume = (message, cause, note) => {
836
- const e = new Error(message, cause ? { cause } : undefined);
837
- e.code = "resume.env_failed";
838
- if (note !== undefined)
839
- e.remoteEnvFailure = note;
840
- throw e;
841
- };
842
- const handle = resume.workspaceHandle;
843
- if (ownedEnv === undefined || !isRemoteExecutionEnv(ownedEnv)) {
844
- failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
845
- }
846
- else if (handle.snapshotId === undefined) {
847
- if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
848
- failResume("checkpoint workspaceHandle has no snapshotId and is not a park_only handle, but the resumed env is suspendable — refusing to resume on a possibly-unrestored workspace (corrupt checkpoint?)");
849
- }
850
- if (handle.mountPath && handle.mountPath !== taskRootPath) {
851
- taskRootPath = handle.mountPath;
852
- }
853
- }
854
- else {
855
- const restoreSignal = spec.signal
856
- ? AbortSignal.any([abortController.signal, spec.signal])
857
- : abortController.signal;
858
- const missingHere = missingRestoreSurface(ownedEnv);
859
- if (missingHere.length > 0) {
860
- failResume(`the resumed execution env cannot restore a workspace snapshot: its adapter does not implement ${missingHere.join(" or ")}. The checkpoint holds snapshot "${handle.snapshotId}" — wire an adapter that implements the full RemoteExecutionEnv restore surface and re-resume.`);
861
- }
862
- const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
863
- abortSignal: restoreSignal,
864
- priorHandle: handle,
865
- });
866
- if (!restored.ok) {
867
- failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
868
- }
869
- else {
870
- const restoredEnv = ownedEnv;
871
- const canonicalInEnv = async (p) => {
872
- try {
873
- const r = await restoredEnv.canonicalPath(p, restoreSignal);
874
- return r.ok ? r.value : undefined;
875
- }
876
- catch {
877
- return undefined;
878
- }
879
- };
880
- let checkpointedCanonical;
881
- let sameRootUnderAlias = false;
882
- if (restored.value.mountPath !== handle.mountPath) {
883
- checkpointedCanonical = await canonicalInEnv(handle.mountPath);
884
- const restoredCanonical = await canonicalInEnv(restored.value.mountPath);
885
- sameRootUnderAlias =
886
- checkpointedCanonical !== undefined && restoredCanonical !== undefined && checkpointedCanonical === restoredCanonical;
887
- }
888
- if (restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
889
- if (resume.executesApprovedAction === true) {
890
- failResume(`resumeVM workspace-root divergence with a pending approved action: checkpointed mountPath "${handle.mountPath}" but the restored handle reports "${restored.value.mountPath}" — the approved args are bound to the checkpointed root; refusing to execute them against a moved workspace (adapter should honor priorHandle)`);
891
- }
892
- try {
893
- deps.onError?.(new Error(`resumeVM workspace-root divergence: checkpointed mountPath "${handle.mountPath}" but the restored handle reports "${restored.value.mountPath}" — the resumed task follows the restored root`), { phase: "config", sessionId });
894
- }
895
- catch {
896
- }
897
- }
898
- if (restored.value.mountPath && restored.value.mountPath !== taskRootPath) {
899
- taskRootPath = restored.value.mountPath;
900
- }
901
- if (restored.value.mountPath && restored.value.mountPath !== handle.mountPath && !sameRootUnderAlias) {
902
- const from = checkpointedCanonical !== undefined && checkpointedCanonical !== handle.mountPath
903
- ? [handle.mountPath, checkpointedCanonical]
904
- : [handle.mountPath];
905
- restoredRootRebase = { from, to: restored.value.mountPath };
906
- try {
907
- deps.onError?.(new Error(from.length > 1
908
- ? `resumeVM workspace-root rebase accepts both spellings of the checkpointed root (${from.map((f) => `"${f}"`).join(" and ")}) when migrating persisted paths to "${restored.value.mountPath}"`
909
- : `resumeVM workspace-root rebase is spelling-exact: it matches the checkpointed root "${handle.mountPath}" only, so persisted paths recorded under an equivalent alias of it are NOT migrated to "${restored.value.mountPath}" and stay as written`), { phase: "config", sessionId });
910
- }
911
- catch {
912
- }
913
- }
914
- }
915
- const init = await ownedEnv.postResumeInit();
916
- if (!init.ok) {
917
- failResume(`postResumeInit failed (${init.error.code}): ${init.error.message}`, init.error, remoteEnvFailureNote("postResumeInit", init.error, 1));
918
- }
919
- }
920
- }
921
- if (internals?.onWorkspaceResolved !== undefined) {
922
- try {
923
- const workspaceEnv = ownedEnv ?? deps.executionEnv;
924
- internals.onWorkspaceResolved({
925
- cwd: taskRootPath,
926
- isolated: internals.isolation === "worktree",
927
- remote: workspaceEnv !== undefined &&
928
- (isRemoteExecutionEnv(workspaceEnv) || workspaceEnv.hostLocalPaths === false),
929
- });
930
- }
931
- catch {
932
- }
933
- }
397
+ const { taskRootFinal, rebaseRestoredPath } = await prepareWorkspaceRestore({ ownedEnv, deps, spec, internals, resume, taskRootInitial, abortController, sessionId });
934
398
  const rewindNotes = [];
935
399
  const rewindCaptureRequested = spec.rewindFiles === true;
936
400
  if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
@@ -987,7 +451,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
987
451
  }
988
452
  rewindTarget = anchor;
989
453
  }
990
- const restoreRoot = taskRootPath;
454
+ const restoreRoot = taskRootFinal;
991
455
  const restoreSignal = spec.signal ? AbortSignal.any([abortController.signal, spec.signal]) : abortController.signal;
992
456
  const restored = await deps.fileSnapshotStore.restore(sessionId, rewindTarget, executionEnv, restoreRoot, restoreSignal);
993
457
  if (!restored.ok) {
@@ -1032,7 +496,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1032
496
  planModeRef.active = true;
1033
497
  };
1034
498
  const subagentRetain = spec.retainSubagentSessions ? new SubagentRetainLedger(spec.retainSubagentSessions) : undefined;
1035
- const worktreeIsolation = spec.handsReadOnly !== true ? createSubagentWorktreeHelper(executionEnv, taskRootPath) : undefined;
499
+ const worktreeIsolation = spec.handsReadOnly !== true ? createSubagentWorktreeHelper(executionEnv, taskRootFinal) : undefined;
1036
500
  const liveInheritedGate = internals?.inheritedGate;
1037
501
  const seedInheritedGate = resume?.seed.inheritedGate;
1038
502
  const inheritedAncestorRules = (() => {
@@ -1090,12 +554,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1090
554
  const ownOrgVerdictRef = { current: undefined };
1091
555
  const orgAdmissionCheckpointState = () => inheritedAdmittedOrgScopes !== undefined || ownOrgVerdictRef.current !== undefined || orgGovernedProvenance;
1092
556
  const faceCheckpointSection = () => {
1093
- if (resolvedReadFace === undefined) {
1094
- const seed = resume?.seed.readFace;
557
+ if (resolvedReadFace === undefined && resume !== undefined) {
558
+ const seed = resume.seed.readFace;
1095
559
  return seed !== undefined ? { face: seed.face, ...(seed.denyEntries !== undefined ? { denyEntries: seed.denyEntries.map((e) => ({ ...e })) } : {}) } : undefined;
1096
560
  }
1097
- if (resolvedReadFace === "open" || readDenyAdditionsNormalized.length > 0) {
1098
- return { face: resolvedReadFace, ...(readDenyAdditionsNormalized.length > 0 ? { denyEntries: readDenyAdditionsNormalized.map((e) => ({ ...e })) } : {}) };
561
+ const face = resolvedReadFace ?? handsLessResolvedFace;
562
+ if (face === undefined)
563
+ return undefined;
564
+ if (face === "open" || readDenyAdditionsNormalized.length > 0) {
565
+ return { face, ...(readDenyAdditionsNormalized.length > 0 ? { denyEntries: readDenyAdditionsNormalized.map((e) => ({ ...e })) } : {}) };
1099
566
  }
1100
567
  return undefined;
1101
568
  };
@@ -1172,6 +639,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1172
639
  let observersActive = false;
1173
640
  let resolvedReadFace;
1174
641
  let readDenyAdditionsNormalized = [];
642
+ let handsLessResolvedFace;
643
+ const carrierReadFace = () => resolvedReadFace ?? handsLessResolvedFace;
1175
644
  const fullShellReachable = handsEnabled &&
1176
645
  !(executionEnv instanceof StubExecutionEnv) &&
1177
646
  spec.handsReadOnly !== true &&
@@ -1186,7 +655,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1186
655
  ...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
1187
656
  ...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
1188
657
  ...(spec.handsReadOnly === true ? { handsReadOnly: true } : {}),
1189
- ...(resolvedReadFace === "roots" ? { readFace: "roots" } : {}),
658
+ ...(carrierReadFace() === "roots" ? { readFace: "roots" } : {}),
1190
659
  ...(readDenyAdditionsNormalized.length > 0 ? { readDenyPatterns: Object.freeze(readDenyAdditionsNormalized.map((e) => ({ ...e }))) } : {}),
1191
660
  ...(spec.interactiveTools === false ? { interactiveTools: false } : {}),
1192
661
  oneShot: spec.oneShot,
@@ -1200,7 +669,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1200
669
  ...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
1201
670
  ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
1202
671
  getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
1203
- parentCwd: taskRootPath,
672
+ parentCwd: taskRootFinal,
1204
673
  ...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
1205
674
  ...(centerAdoption?.sourceRevision !== undefined ? { centerSourceRevision: centerAdoption.sourceRevision } : {}),
1206
675
  activeSkillScope: () => skillScope.active(),
@@ -1391,9 +860,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1391
860
  ...(resolvedInteractionPosture !== undefined ? { parentInteractionPosture: resolvedInteractionPosture } : {}),
1392
861
  autoModeReview: () => (autoModeDecider !== undefined ? { decider: autoModeDecider } : undefined),
1393
862
  workflowDepth: internals?.workflowDepth,
1394
- parentCwd: taskRootPath,
863
+ parentCwd: taskRootFinal,
1395
864
  parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
1396
- parentReadFace: () => resolvedReadFace,
865
+ parentReadFace: () => carrierReadFace(),
1397
866
  parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
1398
867
  onNotice: deps.onNotice,
1399
868
  parentCheckpointStoreDisabled: spec.checkpointStore === null,
@@ -1613,7 +1082,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1613
1082
  let attachmentRootCanonical;
1614
1083
  let readDenyMatcher;
1615
1084
  if (handsEnabled) {
1616
- const rootRaw = taskRootPath;
1085
+ const rootRaw = taskRootFinal;
1617
1086
  const canon = await executionEnv.canonicalPath(rootRaw);
1618
1087
  const rootCanonical = canon.ok ? canon.value : rootRaw;
1619
1088
  attachmentRootCanonical = rootCanonical;
@@ -1686,11 +1155,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1686
1155
  }
1687
1156
  const seedReadFaceSection = resume?.seed.readFace;
1688
1157
  const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
1689
- readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns");
1690
- {
1691
- const builtinCount = compileReadDeny([], "built-in").entries.length;
1692
- readDenyAdditionsNormalized = readDenyMatcher.entries.slice(builtinCount);
1693
- }
1158
+ const readDenyBuiltinCfg = {
1159
+ ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: [...deps.readDenyBuiltinTiers] } : {}),
1160
+ ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: [...deps.readDenyBuiltinExclude] } : {}),
1161
+ };
1162
+ readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg);
1163
+ readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
1694
1164
  let liveReadFace = resolveReadFace({
1695
1165
  specReadFace: spec.readFace,
1696
1166
  depsReadFace: deps.readFace,
@@ -1727,6 +1197,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1727
1197
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
1728
1198
  ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
1729
1199
  ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
1200
+ ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
1201
+ ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
1730
1202
  readFace: liveReadFace,
1731
1203
  includeShell: handsIncludeShell,
1732
1204
  readOnly: handsReadOnly,
@@ -1797,6 +1269,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1797
1269
  }
1798
1270
  }
1799
1271
  }
1272
+ else {
1273
+ const liveFace = resolveReadFace({
1274
+ specReadFace: spec.readFace,
1275
+ depsReadFace: deps.readFace,
1276
+ readOnlyMount: spec.handsReadOnly === true,
1277
+ orgGoverned: deps.permissionRuleOrg !== undefined,
1278
+ fullShellReachable,
1279
+ });
1280
+ const seed = resume?.seed.readFace;
1281
+ handsLessResolvedFace = resume !== undefined && (seed === undefined || seed.face === "roots") ? "roots" : liveFace;
1282
+ const denyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seed?.denyEntries ?? [])];
1283
+ resolveReadDenyBuiltins({
1284
+ ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: deps.readDenyBuiltinTiers } : {}),
1285
+ ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: deps.readDenyBuiltinExclude } : {}),
1286
+ });
1287
+ if (denyAdditions.length > 0) {
1288
+ readDenyAdditionsNormalized = compileReadDeny(denyAdditions, "readDenyPatterns", { tiers: [] }).entries;
1289
+ }
1290
+ }
1800
1291
  if (backgroundTaskToolsActive || workflowToolsActive) {
1801
1292
  toolEffects.set("TaskOutput", "read");
1802
1293
  toolEffects.set("TaskStop", "write");
@@ -1898,7 +1389,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1898
1389
  };
1899
1390
  if (workspaceStateSettle !== undefined)
1900
1391
  workspaceStateSettle.restoredWorktreeDir = worktreeSessionRef.current?.worktreeDir;
1901
- tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootPath, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => firstPartyOffload(t)));
1392
+ tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => firstPartyOffload(t)));
1902
1393
  }
1903
1394
  if (offloadStore)
1904
1395
  tools.push(createReadToolResultTool(offloadStore));
@@ -1995,7 +1486,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1995
1486
  }
1996
1487
  const lspManager = spec.lspManager ?? deps.lspManager;
1997
1488
  if (lspManager) {
1998
- const lspRoot = taskRootPath;
1489
+ const lspRoot = taskRootFinal;
1999
1490
  tools.push(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }));
2000
1491
  }
2001
1492
  const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
@@ -2004,7 +1495,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2004
1495
  const lspRunIdent = sessionId;
2005
1496
  const nudgeLspOnEdit = lspDiagnostics
2006
1497
  ? (rawPath) => {
2007
- const baseDir = handsCwdRef?.current ?? taskRootPath;
1498
+ const baseDir = handsCwdRef?.current ?? taskRootFinal;
2008
1499
  const filePath = resolveLspPath(rawPath, baseDir);
2009
1500
  lspDiagnostics.fileEdited(lspRunIdent, pathToUri(filePath));
2010
1501
  void lspManager
@@ -2026,7 +1517,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2026
1517
  spec,
2027
1518
  deps,
2028
1519
  sessionId,
2029
- taskRootPath,
1520
+ taskRootPath: taskRootFinal,
2030
1521
  memoryWriteGateRef,
2031
1522
  writeToolsMounted: tools.some((t) => t.name === "Write") &&
2032
1523
  !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
@@ -2115,7 +1606,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2115
1606
  let loaded = null;
2116
1607
  try {
2117
1608
  loaded = await Promise.resolve(deps.loadProjectMemory({
2118
- cwd: taskRootPath,
1609
+ cwd: taskRootFinal,
2119
1610
  handsEnabled,
2120
1611
  isSubagent: isDelegatedNonForkChild(internals),
2121
1612
  ...(internals?.agentName ? { agentName: internals.agentName } : {}),
@@ -2282,7 +1773,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2282
1773
  envFacts.resumeFacts = copy;
2283
1774
  }
2284
1775
  if (handsEnabled) {
2285
- envFacts.cwd = taskRootPath;
1776
+ envFacts.cwd = taskRootFinal;
2286
1777
  if (additionalRootsCanonical.length > 0) {
2287
1778
  envFacts.additionalDirectories = [...additionalRootsCanonical];
2288
1779
  }
@@ -3107,7 +2598,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3107
2598
  if (deps.sessionPolicyStore && spec.sessionId) {
3108
2599
  const rules = await deps.sessionPolicyStore.getRules(spec.sessionId, spec.principal);
3109
2600
  if (rules) {
3110
- sessionRulePolicy = createSessionRulePolicy(rules, { env: executionEnv, rootPath: taskRootPath, toolEffects });
2601
+ sessionRulePolicy = createSessionRulePolicy(rules, { env: executionEnv, rootPath: taskRootFinal, toolEffects });
3111
2602
  ownSessionRulesRef.current = {
3112
2603
  sessionId: spec.sessionId,
3113
2604
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
@@ -3128,13 +2619,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3128
2619
  catch {
3129
2620
  }
3130
2621
  }
3131
- ancestorRulePolicies.push(createSessionRulePolicy(ancestorRules, { env: executionEnv, rootPath: taskRootPath, toolEffects }));
2622
+ ancestorRulePolicies.push(createSessionRulePolicy(ancestorRules, { env: executionEnv, rootPath: taskRootFinal, toolEffects }));
3132
2623
  }
3133
2624
  const skillScopePolicy = hasSkillManifest
3134
2625
  ? createActiveSkillScopePolicy({
3135
2626
  scope: skillScope,
3136
2627
  env: executionEnv,
3137
- rootPath: taskRootPath,
2628
+ rootPath: taskRootFinal,
3138
2629
  toolEffects,
3139
2630
  })
3140
2631
  : undefined;
@@ -3300,7 +2791,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3300
2791
  }
3301
2792
  if (provider === undefined)
3302
2793
  return undefined;
3303
- const root = taskRootPath;
2794
+ const root = taskRootFinal;
3304
2795
  return {
3305
2796
  admits: async (req) => {
3306
2797
  const anonymous = spec.principal === undefined || spec.principal === "";
@@ -3920,6 +3411,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3920
3411
  ...riskAxesOf(req.toolName),
3921
3412
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3922
3413
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3414
+ ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
3415
+ ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
3923
3416
  }, onAsk, abortController.signal);
3924
3417
  const waitMs = Math.max(0, now() - t0);
3925
3418
  if (resolved.approverUnavailable !== true) {
@@ -4402,7 +3895,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4402
3895
  }
4403
3896
  };
4404
3897
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4405
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason) => {
3898
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause) => {
4406
3899
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4407
3900
  if (syncFirstEligible &&
4408
3901
  runtimeCaps?.forceDurableGate !== true &&
@@ -4508,6 +4001,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4508
4001
  args: parkedArgs,
4509
4002
  safety,
4510
4003
  ...(shadowedRule !== undefined ? { shadowedRule } : {}),
4004
+ ...(probeReason !== undefined ? { probeReason } : {}),
4005
+ ...(probeCause !== undefined ? { probeCause } : {}),
4511
4006
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
4512
4007
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
4513
4008
  });
@@ -5054,8 +4549,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
5054
4549
  }
5055
4550
  : undefined;
5056
4551
  overheadState.promptChars = systemPrompt.length;
4552
+ const effectiveReadFaceObserved = carrierReadFace();
4553
+ const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
5057
4554
  const preparedHolder = {};
5058
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), 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, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4555
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), 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, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
5059
4556
  const prepared = buildPrepared();
5060
4557
  preparedHolder.current = prepared;
5061
4558
  return prepared;