@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.
- package/CHANGELOG.md +75 -0
- package/dist/core/ask-origin.d.ts +95 -8
- package/dist/core/ask-origin.js +37 -5
- package/dist/core/gate-bounded-await.d.ts +6 -1
- package/dist/core/gate-bounded-await.js +13 -3
- package/dist/core/gate-fold.js +18 -5
- package/dist/core/gate-lanes.js +6 -12
- package/dist/core/memory-engine/consolidation-driver.d.ts +3 -0
- package/dist/core/memory-engine/consolidation-driver.js +20 -1
- package/dist/core/memory-engine/consolidation.d.ts +7 -0
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +26 -1
- package/dist/core/memory-engine/distiller.js +9 -2
- package/dist/core/path-identity.d.ts +13 -0
- package/dist/core/path-identity.js +14 -0
- package/dist/core/runner/env-capability-census.d.ts +3 -1
- package/dist/core/runner/env-capability-census.js +24 -7
- package/dist/core/runner/gate-exit.d.ts +7 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +1 -0
- package/dist/core/runner/inherited-ask-grants.js +5 -2
- package/dist/core/runner/memory-consolidation-driver.d.ts +73 -2
- package/dist/core/runner/memory-consolidation-driver.js +57 -0
- package/dist/core/runner/prepare-config-doors.js +2 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +1 -0
- package/dist/core/runner/prepare-policy-chain.js +52 -20
- package/dist/core/runner/prepare-wiring-manifest.js +2 -0
- package/dist/core/runner/run-settle-and-teardown.js +5 -0
- package/dist/core/runner-deps.d.ts +21 -0
- package/dist/core/sensitive-path-policy.js +13 -0
- package/dist/core/wiring-manifest.d.ts +16 -0
- package/dist/core/wiring-manifest.js +1 -0
- package/dist/core/write-protect.d.ts +14 -2
- package/dist/core/write-protect.js +36 -13
- package/dist/engine/execution-env/node-execution-env.d.ts +10 -0
- package/dist/engine/execution-env/node-execution-env.js +29 -1
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/tools/fs/bash-readonly-classifier.js +35 -6
- package/dist/tools/fs/read-deny.d.ts +4 -2
- package/dist/tools/fs/read-deny.js +5 -0
- package/dist/tools/fs/safety.d.ts +12 -0
- package/dist/tools/fs/safety.js +43 -38
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.21.0.json +452 -0
- 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
|
|
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
|
-
|
|
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:
|
|
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. */
|
|
@@ -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
|
-
|
|
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
|
|
19
|
-
import type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|