@sema-agent/core 7.20.1 → 7.21.1

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 (47) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/core/ask-origin.d.ts +95 -8
  3. package/dist/core/ask-origin.js +37 -5
  4. package/dist/core/gate-bounded-await.d.ts +6 -1
  5. package/dist/core/gate-bounded-await.js +13 -3
  6. package/dist/core/gate-fold.js +18 -5
  7. package/dist/core/gate-lanes.js +6 -12
  8. package/dist/core/memory-engine/consolidation-driver.d.ts +3 -0
  9. package/dist/core/memory-engine/consolidation-driver.js +20 -1
  10. package/dist/core/memory-engine/consolidation.d.ts +7 -0
  11. package/dist/core/memory-engine/consolidation.js +4 -1
  12. package/dist/core/memory-engine/distiller.d.ts +26 -1
  13. package/dist/core/memory-engine/distiller.js +9 -2
  14. package/dist/core/path-identity.d.ts +13 -0
  15. package/dist/core/path-identity.js +14 -0
  16. package/dist/core/runner/env-capability-census.d.ts +3 -1
  17. package/dist/core/runner/env-capability-census.js +24 -7
  18. package/dist/core/runner/gate-exit.d.ts +7 -0
  19. package/dist/core/runner/inherited-ask-grants.d.ts +1 -0
  20. package/dist/core/runner/inherited-ask-grants.js +5 -2
  21. package/dist/core/runner/memory-consolidation-driver.d.ts +73 -2
  22. package/dist/core/runner/memory-consolidation-driver.js +57 -0
  23. package/dist/core/runner/prepare-config-doors.js +2 -0
  24. package/dist/core/runner/prepare-policy-chain.d.ts +1 -0
  25. package/dist/core/runner/prepare-policy-chain.js +52 -20
  26. package/dist/core/runner/prepare-wiring-manifest.js +2 -0
  27. package/dist/core/runner/run-settle-and-teardown.js +5 -0
  28. package/dist/core/runner-deps.d.ts +21 -0
  29. package/dist/core/sensitive-path-policy.js +13 -0
  30. package/dist/core/wiring-manifest.d.ts +16 -0
  31. package/dist/core/wiring-manifest.js +1 -0
  32. package/dist/core/write-protect.d.ts +14 -2
  33. package/dist/core/write-protect.js +36 -13
  34. package/dist/engine/execution-env/node-execution-env.d.ts +10 -0
  35. package/dist/engine/execution-env/node-execution-env.js +29 -1
  36. package/dist/engine/harness/types.d.ts +10 -0
  37. package/dist/index.d.ts +1 -1
  38. package/dist/internal/harness-types.d.ts +1 -1
  39. package/dist/tools/fs/bash-readonly-classifier.js +35 -6
  40. package/dist/tools/fs/read-deny.d.ts +4 -2
  41. package/dist/tools/fs/read-deny.js +5 -0
  42. package/dist/tools/fs/safety.d.ts +12 -0
  43. package/dist/tools/fs/safety.js +43 -38
  44. package/package.json +2 -1
  45. package/test/export-surface.snapshot.json +3 -1
  46. package/wire-changes/core@7.21.0.json +452 -0
  47. package/wire-changes/core@7.21.1.json +41 -0
@@ -75,6 +75,10 @@ export declare const LLM_DISTILLER_CONTRACTS: Record<string, LlmDistillerContrac
75
75
  * left hanging. The exposure-partitioned topology itself deliberately did NOT bump this version
76
76
  * (the candidate-set composition is topology, not wording; the wording axis stays single-variable
77
77
  * for exactly this A/B).
78
+ *
79
+ * WHERE THAT OBLIGATION NOW LIVES IN CODE: the root-elimination arm is the writing-channel option pair on
80
+ * {@link mintLlmConsolidationPlan} (`writingIntent: "none-stated"` + `productKeyFallback: false`, recorded as
81
+ * `minting.writingChannel`); the A/B readings, verdict and ruling are tracked in `docs/memory-distillation-ga.json`.
78
82
  */
79
83
  export declare const MEMORY_DISTILLER_CONTRACT_V1: LlmDistillerContract;
80
84
  /**
@@ -314,6 +318,12 @@ export interface LlmConsolidationPlan {
314
318
  * single-call-chain plans, INCLUDING the zero-marked degenerate of the partitioned mint
315
319
  * (whose bytes must stay identical to the plain mint's, modulo the attestation echo). */
316
320
  arms?: LlmConsolidationPlanArm[];
321
+ /** pd-2 root-elimination A/B — the WRITING CHANNEL this plan was minted under, PRESENT ONLY when non-default,
322
+ * so a default plan's bytes are unchanged and a reading can attribute its arm from the archive alone. */
323
+ writingChannel?: {
324
+ intent: "grouping" | "none-stated";
325
+ keyFallback: boolean;
326
+ };
317
327
  };
318
328
  }
319
329
  export type MintLlmConsolidationPlanResult = {
@@ -330,7 +340,7 @@ export type MintLlmConsolidationPlanResult = {
330
340
  * it answered is returned inside the plan so the run can be re-read, re-scored and disputed from
331
341
  * the archive alone.
332
342
  */
333
- export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, armLabel, }: {
343
+ export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, armLabel, writingIntent, productKeyFallback, }: {
334
344
  candidates: readonly DistillerCandidate[];
335
345
  chat: DistillerChatFn;
336
346
  /** The requested model id, recorded into the plan (audit anchor). Alias spellings refuse. */
@@ -353,6 +363,21 @@ export declare function mintLlmConsolidationPlan({ candidates, chat, model, base
353
363
  * archive only, and stays compatible with `planParseRepairs`' `startsWith("grouping")` phase
354
364
  * split and its `#N` retry grammar. Absent ⇒ the historical label forms, byte-identical. */
355
365
  armLabel?: "clean" | "marked";
366
+ /**
367
+ * pd-2 root-elimination A/B (the standing obligation on {@link MEMORY_DISTILLER_CONTRACT_V1}) — the WRITING
368
+ * CHANNEL: which grouping-authored bytes reach the writing pass. `"grouping"` (absent ≡ this, the shipped form)
369
+ * fills `{{INTENT}}` with the grouping pass's own `intent`; `"none-stated"` fills it with `(none stated)` for
370
+ * EVERY group, so the clean arm's authoring prompt carries no grouping-authored intent bytes. Mint TOPOLOGY, not
371
+ * wording: the contract's frozen text is untouched and no version moves. Recorded on the plan
372
+ * (`minting.writingChannel`) whenever non-default.
373
+ */
374
+ writingIntent?: "grouping" | "none-stated";
375
+ /**
376
+ * The channel's second byte path: `true` (absent ≡ this) names a product after the grouping-authored `key` when
377
+ * the writing answer carries no `name`; `false` treats a missing or empty `name` as a FORMAT failure (retried,
378
+ * then a counted write failure — R3), so nothing grouping-authored ever becomes a product's name on this arm.
379
+ */
380
+ productKeyFallback?: boolean;
356
381
  }): Promise<MintLlmConsolidationPlanResult>;
357
382
  /** ONE rev sentinel for every face that RECORDS or RE-CHECKS a candidate's rev (the mint roster
358
383
  * below, the driver's cross-cycle roster re-check, the replay's offered map). The snapshot
@@ -259,8 +259,9 @@ function assertMintConfig(chat, model) {
259
259
  throw distillerConfigRefusal(`mintLlmConsolidationPlan needs an EXPLICIT model id, got ${JSON.stringify(model)} — an alias can be re-pointed upstream and would move the run with no trace in the plan.`);
260
260
  }
261
261
  }
262
- export async function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct = DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, contract = MEMORY_DISTILLER_CONTRACT_V1, onProgress = null, armLabel, }) {
262
+ export async function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct = DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, contract = MEMORY_DISTILLER_CONTRACT_V1, onProgress = null, armLabel, writingIntent, productKeyFallback, }) {
263
263
  assertMintConfig(chat, model);
264
+ const writingChannel = writingIntent === "none-stated" || productKeyFallback === false ? { intent: writingIntent ?? "grouping", keyFallback: productKeyFallback !== false } : undefined;
264
265
  const rows = candidates.map((c, i) => ({ n: i + 1, id: c?.entry?.id, rev: c?.entry?.rev, text: renderCandidate(i + 1, c) }));
265
266
  const missingId = rows.filter((r) => typeof r.id !== "string");
266
267
  if (missingId.length > 0)
@@ -310,7 +311,7 @@ export async function mintLlmConsolidationPlan({ candidates, chat, model, baseUr
310
311
  const writeFailures = [];
311
312
  for (const [i, g] of sanitized.groups.entries()) {
312
313
  const memberText = g.members.map((m) => byId.get(m.id)?.text ?? "").join("\n\n");
313
- const user = fill(contract.writing.instructions, { INTENT: g.intent || "(none stated)", MEMBERS: memberText });
314
+ const user = fill(contract.writing.instructions, { INTENT: writingIntent === "none-stated" ? "(none stated)" : g.intent || "(none stated)", MEMBERS: memberText });
314
315
  let written = null;
315
316
  let err = null;
316
317
  for (let attempt = 0; attempt <= contract.retries && written === null; attempt++) {
@@ -330,6 +331,10 @@ export async function mintLlmConsolidationPlan({ candidates, chat, model, baseUr
330
331
  err = "the object carries no non-empty `body`";
331
332
  continue;
332
333
  }
334
+ if (productKeyFallback === false && (typeof value.name !== "string" || value.name.trim() === "")) {
335
+ err = "the object carries no non-empty `name` (no grouping-key fallback on this arm)";
336
+ continue;
337
+ }
333
338
  written = parsed.value;
334
339
  }
335
340
  if (written === null) {
@@ -371,6 +376,7 @@ export async function mintLlmConsolidationPlan({ candidates, chat, model, baseUr
371
376
  usage,
372
377
  prompts: { grouping: groupingUser, writingTemplate: contract.writing.instructions, systems: { grouping: contract.grouping.system, writing: contract.writing.system } },
373
378
  transcript: calls,
379
+ ...(writingChannel !== undefined ? { writingChannel } : {}),
374
380
  },
375
381
  },
376
382
  };
@@ -455,6 +461,7 @@ export async function mintExposurePartitionedPlan(args) {
455
461
  },
456
462
  transcript,
457
463
  ...(arms.length > 0 ? { arms: arms.map((a) => armUsageOf(a.tag, a.res, a.count)) } : {}),
464
+ ...(anchor?.minting.writingChannel !== undefined ? { writingChannel: anchor.minting.writingChannel } : {}),
458
465
  },
459
466
  },
460
467
  };
@@ -0,0 +1,13 @@
1
+ import type { ExecutionEnv } from "../internal/harness-types.js";
2
+ import { type PathFamily } from "../tools/fs/safety.js";
3
+ /**
4
+ * Every spelling the leaf symlink chain of `path` passes through, or `[]`.
5
+ *
6
+ * `[]` has ONE reading — nothing to judge beyond the spelling and the terminal — reached three ways that are
7
+ * deliberately the same word: no `canonicalChain` declared, `ok: false`, or an empty chain. An unanswerable
8
+ * chain is never a refusal: the terminal's own canonicalization already refuses what it cannot resolve, and a
9
+ * second refusal law over one target would be a second rule. The spelling is anchored as the canonicalizer
10
+ * anchors it (`baseCwd`). A refused namespace spelling and a win-form UNC spelling are not walked (the reference
11
+ * CLI's variant set stops at a network spelling); the share is recognized by the throat's own `isUncSpelling`.
12
+ */
13
+ export declare function symlinkChainHops(env: ExecutionEnv, path: string, signal: AbortSignal | undefined, family: PathFamily, baseCwd?: string): Promise<readonly string[]>;
@@ -0,0 +1,14 @@
1
+ import { isAbsoluteForFamily, isUncSpelling, joinForFamily, win32NamespaceScreen } from "../tools/fs/safety.js";
2
+ export async function symlinkChainHops(env, path, signal, family, baseCwd) {
3
+ if (typeof env.canonicalChain !== "function")
4
+ return [];
5
+ const screened = win32NamespaceScreen(path, family);
6
+ if (!screened.ok)
7
+ return [];
8
+ const spelled = screened.spelled;
9
+ if (family === "win" && isUncSpelling(spelled))
10
+ return [];
11
+ const target = baseCwd !== undefined && baseCwd.length > 0 && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled;
12
+ const chain = await env.canonicalChain(target, signal);
13
+ return chain.ok ? chain.value.hops : [];
14
+ }
@@ -1,9 +1,11 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness.js";
2
2
  import type { EngineNotice } from "../engine-notice.js";
3
- /** One capability key whose declared value is not a boolean, with the type it actually carries. */
3
+ /** One declaration whose value is not the shape its seat takes, with the type it actually carries. */
4
4
  export interface MalformedCapabilityDeclaration {
5
5
  readonly key: string;
6
6
  readonly valueType: string;
7
+ /** The shape the seat takes: a `capabilities.*` boolean, or an optional seam method. */
8
+ readonly expected: "boolean" | "function";
7
9
  }
8
10
  /**
9
11
  * Every boolean capability this env DECLARES with a value that is not a boolean.
@@ -1,37 +1,54 @@
1
1
  import { isRemoteExecutionEnv } from "../remote-env.js";
2
2
  import { deliverEngineNotice } from "../engine-notice.js";
3
3
  const BOOLEAN_CAPABILITY_KEYS = ["isolation", "suspendable", "canonicalPathAuthoritative"];
4
+ const OPTIONAL_METHOD_KEYS = ["writeFileExclusive", "writeFileGuarded", "setFileMode", "readLink", "canonicalChain"];
4
5
  export function malformedCapabilityDeclarations(env) {
6
+ const out = [];
7
+ for (const key of OPTIONAL_METHOD_KEYS) {
8
+ let value;
9
+ try {
10
+ value = Reflect.get(env, key);
11
+ }
12
+ catch {
13
+ out.push({ key, valueType: "unreadable", expected: "function" });
14
+ continue;
15
+ }
16
+ if (value === undefined || typeof value === "function")
17
+ continue;
18
+ out.push({ key, valueType: value === null ? "null" : typeof value, expected: "function" });
19
+ }
5
20
  let caps;
6
21
  try {
7
22
  if (!isRemoteExecutionEnv(env))
8
- return [];
23
+ return out;
9
24
  caps = env.capabilities;
10
25
  }
11
26
  catch {
12
- return [{ key: "capabilities", valueType: "unreadable" }];
27
+ out.push({ key: "capabilities", valueType: "unreadable", expected: "boolean" });
28
+ return out;
13
29
  }
14
- const out = [];
15
30
  for (const key of BOOLEAN_CAPABILITY_KEYS) {
16
31
  let value;
17
32
  try {
18
33
  value = caps[key];
19
34
  }
20
35
  catch {
21
- out.push({ key, valueType: "unreadable" });
36
+ out.push({ key, valueType: "unreadable", expected: "boolean" });
22
37
  continue;
23
38
  }
24
39
  if (value === undefined || typeof value === "boolean")
25
40
  continue;
26
- out.push({ key, valueType: value === null ? "null" : typeof value });
41
+ out.push({ key, valueType: value === null ? "null" : typeof value, expected: "boolean" });
27
42
  }
28
43
  return out;
29
44
  }
30
45
  export function announceMalformedCapabilities(env, onNotice) {
31
- for (const { key, valueType } of malformedCapabilityDeclarations(env)) {
46
+ for (const { key, valueType, expected } of malformedCapabilityDeclarations(env)) {
32
47
  deliverEngineNotice(onNotice, {
33
48
  code: "config.execution_env_capability_invalid",
34
- message: `The execution environment declares capabilities.${key} as a ${valueType}, not a boolean. What a reader makes of that is the reader's own business and differs between them — the containment readers take only the boolean, so this reads as undeclared, while the suspend reader takes any truthy value, so a non-empty string reads as TRUE. That is why a declaration of the wrong shape cannot pass quietly: the engine may act on something other than what the adapter meant. Fix the adapter — this is a wiring fault, not a supported way to leave the capability unset.`,
49
+ message: expected === "function"
50
+ ? `The execution environment declares ${key} as a ${valueType}, not a function. This seat is declared by being present as a callable method and undeclared by being absent, so every reader takes the absent reading — the reach the adapter meant to declare is silently the old one. Fix the adapter — this is a wiring fault, not a supported way to leave the seat unset.`
51
+ : `The execution environment declares capabilities.${key} as a ${valueType}, not a boolean. What a reader makes of that is the reader's own business and differs between them — the containment readers take only the boolean, so this reads as undeclared, while the suspend reader takes any truthy value, so a non-empty string reads as TRUE. That is why a declaration of the wrong shape cannot pass quietly: the engine may act on something other than what the adapter meant. Fix the adapter — this is a wiring fault, not a supported way to leave the capability unset.`,
35
52
  detail: { key, valueType },
36
53
  });
37
54
  }
@@ -251,6 +251,13 @@ export interface GatePass {
251
251
  * leaves the call's boundary UNKNOWN: the allow layer (a stored allow rule, the read-only reader) must
252
252
  * not clear the surviving ask — a person confirms it. Writer: the fold. Reader: the lanes' mandate. */
253
253
  probeUnanswered?: true;
254
+ /** borrowed-mutable — the write-protection judge covered this call and its table MATCHED: the allow
255
+ * layer must not clear the surviving ask, a live decider still may. Writer: the fold's fourth tighten,
256
+ * on BOTH its arms. Reader: the lanes' mandate. */
257
+ writeProtected?: true;
258
+ /** borrowed-mutable — the same judge could NOT RESOLVE the target: no automatic lane may answer the
259
+ * ask. Writer and reader as above; the pair is mutually exclusive (one verdict per judgment). */
260
+ writeProtectionUnresolved?: true;
254
261
  /** borrowed-mutable — the rewrite remembered BEFORE a hook-ask promotion / tighten, re-applied on the
255
262
  * approved path (a redaction is not cancelled because a person confirmed). Writer: the fold. Readers: the
256
263
  * fold's probe and write-protection judges, the lanes, the allow exit. */
@@ -33,6 +33,7 @@ export declare function consumeInheritedAskGrant(grants: Map<string, {
33
33
  argsJson: string;
34
34
  presented: unknown;
35
35
  waitMs: number;
36
+ settled?: object;
36
37
  }>, onAsk: OnAsk | undefined, humanReviewRef: {
37
38
  count: number;
38
39
  totalWaitMs: number;
@@ -1,5 +1,5 @@
1
1
  import { primaryActivityArg } from "../arg-summary.js";
2
- import { askApproverIdentity } from "../tool-policy.js";
2
+ import { askApproverIdentity, carryEngineAttestations } from "../tool-policy.js";
3
3
  export function askGrantShapeOf(args) {
4
4
  try {
5
5
  return JSON.stringify(args ?? null);
@@ -25,5 +25,8 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
25
25
  toolName: req.toolName,
26
26
  ...(toolArg !== undefined ? { toolArg } : {}),
27
27
  });
28
- return { action: "allow", presentedInput: grant.presented };
28
+ const reused = { action: "allow", presentedInput: grant.presented };
29
+ if (grant.settled !== undefined)
30
+ carryEngineAttestations(grant.settled, reused);
31
+ return reused;
29
32
  }
@@ -15,9 +15,10 @@
15
15
  import type { Model } from "../../internal/llm.js";
16
16
  import type { Brain, ModelRoles, ModelRef, TaskSpec } from "../types.js";
17
17
  import { type DistillerChatFn } from "../memory-engine/distiller.js";
18
- import type { RunMemoryConsolidationOptions } from "../memory-engine/consolidation-driver.js";
19
- import type { MemoryConsolidationOptions } from "../memory-engine/consolidation.js";
18
+ import { type RunMemoryConsolidationOptions, type ConsolidationDriverEngine } from "../memory-engine/consolidation-driver.js";
19
+ import { type MemoryConsolidationOptions } from "../memory-engine/consolidation.js";
20
20
  import type { LlmDistillerContract } from "../memory-engine/distiller.js";
21
+ import type { EngineNotice } from "../engine-notice.js";
21
22
  /** The slice of `RunnerDeps` this resolver reads (pass the deps object itself). */
22
23
  export interface MemoryConsolidationDriverDeps {
23
24
  brain: Brain;
@@ -47,3 +48,73 @@ export interface MemoryConsolidationDriverDeps {
47
48
  * coded here or in the verb's own screen, never folds to a default.
48
49
  */
49
50
  export declare function resolveMemoryConsolidationDriver(deps: MemoryConsolidationDriverDeps): RunMemoryConsolidationOptions;
51
+ /**
52
+ * "Is there ANY model seat to resolve a consolidation run from?" (the question the
53
+ * `autoRunOnRecommendation` construction door asks). It mirrors {@link resolveMemoryConsolidationDriver}'s
54
+ * seat BRANCH — an explicit chat seat (function or endpoint triple) OR a resolvable
55
+ * `consolidate`/`summarize` role — WITHOUT the value validation (a present-but-alias model id is a
56
+ * DIFFERENT bad value, caught loudly by the driver's own screen when it runs). False ⇔ the resolve
57
+ * verb would throw its "no model seat" refusal. Pinned against that resolve for drift.
58
+ */
59
+ export declare function consolidationSeatResolvable(deps: MemoryConsolidationDriverDeps): boolean;
60
+ /**
61
+ * The advisory-to-execution bridge: fire ONE host consolidation run for the
62
+ * recommended `scope`, fire-and-forget. Called from the post-task settle tail (NOT the harvest tail)
63
+ * exactly once, only when {@link RunnerDeps.autoRunOnRecommendation} is armed AND the terminal
64
+ * harvest recommended consolidation. It NEVER throws and NEVER rejects: the driver's own R1–R7
65
+ * failure table settles the run (zero-write on mint failure, `_incomplete` on non-convergence), this
66
+ * bridge only relays the run's advisory notices to `onNotice` and routes a seat/engine fault to
67
+ * `onError(phase:"memory")`. Adds no failure semantics of its own — the trigger is the only new thing.
68
+ */
69
+ export declare function autoRunConsolidationFromRecommendation(args: {
70
+ deps: MemoryConsolidationDriverDeps;
71
+ engine: ConsolidationDriverEngine;
72
+ scope: string;
73
+ sessionId: string;
74
+ onNotice?: (notice: EngineNotice) => void;
75
+ onError?: (err: unknown, ctx: {
76
+ phase: "memory";
77
+ sessionId: string;
78
+ }) => void;
79
+ }): Promise<void>;
80
+ /**
81
+ * THE shipped default for `RunnerDeps.autoRunOnRecommendation` when the seat is
82
+ * ABSENT: ON — the reference CLI's auto-dream form (auto-execute under the same 24h ∧ 5-session
83
+ * threshold gate the recommendation edge already uses; an explicit `false` is the deployment's off
84
+ * switch). One named value so the polarity is this one line; every sentence about the default names
85
+ * this constant rather than spelling a polarity. Read ONLY through {@link autoRunOnRecommendationEffective}.
86
+ */
87
+ export declare const AUTO_RUN_ON_RECOMMENDATION_DEFAULT = true;
88
+ /**
89
+ * The ONE resolver of "is auto-consolidation armed on this deployment", read by the
90
+ * prepare door's manifest fact and by the settle-tail trigger (never re-derived at a call site):
91
+ * - explicit `true` ⇒ armed (the door has already refused it unless it can run);
92
+ * - explicit `false` ⇒ off — the deployment's own off switch, and it beats the default;
93
+ * - ABSENT ⇒ {@link AUTO_RUN_ON_RECOMMENDATION_DEFAULT}, and ONLY where it can run (the protocol
94
+ * switch wired AND a model seat resolvable) — the reference CLI's "auto-dream: off while
95
+ * auto-memory is off": a default-on world must not arm a deployment that wired no consolidation;
96
+ * - any other value ⇒ off (never arm on garbage; the door refuses it at prepare anyway).
97
+ */
98
+ export declare function autoRunOnRecommendationEffective(deps: MemoryConsolidationDriverDeps & {
99
+ autoRunOnRecommendation?: unknown;
100
+ }): boolean;
101
+ /**
102
+ * The settle tail's ONE question: does this scope's gate row hold a recommendation
103
+ * that no completed run has consumed? Asked of the GATE ROW through the same predicate the
104
+ * recommendation edge is the negation of ({@link recommendationUnconsumed}) — never of a single
105
+ * harvest's report: a crossing recorded by an earlier checkpoint harvest of the same run, or by a
106
+ * session whose settle never fired, is still owed a run here. (The report-based reading stranded such
107
+ * a scope forever: the edge sat disarmed, nothing advanced `lastRunAt`.) A gate the engine cannot read
108
+ * answers `false` — the verbs would refuse loudly on it anyway.
109
+ */
110
+ export declare function scopeHasUnconsumedRecommendation(engine: Pick<ConsolidationDriverEngine, "readConsolidationGate">, scope: string): boolean;
111
+ /**
112
+ * The auto-consolidation ARMING door (bad-value loudness, three states), called from
113
+ * the prepare config doors at construction on every leg. A garbage value refuses; a CONTRADICTION
114
+ * (`true` with nothing to run) refuses — never folded to off, so an armed-but-unrunnable trigger (the
115
+ * invisible-failure family) cannot ship: a deployment learns at boot, not one silently dropped
116
+ * recommendation later. Lives beside {@link consolidationSeatResolvable}, the one question it asks.
117
+ */
118
+ export declare function assertAutoConsolidationWirable(deps: MemoryConsolidationDriverDeps & {
119
+ autoRunOnRecommendation?: unknown;
120
+ }): void;
@@ -1,6 +1,8 @@
1
1
  import { brainToRuntime } from "../runtime.js";
2
2
  import { expandTiers, roleModelIfSet } from "../roles.js";
3
3
  import { isAliasModelId, openAiCompatChatSeat } from "../memory-engine/distiller.js";
4
+ import { runMemoryConsolidationDriver } from "../memory-engine/consolidation-driver.js";
5
+ import { recommendationUnconsumed } from "../memory-engine/consolidation.js";
4
6
  function seatRefusal(message) {
5
7
  const e = new Error(message);
6
8
  e.code = "config.memory_consolidation_driver";
@@ -58,3 +60,58 @@ export function resolveMemoryConsolidationDriver(deps) {
58
60
  ...(deps.memoryConsolidation !== undefined ? { consolidation: deps.memoryConsolidation } : {}),
59
61
  };
60
62
  }
63
+ export function consolidationSeatResolvable(deps) {
64
+ if (deps.memoryConsolidationDriver?.chat !== undefined)
65
+ return true;
66
+ const models = expandTiers(deps.models, deps.tiers);
67
+ return (roleModelIfSet("consolidate", {}, { models, roles: deps.roles }) ?? roleModelIfSet("summarize", {}, { models, roles: deps.roles })) !== undefined;
68
+ }
69
+ export function autoRunConsolidationFromRecommendation(args) {
70
+ const { deps, engine, scope, sessionId, onNotice, onError } = args;
71
+ return (async () => {
72
+ try {
73
+ await runMemoryConsolidationDriver(engine, scope, { ...resolveMemoryConsolidationDriver(deps), onNotice });
74
+ }
75
+ catch (err) {
76
+ try {
77
+ onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
78
+ }
79
+ catch {
80
+ }
81
+ }
82
+ })();
83
+ }
84
+ export const AUTO_RUN_ON_RECOMMENDATION_DEFAULT = true;
85
+ export function autoRunOnRecommendationEffective(deps) {
86
+ const raw = deps.autoRunOnRecommendation;
87
+ if (raw === true)
88
+ return true;
89
+ if (raw !== undefined)
90
+ return false;
91
+ return AUTO_RUN_ON_RECOMMENDATION_DEFAULT && deps.memoryConsolidation !== undefined && consolidationSeatResolvable(deps);
92
+ }
93
+ export function scopeHasUnconsumedRecommendation(engine, scope) {
94
+ const row = engine.readConsolidationGate(scope);
95
+ return row !== undefined && recommendationUnconsumed(row);
96
+ }
97
+ function autoRunRefusal(message) {
98
+ const e = new Error(message);
99
+ e.code = "config.auto_consolidation";
100
+ return e;
101
+ }
102
+ export function assertAutoConsolidationWirable(deps) {
103
+ const raw = deps.autoRunOnRecommendation;
104
+ if (raw === undefined)
105
+ return;
106
+ if (typeof raw !== "boolean") {
107
+ throw autoRunRefusal(`RunnerDeps.autoRunOnRecommendation must be a boolean when present (got ${JSON.stringify(raw)}) — a non-boolean would silently read as off, arming or disarming whole-library egress by accident.`);
108
+ }
109
+ if (raw !== true)
110
+ return;
111
+ if (deps.memoryConsolidation === undefined) {
112
+ throw autoRunRefusal("RunnerDeps.autoRunOnRecommendation is true but RunnerDeps.memoryConsolidation is absent — the recommendation that would trigger a run is never minted (consolidation is OFF) and the engine's verbs refuse coded, so the trigger can never fire. An armed-but-unrunnable trigger is refused, never folded to off: wire memoryConsolidation, or leave autoRunOnRecommendation unset.");
113
+ }
114
+ if (!consolidationSeatResolvable(deps)) {
115
+ throw autoRunRefusal("RunnerDeps.autoRunOnRecommendation is true but no consolidation model seat resolves — configure RunnerDeps.memoryConsolidationDriver.chat (an endpoint triple or a chat function), or declare roles.consolidate / roles.summarize. Arming auto-run with no model to run it is refused, never folded to off (a whole-library distillation must not silently ride the default main model).");
116
+ }
117
+ }
@@ -3,6 +3,7 @@ import { assertContainmentFaceValue } from "../../tools/fs/index.js";
3
3
  import { MICRO_COMPACT_MACHINES } from "../context-edit.js";
4
4
  import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js";
5
5
  import { resolveCheckpointStore } from "../checkpoint-store.js";
6
+ import { assertAutoConsolidationWirable } from "./memory-consolidation-driver.js";
6
7
  import { isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
7
8
  import { preflightLockedConfig } from "../locked-config.js";
8
9
  import { assertRetentionCapability } from "../retention.js";
@@ -237,6 +238,7 @@ export function prepareConfigDoors(input) {
237
238
  throw e;
238
239
  }
239
240
  const memoryCapturePolicy = memoryCapturePolicyRaw === "governed" ? "governed" : memoryCapturePolicyRaw === "capture-required" ? "capture-required" : "open";
241
+ assertAutoConsolidationWirable(deps);
240
242
  if (spec.resumeAtMode !== undefined) {
241
243
  if (spec.resumeAt === undefined) {
242
244
  const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
@@ -155,6 +155,7 @@ export interface PreparePolicyChainResult {
155
155
  argsJson: string;
156
156
  presented: unknown;
157
157
  waitMs: number;
158
+ settled?: object;
158
159
  }>;
159
160
  /** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
160
161
  askSourceIdentity: () => {
@@ -152,7 +152,7 @@ export async function preparePolicyChain(input) {
152
152
  }, "mandate_unreconstructible", creq);
153
153
  const markerChannelFullDeny = (creq) => attestAskUnresolvableCause({ action: "deny", message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed` }, "approver_unavailable", creq);
154
154
  const inheritedAskGrants = new Map();
155
- const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs) => {
155
+ const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled) => {
156
156
  if (typeof approver !== "function")
157
157
  return;
158
158
  if (presented === undefined)
@@ -162,7 +162,7 @@ export async function preparePolicyChain(input) {
162
162
  const argsJson = askGrantShapeOf(presented);
163
163
  if (argsJson === undefined)
164
164
  return;
165
- inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs });
165
+ inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled });
166
166
  };
167
167
  const sandboxDeclared = isIsolated(executionEnv);
168
168
  const sandboxAdmissionArmed = (() => {
@@ -277,37 +277,51 @@ export async function preparePolicyChain(input) {
277
277
  cwdRef: handsCwdRef,
278
278
  deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
279
279
  });
280
- const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider, ancestorTracker) => {
280
+ const recheckApprovedEdit = async (pol, onAskOf, creq, edit, approved, csignal, ancestorDecider, ancestorTracker) => {
281
281
  let editArgs = edit;
282
+ let settledBy = approved;
283
+ const vetoed = (d) => {
284
+ const out = { ...d };
285
+ carryEngineAttestations(d, out);
286
+ carryPrecedingSettlement(settledBy, out);
287
+ return out;
288
+ };
282
289
  for (let round = 0;; round++) {
283
290
  if (round >= 3) {
284
- return {
291
+ return vetoed({
285
292
  action: "deny",
286
293
  message: `the approval-edit chain for "${creq.toolName}" exceeded the re-approval cap (inherited parent policy) — denied fail-closed; re-submit the edited action directly`,
287
- };
294
+ });
288
295
  }
289
296
  let re;
290
297
  try {
291
298
  re = await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal);
292
299
  }
293
300
  catch (err) {
294
- return {
301
+ return vetoed({
295
302
  action: "deny",
296
303
  message: `inherited parent policy re-check of the approved edit errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
297
- };
304
+ });
298
305
  }
299
306
  if (re.action === "deny")
300
- return re;
307
+ return vetoed(re);
301
308
  if (re.action === "allow") {
302
- return { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs };
309
+ const out = { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs };
310
+ carryEngineAttestations(re, out);
311
+ carryPrecedingSettlement(settledBy, out);
312
+ return out;
303
313
  }
304
314
  if (re.updatedInput !== undefined)
305
315
  editArgs = re.updatedInput;
306
316
  const judged = await judgeInheritedClassifier({ autoMode: ancestorDecider !== undefined ? { decider: ancestorDecider, denialTracking: ancestorTracker } : undefined, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs });
307
- if (judged.kind === "allow")
308
- return judged.result;
317
+ if (judged.kind === "allow") {
318
+ const cleared = { ...judged.result };
319
+ carryEngineAttestations(judged.result, cleared);
320
+ carryPrecedingSettlement(settledBy, cleared);
321
+ return cleared;
322
+ }
309
323
  if (judged.kind === "deny")
310
- return judged.result;
324
+ return vetoed(judged.result);
311
325
  const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
312
326
  re = editAsk;
313
327
  const rr = await resolveAsk({
@@ -326,8 +340,12 @@ export async function preparePolicyChain(input) {
326
340
  settleDenialLimitFallback({ fallback: editFallback, mintedHere: editMintedHere, tracker: ancestorTracker, resolved: rr, headless: headlessDenyAtRecheck, stop: stopForDenialLimit, toolName: creq.toolName, toolCallId: creq.toolCallId });
327
341
  if (rr.action !== "allow")
328
342
  return rr;
329
- if (rr.updatedInput === undefined)
330
- return { action: "allow", updatedInput: editArgs };
343
+ settledBy = rr;
344
+ if (rr.updatedInput === undefined) {
345
+ const out = { action: "allow", updatedInput: editArgs };
346
+ carryPrecedingSettlement(rr, out);
347
+ return out;
348
+ }
331
349
  editArgs = rr.updatedInput;
332
350
  }
333
351
  };
@@ -404,9 +422,9 @@ export async function preparePolicyChain(input) {
404
422
  if (resolved.action !== "allow")
405
423
  return resolved;
406
424
  if (resolved.updatedInput !== undefined) {
407
- return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
425
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
408
426
  }
409
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
427
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
410
428
  return { action: "allow" };
411
429
  },
412
430
  }
@@ -473,12 +491,16 @@ export async function preparePolicyChain(input) {
473
491
  if (resolved.action !== "allow")
474
492
  return resolved;
475
493
  if (resolved.updatedInput !== undefined) {
476
- return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
494
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
477
495
  }
478
496
  if (pc.preToolUse === undefined) {
479
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
497
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
480
498
  }
481
- return decision.updatedInput !== undefined ? { ...resolved, updatedInput: decision.updatedInput } : resolved;
499
+ if (decision.updatedInput === undefined)
500
+ return resolved;
501
+ const approvedWithRewrite = { ...resolved, updatedInput: decision.updatedInput };
502
+ carryEngineAttestations(resolved, approvedWithRewrite);
503
+ return approvedWithRewrite;
482
504
  },
483
505
  });
484
506
  const rewriteCapableLayers = policy !== undefined || parentConstraintWrappers.length > 0;
@@ -540,6 +562,7 @@ export async function preparePolicyChain(input) {
540
562
  const recheckLayers = [...recheckPrefix, ...postEmitterSameInstanceAdapters];
541
563
  const finalReq = { ...req, args: decision.updatedInput };
542
564
  let asked;
565
+ const revalidated = {};
543
566
  try {
544
567
  for (const layer of recheckLayers) {
545
568
  const d = await layer.check(finalReq, signal);
@@ -558,13 +581,22 @@ export async function preparePolicyChain(input) {
558
581
  }
559
582
  if (d.action === "ask" && asked === undefined)
560
583
  asked = d;
584
+ carryEngineAttestations(d, revalidated);
561
585
  }
562
586
  }
563
587
  finally {
564
588
  rewriteEmitterIndex.delete(req.toolCallId);
565
589
  }
566
590
  if (asked !== undefined && decision.action === "allow") {
567
- return { ...asked, updatedInput: decision.updatedInput };
591
+ const surfaced = { ...asked, updatedInput: decision.updatedInput };
592
+ carryEngineAttestations(asked, surfaced);
593
+ return surfaced;
594
+ }
595
+ if (decision.action === "allow") {
596
+ const owned = { ...decision };
597
+ carryEngineAttestations(decision, owned);
598
+ carryEngineAttestations(revalidated, owned);
599
+ return owned;
568
600
  }
569
601
  return decision;
570
602
  },
@@ -8,6 +8,7 @@ import { durableMandateSourceOf, isLiveApproverSeat } from "../tool-policy.js";
8
8
  import { deliverEngineNotice } from "../types.js";
9
9
  import { inlineUntrusted } from "../untrusted-text.js";
10
10
  import { mountWriteProtection } from "./write-protection-station.js";
11
+ import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
11
12
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
12
13
  import { LSP_CONTRACT_ID, countElicitOptIns, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
13
14
  import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
@@ -207,6 +208,7 @@ export function prepareWiringManifest(input) {
207
208
  autoMode: { ...autoModeArmReason },
208
209
  mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
209
210
  ...(writeProtection !== undefined ? { writeProtectionTargetView: writeProtection.targetView } : {}),
211
+ autoConsolidationOnRecommendation: autoRunOnRecommendationEffective(deps),
210
212
  tools: toolRoster,
211
213
  hooks: wiredHookEntries(hooks, spec.hooks !== undefined ? "spec" : "deps", preToolUseObservational),
212
214
  ...(lspManagerWired ? { lsp: deriveLspSeam(toolRoster.entries.some((e) => e.contract.contractId === LSP_CONTRACT_ID), lspDiagnosticsLive) } : {}),
@@ -18,6 +18,7 @@ import { resumeDecisionWasNegative } from "./decide-continuation.js";
18
18
  import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
19
19
  import { generatePromptSuggestions } from "./prompt-suggestions.js";
20
20
  import { settleTeardownLeg } from "./teardown-bounded.js";
21
+ import { autoRunConsolidationFromRecommendation, autoRunOnRecommendationEffective, scopeHasUnconsumedRecommendation } from "./memory-consolidation-driver.js";
21
22
  import { amendTerminal, terminalProjection } from "./terminal-projection.js";
22
23
  export async function runSettleAndTeardown(input) {
23
24
  const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions } = input;
@@ -240,6 +241,10 @@ export async function runSettleAndTeardown(input) {
240
241
  if (prepared.memoryEngineSession) {
241
242
  const mes = prepared.memoryEngineSession;
242
243
  await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
244
+ const scope = mes.handle.writeScope;
245
+ if (scope !== null && autoRunOnRecommendationEffective(runner.deps) && scopeHasUnconsumedRecommendation(mes.engine, scope)) {
246
+ void autoRunConsolidationFromRecommendation({ deps: runner.deps, engine: mes.engine, scope, sessionId: prepared.sessionId, onNotice: runner.deps.onNotice, onError: runner.deps.onError });
247
+ }
243
248
  }
244
249
  emitTrace(rs.telemetry.tracer, () => ({
245
250
  kind: "task.end",