@sema-agent/core 5.58.0 → 5.59.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.
- package/CHANGELOG.md +48 -0
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +52 -1
- package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
- package/dist/core/memory-engine/consolidation-driver.js +54 -5
- package/dist/core/memory-engine/consolidation.d.ts +73 -1
- package/dist/core/memory-engine/consolidation.js +21 -1
- package/dist/core/memory-engine/engine.d.ts +97 -8
- package/dist/core/memory-engine/engine.js +112 -20
- package/dist/core/memory-engine/file-backend.d.ts +13 -1
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/permission-rule-model.d.ts +42 -1
- package/dist/core/permission-rule-model.js +12 -0
- package/dist/core/runner/prepare-task.js +3 -3
- package/dist/core/runner/runtask.js +4 -4
- package/dist/core/tool-policy.d.ts +58 -0
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/types.d.ts +45 -20
- package/dist/core/types.js +4 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1771 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.59.0 — 2026-08-24
|
|
4
|
+
|
|
5
|
+
### BREAKING (narrow, named)
|
|
6
|
+
- **`ConsolidationDriverEngine` requires `readConsolidationPlanFoldEvidence`** (the member-level
|
|
7
|
+
conservation wall's read face). `MemoryEngine` satisfies it as before; a custom driver-engine stub
|
|
8
|
+
must add the member (compile error otherwise). Default-off feature shipped one version ago —
|
|
9
|
+
blast radius is stub authors only.
|
|
10
|
+
|
|
11
|
+
### Changed — consolidation safety narrowings (design/376 slice ③; default-off face, all fail-closed)
|
|
12
|
+
- **Member-level conservation wall**: a run whose planned members were deleted/excluded/folded by
|
|
13
|
+
another writer mid-run now settles `driver_failed` naming the missing ids, where a numeric offset
|
|
14
|
+
previously produced a false `converged`. Attribution uses the exact plan-frozen cycle-id grammar
|
|
15
|
+
(prefix-shadow ids cannot re-satisfy a member). Count witnesses remain the fast pass.
|
|
16
|
+
- **C-2 scan-refusal reach**: scan findings refuse per PRODUCT (disclosed; whole-plan refusal past
|
|
17
|
+
`min(floor(0.25×submitted), 8)` — knobs `scanRefusalRatioCap`/`scanRefusalMaxPerPlan`, bad values
|
|
18
|
+
refuse loudly); an empty or frozen-empty plan refuses instead of settling a vacuous completed;
|
|
19
|
+
refused input ids are excluded from the completion fingerprint and ride the plan file under a
|
|
20
|
+
bidirectional v2⇔member version gate (a pre-field worker parks loudly on a v2 plan instead of
|
|
21
|
+
settling wrong). WIDENING, named: refused products' edges no longer trip the supersession fuse.
|
|
22
|
+
- **C-3 instruction-form hard gate is unconditional**: a clean visible set refuses instruction-form
|
|
23
|
+
products by default (previously committed), naming the host escape hatch
|
|
24
|
+
`consolidation.allowInstructionProducts: true` (screened exactly-true; opens the clean arm only —
|
|
25
|
+
a marked set refuses with no opening knob).
|
|
26
|
+
- Registry reads are journal-aware (a committed journal wins; an unreadable journal throws
|
|
27
|
+
fail-closed instead of serving a clean-looking stale registry).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **`NOTICE_AUDIENCE` registry (closed)**: `ENGINE_NOTICE_CODES` is the 27-code catalog,
|
|
31
|
+
`NOTICE_AUDIENCE` answers for every member (8 user / 19 operator) in compile-time lockstep both
|
|
32
|
+
directions; `task.user_steer_undrained`/`task.user_followup_undrained` now carry `sessionId`
|
|
33
|
+
filled from the real run; catalog/code/audience types root-exported for downstream table diffing.
|
|
34
|
+
- **`AskRequest.hasBidiControls`** presence bit (additive): minted at the ask throat over
|
|
35
|
+
execution-snapshot args + tool preview for the directional-control class (U+061C/200E/200F/
|
|
36
|
+
202A-202E/2066-2069); presence-only (never `false`; absence covers budget exhaustion), zero
|
|
37
|
+
rewriting; the scan charges every character — values, object keys, Map keys, inherited names —
|
|
38
|
+
through ONE bounded reader (node budget + 1M code-unit allowance). Plus
|
|
39
|
+
**`renderUntrustedCommandText`** root export: the lossy DISPLAY baseline for raw command bytes
|
|
40
|
+
(strips Cf, folds controls to spaces, code-point cap with ellipsis; identity stays on raw).
|
|
41
|
+
- **Read faces**: `readConsolidationGateState` (present|absent|corrupt-with-detail; legacy face
|
|
42
|
+
byte-unchanged), `readConsolidationPlanFoldEvidence` (ok/absent/corrupt; applied-only edges),
|
|
43
|
+
`MemoryEngine.listMemoryScopes()`/File-backend `listScopes()` with the `supported` discriminator
|
|
44
|
+
(a backend that cannot enumerate answers `supported:false` with a reason, never `[]`).
|
|
45
|
+
- **Export tiers v1**: the export-surface snapshot carries a machine-readable `tiers` column
|
|
46
|
+
(stable/advanced/internal; self-declared heuristic) with a three-leg gate (every live export
|
|
47
|
+
tiered, no stale rows, live derivation ≡ frozen column).
|
|
48
|
+
- `OriginClearanceRow`/`OriginClearanceEvent` root type exports (server design/316 car-B compile
|
|
49
|
+
prerequisite). Export surface 1749 → 1758, all additive.
|
|
50
|
+
|
|
3
51
|
## 5.58.0 — 2026-08-24
|
|
4
52
|
|
|
5
53
|
### BREAKING (design/375 case A — per-segment batch consent; ships as one window, no compatibility arms)
|
|
@@ -174,7 +174,13 @@ export declare function adjudicateDerivedRoute(args: {
|
|
|
174
174
|
*/
|
|
175
175
|
export declare function sameRouteIdentity(a: Pick<Model, "id" | "baseUrl">, b: Pick<Model, "id" | "baseUrl">): boolean;
|
|
176
176
|
/** The `route.fallback_to_primary` announcement — one shared shape for every derived-leg seat, so
|
|
177
|
-
* a consumer can rely on `detail.{seat, from, to, cause}` regardless of which seat fell back.
|
|
177
|
+
* a consumer can rely on `detail.{seat, from, to, cause}` regardless of which seat fell back.
|
|
178
|
+
*
|
|
179
|
+
* `sessionId` (#433, additive): the swap happened while resolving ONE run's seats, so the run is
|
|
180
|
+
* what an operator correlates the line against ("which task ran on the fallback model"). The
|
|
181
|
+
* AUDIENCE stays `"operator"` — the fix is a credential/catalog fix, and no end user can act on it;
|
|
182
|
+
* attribution is a routing key, not an entitlement. Omitted when a caller has no session in hand,
|
|
183
|
+
* never fabricated. */
|
|
178
184
|
export declare function fallbackToPrimaryNotice(args: {
|
|
179
185
|
seat: string;
|
|
180
186
|
from: string;
|
|
@@ -182,6 +188,7 @@ export declare function fallbackToPrimaryNotice(args: {
|
|
|
182
188
|
verdict: Extract<RouteAdjudication, {
|
|
183
189
|
ok: false;
|
|
184
190
|
}>;
|
|
191
|
+
sessionId?: string;
|
|
185
192
|
}): EngineNotice;
|
|
186
193
|
/** The auth carriers of a header bag, canonicalized (case-fold name → value, sorted) — the
|
|
187
194
|
* Model-visible credential fingerprint used by catalog-change disclosure (a same-name entry whose
|
|
@@ -173,7 +173,14 @@ export function fallbackToPrimaryNotice(args) {
|
|
|
173
173
|
code: "route.fallback_to_primary",
|
|
174
174
|
message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) — ` +
|
|
175
175
|
`falling back to the primary model "${args.to}". ${args.verdict.detail.fixHint}`,
|
|
176
|
-
detail: {
|
|
176
|
+
detail: {
|
|
177
|
+
seat: args.seat,
|
|
178
|
+
from: args.from,
|
|
179
|
+
to: args.to,
|
|
180
|
+
cause: args.verdict.code,
|
|
181
|
+
fixHint: args.verdict.detail.fixHint,
|
|
182
|
+
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
183
|
+
},
|
|
177
184
|
};
|
|
178
185
|
}
|
|
179
186
|
export function authCarrierFingerprint(headers) {
|
|
@@ -68,15 +68,42 @@ export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
|
|
|
68
68
|
* fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
|
|
69
69
|
export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
71
|
+
* WHO a notice code is for. `"user"` = a session-scoped disclosure the end user of that session is
|
|
72
|
+
* entitled to see (safe to project onto that session's event stream); `"operator"` = a
|
|
73
|
+
* deployment/config/ops fact for whoever runs the process.
|
|
74
|
+
*
|
|
75
|
+
* Two values, deliberately. "Can this notice be routed to a session" is NOT an audience value — it
|
|
76
|
+
* is the presence of `EngineNotice.sessionId`, a per-EMISSION fact. A third value would fold a
|
|
77
|
+
* routing property into an entitlement vocabulary, and the two answer different questions: several
|
|
78
|
+
* codes below carry a session attribution while the person entitled to the fact is still the
|
|
79
|
+
* operator (a failed offload write, a routing fallback, a consolidation recommendation).
|
|
80
|
+
*/
|
|
81
|
+
export type NoticeAudience = "user" | "operator";
|
|
82
|
+
/**
|
|
83
|
+
* The CLOSED catalog of EngineNotice codes this engine mints — the enumeration half of the
|
|
84
|
+
* presentation registry below, and the thing a consumer's own audience table can be diffed against
|
|
85
|
+
* (that is the point of exporting it: a downstream table with a row missing, or a row for a code
|
|
86
|
+
* this engine no longer mints, is a mechanically detectable disagreement rather than an argument).
|
|
87
|
+
*
|
|
88
|
+
* Kept in LOCKSTEP with {@link NOTICE_AUDIENCE} by the compiler (`satisfies Record<EngineNoticeCode,
|
|
89
|
+
* …>` below: a catalog entry with no audience row, or an audience row for a non-member, is a tsc
|
|
90
|
+
* error — a mirrored enumeration that is only checked at runtime is a mirror that drifts). The
|
|
91
|
+
* other half — "is the catalog itself still complete?" — cannot be a type: `EngineNotice.code` is
|
|
92
|
+
* declared `string` (a host may forward its own), so the gate test (governance-codes.test.ts) scans
|
|
93
|
+
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
94
|
+
* no longer minted.
|
|
95
|
+
*/
|
|
96
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
|
|
97
|
+
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
98
|
+
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
99
|
+
* sink is a supported shape, and narrowing that field would break it. */
|
|
100
|
+
export type EngineNoticeCode = (typeof ENGINE_NOTICE_CODES)[number];
|
|
101
|
+
/**
|
|
102
|
+
* The presentation-tier registry — see {@link NOTICE_AUDIENCE_TABLE} for the per-row judgment.
|
|
103
|
+
* The exported TYPE stays the open `Record<string, …>` on purpose: a consumer indexing it with a
|
|
104
|
+
* runtime string (a code off a wire, a host's own) must keep compiling. The lockstep with
|
|
105
|
+
* {@link ENGINE_NOTICE_CODES} is enforced on the table above, where narrowing costs nobody anything.
|
|
79
106
|
*/
|
|
80
|
-
export declare const NOTICE_AUDIENCE: Readonly<Record<string,
|
|
107
|
+
export declare const NOTICE_AUDIENCE: Readonly<Record<string, NoticeAudience>>;
|
|
81
108
|
/** The audience for `code` — table lookup with the conservative `"operator"` default. */
|
|
82
|
-
export declare function noticeAudienceOf(code: string):
|
|
109
|
+
export declare function noticeAudienceOf(code: string): NoticeAudience;
|
|
@@ -84,14 +84,65 @@ export const RULE_SYNC_DROP_CODES = {
|
|
|
84
84
|
below_gc_frontier: "local-quarantined",
|
|
85
85
|
server_rejected: "local-quarantined",
|
|
86
86
|
};
|
|
87
|
-
export const
|
|
87
|
+
export const ENGINE_NOTICE_CODES = [
|
|
88
|
+
"config.env_timeout_discarded",
|
|
89
|
+
"config.materialize_env_discarded",
|
|
90
|
+
"config.models_swapped",
|
|
91
|
+
"config.read_face_deployment_clamped",
|
|
92
|
+
"config.tool_model_gate_removed",
|
|
93
|
+
"config.tool_model_gate_unknown_class",
|
|
94
|
+
"config.tool_model_gate_env_invalid",
|
|
95
|
+
"delegation.transcript_integrity",
|
|
96
|
+
"mcp.revocation_probe_failed",
|
|
97
|
+
"workflow.governance_key_stripped",
|
|
98
|
+
"memory.session_polluted",
|
|
99
|
+
"memory.harvest_quarantined",
|
|
100
|
+
"memory.delegation_static_mark_waived",
|
|
101
|
+
"memory.hold_opened",
|
|
102
|
+
"memory.hold_released",
|
|
103
|
+
"memory.hold_disposed",
|
|
104
|
+
"memory.consolidation_recommended",
|
|
105
|
+
"memory.consolidation_committed",
|
|
106
|
+
"memory.consolidation_conflict",
|
|
107
|
+
"memory.consolidation_incomplete",
|
|
108
|
+
"memory.consolidation_refused",
|
|
109
|
+
"route.fallback_to_primary",
|
|
110
|
+
"route.base_url_changed_key_unchanged",
|
|
111
|
+
"task.user_steer_undrained",
|
|
112
|
+
"task.user_followup_undrained",
|
|
113
|
+
"task.injection_priority_unimplemented",
|
|
114
|
+
"tool_result.offload_put_failed",
|
|
115
|
+
];
|
|
116
|
+
const NOTICE_AUDIENCE_TABLE = {
|
|
88
117
|
"memory.session_polluted": "user",
|
|
89
118
|
"memory.harvest_quarantined": "user",
|
|
90
119
|
"memory.delegation_static_mark_waived": "user",
|
|
91
120
|
"memory.hold_opened": "user",
|
|
92
121
|
"memory.hold_released": "user",
|
|
93
122
|
"memory.hold_disposed": "user",
|
|
123
|
+
"task.user_steer_undrained": "user",
|
|
124
|
+
"task.user_followup_undrained": "user",
|
|
125
|
+
"config.env_timeout_discarded": "operator",
|
|
126
|
+
"config.materialize_env_discarded": "operator",
|
|
127
|
+
"config.models_swapped": "operator",
|
|
128
|
+
"config.read_face_deployment_clamped": "operator",
|
|
129
|
+
"config.tool_model_gate_removed": "operator",
|
|
130
|
+
"config.tool_model_gate_unknown_class": "operator",
|
|
131
|
+
"config.tool_model_gate_env_invalid": "operator",
|
|
132
|
+
"delegation.transcript_integrity": "operator",
|
|
133
|
+
"mcp.revocation_probe_failed": "operator",
|
|
134
|
+
"workflow.governance_key_stripped": "operator",
|
|
135
|
+
"memory.consolidation_recommended": "operator",
|
|
136
|
+
"memory.consolidation_committed": "operator",
|
|
137
|
+
"memory.consolidation_conflict": "operator",
|
|
138
|
+
"memory.consolidation_incomplete": "operator",
|
|
139
|
+
"memory.consolidation_refused": "operator",
|
|
140
|
+
"route.fallback_to_primary": "operator",
|
|
141
|
+
"route.base_url_changed_key_unchanged": "operator",
|
|
142
|
+
"task.injection_priority_unimplemented": "operator",
|
|
143
|
+
"tool_result.offload_put_failed": "operator",
|
|
94
144
|
};
|
|
145
|
+
export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
|
|
95
146
|
export function noticeAudienceOf(code) {
|
|
96
147
|
return NOTICE_AUDIENCE[code] ?? "operator";
|
|
97
148
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { EngineNotice } from "../types.js";
|
|
24
24
|
import { type ConsolidationGateRow, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
|
|
25
|
-
import { type ConsolidationPlanSummary } from "./engine.js";
|
|
25
|
+
import { type ConsolidationPlanFoldEvidence, type ConsolidationPlanSummary } from "./engine.js";
|
|
26
26
|
import { mintLlmConsolidationPlan, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationFoldState, type DistillerChatFn, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract } from "./distiller.js";
|
|
27
27
|
export declare const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json";
|
|
28
28
|
/** §1.6-4 — the mint archive: what was shown to the model and what it answered, verbatim, per run
|
|
@@ -114,10 +114,14 @@ export declare function readConsolidationDriverRun(controlDir: string, scope: st
|
|
|
114
114
|
* run row's `planArchive` (written under the attempt guard) names the one that counts. */
|
|
115
115
|
export declare function distillerPlanArchiveName(runId: string, attempt: number): string;
|
|
116
116
|
export declare function archiveDistillerPlan(controlDir: string, runId: string, plan: LlmConsolidationPlan, attempt?: number): string;
|
|
117
|
-
/** The engine face this verb consumes — `MemoryEngine` satisfies it structurally.
|
|
117
|
+
/** The engine face this verb consumes — `MemoryEngine` satisfies it structurally.
|
|
118
|
+
* (`readConsolidationPlanFoldEvidence` joined in design/376 slice ③, #436: the conservation
|
|
119
|
+
* wall's terminal witness is member-level, so the face is REQUIRED — an engine without it could
|
|
120
|
+
* only offer the store-delta counts an interleaved fold can numerically offset.) */
|
|
118
121
|
export interface ConsolidationDriverEngine extends ConsolidationDriveEngine {
|
|
119
122
|
readConsolidationGate(scope: string): ConsolidationGateRow | undefined;
|
|
120
123
|
listConsolidationPlans(): ConsolidationPlanSummary[];
|
|
124
|
+
readConsolidationPlanFoldEvidence(planId: string): ConsolidationPlanFoldEvidence;
|
|
121
125
|
readConsolidationFoldState(scope: string): Promise<ConsolidationFoldState>;
|
|
122
126
|
readonly controlPlaneRoot: string;
|
|
123
127
|
}
|
|
@@ -182,6 +182,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
182
182
|
return settle("parked", undefined, parkedSeat);
|
|
183
183
|
}
|
|
184
184
|
let staleResidue = [];
|
|
185
|
+
let staleResidueMemberIds = new Set();
|
|
185
186
|
const distill = async (candidates, snapshot, s) => {
|
|
186
187
|
if (run.planCache === undefined) {
|
|
187
188
|
let minted;
|
|
@@ -233,11 +234,12 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
233
234
|
writeDriverRun(controlDir, run);
|
|
234
235
|
archiveDistillerPlan(controlDir, run.runId, plan, run.attempt);
|
|
235
236
|
}
|
|
237
|
+
const offered = new Map();
|
|
238
|
+
for (const c of candidates)
|
|
239
|
+
if (typeof c?.entry?.id === "string")
|
|
240
|
+
offered.set(c.entry.id, c.entry.rev);
|
|
241
|
+
staleResidueMemberIds = new Set();
|
|
236
242
|
staleResidue = (run.planCache?.products ?? []).flatMap((p) => {
|
|
237
|
-
const offered = new Map();
|
|
238
|
-
for (const c of candidates)
|
|
239
|
-
if (typeof c?.entry?.id === "string")
|
|
240
|
-
offered.set(c.entry.id, c.entry.rev);
|
|
241
243
|
const ids = p.inputIds ?? [];
|
|
242
244
|
const live = ids.filter((id) => offered.has(id));
|
|
243
245
|
if (live.length === 0)
|
|
@@ -245,7 +247,11 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
245
247
|
const stale = p.inputRevs !== undefined && live.some((id) => p.inputRevs[id] !== undefined && offered.get(id) !== p.inputRevs[id]);
|
|
246
248
|
const vanished = live.length < ids.length;
|
|
247
249
|
const unanchored = p.inputRevs === undefined;
|
|
248
|
-
|
|
250
|
+
if (!(stale || vanished || unanchored))
|
|
251
|
+
return [];
|
|
252
|
+
for (const id of live)
|
|
253
|
+
staleResidueMemberIds.add(id);
|
|
254
|
+
return [{ name: p.name, targets: live.length }];
|
|
249
255
|
});
|
|
250
256
|
const replayView = run.planCache !== undefined && run.planCache.products.some((p) => p.inputRevs === undefined)
|
|
251
257
|
? { ...run.planCache, products: run.planCache.products.filter((p) => p.inputRevs !== undefined) }
|
|
@@ -329,6 +335,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
329
335
|
const writeResidue = run.writeFailures.map((w) => ({ name: w.key, targets: w.members }));
|
|
330
336
|
const driverResidue = [...staleResidue, ...writeResidue];
|
|
331
337
|
if (drive.converged) {
|
|
338
|
+
const plannedIds = [...new Set((run.planCache?.products ?? []).flatMap((p) => p.inputIds ?? []))];
|
|
332
339
|
const plannedMembers = (run.planCache?.products ?? []).reduce((a, p) => a + (p.inputIds?.length ?? 0), 0);
|
|
333
340
|
const rowAccount = run.entriesSuperseded ?? 0;
|
|
334
341
|
const storeAccount = run.foldBaseline !== undefined ? Math.max(0, drive.foldState.after.superseded - run.foldBaseline.superseded) : rowAccount;
|
|
@@ -339,6 +346,24 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
339
346
|
run.residue = driverResidue;
|
|
340
347
|
return settle("driver_failed", drive, `the drive reached a fixpoint but the account does not balance: ${plannedMembers} planned member(s), ${supersededAttested} superseded attested for this run (row account ${rowAccount}, store delta ${run.foldBaseline !== undefined ? storeAccount : "unavailable — the row predates the store anchor"}), ${residueTargets} in named residue — ${plannedMembers - accounted} member(s) left the candidate set without a fold the store attests (deleted or excluded mid-run${run.foldBaseline !== undefined ? "" : ", or folded in a window whose account write did not survive"}); their entries stand (or revive with their exclusion), and a fresh run re-mints over the current library`);
|
|
341
348
|
}
|
|
349
|
+
if (plannedIds.length > 0) {
|
|
350
|
+
const evidence = collectRunFoldEvidence(engine, scope, run.requestId);
|
|
351
|
+
if (evidence.skipped.length > 0) {
|
|
352
|
+
const named = evidence.skipped.slice(0, 5).map((s) => `${s.planId} (${s.state})`).join(", ");
|
|
353
|
+
announce([
|
|
354
|
+
`memory consolidation conservation wall for scope ${inlineUntrusted(scope, 80)}: ${evidence.skipped.length} same-scope plan file(s) answered unreadable and were SKIPPED as foreign to run ${run.runId} — they attest nothing for this run and need out-of-band repair: ${named}${evidence.skipped.length > 5 ? ", …" : ""}`,
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
if (evidence.unreadable !== undefined) {
|
|
358
|
+
run.residue = driverResidue;
|
|
359
|
+
return settle("driver_failed", drive, `the drive reached a fixpoint but the member-level fold evidence is incomplete: plan ${evidence.unreadable} — one of THIS run's own cycles — answered ${evidence.unreadableState} (control-plane damage on run-owned evidence, not a transient race); a converged label needs attestable members; the library stands as committed and a fresh run re-mints over the current bytes`);
|
|
360
|
+
}
|
|
361
|
+
const missing = plannedIds.filter((id) => !evidence.targets.has(id) && !staleResidueMemberIds.has(id));
|
|
362
|
+
if (missing.length > 0) {
|
|
363
|
+
run.residue = driverResidue;
|
|
364
|
+
return settle("driver_failed", drive, `the drive reached a fixpoint but ${missing.length} of ${plannedIds.length} planned member(s) left the candidate set without a fold this run's plans attest (deleted, excluded, or superseded by another writer mid-run; e.g. ${missing.slice(0, 5).join(", ")}) — a store-delta count can be numerically offset by an unrelated interleaved fold, so member identity is the final judge; the entries stand (or revive with their exclusion) and a fresh run re-mints over the current library`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
342
367
|
if (driverResidue.length > 0) {
|
|
343
368
|
run.residue = driverResidue;
|
|
344
369
|
return settle("driver_failed", drive, `the drive reached the surviving plan's fixpoint, but the driver's own half left ${driverResidue.length} group(s) unfolded (${staleResidue.length} stale against edited inputs, ${writeResidue.length} writing-pass failure(s)) — their members stay as they were; a fresh run re-mints over the current bytes`);
|
|
@@ -356,6 +381,30 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
356
381
|
return settle("parked", drive, stop.reason);
|
|
357
382
|
return settle("refused", drive, stop?.reason ?? "the drive stopped without a stated reason");
|
|
358
383
|
}
|
|
384
|
+
function collectRunFoldEvidence(engine, scope, requestId) {
|
|
385
|
+
const cycleId = new RegExp(`^${requestId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-c\\d+$`);
|
|
386
|
+
const targets = new Set();
|
|
387
|
+
const skipped = [];
|
|
388
|
+
for (const summary of engine.listConsolidationPlans()) {
|
|
389
|
+
if (summary.corrupt === true)
|
|
390
|
+
continue;
|
|
391
|
+
if (summary.scope !== scope)
|
|
392
|
+
continue;
|
|
393
|
+
const ev = engine.readConsolidationPlanFoldEvidence(summary.planId);
|
|
394
|
+
if (ev.state !== "ok") {
|
|
395
|
+
if (ev.state === "corrupt" && ev.requestId !== undefined && cycleId.test(ev.requestId)) {
|
|
396
|
+
return { targets, unreadable: summary.planId, unreadableState: ev.state, skipped };
|
|
397
|
+
}
|
|
398
|
+
skipped.push({ planId: summary.planId, state: ev.state });
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ev.scope !== scope || !cycleId.test(ev.requestId))
|
|
402
|
+
continue;
|
|
403
|
+
for (const t of ev.supersededTargets)
|
|
404
|
+
targets.add(t);
|
|
405
|
+
}
|
|
406
|
+
return { targets, skipped };
|
|
407
|
+
}
|
|
359
408
|
async function recoverSeat(engine, scope, { sleep, windowMs }) {
|
|
360
409
|
await engine.reconcileConsolidation(scope);
|
|
361
410
|
let seat = engine.readConsolidationGate(scope)?.openPlanId;
|
|
@@ -91,6 +91,25 @@ export interface MemoryConsolidationOptions {
|
|
|
91
91
|
maxDirectedPatchesPerPlan?: number;
|
|
92
92
|
minRunIntervalMs?: number;
|
|
93
93
|
minSessionsBetweenRuns?: number;
|
|
94
|
+
/** design/376 C-2② — the scan-refusal fuse's ratio leg, in (0, 1]: scan findings refuse the
|
|
95
|
+
* affected PRODUCT (disclosed beside eligibility drift), and the whole plan refuses only when
|
|
96
|
+
* more than max(1, ⌊ratio × submitted products⌋) of them fail the scan (the floor keeps the
|
|
97
|
+
* per-product tolerance alive on 1-4 product plans — the drive lane's common size) — see
|
|
98
|
+
* {@link scanRefusalCeiling}. Default 0.25. */
|
|
99
|
+
scanRefusalRatioCap?: number;
|
|
100
|
+
/** design/376 C-2② — the fuse's absolute leg (positive integer): the large-batch upper bound on
|
|
101
|
+
* tolerated scan refusals per plan, so the ratio cannot scale the tolerance without limit.
|
|
102
|
+
* Default 8 (the point where the 0.25 ratio hands over at 32 submitted products — the family's
|
|
103
|
+
* "小 N" quantification; below that the ratio leg alone governs). */
|
|
104
|
+
scanRefusalMaxPerPlan?: number;
|
|
105
|
+
/** design/376 C-3 — the EXPLICIT escape hatch for the unconditional instruction-form gate:
|
|
106
|
+
* instruction-form consolidation products (`type: "feedback"` / v2 hint carriers) are refused
|
|
107
|
+
* by default REGARDLESS of the visible set's markers — a clean-set instruction product is the
|
|
108
|
+
* model-reachable privilege route (N ordinary notes in, one instruction entry out). `true` is
|
|
109
|
+
* the only accepted value (a posture declaration, like {@link multiNode}); it opens the
|
|
110
|
+
* CLEAN-set arm only — a marked visible set refuses instruction forms under the laundering
|
|
111
|
+
* hard gate no matter what (privilege and hard gate enter and exit through one door). */
|
|
112
|
+
allowInstructionProducts?: true;
|
|
94
113
|
/** D-13a — the multi-node POSTURE declaration: present ⇒ `lease` must be injected (constructor
|
|
95
114
|
* refusal otherwise). Multi-node without declaring is OUT OF CONTRACT (structurally
|
|
96
115
|
* undetectable — the declaration turns a silent omission into an explicit choice). */
|
|
@@ -113,6 +132,8 @@ export declare const CONSOLIDATION_DEFAULTS: {
|
|
|
113
132
|
readonly maxDirectedPatchesPerPlan: 32;
|
|
114
133
|
readonly minRunIntervalMs: number;
|
|
115
134
|
readonly minSessionsBetweenRuns: 5;
|
|
135
|
+
readonly scanRefusalRatioCap: 0.25;
|
|
136
|
+
readonly scanRefusalMaxPerPlan: 8;
|
|
116
137
|
};
|
|
117
138
|
/**
|
|
118
139
|
* §2.2 — the HARD throttle floor: even a `force` run cannot start a second same-scope run within
|
|
@@ -142,6 +163,9 @@ export interface ScreenedConsolidationOptions {
|
|
|
142
163
|
maxDirectedPatchesPerPlan: number;
|
|
143
164
|
minRunIntervalMs: number;
|
|
144
165
|
minSessionsBetweenRuns: number;
|
|
166
|
+
scanRefusalRatioCap: number;
|
|
167
|
+
scanRefusalMaxPerPlan: number;
|
|
168
|
+
allowInstructionProducts: boolean;
|
|
145
169
|
multiNode: boolean;
|
|
146
170
|
lease?: ConsolidationLeaseSeat;
|
|
147
171
|
}
|
|
@@ -159,6 +183,21 @@ export declare function screenConsolidationOptions(raw: MemoryConsolidationOptio
|
|
|
159
183
|
/** §1.2-2 — the supersession fuse ceiling: min(N-1, max(floor, ⌊ratio×N⌋)); N ≤ 1 ⇒ 0 (one plan
|
|
160
184
|
* may NEVER empty the active set — the N-1 ceiling is unconditional, G21). */
|
|
161
185
|
export declare function supersessionFuseCeiling(activeSetSize: number, opts: Pick<ScreenedConsolidationOptions, "supersedeRatioCap" | "supersedeAbsoluteFloor">): number;
|
|
186
|
+
/**
|
|
187
|
+
* design/376 C-2② — the scan-refusal fuse ceiling: how many scan-refused products ONE plan may
|
|
188
|
+
* carry before the whole plan refuses instead of committing around them.
|
|
189
|
+
* `min(max(1, ⌊ratio × submitted⌋), absolute max)`; the ratio leg governs small plans, the
|
|
190
|
+
* absolute leg is the large-batch upper bound, and the FLOOR of one keeps C-2①'s per-product
|
|
191
|
+
* tolerance alive at the drive lane's common plan sizes — under the default 0.25 ratio a bare
|
|
192
|
+
* floor answers 0 for every ≤3-product plan, so one stray refused product re-killed the whole
|
|
193
|
+
* plan there, exactly the shape C-2① was adopted to remove (an adopted review finding). The
|
|
194
|
+
* floor is NOT a hole for the fully-refused tiny plan: a plan whose every product is refused
|
|
195
|
+
* (and which carries no directed work) still dies at the freeze-emptied C-2③ backstop, never a
|
|
196
|
+
* quiet mostly-no-op — what the floor tolerates is exactly one refused product beside at least
|
|
197
|
+
* one survivor. Submitted count = the proposal's product count (per THIS plan, never a
|
|
198
|
+
* cross-run tally).
|
|
199
|
+
*/
|
|
200
|
+
export declare function scanRefusalCeiling(submittedProducts: number, opts: Pick<ScreenedConsolidationOptions, "scanRefusalRatioCap" | "scanRefusalMaxPerPlan">): number;
|
|
162
201
|
/** §3.1 — the type axis of the eligibility predicate: episodic entries are read-only evidence
|
|
163
202
|
* (never candidates, never supersession/directed targets, A-7); procedural promotion is a v3
|
|
164
203
|
* follow-on ticket. Unknown/absent types are ordinary semantic notes — eligible. */
|
|
@@ -231,6 +270,24 @@ export declare function updateConsolidationGate<T>(controlDir: string, fn: (file
|
|
|
231
270
|
export declare function readConsolidationGateFile(controlDir: string): ConsolidationGateFile;
|
|
232
271
|
/** True ⇔ the gate store file exists at all (G1's zero-file pin reads this negatively). */
|
|
233
272
|
export declare function consolidationGateFileExists(controlDir: string): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* design/376 slice ③ (#437②) — the gate read face's DISCRIMINATED answer
|
|
275
|
+
* ({@link import("./engine.js").MemoryEngine.readConsolidationGateState}): `present` carries the
|
|
276
|
+
* row; `absent` means the store is readable and simply has no row for the scope (a never-run
|
|
277
|
+
* scope, or no gate file at all); `corrupt` means the store cannot be trusted and carries the
|
|
278
|
+
* refusal detail. The undiscriminated `readConsolidationGate` face folds corrupt into `undefined`
|
|
279
|
+
* — an operator (or a driver's scheduling read) could not tell a healthy never-run scope from a
|
|
280
|
+
* store every verb will refuse fail-closed.
|
|
281
|
+
*/
|
|
282
|
+
export type ConsolidationGateRead = {
|
|
283
|
+
state: "present";
|
|
284
|
+
row: ConsolidationGateRow;
|
|
285
|
+
} | {
|
|
286
|
+
state: "absent";
|
|
287
|
+
} | {
|
|
288
|
+
state: "corrupt";
|
|
289
|
+
detail: string;
|
|
290
|
+
};
|
|
234
291
|
/**
|
|
235
292
|
* §2.2 — the engine-minted session count (harvest-tail bookkeeping; ≤1 row write per terminal
|
|
236
293
|
* harvest; the OFF mode never calls this — D-9a zero-write). Returns the recommendation edge:
|
|
@@ -304,7 +361,15 @@ export interface ConsolidationDirectedPatch {
|
|
|
304
361
|
}
|
|
305
362
|
export type ConsolidationPlanState = "open" | "applying" | "completed" | "conflict" | "discarded";
|
|
306
363
|
export interface ConsolidationPlanFile {
|
|
307
|
-
|
|
364
|
+
/** Plan schema version. `2` ⇔ the plan carries {@link freezeRefusedInputIds} — that member is a
|
|
365
|
+
* LOAD-BEARING settle input (C-2④ keeps the named ids out of the completion fingerprint), and
|
|
366
|
+
* a reader that predates it would accept the file as v1, ignore the member, and settle a
|
|
367
|
+
* fingerprint that stamps the refused members (silently blinding the incremental face — the
|
|
368
|
+
* exact hole the field closes). Version-gating the member makes such a reader answer its
|
|
369
|
+
* "unrecognized version" corrupt arm instead: the plan parks loudly for the valve, never
|
|
370
|
+
* settles wrong (mixed-fleet crash window between the durable plan write and gate settlement).
|
|
371
|
+
* Plans without the member stay v1 — zero noise on the common path. */
|
|
372
|
+
v: 1 | 2;
|
|
308
373
|
planId: string;
|
|
309
374
|
scope: string;
|
|
310
375
|
requestId: string;
|
|
@@ -318,6 +383,13 @@ export interface ConsolidationPlanFile {
|
|
|
318
383
|
/** Frozen products — FULL entry bytes (id/frontmatter incl. distilled + origin/body/rev). */
|
|
319
384
|
products: MemoryEntry[];
|
|
320
385
|
productStates: Record<string, "pending" | "applied" | "conflict">;
|
|
386
|
+
/** design/376 C-2④ — the declared input ids of every product the FREEZE refused (scan findings
|
|
387
|
+
* and eligibility drift alike). Settlement keeps these ids OUT of the completion fingerprint:
|
|
388
|
+
* a scan-refused input's rev never moved, so stamping it would make every later incremental
|
|
389
|
+
* snapshot read it as unchanged and never offer it again — the refused member must stay a
|
|
390
|
+
* candidate. Absent on plans frozen before the field existed, and on plans with nothing
|
|
391
|
+
* refused (either way the settle filter simply has nothing extra to exclude). */
|
|
392
|
+
freezeRefusedInputIds?: string[];
|
|
321
393
|
directed: ConsolidationDirectedPatch[];
|
|
322
394
|
intents: Array<{
|
|
323
395
|
requestId: string;
|
|
@@ -40,6 +40,8 @@ export const CONSOLIDATION_DEFAULTS = {
|
|
|
40
40
|
maxDirectedPatchesPerPlan: 32,
|
|
41
41
|
minRunIntervalMs: 24 * 60 * 60 * 1000,
|
|
42
42
|
minSessionsBetweenRuns: 5,
|
|
43
|
+
scanRefusalRatioCap: 0.25,
|
|
44
|
+
scanRefusalMaxPerPlan: 8,
|
|
43
45
|
};
|
|
44
46
|
export const CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS = 60_000;
|
|
45
47
|
export const CONSOLIDATION_SESSION_RING_MAX = 64;
|
|
@@ -70,12 +72,18 @@ export function screenConsolidationOptions(raw, provenance) {
|
|
|
70
72
|
maxDirectedPatchesPerPlan: num("maxDirectedPatchesPerPlan", raw.maxDirectedPatchesPerPlan, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
|
|
71
73
|
minRunIntervalMs: num("minRunIntervalMs", raw.minRunIntervalMs, (n) => n > 0, "a positive number of milliseconds"),
|
|
72
74
|
minSessionsBetweenRuns: num("minSessionsBetweenRuns", raw.minSessionsBetweenRuns, (n) => Number.isInteger(n) && n >= 0, "a non-negative integer"),
|
|
75
|
+
scanRefusalRatioCap: num("scanRefusalRatioCap", raw.scanRefusalRatioCap, (n) => n > 0 && n <= 1, "a number in (0, 1]"),
|
|
76
|
+
scanRefusalMaxPerPlan: num("scanRefusalMaxPerPlan", raw.scanRefusalMaxPerPlan, (n) => Number.isInteger(n) && n >= 1, "a positive integer"),
|
|
77
|
+
allowInstructionProducts: raw.allowInstructionProducts === true,
|
|
73
78
|
multiNode: raw.multiNode === true,
|
|
74
79
|
...(raw.lease !== undefined ? { lease: raw.lease } : {}),
|
|
75
80
|
};
|
|
76
81
|
if (raw.multiNode !== undefined && raw.multiNode !== true) {
|
|
77
82
|
throw coded(`MemoryEngineOptions.consolidation.multiNode must be exactly true when present (a posture declaration, not a boolean knob).`, "config.memory_consolidation");
|
|
78
83
|
}
|
|
84
|
+
if (raw.allowInstructionProducts !== undefined && raw.allowInstructionProducts !== true) {
|
|
85
|
+
throw coded(`MemoryEngineOptions.consolidation.allowInstructionProducts must be exactly true when present (a posture declaration, not a boolean knob — absent means the instruction-form gate stays closed).`, "config.memory_consolidation");
|
|
86
|
+
}
|
|
79
87
|
if (screened.multiNode && screened.lease === undefined) {
|
|
80
88
|
throw coded(`MemoryEngineOptions.consolidation.multiNode declared without a lease seat — a multi-node deployment MUST inject the host's global lease (the local plan-seat CAS does not reach across nodes). Inject consolidation.lease or remove the multiNode declaration.`, "config.memory_consolidation_lease_required");
|
|
81
89
|
}
|
|
@@ -89,6 +97,11 @@ export function supersessionFuseCeiling(activeSetSize, opts) {
|
|
|
89
97
|
return 0;
|
|
90
98
|
return Math.min(activeSetSize - 1, Math.max(opts.supersedeAbsoluteFloor, Math.floor(opts.supersedeRatioCap * activeSetSize)));
|
|
91
99
|
}
|
|
100
|
+
export function scanRefusalCeiling(submittedProducts, opts) {
|
|
101
|
+
if (submittedProducts <= 0)
|
|
102
|
+
return 0;
|
|
103
|
+
return Math.min(Math.max(1, Math.floor(opts.scanRefusalRatioCap * submittedProducts)), opts.scanRefusalMaxPerPlan);
|
|
104
|
+
}
|
|
92
105
|
export function consolidationTypeEligible(type) {
|
|
93
106
|
return type !== "episodic" && type !== "procedural";
|
|
94
107
|
}
|
|
@@ -186,7 +199,7 @@ function planShapeInvalid(raw, planId) {
|
|
|
186
199
|
if (typeof raw !== "object" || raw === null)
|
|
187
200
|
return "not an object";
|
|
188
201
|
const p = raw;
|
|
189
|
-
if (p["v"] !== 1)
|
|
202
|
+
if (p["v"] !== 1 && p["v"] !== 2)
|
|
190
203
|
return "unrecognized version";
|
|
191
204
|
if (p["planId"] !== planId)
|
|
192
205
|
return "planId mismatch";
|
|
@@ -248,6 +261,13 @@ function planShapeInvalid(raw, planId) {
|
|
|
248
261
|
}
|
|
249
262
|
if (!p["directed"].every((d) => intentIds.has(d.intentRequestId)))
|
|
250
263
|
return "directed patch references an undeclared intent";
|
|
264
|
+
const freezeRefused = p["freezeRefusedInputIds"];
|
|
265
|
+
if (freezeRefused !== undefined && (!Array.isArray(freezeRefused) || !freezeRefused.every((x) => typeof x === "string"))) {
|
|
266
|
+
return "malformed freezeRefusedInputIds";
|
|
267
|
+
}
|
|
268
|
+
if ((p["v"] === 2) !== (freezeRefused !== undefined)) {
|
|
269
|
+
return p["v"] === 2 ? "v2 plan without its freezeRefusedInputIds member" : "freezeRefusedInputIds on a v1 plan";
|
|
270
|
+
}
|
|
251
271
|
if (!Array.isArray(p["audit"]))
|
|
252
272
|
return "malformed audit trail";
|
|
253
273
|
return undefined;
|