@sema-agent/core 5.43.0 → 5.45.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.
@@ -66,6 +66,18 @@ export interface MemoryEngineToolsOptions {
66
66
  planes: ReadonlyArray<MemoryEnginePlane>;
67
67
  /** Clock (tests); default Date.now. Drives the natural-language age rendering only. */
68
68
  now?: () => number;
69
+ /**
70
+ * design/178 §3 (#324b) — the session's pollution READ face: the record when this session is marked
71
+ * polluted, `undefined` when it is clean. Read-only and side-effect-free (the mark is one-way and
72
+ * lives at the runner's mark seat); consulted ONLY on the empty outcomes, where a session whose
73
+ * writes are being withheld deserves to hear it instead of being told to search again.
74
+ *
75
+ * Absent ⇒ no pollution face is wired (engine-direct hosts, tests): the empty-outcome wording is
76
+ * then exactly the clean-session wording — nothing is inferred from the seat's absence.
77
+ */
78
+ sessionPollution?: () => {
79
+ reason: string;
80
+ } | undefined;
69
81
  }
70
82
  export interface MemorySearchHit {
71
83
  id: string;
@@ -91,6 +91,26 @@ export function skipBytes(text, startBytes) {
91
91
  export function createMemoryEngineTools(opts) {
92
92
  const { planes } = opts;
93
93
  const now = opts.now ?? Date.now;
94
+ const pollutionSentence = () => {
95
+ let reason;
96
+ try {
97
+ reason = opts.sessionPollution?.()?.reason;
98
+ }
99
+ catch {
100
+ return "";
101
+ }
102
+ if (reason === undefined)
103
+ return "";
104
+ return ` This session is marked polluted (${inlineUntrusted(reason, 200)}): the memory it writes is not admitted to the library at harvest, so writing this down now would not make it retrievable later either.`;
105
+ };
106
+ const noMatch = (query) => {
107
+ const details = { outcome: "ok", hits: [] };
108
+ return {
109
+ content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.` +
110
+ pollutionSentence(),
111
+ details,
112
+ };
113
+ };
94
114
  const getWithinScopes = async (plane, ids) => {
95
115
  const scopeSet = new Set(plane.scopes);
96
116
  return (await plane.backend.getByIds(ids)).filter((e) => scopeSet.has(e.scope) && e.frontmatter.deleted !== true);
@@ -102,7 +122,7 @@ export function createMemoryEngineTools(opts) {
102
122
  defer: true,
103
123
  offload: false,
104
124
  contentOrigin: "local",
105
- contract: { contractId: "core.memory_search@1", implementationRevision: "1" },
125
+ contract: { contractId: "core.memory_search@1", implementationRevision: "2" },
106
126
  parameters: Type.Object({
107
127
  query: Type.String({ description: "Keywords to look for (lexical match against entry names, descriptions and bodies)." }),
108
128
  limit: Type.Optional(Type.Number({ description: `Maximum hits to return (default ${MEMORY_SEARCH_DEFAULT_LIMIT}, max ${MEMORY_SEARCH_MAX_LIMIT}).` })),
@@ -144,10 +164,8 @@ export function createMemoryEngineTools(opts) {
144
164
  }
145
165
  merged.sort(contractOrder);
146
166
  const top = merged.slice(0, limit);
147
- if (top.length === 0) {
148
- const details = { outcome: "ok", hits: [] };
149
- return { content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.`, details };
150
- }
167
+ if (top.length === 0)
168
+ return noMatch(query);
151
169
  const bodyById = new Map();
152
170
  try {
153
171
  for (let i = 0; i < planes.length; i++) {
@@ -171,10 +189,8 @@ export function createMemoryEngineTools(opts) {
171
189
  return refusedSearch("challenge_ledger_unavailable", "Memory search is unavailable: the challenge ledger for a mounted memory plane cannot be read (fail-closed). Report this to the operator.", "failed");
172
190
  }
173
191
  const live = top.filter((h) => bodyById.has(h.id) && terminalExclusions[h.planeIndex]?.has(h.id) !== true);
174
- if (live.length === 0) {
175
- const details = { outcome: "ok", hits: [] };
176
- return { content: `No memory entries matched ${JSON.stringify(inlineUntrusted(query, 120))}. If the user expected you to know this, say that you checked memory and found nothing.`, details };
177
- }
192
+ if (live.length === 0)
193
+ return noMatch(query);
178
194
  for (let i = 0; i < planes.length; i++) {
179
195
  const ids = live.filter((h) => h.planeIndex === i).map((h) => h.id);
180
196
  if (ids.length === 0)
@@ -219,7 +235,7 @@ export function createMemoryEngineTools(opts) {
219
235
  defer: true,
220
236
  offload: false,
221
237
  contentOrigin: "local",
222
- contract: { contractId: "core.memory_get@1", implementationRevision: "2" },
238
+ contract: { contractId: "core.memory_get@1", implementationRevision: "3" },
223
239
  parameters: Type.Object({
224
240
  id: Type.Optional(Type.String({ description: "Entry id (exact lookup). Pass either id or slug, not both." })),
225
241
  slug: Type.Optional(Type.String({ description: "Entry slug (its file path without .md). Ambiguous across scopes unless scope is also passed." })),
@@ -275,7 +291,10 @@ export function createMemoryEngineTools(opts) {
275
291
  }
276
292
  if (matches.length === 0) {
277
293
  const where = scope !== undefined ? ` in scope ${JSON.stringify(inlineUntrusted(scope, 80))}` : "";
278
- return refusedGet("not_found", `No memory entry with slug ${JSON.stringify(inlineUntrusted(slug, 160))}${where} is mounted in this session. Try ${MEMORY_SEARCH_TOOL_NAME}.`, "not_found", { slug: slug, ...(scope !== undefined ? { scope } : {}) });
294
+ return refusedGet("not_found", `No memory entry with slug ${JSON.stringify(inlineUntrusted(slug, 160))}${where} is mounted in this session. ` +
295
+ `Only entries carrying a committed entry id are served here, and a memory file written into the memory root during ` +
296
+ `this session gets its id when the session-boundary harvest admits it — a file written in this session is not served here before then.` +
297
+ pollutionSentence(), "not_found", { slug: slug, ...(scope !== undefined ? { scope } : {}) });
279
298
  }
280
299
  if (matches.length > 1) {
281
300
  const candidates = matches.map((m) => ({ scope: m.header.scope, slug: m.header.slug, id: m.header.id }));
@@ -133,6 +133,11 @@ export interface ResultFlags {
133
133
  * read-posture seats above: present iff prepare completed (the memory-less states are their own
134
134
  * values — absence means only "prepare never ran to completion"). */
135
135
  effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
136
+ /** #327 — the leg's effective reasoning resolution, echoed on `TaskResult.effectiveReasoning`.
137
+ * Pure pass-through on every terminal (same law as the read-posture seats): the SAME object the
138
+ * `reasoning.resolved` trace frame carried (runtask resolves once per leg — two faces, one mint);
139
+ * absent when thinking was off/unset for the leg, so the key is omitted. */
140
+ effectiveReasoning?: TaskResult["effectiveReasoning"];
136
141
  /** ruled 2026-08-04 — the usage-governance wait hint carried by the platform terminal the run adopted
137
142
  * (`undefined` for every other cause: an expiring environment has no return time to give, and a store
138
143
  * failure is not a window). Passed as data rather than read back off `threw` so the seat has a typed
@@ -165,5 +165,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
165
165
  void _internalCompaction;
166
166
  if (flags.unpricedSpend)
167
167
  delete publicStats.costMicroUsd;
168
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
168
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
169
  }
@@ -144,6 +144,12 @@ export interface PrepareConfigDoorsResult {
144
144
  * material; the in-force arm never lands here — it throws at the door). */
145
145
  discardedEnvRaw: string | undefined;
146
146
  };
147
+ /** owned — the NORMALIZED per-leg delegation-evidence standard (design/324): the deps seat read
148
+ * ONCE, in this synchronous pre-await stretch, screened, and absent folded to the "static-face"
149
+ * default. The delegation wrap consumes THIS value, never the live deps object — a deps seat
150
+ * mutated (or getter-backed) after the door must not present an unscreened value to the mark
151
+ * branch (the #245 5.33 read-once posture). */
152
+ memoryDelegationEvidence: "static-face" | "attested-only";
147
153
  /** owned — validated deployment governance windows (undefined = ungoverned). */
148
154
  usageWindows: readonly UsageWindow[] | undefined;
149
155
  /** owned, out-param cell — created EMPTY here; the brain-call wiring later installs into
@@ -132,6 +132,22 @@ export function prepareConfigDoors(input) {
132
132
  }
133
133
  assertReadFaceValue(spec.readFace, "TaskSpec.readFace");
134
134
  assertReadFaceValue(deps.readFace, "readFace (deployment seat)");
135
+ const memoryDelegationEvidenceRaw = deps.memoryDelegationEvidence;
136
+ if (memoryDelegationEvidenceRaw !== undefined && memoryDelegationEvidenceRaw !== "static-face" && memoryDelegationEvidenceRaw !== "attested-only") {
137
+ const evidence = memoryDelegationEvidenceRaw;
138
+ const got = typeof evidence === "string"
139
+ ? JSON.stringify(evidence.length > 64 ? `${evidence.slice(0, 64)}…` : evidence)
140
+ : evidence === null
141
+ ? "null"
142
+ : Array.isArray(evidence)
143
+ ? "an array"
144
+ : typeof evidence;
145
+ const e = new Error(`RunnerDeps.memoryDelegationEvidence must be "static-face" or "attested-only" when present (got ${got}) — ` +
146
+ `an unevaluable evidence standard is refused loudly, never folded to either standard.`);
147
+ e.code = "config.memory_delegation_evidence";
148
+ throw e;
149
+ }
150
+ const memoryDelegationEvidence = memoryDelegationEvidenceRaw === "attested-only" ? "attested-only" : "static-face";
135
151
  if (spec.resumeAtMode !== undefined) {
136
152
  if (spec.resumeAt === undefined) {
137
153
  const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
@@ -294,6 +310,7 @@ export function prepareConfigDoors(input) {
294
310
  compModel,
295
311
  fableMitigations,
296
312
  modelGate,
313
+ memoryDelegationEvidence,
297
314
  usageWindows,
298
315
  brainCallGuardrailRef,
299
316
  brainCallGuardrailMs,
@@ -1,9 +1,10 @@
1
1
  import { sep } from "node:path";
2
+ import { deliverEngineNotice } from "../types.js";
2
3
  import { admitMemoryScopes } from "../memory-admission.js";
3
4
  import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
4
5
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
5
6
  import { normalizeMemorySpec } from "../memory.js";
6
- import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
7
+ import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine, memoryHarvestQuarantinedNotice, memorySessionPollutedNotice, pollutionContainmentCounts, } from "../memory-engine/engine.js";
7
8
  import { createMemoryEngineTools } from "../memory-engine/tools.js";
8
9
  import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
9
10
  import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
@@ -298,12 +299,26 @@ export async function prepareMemory(input) {
298
299
  }
299
300
  return writeEngine.gateWrite(writeHandle, w.key, w.content);
300
301
  };
302
+ let pollutionAnnounced = false;
303
+ const announceHarvestContainment = (report) => {
304
+ const { count, moved, escalated } = pollutionContainmentCounts(report);
305
+ if (count === 0)
306
+ return;
307
+ let reason;
308
+ try {
309
+ reason = writeEngine.sessionPollution(sessionId)?.reason;
310
+ }
311
+ catch {
312
+ }
313
+ deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(reason !== undefined ? { reason } : {}), sessionId }));
314
+ };
301
315
  const harvestSafe = async (phase = "terminal") => {
302
316
  try {
303
317
  const report = await harvestBoth();
304
318
  if (!report.ok && report.incident) {
305
319
  deps.onError?.(new Error(`memory harvest refused (${report.incident.kind}): ${report.incident.detail}`), { phase: "memory", sessionId });
306
320
  }
321
+ announceHarvestContainment(report);
307
322
  try {
308
323
  deps.onMemoryHarvestReport?.(report, { sessionId, phase });
309
324
  }
@@ -327,7 +342,13 @@ export async function prepareMemory(input) {
327
342
  harvest: harvestSafe,
328
343
  pollution: {
329
344
  polluted: () => writeEngine.sessionPollution(sessionId),
330
- markPolluted: (reason) => writeEngine.markSessionPolluted(sessionId, reason),
345
+ markPolluted: (reason) => {
346
+ const outcome = writeEngine.markSessionPolluted(sessionId, reason);
347
+ if (outcome === "existed" || (outcome === "unpersisted" && pollutionAnnounced))
348
+ return;
349
+ pollutionAnnounced = true;
350
+ deliverEngineNotice(deps.onNotice, memorySessionPollutedNotice({ reason, sessionId }));
351
+ },
331
352
  },
332
353
  contentSafety: {
333
354
  trustedTools: new Set(memorySpec.trustedTools ?? []),
@@ -341,7 +362,7 @@ export async function prepareMemory(input) {
341
362
  writeScope: memorySpec.writeScope,
342
363
  };
343
364
  if (input.memorySearchToolsPlanned)
344
- memoryTools = createMemoryEngineTools({ planes: toolPlanes });
365
+ memoryTools = createMemoryEngineTools({ planes: toolPlanes, sessionPollution: () => writeEngine.sessionPollution(sessionId) });
345
366
  }
346
367
  catch (err) {
347
368
  if (typeof err.code === "string" &&
@@ -1092,7 +1092,9 @@ export interface RunInternals {
1092
1092
  * it); `contentSafety` is the chain's FROZEN classification snapshot — the child may narrow it
1093
1093
  * with its own config, never widen (design/180 A-2). Trusted internals chain only, same posture as
1094
1094
  * {@link inheritedGate}. Absent ⇒ the child records nothing (its deliveries then read `unknown`,
1095
- * and every judgment falls back to the static floor — fail-closed by construction).
1095
+ * and every judgment falls back to the static floor — fail-closed by construction; whether the
1096
+ * floor's verdict MARKS the judging session follows that run's deployment evidence standard,
1097
+ * {@link RunnerDeps.memoryDelegationEvidence}).
1096
1098
  */
1097
1099
  delegationProvenance?: {
1098
1100
  ref: {
@@ -333,7 +333,7 @@ function cwdConflictsRestoreError(requestedCwd) {
333
333
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
334
334
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
335
335
  spec = doors.spec;
336
- const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
336
+ const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
337
337
  announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
338
338
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
339
339
  let shellGatedBash = false;
@@ -1855,6 +1855,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1855
1855
  const delegationProvenanceChannel = internals?.delegationProvenance;
1856
1856
  if (memoryEngineSession !== undefined || delegationProvenanceChannel !== undefined) {
1857
1857
  const pollution = memoryEngineSession?.pollution;
1858
+ const delegationEvidenceAttestedOnly = memoryDelegationEvidence === "attested-only";
1859
+ let staticMarkWaiverAnnounced = false;
1858
1860
  const effectiveSafety = narrowContentSafety(delegationProvenanceChannel?.contentSafety, memoryEngineSession?.contentSafety);
1859
1861
  const trustedTools = effectiveSafety.trustedTools;
1860
1862
  const execIsExternalContent = effectiveSafety.execIsExternalContent;
@@ -1934,8 +1936,28 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1934
1936
  }
1935
1937
  else if (att !== "clean") {
1936
1938
  if (external) {
1937
- mark(`tool "${t.name}" returned the output of a delegated agent whose tool face can reach external content` +
1938
- (typeof requested === "string" ? ` (subagent_type "${requested}")` : " (no subagent_type named — classified fail-closed)"));
1939
+ const staticFaceReason = `tool "${t.name}" returned the output of a delegated agent whose tool face can reach external content` +
1940
+ (typeof requested === "string" ? ` (subagent_type "${requested}")` : " (no subagent_type named — classified fail-closed)");
1941
+ if (!delegationEvidenceAttestedOnly) {
1942
+ mark(staticFaceReason);
1943
+ }
1944
+ else if (pollution !== undefined && !staticMarkWaiverAnnounced) {
1945
+ staticMarkWaiverAnnounced = true;
1946
+ const waivedReason = inlineUntrusted(staticFaceReason, 200);
1947
+ deliverEngineNotice(deps.onNotice, {
1948
+ code: "memory.delegation_static_mark_waived",
1949
+ message: `Delegation static-face pollution mark waived: ${waivedReason}. The deployment configured ` +
1950
+ `memoryDelegationEvidence="attested-only", so a static tool-face verdict alone does not mark this ` +
1951
+ `session's memory polluted; a delivered "external" attestation and a direct polluting-class ` +
1952
+ `invocation still mark, and on a provenance-armed chain this delegation still records as ` +
1953
+ `incomplete. At most one notice per prepared task leg.`,
1954
+ detail: {
1955
+ reason: waivedReason,
1956
+ ...(typeof requested === "string" ? { subagentType: inlineUntrusted(requested, 120) } : {}),
1957
+ sessionId,
1958
+ },
1959
+ });
1960
+ }
1939
1961
  }
1940
1962
  recordIncomplete();
1941
1963
  }
@@ -1661,6 +1661,7 @@ export class Runner {
1661
1661
  errorCode: code,
1662
1662
  ...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
1663
1663
  ...(taskIdRef.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: taskIdRef.effectiveMemoryScopes } : {}),
1664
+ ...(taskIdRef.effectiveReasoning !== undefined ? { effectiveReasoning: taskIdRef.effectiveReasoning } : {}),
1664
1665
  ...(() => {
1665
1666
  const hinted = err.retryAfterMs;
1666
1667
  return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
@@ -2468,23 +2469,24 @@ export class Runner {
2468
2469
  ts: Date.now(),
2469
2470
  }));
2470
2471
  }
2471
- if (prepared.thinking && prepared.thinking !== "off" && prepared.model.reasoning === true) {
2472
- emitTrace(rs.telemetry.tracer, () => {
2473
- const r = resolveReasoning(prepared.thinking, prepared.model);
2474
- return {
2475
- kind: "reasoning.resolved",
2476
- version: 1,
2477
- taskId: rs.telemetry.taskId,
2478
- model: prepared.model.id,
2479
- requested: r.requested,
2480
- effective: r.effective,
2481
- graded: r.graded,
2482
- clamped: r.clamped,
2483
- format: r.format,
2484
- endpoint: r.endpoint,
2485
- ts: Date.now(),
2486
- };
2487
- });
2472
+ const reasoningResolution = prepared.thinking && prepared.thinking !== "off" ? resolveReasoning(prepared.thinking, prepared.model) : undefined;
2473
+ if (taskIdRef && reasoningResolution !== undefined)
2474
+ taskIdRef.effectiveReasoning = reasoningResolution;
2475
+ if (reasoningResolution !== undefined) {
2476
+ emitTrace(rs.telemetry.tracer, () => ({
2477
+ kind: "reasoning.resolved",
2478
+ version: 1,
2479
+ taskId: rs.telemetry.taskId,
2480
+ model: prepared.model.id,
2481
+ requested: reasoningResolution.requested,
2482
+ effective: reasoningResolution.effective,
2483
+ graded: reasoningResolution.graded,
2484
+ clamped: reasoningResolution.clamped,
2485
+ format: reasoningResolution.format,
2486
+ endpoint: reasoningResolution.endpoint,
2487
+ ...(reasoningResolution.dropped === true ? { dropped: true } : {}),
2488
+ ts: Date.now(),
2489
+ }));
2488
2490
  }
2489
2491
  const effectiveTimeoutMs = spec.limits?.maxWalltimeMs;
2490
2492
  const walltimeMonotonicDeadline = effectiveTimeoutMs !== undefined ? rs.telemetry.taskStartMonotonic + effectiveTimeoutMs : undefined;
@@ -3458,6 +3460,7 @@ export class Runner {
3458
3460
  effectiveReadFace: prepared.effectiveReadFace,
3459
3461
  effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
3460
3462
  effectiveMemoryScopes: prepared.effectiveMemoryScopes,
3463
+ effectiveReasoning: reasoningResolution,
3461
3464
  retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
3462
3465
  abortedForTimeout: timeout.fired,
3463
3466
  abortedForTurns: rs.limits.turnsExceeded,
@@ -143,8 +143,13 @@ export type TraceEvent = {
143
143
  /**
144
144
  * How a task's requested reasoning intensity RESOLVED against the model's real capability (design/96 S6).
145
145
  * Emitted once at task start when thinking is on, so a deployment can SEE — not silently swallow (§E
146
- * honesty red-line) — that a binary provider ignored the tier (`graded:false`) or that an effort endpoint
147
- * clamped it down (`clamped:true`). Metadata-only (tiers + format + endpoint, never prompt content).
146
+ * honesty red-line) — that a binary provider ignored the tier (`graded:false`), that an effort endpoint
147
+ * clamped it down (`clamped:true`), or that a NON-reasoning model dropped the request entirely
148
+ * (`dropped:true` — the frame fires for that model too; it used to be the one arm with no report).
149
+ * Metadata-only (tiers + format + endpoint, never prompt content). The resolution is against the
150
+ * task's PRIMARY serving model at leg entry (same law as `TaskResult.model`): a mid-run
151
+ * degradation does not re-emit this frame — the switch is observed on its own seats
152
+ * (`TaskResult.degraded`).
148
153
  */
149
154
  kind: "reasoning.resolved";
150
155
  version: 1;
@@ -163,6 +168,16 @@ export type TraceEvent = {
163
168
  format: string;
164
169
  /** Coarse endpoint label (`model.api`, e.g. `openai-completions` / `anthropic-messages`). */
165
170
  endpoint: string;
171
+ /**
172
+ * Present (true) only when the model declares NO reasoning capability (`Model.reasoning` falsy): the
173
+ * brains send no thinking parameter at all, so the requested tier was DROPPED — not clamped.
174
+ * `effective:"off"` states the ENGINE side (nothing was requested), not a measured gateway state:
175
+ * on the binary enable-only formats (qwen/zai/qwen-chat-template) parameter absence is
176
+ * provider-default and a default-on gateway may still reason at its own tier — read `format` to
177
+ * know which family applies. `format`/`endpoint` report the family that WOULD have carried the
178
+ * tier. Absent on every reasoning-capable resolution (old consumers see identical frames).
179
+ */
180
+ dropped?: true;
166
181
  /** design/148 S3 (additive, §10.2): the nine-element cache identity — digests only. */
167
182
  snapshot?: {
168
183
  cacheIdentity: string;
@@ -846,7 +846,9 @@ export interface ToolExecuteContext {
846
846
  * content-safety snapshot — the tool then mints the child's recorder ref and threads both into
847
847
  * the child's trusted `RunInternals.delegationProvenance`. Undefined return / absent field ⇒ the
848
848
  * child spawns without a recorder (its deliveries read `unknown` and every judgment stays on the
849
- * static floor — v1 behavior byte-identical). Same trust posture as
849
+ * static floor — whose MARK action follows the judging run's deployment evidence standard,
850
+ * {@link RunnerDeps.memoryDelegationEvidence}; under the `"static-face"` default this is v1
851
+ * behavior byte-identical). Same trust posture as
850
852
  * {@link inheritedGateForChildren}: never a model/tool argument, never a TaskSpec field.
851
853
  */
852
854
  delegationProvenanceForChildren?: () => import("./memory-engine/delegation-provenance.js").DelegationContentSafety | undefined;
@@ -2839,6 +2841,29 @@ export interface TaskResult {
2839
2841
  * `TaskSpec.model` ref (which may be a role / name / `@mention`). Lets a UI echo "served by X" instead of the
2840
2842
  * requested ref. A mid-run degradation is observed separately (see the degraded-model fields). */
2841
2843
  model?: string;
2844
+ /**
2845
+ * #327 — the leg's effective REASONING resolution: the result-face twin of {@link model} for the thinking
2846
+ * knob. How the requested tier resolved against the serving model's real capability
2847
+ * (`requested`/`effective`/`graded`/`clamped`/`format`/`endpoint`, plus `dropped:true` when a
2848
+ * non-reasoning model dropped the request entirely — field semantics on
2849
+ * {@link import("../brain/reasoning.js").ResolvedReasoning}). It is the SAME resolver output the
2850
+ * `reasoning.resolved` trace frame carries, computed once per leg — the two faces cannot tell different
2851
+ * stories; this seat serves consumers without a tracer (the trace frame is the deployment-observability
2852
+ * face, this is the caller face).
2853
+ *
2854
+ * **In-presence condition** — mirrors the trace frame exactly: present on every terminal of a leg that ran
2855
+ * with a REQUESTED thinking tier other than off/unset (the `spec > role > model.defaultThinking` chain);
2856
+ * absent when thinking was off/unset for the leg, and on prepare failures (the resolution is minted after
2857
+ * prepare). On a resumed task each leg re-resolves against the leg's own serving model.
2858
+ *
2859
+ * **Degradation law** — same as {@link model}, whose resolution this is: the seat describes the leg's
2860
+ * PRIMARY serving model at leg entry. A mid-run degradation (reactive fallback / near-budget switch)
2861
+ * changes the serving model WITHOUT re-minting this seat or its trace twin — read {@link degraded} to see
2862
+ * the switch; the fallback's own reasoning capability is deliberately NOT re-reported here (re-resolving
2863
+ * one face would desync it from the task-start `reasoning.resolved` frame). A consumer needing the
2864
+ * fallback's reasoning posture resolves `degraded.to` itself (`resolveReasoning` is exported).
2865
+ */
2866
+ effectiveReasoning?: import("../brain/reasoning.js").ResolvedReasoning;
2842
2867
  /** Final assistant text. */
2843
2868
  result: string;
2844
2869
  /**
@@ -4683,6 +4708,50 @@ export interface EngineNotice {
4683
4708
  * `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
4684
4709
  * (the terminal sweep is a single site).
4685
4710
  *
4711
+ * - `"memory.session_polluted"` (design/178 §3, #324a) — this session's memory crossed into the
4712
+ * one-way POLLUTED state (a tool classified as an external content source was invoked, directly
4713
+ * or through a delegated child): its memory writes are no longer eligible for the long-term
4714
+ * library. The notice states that VERDICT plus what a harvest does when it collects — it does
4715
+ * not promise quarantine, because a `writeScope`-null layering and a declared-unavailable
4716
+ * session both reach the zero-admission harvest arm (nothing collected ⇒ nothing captured), and
4717
+ * a mark landing after the pre-commit pollution read leaves already-committed rows to the
4718
+ * challenge sweep; what a harvest actually contained is the other code below. The challenge
4719
+ * clause is a policy statement, not a receipt — a sweep that cannot run announces
4720
+ * `"memory.challenge_sweep_failed"` on the engine's incident seat. Announced ONCE PER
4721
+ * SESSION, at the mark seat: a repeat mark inside the process and a resumed session whose
4722
+ * durable marker already exists stay quiet (the state is one-way, so a second line would carry
4723
+ * no new fact); a pollution state that cannot be read announces rather than assuming it was
4724
+ * already said. `detail: { reason, sessionId? }` — `reason` names the invoked tool and is
4725
+ * neutralized/length-bounded (the name comes from the host/protocol roster).
4726
+ * - `"memory.harvest_quarantined"` (design/178 §3, #324a) — a polluted session's harvest ran its
4727
+ * containment: `count` ENTRY files written or changed in the session were withheld from the
4728
+ * library, `moved` of them were physically moved into the control-plane quarantine directory,
4729
+ * and `escalated` of them carry a `HarvestReport.quarantineFailures` row (a failed capture, a
4730
+ * failed removal, or a file tombstoned in place — which counts as moved AND escalated, so the
4731
+ * two numbers are read off the report's rows, never subtracted from each other). One notice PER
4732
+ * HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
4733
+ * are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
4734
+ * reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read at report
4735
+ * time — the withheld count stays true either way). Registered gap: the derived index (`MEMORY.md`) is
4736
+ * contained on a path that mints no rejection row, so an index-ONLY containment produces no
4737
+ * notice and is disclosed by the harvest report's warnings alone.
4738
+ * - `"memory.delegation_static_mark_waived"` (design/324, #324 ruling ①) — the deployment set
4739
+ * {@link RunnerDeps.memoryDelegationEvidence} to `"attested-only"` and a delegation call whose
4740
+ * STATIC tool-face verdict would have marked this session's memory polluted (attestation
4741
+ * missing/unknown + face can reach external content) was not marked: the waiver is announced —
4742
+ * the explicit dual of `"memory.session_polluted"` for this arm, so the arm has a voice where a
4743
+ * mark used to land. MINT-side guarantee only: delivery rides the shared guarded form
4744
+ * ({@link deliverEngineNotice} — a wired sink that throws owns that loss, exactly as at every
4745
+ * de-duplicating station), and the leg latch is consumed at mint. Minted only when a pollution
4746
+ * face is mounted (a
4747
+ * recorder-only child's mark is a no-op — nothing is waived there), at most ONCE PER PREPARED
4748
+ * TASK LEG (a resume leg may announce again — a new leg's audit stream is a new fact;
4749
+ * deliberately no durable once-per-session state), never on the `"static-face"` default, and
4750
+ * never for a delivered `"external"` attestation (that mark still lands);
4751
+ * `detail: { reason, subagentType?, sessionId? }` — `reason` is the same sentence the waived
4752
+ * mark would have carried, neutralized/length-bounded (tool and agent-type names are
4753
+ * host/model-controlled inputs).
4754
+ *
4686
4755
  * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
4687
4756
  * transient network failure being retried). Those are per-attempt liveness frames with their own
4688
4757
  * frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
@@ -4830,6 +4899,36 @@ export interface RunnerDeps {
4830
4899
  * bypasses the gate as always — the human already adjudicated it).
4831
4900
  */
4832
4901
  writeProtectedPaths?: readonly import("./write-protect.js").WriteProtectedEntry[];
4902
+ /**
4903
+ * design/324 (#324 ruling ① containment) — the EVIDENCE STANDARD the delegation arm of the
4904
+ * content-origin wrap applies when deciding whether a delegation call marks THIS session's memory
4905
+ * polluted (design/178 §3 / design/180 half A):
4906
+ * - `"static-face"` (absent ≡ this; the default) — today's behavior, byte-identical: a delegation
4907
+ * whose delivered attestation is missing/unknown and whose static tool face can reach external
4908
+ * content marks the session (the capability over-approximation: possibility counts as exposure).
4909
+ * - `"attested-only"` — exactly that ONE static-face mark is waived, and each prepared leg
4910
+ * announces the first waiver (`"memory.delegation_static_mark_waived"`). Everything else is
4911
+ * unchanged: a delivered `"external"` attestation still marks, the chain's `incomplete`
4912
+ * recording still happens, a non-delegation polluting-class tool still marks, and a delegation
4913
+ * tool that is ITSELF classified polluting still marks pre-call.
4914
+ * ACCEPTED COST (the deployment's to own, stated as mechanism, not as absence of risk): under
4915
+ * `"attested-only"` a BACKGROUND child's real external contact does not mark this session — its
4916
+ * content re-enters through the TaskOutput result, the task-notification injection, or the
4917
+ * AgentTranscript step summaries, none of which carries an attestation — and a foreground child
4918
+ * that ended abnormally (crash/salvage) is likewise not marked on its face alone (the chain still
4919
+ * records `incomplete`). Already-marked sessions are never retroactively cleaned; the key only
4920
+ * governs NEW marks.
4921
+ * DEPLOYMENT seat ONLY (same posture as {@link readDenyBuiltinTiers}): deliberately no TaskSpec
4922
+ * twin and not in the governed workflow whitelist — a task author or governed script gets no
4923
+ * channel to loosen the evidence standard below its deployment. Not frozen into checkpoints: a
4924
+ * resumed leg follows the CURRENT deployment configuration. Each run reads the deps of the Runner
4925
+ * that PREPARES it — a multi-runner assembly should configure every runner with the same value
4926
+ * (drift is the deployment's own configuration hazard; recorder/attestation semantics are
4927
+ * value-independent, so the chain's evidence quality never varies with this key). Any other value
4928
+ * refuses loudly at prepare (`config.memory_delegation_evidence`, #123 — exact spellings only,
4929
+ * never truthiness).
4930
+ */
4931
+ memoryDelegationEvidence?: "static-face" | "attested-only";
4833
4932
  /**
4834
4933
  * design/199 件A — the DEPLOYMENT's read-face declaration
4835
4934
  * ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
@@ -5,9 +5,10 @@ import type { ReadFace } from "./read-face.js";
5
5
  /**
6
6
  * The Grep card's details assembly, PURE over the engine text ({mode, offset} from the request) —
7
7
  * exported so the text→structured mapping is pinnable with synthetic texts (the byte-truncation and
8
- * fenced-partial shapes are impractical to construct through a live tool call). Follow-up #313 will
9
- * replace the text parsing with structured rows from runGrep; until then this seam is the honesty
10
- * boundary.
8
+ * fenced-partial shapes are impractical to construct through a live tool call). #313 moved PATH
9
+ * IDENTITY off the text and onto the engines' served rows (both legs supply them now); the counts
10
+ * and totals below still read the engine's own honesty markers out of the text, which is what keeps
11
+ * this seam the honesty boundary.
11
12
  */
12
13
  export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
13
14
  export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
@@ -190,7 +190,27 @@ export interface ReadDenyJudge {
190
190
  /** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
191
191
  * design/199 件B: `deny` prunes the walk (note appended to every output shape); `denyOut` is the
192
192
  * structured twin — a sink because this function's dozen error returns predate the facts (the sink
193
- * is written the moment the walk lands, whatever the scan then returns). */
193
+ * is written the moment the walk lands, whatever the scan then returns).
194
+ *
195
+ * backlog #313 — the SERVED rows ride back next to the text. This scanner knows every result's
196
+ * identity natively (the walk hands it whole paths), so a row's `path` is that walked identity,
197
+ * never a cut of the rendered line — the mis-split family the text re-parse carries cannot exist
198
+ * here. The rows ARE the window `text` shows: post offset, post row cap, post byte ceiling, in the
199
+ * same order. The unit is the RECORD, which is the unit the cap has always counted: one record is
200
+ * one rendered line EXCEPT where the file's own name carries a newline, and then the one record
201
+ * spans two physical lines — the same relation {@link formatRgRecords} states for the rg leg, and
202
+ * precisely the case where a row's identity beats anything the text can be split into.
203
+ * Every row carries a path (this leg emits no group-separator row — the one pathless shape
204
+ * ripgrep's output can carry), and a no-match run's row set is `[]`, not absent. A typed
205
+ * `Error (grep): …` return carries no rows at all: a refusal is not a row set. */
206
+ export declare function jsGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
207
+ withheld?: ReadDenyWithheld;
208
+ }): Promise<{
209
+ text: string;
210
+ rows?: readonly GrepRow[];
211
+ }>;
212
+ /** Text-only wrapper of {@link jsGrepDetailed} (the shape callers that never look at the served rows
213
+ * want — the same relation {@link runGrep} has to {@link runGrepDetailed}). */
194
214
  export declare function jsGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
195
215
  withheld?: ReadDenyWithheld;
196
216
  }): Promise<string>;
@@ -210,18 +230,34 @@ export type GrepDegradation = {
210
230
  };
211
231
  /** Structured grep result: the model-facing text plus the degradation facts, so the tool layer can
212
232
  * ship them on the structured frame instead of leaving them prose-only. */
213
- /** #313 (first stage, rg legs) — one SERVED result row with its path read from ripgrep's own
214
- * field ({@link parseRgRecords}), exactly the window `text` shows (post cap/offset). The tool
215
- * layer prefers these over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
216
- * itself contains `:digits:`); absent = a leg that has no structured rows yet (the JS scanner —
217
- * its rows stage is the ticket's remainder) and the text parse is the honest fallback. */
233
+ /** #313 — one SERVED result RECORD with its path taken from the engine's own knowledge, exactly the
234
+ * window `text` shows (post cap/offset): ripgrep's `--null` path field ({@link parseRgRecords}) on
235
+ * the rg legs, the walked file identity ({@link jsGrepDetailed}) on the JS-scanner legs. A record is
236
+ * one rendered line except where a path carries a newline (see {@link formatRgRecords}), which is
237
+ * also the case the text can no longer be split into records at all. The tool layer prefers these
238
+ * over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
239
+ * itself contains `:digits:`).
240
+ *
241
+ * KNOWN EXCEPTION to "exactly the window `text` shows", stated because it is measurable: on the two
242
+ * ripgrep PARTIAL legs (timeout / error-exit with output) the delivered text is fenced by
243
+ * `delimitUntrusted`, which NEUTRALIZES fence markers inside the body, while the rows carry the
244
+ * record bytes as ripgrep emitted them. A row's `text` there is therefore the un-neutralized form —
245
+ * identity (`path`) is unaffected. Consume `text` for anything rendered; the rows' contract is
246
+ * identity. Reconciling the two (defuse per record, or withhold rows where the text is fenced) is a
247
+ * contract choice recorded for the backlog, not settled here.
248
+ *
249
+ * `path` is absent only where the record genuinely carries none —
250
+ * ripgrep's `--` group separator and the unaccountable shapes listed on {@link RgRecord} — and a
251
+ * row set carrying such a row sends the tool layer back to the text parse for identity. */
218
252
  export interface GrepRow {
219
253
  path?: string;
220
254
  text: string;
221
255
  }
222
256
  export interface GrepRunResult {
223
257
  text: string;
224
- /** Served rows (#313): present on the ripgrep legs, absent on the JS-scanner legs. */
258
+ /** Served rows (#313): present on BOTH engine legs ripgrep's and the JS scanner's, including the
259
+ * rescan legs. Absent only when the JS scanner refused the search outright (a typed
260
+ * `Error (grep): …` text): a refusal has no row set, truthfully, rather than an empty one. */
225
261
  rows?: readonly GrepRow[];
226
262
  degraded?: GrepDegradation;
227
263
  /** design/199 件B — deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing