@sema-agent/core 5.36.0 → 5.38.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 (56) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/agents/subagent.d.ts +10 -0
  3. package/dist/agents/subagent.js +6 -0
  4. package/dist/agents/teacher.js +3 -0
  5. package/dist/agents/team.d.ts +7 -1
  6. package/dist/agents/team.js +11 -9
  7. package/dist/agents/verify.js +3 -0
  8. package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
  9. package/dist/core/auto-mode-prompt-assets.js +1 -1
  10. package/dist/core/checkpoint-store.d.ts +26 -1
  11. package/dist/core/governance-codes.js +4 -0
  12. package/dist/core/hooks.d.ts +129 -2
  13. package/dist/core/hooks.js +20 -3
  14. package/dist/core/memory-engine/engine.d.ts +142 -0
  15. package/dist/core/memory-engine/engine.js +264 -2
  16. package/dist/core/memory-engine/file-backend.d.ts +490 -16
  17. package/dist/core/memory-engine/file-backend.js +1099 -36
  18. package/dist/core/memory-engine/index.d.ts +2 -2
  19. package/dist/core/memory-engine/index.js +1 -1
  20. package/dist/core/memory-engine/layout.d.ts +42 -2
  21. package/dist/core/memory-engine/layout.js +76 -12
  22. package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
  23. package/dist/core/memory-engine/memory-backend-contract.js +89 -0
  24. package/dist/core/protocol-table.d.ts +4 -4
  25. package/dist/core/runner/assemble-result.d.ts +5 -0
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/prepare-config-doors.d.ts +17 -0
  28. package/dist/core/runner/prepare-config-doors.js +33 -2
  29. package/dist/core/runner/prepare-memory.d.ts +11 -1
  30. package/dist/core/runner/prepare-memory.js +48 -2
  31. package/dist/core/runner/prepare-task.d.ts +22 -2
  32. package/dist/core/runner/prepare-task.js +125 -42
  33. package/dist/core/runner/runtask.js +50 -11
  34. package/dist/core/tool-model-gate.d.ts +125 -0
  35. package/dist/core/tool-model-gate.js +303 -0
  36. package/dist/core/tool-policy.d.ts +1 -1
  37. package/dist/core/types.d.ts +284 -1
  38. package/dist/core/types.js +21 -0
  39. package/dist/core/untrusted-text.d.ts +1 -1
  40. package/dist/index.d.ts +5 -4
  41. package/dist/index.js +3 -2
  42. package/dist/orchestration/builtin-workflows.d.ts +68 -6
  43. package/dist/orchestration/builtin-workflows.js +26 -9
  44. package/dist/orchestration/run-workflow-tool.d.ts +10 -1
  45. package/dist/orchestration/run-workflow-tool.js +70 -27
  46. package/dist/orchestration/workflow-script-store.d.ts +8 -3
  47. package/dist/prompts/coordinator.d.ts +4 -1
  48. package/dist/prompts/coordinator.js +8 -0
  49. package/dist/prompts/default.d.ts +14 -4
  50. package/dist/prompts/default.js +2 -1
  51. package/dist/scenarios/full-body.d.ts +5 -0
  52. package/dist/scenarios/full-body.js +8 -4
  53. package/dist/tools/fs/fs-shared.d.ts +3 -2
  54. package/dist/tools/fs/fs-shared.js +19 -9
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +24 -1
@@ -12,12 +12,16 @@ export async function prepareMemory(input) {
12
12
  const memorySpec = normalizeMemorySpec(spec.memory);
13
13
  let admittedOrgScopes = [];
14
14
  let ownOrgVerdict;
15
+ const deploymentScopeSet = new Set(deps.deploymentMemoryScopes ?? []);
16
+ const scopeOriginsSnapshot = memorySpec?.scopeOrigins !== undefined ? { ...memorySpec.scopeOrigins } : undefined;
17
+ if (memorySpec && scopeOriginsSnapshot !== undefined)
18
+ memorySpec.scopeOrigins = scopeOriginsSnapshot;
15
19
  if (memorySpec && memorySpec.enabled && deps.memoryBackend) {
16
20
  const outcome = await admitMemoryScopes({
17
21
  memorySpec,
18
22
  principal: spec.principal,
19
23
  admission: deps.memoryScopeAdmission,
20
- deploymentScopes: new Set(deps.deploymentMemoryScopes ?? []),
24
+ deploymentScopes: deploymentScopeSet,
21
25
  orgMemoryDenied: admissionCtx.orgMemoryDenied,
22
26
  complianceDegraded: admissionCtx.complianceDegraded,
23
27
  parentAdmittedOrgScopes: admissionCtx.parentAdmittedOrgScopes,
@@ -38,6 +42,15 @@ export async function prepareMemory(input) {
38
42
  const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
39
43
  let memoryEngineSession;
40
44
  let memoryTools;
45
+ let effectiveMemoryScopes;
46
+ const originOf = (scope) => scopeOriginsSnapshot !== undefined
47
+ ? scopeOriginsSnapshot[scope] === "deployment"
48
+ ? "deployment"
49
+ : "request"
50
+ : deploymentScopeSet.has(scope)
51
+ ? "deployment"
52
+ : "request";
53
+ const materializedResidue = [];
41
54
  if (useMemoryEngine && memorySpec) {
42
55
  try {
43
56
  const backend = deps.memoryBackend;
@@ -133,6 +146,12 @@ export async function prepareMemory(input) {
133
146
  b)
134
147
  : (b.retrievalView?.() ?? b);
135
148
  const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
149
+ const mountedScopeRows = (dual
150
+ ? [
151
+ ...planeScopes(planes.project, planes.writePlane === "project" ? memorySpec.writeScope : null),
152
+ ...planeScopes(planes.personal, planes.writePlane === "personal" ? memorySpec.writeScope : null),
153
+ ]
154
+ : planeScopes(memorySpec.scopes, memorySpec.writeScope)).map((scope) => ({ scope, origin: originOf(scope) }));
136
155
  const pollutedOpts = (engine) => {
137
156
  const rec = engine.sessionPollution(sessionId);
138
157
  return rec !== undefined ? { polluted: { reason: rec.reason } } : {};
@@ -179,7 +198,9 @@ export async function prepareMemory(input) {
179
198
  const personalEngine = personal.engine;
180
199
  const p = planes;
181
200
  const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
201
+ materializedResidue.push(...planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null));
182
202
  const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
203
+ materializedResidue.push(...planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null));
183
204
  const writeIsPersonal = p.writePlane === "personal";
184
205
  writeEngine = writeIsPersonal ? personalEngine : projectEngine;
185
206
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
@@ -222,6 +243,7 @@ export async function prepareMemory(input) {
222
243
  const personal = createPersonalEngine(choosePersonalBackend());
223
244
  const personalEngine = personal.engine;
224
245
  const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
246
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
225
247
  writeEngine = personalEngine;
226
248
  writeHandle = handle;
227
249
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -243,6 +265,7 @@ export async function prepareMemory(input) {
243
265
  onIncident: onEngineIncident,
244
266
  });
245
267
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
268
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
246
269
  writeEngine = engine;
247
270
  writeHandle = handle;
248
271
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -311,6 +334,12 @@ export async function prepareMemory(input) {
311
334
  execIsExternalContent: memorySpec.execIsExternalContent === true,
312
335
  },
313
336
  };
337
+ effectiveMemoryScopes = {
338
+ state: "mounted",
339
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
340
+ scopes: mountedScopeRows,
341
+ writeScope: memorySpec.writeScope,
342
+ };
314
343
  if (input.memorySearchToolsPlanned)
315
344
  memoryTools = createMemoryEngineTools({ planes: toolPlanes });
316
345
  }
@@ -322,6 +351,16 @@ export async function prepareMemory(input) {
322
351
  throw err;
323
352
  }
324
353
  deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
354
+ if (effectiveMemoryScopes === undefined) {
355
+ effectiveMemoryScopes = {
356
+ state: "memoryless",
357
+ reason: "mount-failed",
358
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
359
+ scopes: [],
360
+ writeScope: null,
361
+ ...(materializedResidue.length > 0 ? { materializedResidue: [...new Set(materializedResidue)] } : {}),
362
+ };
363
+ }
325
364
  }
326
365
  }
327
366
  let memoryBlock;
@@ -359,5 +398,12 @@ export async function prepareMemory(input) {
359
398
  if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
360
399
  seedFiles = [injection.indexSeed];
361
400
  }
362
- return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
401
+ if (effectiveMemoryScopes === undefined) {
402
+ effectiveMemoryScopes = memorySpec
403
+ ? memorySpec.enabled
404
+ ? { state: "memoryless", reason: "no-backend", contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy", scopes: [], writeScope: null }
405
+ : { state: "none", reason: "disabled", scopes: [], writeScope: null }
406
+ : { state: "none", reason: "no-spec", scopes: [], writeScope: null };
407
+ }
408
+ return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, effectiveMemoryScopes, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
363
409
  }
@@ -11,7 +11,7 @@ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
11
11
  import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
12
12
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
13
13
  import type { SessionPermissionRules } from "../session-policy-store.js";
14
- import { type Hooks, type OrgGateVerdict } from "../hooks.js";
14
+ import { type Hooks, type HookInvocationIdentity, type OrgGateVerdict } from "../hooks.js";
15
15
  import type { RecoveredOrphan } from "../session-reconcile.js";
16
16
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
17
17
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
@@ -29,8 +29,14 @@ import { type WiringManifest } from "../wiring-manifest.js";
29
29
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
30
30
  import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
31
31
  import type { RepairBundle } from "../../agents/repair-loop.js";
32
- /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
32
+ /** Test seam (mirrors `__resetToolModelGateAnnouncements`): never called by production code.
33
+ * Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). */
33
34
  export declare function __resetMaterializeEnvAnnouncements(): void;
35
+ /** Test seam (mirrors `__resetMaterializeEnvAnnouncements`): never called by production code.
36
+ * Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). Deliberately UNLIKE the read-face
37
+ * seam below (console latch only): tests here legitimately reuse ONE sink across prepares to pin
38
+ * the per-sink dedup itself, so the seam must be able to re-arm a still-referenced sink. */
39
+ export declare function __resetToolModelGateAnnouncements(): void;
34
40
  /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
35
41
  * Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
36
42
  * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
@@ -205,6 +211,11 @@ export interface Prepared {
205
211
  * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
206
212
  * copy (the wide-scope working array stays the engine's own). */
207
213
  effectiveReadDenyPatterns?: readonly import("../../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
214
+ /** design/178 v2 §2.3 (件①) — the memory-visibility observation prepareMemory minted (echoed on
215
+ * `TaskResult.effectiveMemoryScopes`). Always present on a completed prepare (the memory-less
216
+ * states are their own values); the seat is optional only so a Prepared shape without the phase
217
+ * cannot fabricate one. */
218
+ effectiveMemoryScopes?: import("../types.js").EffectiveMemoryScopes;
208
219
  /** design/99 §E13 — the per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
209
220
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
210
221
  cwdRef?: CwdRef;
@@ -274,6 +285,15 @@ export interface Prepared {
274
285
  * model/tool interaction. Host/operator plane — never enters model context.
275
286
  */
276
287
  wiringManifest: WiringManifest;
288
+ /**
289
+ * #281 件A — this leg's frozen identity envelope, minted ONCE in prepare beside the wiring
290
+ * manifest (same leg derivation, one mint — {@link mintHookInvocationIdentity}). Every hook
291
+ * station runtask drives (stop/stopFailure/userPromptSubmit/postToolBatch, the compaction
292
+ * wrapper) and the 件B delegation-lifecycle observer read THIS object; prepare's own stations
293
+ * (the tool gate, the post-tool contexts) close over the same const. Always present — a prepared
294
+ * leg always knows its identity.
295
+ */
296
+ hookIdentity: HookInvocationIdentity;
277
297
  promptManifest: {
278
298
  constitution: "core" | "replaced" | "provider-assembled";
279
299
  blocks: Array<{
@@ -29,7 +29,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
29
29
  import { policyAskClassOf } from "../ask-class.js";
30
30
  import { emitTrace } from "../trace.js";
31
31
  import { createSessionRulePolicy } from "./session-rule-policy.js";
32
- import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
32
+ import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, mintHookInvocationIdentity, persistedRuleMandateOf, runToolGate } from "../hooks.js";
33
33
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
34
34
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
35
35
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
@@ -92,13 +92,104 @@ import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDec
92
92
  import { durableParkGapFor } from "../park-selfcheck.js";
93
93
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
94
94
  import { deliverEngineNotice } from "../types.js";
95
- const announcedMaterializeEnv = new Set();
95
+ let announcedMaterializeEnvBySink = new WeakMap();
96
+ const announcedMaterializeEnvConsole = new Set();
97
+ function materializeEnvLedger(onNotice) {
98
+ if (typeof onNotice !== "function")
99
+ return announcedMaterializeEnvConsole;
100
+ let lines = announcedMaterializeEnvBySink.get(onNotice);
101
+ if (lines === undefined) {
102
+ lines = new Set();
103
+ announcedMaterializeEnvBySink.set(onNotice, lines);
104
+ }
105
+ return lines;
106
+ }
96
107
  export function __resetMaterializeEnvAnnouncements() {
97
- announcedMaterializeEnv.clear();
108
+ announcedMaterializeEnvBySink = new WeakMap();
109
+ announcedMaterializeEnvConsole.clear();
98
110
  }
99
111
  function emitMaterializeEnvNotice(onNotice, message, detail) {
112
+ const ledger = materializeEnvLedger(onNotice);
113
+ if (ledger.has(message))
114
+ return;
115
+ ledger.add(message);
100
116
  deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
101
117
  }
118
+ function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
119
+ if (compModel === undefined)
120
+ return;
121
+ const compWindow = compModel.contextTokens ?? compModel.contextWindow;
122
+ const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
123
+ if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
124
+ const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
125
+ const sanitized = sanitizeCompactionSettings(merged, mainWindow);
126
+ const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
127
+ const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
128
+ emitTrace(tracer, () => ({
129
+ kind: "compaction.window_config_warning",
130
+ version: 1,
131
+ taskId: hostTaskId,
132
+ compactionModelWindow: compWindow,
133
+ mainModelWindow: mainWindow,
134
+ ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
135
+ ts: Date.now(),
136
+ }));
137
+ }
138
+ }
139
+ let announcedToolModelGateBySink = new WeakMap();
140
+ const announcedToolModelGateConsole = new Set();
141
+ function toolModelGateLedger(onNotice) {
142
+ if (typeof onNotice !== "function")
143
+ return announcedToolModelGateConsole;
144
+ let lines = announcedToolModelGateBySink.get(onNotice);
145
+ if (lines === undefined) {
146
+ lines = new Set();
147
+ announcedToolModelGateBySink.set(onNotice, lines);
148
+ }
149
+ return lines;
150
+ }
151
+ export function __resetToolModelGateAnnouncements() {
152
+ announcedToolModelGateBySink = new WeakMap();
153
+ announcedToolModelGateConsole.clear();
154
+ }
155
+ function announceToolModelGate(onNotice, modelId, gate) {
156
+ const ledger = toolModelGateLedger(onNotice);
157
+ for (const [gateClass, removed] of gate.removedByClass) {
158
+ const line = `Model gate: default-mounted tool(s) ${removed.map((n) => JSON.stringify(n)).join(", ")} (class ${JSON.stringify(gateClass)}) ` +
159
+ `were not mounted for model ${JSON.stringify(modelId)} — the gate table marks this model as managing multi-step work without the scaffold. ` +
160
+ `Explicitly composed tools are exempt; restore via TaskSpec.restoreGatedTools, SEMA_TOOL_MODEL_GATE=off, or RunnerDeps.toolModelGate: false.`;
161
+ if (!ledger.has(line)) {
162
+ ledger.add(line);
163
+ deliverEngineNotice(onNotice, {
164
+ code: "config.tool_model_gate_removed",
165
+ message: line,
166
+ detail: {
167
+ modelId,
168
+ gateClass,
169
+ removed: [...removed],
170
+ restore: { spec: "TaskSpec.restoreGatedTools", env: "SEMA_TOOL_MODEL_GATE=off", deps: "RunnerDeps.toolModelGate: false" },
171
+ },
172
+ });
173
+ }
174
+ }
175
+ for (const gateClass of gate.unknownClasses) {
176
+ const line = `ToolSpec.modelGate names gate class ${JSON.stringify(gateClass)}, which no row of the merged gate table defines — the tag is inert ` +
177
+ `(fail-open: the tool stays mounted). Fix the tag, or define the class via RunnerDeps.toolModelGate.classes.`;
178
+ if (!ledger.has(line)) {
179
+ ledger.add(line);
180
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_unknown_class", message: line, detail: { gateClass } });
181
+ }
182
+ }
183
+ if (gate.discardedEnvRaw !== undefined) {
184
+ const line = `SEMA_TOOL_MODEL_GATE=${JSON.stringify(gate.discardedEnvRaw)} is not in the closed set (on|1|true|off|0|false, case-insensitive) — ` +
185
+ `not in force on this task (nothing the model gate would remove), but a task where the gate WOULD trim a default-mounted tool ` +
186
+ `will refuse to prepare under it (config.tool_model_gate_env_invalid). Fix or unset the flag.`;
187
+ if (!ledger.has(line)) {
188
+ ledger.add(line);
189
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_env_invalid", message: line, detail: { raw: gate.discardedEnvRaw } });
190
+ }
191
+ }
192
+ }
102
193
  const readFaceClampAnnouncedSinks = new WeakSet();
103
194
  let readFaceClampConsoleAnnounced = false;
104
195
  export function __resetReadFaceClampAnnouncement() {
@@ -153,8 +244,9 @@ export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-worksp
153
244
  function hasConversationContent(branch) {
154
245
  return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
155
246
  }
156
- function isDelegatedNonForkChild(internals) {
157
- return internals?.isDelegatedChild === true && internals?.insideFork !== true;
247
+ function effectiveDelegationFacts(internals, seedIsDelegatedChild) {
248
+ const isDelegatedChild = internals?.isDelegatedChild !== undefined ? internals.isDelegatedChild === true : seedIsDelegatedChild === true;
249
+ return { isDelegatedChild, isNonForkChild: isDelegatedChild && internals?.insideFork !== true };
158
250
  }
159
251
  export function batchContextAt(messages, currentId) {
160
252
  let batch = [];
@@ -238,6 +330,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
238
330
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
239
331
  spec = doors.spec;
240
332
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
333
+ announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
241
334
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
242
335
  let shellGatedBash = false;
243
336
  let shellGatedMonitor = false;
@@ -249,25 +342,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
249
342
  const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
250
343
  const sessionId = acquired.sessionId;
251
344
  const hostTaskId = spec.taskId ?? sessionId;
252
- if (compModel !== undefined) {
253
- const compWindow = compModel.contextTokens ?? compModel.contextWindow;
254
- const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
255
- if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
256
- const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
257
- const sanitized = sanitizeCompactionSettings(merged, mainWindow);
258
- const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
259
- const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
260
- emitTrace(deps.tracer, () => ({
261
- kind: "compaction.window_config_warning",
262
- version: 1,
263
- taskId: hostTaskId,
264
- compactionModelWindow: compWindow,
265
- mainModelWindow: mainWindow,
266
- ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
267
- ts: Date.now(),
268
- }));
269
- }
270
- }
345
+ const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
346
+ warnCompactionWindowHazard(deps.tracer, spec, model, compModel, hostTaskId);
271
347
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
272
348
  internals?.peerSelfRef?.addAxis("s", sessionId);
273
349
  internals?.peerSelfRef?.addAxis("t", hostTaskId);
@@ -705,6 +781,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
705
781
  excludeTools: toolFaceSnapshot.exclude,
706
782
  deferTools: toolFaceSnapshot.defer,
707
783
  alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
784
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { restoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
708
785
  promptProfile,
709
786
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
710
787
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
@@ -876,6 +953,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
876
953
  parentExcludeTools: toolFaceSnapshot.exclude,
877
954
  parentDeferTools: toolFaceSnapshot.defer,
878
955
  parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
956
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { parentRestoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
879
957
  parentPromptProfile: promptProfile,
880
958
  models: deps.models,
881
959
  agents: deps.agents,
@@ -1555,7 +1633,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1555
1633
  const memoryPairExcludedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => (toolFaceSnapshot.exclude ?? []).includes(n));
1556
1634
  const memoryPairOccupiedName = MEMORY_ENGINE_TOOL_NAMES.find((n) => memoryPairNameDomain.has(n));
1557
1635
  const memorySearchToolsPlanned = memoryPairExcludedName === undefined && memoryPairOccupiedName === undefined;
1558
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, } = await prepareMemory({
1636
+ const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
1559
1637
  spec,
1560
1638
  deps,
1561
1639
  sessionId,
@@ -1581,7 +1659,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1581
1659
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
1582
1660
  complianceDegraded,
1583
1661
  parentAdmittedOrgScopes: foldAdmissionFreeze({
1584
- delegated: internals?.isDelegatedChild === true ||
1662
+ delegated: delegation.isDelegatedChild ||
1585
1663
  internals?.inheritedGate !== undefined ||
1586
1664
  (resume?.seed.inheritedGate !== undefined &&
1587
1665
  (resume.seed.inheritedGate.ancestorRules !== undefined ||
@@ -1650,7 +1728,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1650
1728
  loaded = await Promise.resolve(deps.loadProjectMemory({
1651
1729
  cwd: taskRootFinal,
1652
1730
  handsEnabled,
1653
- isSubagent: isDelegatedNonForkChild(internals),
1731
+ isSubagent: delegation.isNonForkChild,
1654
1732
  ...(internals?.agentName ? { agentName: internals.agentName } : {}),
1655
1733
  sessionId,
1656
1734
  phase: projectMemoryPhase,
@@ -1775,7 +1853,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1775
1853
  awarenessEnabled: thinking !== undefined && ULTRA_REASONING_TIERS.has(thinking),
1776
1854
  worktreeIsolated: internals?.isolation === "worktree" && ownedEnv !== undefined,
1777
1855
  withinTaskCompactionEnabled: (spec.compaction?.enabled ?? true) && (spec.compaction?.withinTask ?? true),
1778
- isSubagent: isDelegatedNonForkChild(internals),
1856
+ isSubagent: delegation.isNonForkChild,
1779
1857
  };
1780
1858
  const userSystemPrompt = spec.systemPrompt ?? resolvedRole.systemPrompt ?? internals?.defaultSystemPrompt;
1781
1859
  const userAppendSystemPrompt = spec.appendSystemPrompt;
@@ -2209,10 +2287,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2209
2287
  const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2210
2288
  if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
2211
2289
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
2212
- if (!announcedMaterializeEnv.has(line)) {
2213
- announcedMaterializeEnv.add(line);
2214
- emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2215
- }
2290
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2216
2291
  }
2217
2292
  }
2218
2293
  if (deferred.size > 0) {
@@ -2231,10 +2306,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2231
2306
  }
2232
2307
  if (envStrategyInvalid) {
2233
2308
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
2234
- if (!announcedMaterializeEnv.has(line)) {
2235
- announcedMaterializeEnv.add(line);
2236
- emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2237
- }
2309
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2238
2310
  }
2239
2311
  const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
2240
2312
  const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
@@ -2785,7 +2857,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2785
2857
  ...((internals?.explicitAgentName ?? internals?.agentName) !== undefined
2786
2858
  ? { sourceAgentName: internals?.explicitAgentName ?? internals?.agentName }
2787
2859
  : {}),
2788
- ...(internals?.isDelegatedChild === true ? { isDelegatedChild: true } : {}),
2860
+ ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
2789
2861
  });
2790
2862
  const riskAxesOf = (toolName) => {
2791
2863
  const tier = irreversibilityTier.get(toolName);
@@ -3347,6 +3419,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3347
3419
  retentionPolicyWired: deps.retentionPolicy !== undefined,
3348
3420
  });
3349
3421
  const parkLaneArmed = wiringManifest.parkLane.effective === true;
3422
+ const hookIdentity = mintHookInvocationIdentity({
3423
+ sessionId,
3424
+ taskId: spec.taskId ?? sessionId,
3425
+ legKind: wiringManifest.leg.kind,
3426
+ isDelegatedChild: delegation.isDelegatedChild,
3427
+ ...(internals?.insideFork === true ? { insideFork: true } : {}),
3428
+ ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
3429
+ ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
3430
+ });
3350
3431
  const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
3351
3432
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
3352
3433
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
@@ -3556,6 +3637,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3556
3637
  : undefined,
3557
3638
  gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
3558
3639
  delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
3640
+ isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
3559
3641
  });
3560
3642
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3561
3643
  if (!checkpointStore)
@@ -4155,7 +4237,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4155
4237
  if (blockedTracked)
4156
4238
  blockedToolCalls.add(e.toolCallId);
4157
4239
  if (notifyPermissionDenied) {
4158
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety" });
4240
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety", identity: hookIdentity });
4159
4241
  }
4160
4242
  return { block: true, reason: formatHookFeedback(complianceDeny), preToolContext: [] };
4161
4243
  }
@@ -4166,7 +4248,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4166
4248
  const planDenyReason = `Plan mode is active — "${e.toolName}" is a write/mutating tool and is read-only-blocked. ` +
4167
4249
  `Research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
4168
4250
  if (notifyPermissionDenied) {
4169
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode" });
4251
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode", identity: hookIdentity });
4170
4252
  }
4171
4253
  return {
4172
4254
  block: true,
@@ -4179,6 +4261,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4179
4261
  result = await runToolGate({
4180
4262
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
4181
4263
  event: e,
4264
+ identity: hookIdentity,
4182
4265
  preToolUse: ownGatePreToolUse,
4183
4266
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
4184
4267
  adjudicate,
@@ -4291,7 +4374,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4291
4374
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
4292
4375
  content: e.content.map((c) => ({ ...c })),
4293
4376
  details: clonedDetails,
4294
- }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4377
+ }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4295
4378
  if (patch?.additionalContext) {
4296
4379
  content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext) }];
4297
4380
  changed = true;
@@ -4299,7 +4382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4299
4382
  }
4300
4383
  }
4301
4384
  else if (hooks?.postToolUse) {
4302
- const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4385
+ const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4303
4386
  if (patch?.updatedOutput) {
4304
4387
  content = patch.updatedOutput;
4305
4388
  changed = true;
@@ -4586,7 +4669,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4586
4669
  const effectiveReadFaceObserved = carrierReadFace();
4587
4670
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4588
4671
  const preparedHolder = {};
4589
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, 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, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4672
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4590
4673
  const prepared = buildPrepared();
4591
4674
  preparedHolder.current = prepared;
4592
4675
  return prepared;