@sema-agent/core 5.54.0 → 5.55.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 +94 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +46 -0
- package/dist/core/mcp.js +132 -6
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rules.d.ts +22 -0
- package/dist/core/permission-rules.js +60 -6
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +31 -1
- package/dist/core/runner/prepare-task.js +31 -14
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +12 -10
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/tool-policy.d.ts +18 -0
- package/dist/core/tool-policy.js +19 -8
- package/dist/core/types.d.ts +89 -6
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +416 -6
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/prompts/default.d.ts +12 -2
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
package/dist/core/mcp.js
CHANGED
|
@@ -537,6 +537,82 @@ export async function mapContent(content, serverName, imageResizer) {
|
|
|
537
537
|
}
|
|
538
538
|
return out;
|
|
539
539
|
}
|
|
540
|
+
export const MCP_INVISIBLE_TEXT_RE = /[\p{Cf}\p{Co}\p{Cn}]/gu;
|
|
541
|
+
const MCP_SANITIZE_MAX_ROUNDS = 10;
|
|
542
|
+
function sanitizeMcpModelFacingText(text) {
|
|
543
|
+
let out = text;
|
|
544
|
+
for (let round = 0; round < MCP_SANITIZE_MAX_ROUNDS; round++) {
|
|
545
|
+
const next = out.normalize("NFKC").replace(MCP_INVISIBLE_TEXT_RE, "");
|
|
546
|
+
if (next === out)
|
|
547
|
+
return out;
|
|
548
|
+
out = next;
|
|
549
|
+
}
|
|
550
|
+
return out.replace(MCP_INVISIBLE_TEXT_RE, "");
|
|
551
|
+
}
|
|
552
|
+
const MCP_SCHEMA_PROSE_KEYS = new Set(["description", "title", "$comment"]);
|
|
553
|
+
const MCP_SCHEMA_CHILD_SEAT = new Map([
|
|
554
|
+
...["properties", "definitions", "$defs", "dependentSchemas"].map((k) => [k, "properties"]),
|
|
555
|
+
...["patternProperties"].map((k) => [k, "schemaMap"]),
|
|
556
|
+
...["items", "prefixItems", "additionalItems", "additionalProperties", "unevaluatedItems", "unevaluatedProperties", "contains", "propertyNames", "contentSchema", "not", "if", "then", "else", "allOf", "anyOf", "oneOf"].map((k) => [k, "schema"]),
|
|
557
|
+
...["required", "dependentRequired", "dependencies", "$ref", "$dynamicRef", "$id", "$anchor", "$dynamicAnchor"].map((k) => [k, "nameRefs"]),
|
|
558
|
+
]);
|
|
559
|
+
class McpPayloadKeyCollision extends Error {
|
|
560
|
+
}
|
|
561
|
+
function sanitizeMcpSchemaNameRefs(value) {
|
|
562
|
+
if (typeof value === "string")
|
|
563
|
+
return sanitizeMcpModelFacingText(value);
|
|
564
|
+
if (Array.isArray(value))
|
|
565
|
+
return value.map(sanitizeMcpSchemaNameRefs);
|
|
566
|
+
if (value !== null && typeof value === "object") {
|
|
567
|
+
const out = Object.create(null);
|
|
568
|
+
for (const [key, v] of Object.entries(value)) {
|
|
569
|
+
const neutralized = sanitizeMcpModelFacingText(key);
|
|
570
|
+
if (Object.hasOwn(out, neutralized))
|
|
571
|
+
throw new McpPayloadKeyCollision(neutralized);
|
|
572
|
+
out[neutralized] = Array.isArray(v) ? sanitizeMcpSchemaNameRefs(v) : sanitizeMcpModelFacingValue(v, "schema");
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
function sanitizeMcpModelFacingValue(value, seat = "schema") {
|
|
579
|
+
if (seat === "data")
|
|
580
|
+
return value;
|
|
581
|
+
if (seat === "nameRefs")
|
|
582
|
+
return sanitizeMcpSchemaNameRefs(value);
|
|
583
|
+
if (Array.isArray(value))
|
|
584
|
+
return value.map((v) => sanitizeMcpModelFacingValue(v, seat === "schema" ? "schema" : "data"));
|
|
585
|
+
if (value !== null && typeof value === "object") {
|
|
586
|
+
const out = Object.create(null);
|
|
587
|
+
for (const [key, v] of Object.entries(value)) {
|
|
588
|
+
if (seat === "schemaMap") {
|
|
589
|
+
out[key] = sanitizeMcpModelFacingValue(v, "schema");
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const neutralized = sanitizeMcpModelFacingText(key);
|
|
593
|
+
if (Object.hasOwn(out, neutralized))
|
|
594
|
+
throw new McpPayloadKeyCollision(neutralized);
|
|
595
|
+
if (seat === "properties") {
|
|
596
|
+
out[neutralized] = sanitizeMcpModelFacingValue(v, "schema");
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (MCP_SCHEMA_PROSE_KEYS.has(neutralized)) {
|
|
600
|
+
out[neutralized] = typeof v === "string" ? sanitizeMcpModelFacingText(v) : v;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
out[neutralized] = sanitizeMcpModelFacingValue(v, MCP_SCHEMA_CHILD_SEAT.get(neutralized) ?? "data");
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
}
|
|
609
|
+
const MCP_TOOL_DESCRIPTION_MAX_CHARS = 2048;
|
|
610
|
+
const MCP_TOOL_DESCRIPTION_TRUNCATION_MARK = "… [truncated]";
|
|
611
|
+
function capMcpToolDescription(description) {
|
|
612
|
+
if (description.length <= MCP_TOOL_DESCRIPTION_MAX_CHARS)
|
|
613
|
+
return description;
|
|
614
|
+
return sliceHeadSafe(description, MCP_TOOL_DESCRIPTION_MAX_CHARS) + MCP_TOOL_DESCRIPTION_TRUNCATION_MARK;
|
|
615
|
+
}
|
|
540
616
|
const MCP_SCHEMA_COMBINATOR_KEYS = ["anyOf", "oneOf", "allOf"];
|
|
541
617
|
const MCP_SCHEMA_PROP_NAME_RE = /^[a-zA-Z0-9_.-]{1,64}$/;
|
|
542
618
|
const MCP_SCHEMA_CARRY_KEYS = ["$defs", "definitions", "$schema", "additionalProperties", "description", "title"];
|
|
@@ -643,6 +719,19 @@ export function normalizeMcpToolSchema(schema) {
|
|
|
643
719
|
return { outcome: "drop", reason: `input schema uses top-level ${combinatorsPresent.join("/")} and could not be normalized` };
|
|
644
720
|
}
|
|
645
721
|
}
|
|
722
|
+
export function mcpToolSchemaAdvisory(schema) {
|
|
723
|
+
if (schema === undefined || !isPlainSchemaObject(schema))
|
|
724
|
+
return undefined;
|
|
725
|
+
const properties = schema.properties;
|
|
726
|
+
if (!isPlainSchemaObject(properties))
|
|
727
|
+
return undefined;
|
|
728
|
+
for (const key of Object.keys(properties)) {
|
|
729
|
+
if (!MCP_SCHEMA_PROP_NAME_RE.test(key)) {
|
|
730
|
+
return `its parameter name ${inlineUntrusted(JSON.stringify(key), 80)} is outside the character set some providers accept for tool parameters (${MCP_SCHEMA_PROP_NAME_RE.source}); requests carrying this tool may be rejected by such a provider`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
646
735
|
export function mcpToolSchemaProblem(schema) {
|
|
647
736
|
if (schema === undefined)
|
|
648
737
|
return undefined;
|
|
@@ -741,6 +830,11 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
741
830
|
resourceServers.push(s.resourceServer);
|
|
742
831
|
for (const d of s.dropped)
|
|
743
832
|
droppedTools.push({ server: inlineUntrusted(spec.name, 160), ...d });
|
|
833
|
+
for (const a of s.schemaAdvisories)
|
|
834
|
+
warnings.push(schemaAdvisoryWarning(spec.name, a.tool, a.reason));
|
|
835
|
+
if (s.toolsCapabilityAbsent === true && spec.allowTools !== undefined && spec.allowTools.length > 0) {
|
|
836
|
+
warnings.push(toolsCapabilityAbsentWarning(spec.name));
|
|
837
|
+
}
|
|
744
838
|
if (s.listingIncomplete)
|
|
745
839
|
warnings.push(listingIncompleteWarning(spec.name, s.listingIncomplete));
|
|
746
840
|
statuses.push(s.status);
|
|
@@ -1329,6 +1423,8 @@ export function parseCallToolResultLenient(data) {
|
|
|
1329
1423
|
}
|
|
1330
1424
|
const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient };
|
|
1331
1425
|
async function listToolsLenient(client, options) {
|
|
1426
|
+
if (!client.getServerCapabilities()?.tools)
|
|
1427
|
+
return { tools: [], toolsCapabilityAbsent: true };
|
|
1332
1428
|
const walk = await walkMcpListPages(async (cursor, remainingMs) => {
|
|
1333
1429
|
const page = await client.request({ method: "tools/list", params: cursor === undefined ? {} : { cursor } }, LenientListToolsResultSchema, { ...options, timeout: remainingMs });
|
|
1334
1430
|
return {
|
|
@@ -1379,7 +1475,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1379
1475
|
};
|
|
1380
1476
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1381
1477
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1382
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1478
|
+
const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1383
1479
|
const caps = client.getServerCapabilities();
|
|
1384
1480
|
const resourceInfo = caps?.resources
|
|
1385
1481
|
? {
|
|
@@ -1403,6 +1499,8 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1403
1499
|
tools: serverTools,
|
|
1404
1500
|
axes: serverAxes,
|
|
1405
1501
|
dropped,
|
|
1502
|
+
schemaAdvisories: advisories,
|
|
1503
|
+
...(listed.toolsCapabilityAbsent === true ? { toolsCapabilityAbsent: true } : {}),
|
|
1406
1504
|
listedTools: listed.tools,
|
|
1407
1505
|
...(instructions ? { instructions } : {}),
|
|
1408
1506
|
...(listed.incomplete !== undefined ? { listingIncomplete: listed.incomplete } : {}),
|
|
@@ -1418,14 +1516,28 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1418
1516
|
const serverTools = [];
|
|
1419
1517
|
const serverAxes = [];
|
|
1420
1518
|
const dropped = [];
|
|
1519
|
+
const advisories = [];
|
|
1421
1520
|
const mintedNames = new Map();
|
|
1422
1521
|
for (const t of listed.tools) {
|
|
1423
1522
|
if (spec.allowTools && !spec.allowTools.includes(t.name)) {
|
|
1424
1523
|
continue;
|
|
1425
1524
|
}
|
|
1426
|
-
|
|
1427
|
-
let
|
|
1428
|
-
|
|
1525
|
+
let modelFacingSchema;
|
|
1526
|
+
let sanitizedDescription;
|
|
1527
|
+
try {
|
|
1528
|
+
modelFacingSchema = sanitizeMcpModelFacingValue(t.inputSchema);
|
|
1529
|
+
sanitizedDescription = t.description !== undefined ? sanitizeMcpModelFacingText(t.description) : undefined;
|
|
1530
|
+
}
|
|
1531
|
+
catch (err) {
|
|
1532
|
+
const reason = err instanceof McpPayloadKeyCollision
|
|
1533
|
+
? `two of its advertised names neutralize to the same model-facing spelling ${inlineUntrusted(JSON.stringify(err.message), 80)}; one parameter would have vanished from the schema the model is given while the server still expects it`
|
|
1534
|
+
: "advertised tool payload could not be neutralized for model-facing display (pathological nesting)";
|
|
1535
|
+
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(reason, 240) });
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const normalized = normalizeMcpToolSchema(modelFacingSchema);
|
|
1539
|
+
let effectiveInputSchema = modelFacingSchema;
|
|
1540
|
+
let effectiveDescription = sanitizedDescription !== undefined ? sanitizeUntrustedText(sanitizedDescription) : undefined;
|
|
1429
1541
|
if (normalized.outcome === "drop") {
|
|
1430
1542
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(normalized.reason, 240) });
|
|
1431
1543
|
continue;
|
|
@@ -1439,6 +1551,10 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1439
1551
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaProblem, 240) });
|
|
1440
1552
|
continue;
|
|
1441
1553
|
}
|
|
1554
|
+
const schemaAdvisory = mcpToolSchemaAdvisory(effectiveInputSchema);
|
|
1555
|
+
if (schemaAdvisory !== undefined) {
|
|
1556
|
+
advisories.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaAdvisory, 240) });
|
|
1557
|
+
}
|
|
1442
1558
|
const remoteName = t.name;
|
|
1443
1559
|
const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
|
|
1444
1560
|
const mintedBy = mintedNames.get(namespacedName);
|
|
@@ -1462,7 +1578,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1462
1578
|
const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
|
|
1463
1579
|
serverTools.push({
|
|
1464
1580
|
name: namespacedName,
|
|
1465
|
-
description: effectiveDescription ?? `MCP tool ${remoteName} from ${spec.name}
|
|
1581
|
+
description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
|
|
1466
1582
|
label: `${spec.name}:${remoteName}`,
|
|
1467
1583
|
parameters: (effectiveInputSchema ?? { type: "object" }),
|
|
1468
1584
|
...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
|
|
@@ -1564,13 +1680,23 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1564
1680
|
},
|
|
1565
1681
|
});
|
|
1566
1682
|
}
|
|
1567
|
-
return { serverTools, serverAxes, dropped };
|
|
1683
|
+
return { serverTools, serverAxes, dropped, advisories };
|
|
1568
1684
|
}
|
|
1569
1685
|
function asServerWarning(spec, err) {
|
|
1570
1686
|
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
|
|
1571
1687
|
warning.code = "mcp.server_unavailable";
|
|
1572
1688
|
return warning;
|
|
1573
1689
|
}
|
|
1690
|
+
function schemaAdvisoryWarning(server, tool, reason) {
|
|
1691
|
+
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" tool "${tool}" is mounted, but ${reason}.`);
|
|
1692
|
+
warning.code = "mcp.tool_schema_advisory";
|
|
1693
|
+
return warning;
|
|
1694
|
+
}
|
|
1695
|
+
function toolsCapabilityAbsentWarning(server) {
|
|
1696
|
+
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" declared no "tools" capability, so no tool listing was requested and none of the tools named in this server's allowTools can be mounted. If the server does serve tools, it must declare the capability at initialize.`);
|
|
1697
|
+
warning.code = "mcp.tools_capability_absent";
|
|
1698
|
+
return warning;
|
|
1699
|
+
}
|
|
1574
1700
|
function listingIncompleteWarning(server, flag) {
|
|
1575
1701
|
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" listed its tools INCOMPLETELY — ${listingIncompleteNote(flag)}. The tools beyond the ${flag.pages} page${flag.pages === 1 ? "" : "s"} retrieved are NOT mounted for this task.`);
|
|
1576
1702
|
warning.code = "mcp.listing_incomplete";
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/339 — the v3 consolidation WRITE PROTOCOL's engine-side module: active-set derivation
|
|
3
|
+
* (§1.4.3), the gate store / plan store control-plane sidecars (§2.3), option screening (§6.1) and
|
|
4
|
+
* the model-visible wording the read-side supersession treatment mints (§3.4).
|
|
5
|
+
*
|
|
6
|
+
* The derivation here is the ONE predicate every read face consumes (index mechanical rows,
|
|
7
|
+
* memory_search's default filter, memory_get's disclosure, and the v2 retention/recall knives when
|
|
8
|
+
* they land) — a second implementation would be a drift seam, exactly the class the challenge
|
|
9
|
+
* exclusion view closed.
|
|
10
|
+
*/
|
|
11
|
+
import type { MemoryEntry, MemoryEntryHeader, MemoryEntryOrigin, NotePatch } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Derive the SUPERSEDED set from a committed header listing: superseded entry id → the standing
|
|
14
|
+
* carrier (product) id whose edge suppresses it. `active(x) ⇔ !returned.has(x.id)` over non-
|
|
15
|
+
* tombstoned entries (tombstones never reach a header listing).
|
|
16
|
+
*
|
|
17
|
+
* Edge validity (§1.4.3, the r1-2/r3-2 amended law) — an edge (target, rev) carried by product `p`
|
|
18
|
+
* is VALID ⇔ all of:
|
|
19
|
+
* - `p` stands in the listing (a tombstoned carrier lists nothing — its edges die with it);
|
|
20
|
+
* - `p` is not excluded by the local governance overlay (`excluded`: challenge/lineage-latch —
|
|
21
|
+
* a QUESTIONED replacement must not keep suppressing its net inputs; the inputs revive, the
|
|
22
|
+
* information never falls into a double black hole). The overlay is deliberately LOCAL (the
|
|
23
|
+
* challenge ledger does not travel) — a deployment without the account derives from committed
|
|
24
|
+
* facts alone and shows MORE evidence, the safe direction;
|
|
25
|
+
* - `p`'s committed rev still equals the block's `carrierRev` (the CARRIER-side rev anchor,
|
|
26
|
+
* r3-2): a legitimate edit of the product suspends its edges — the edited product may have
|
|
27
|
+
* rewritten or retracted what it integrated, and the only accurate copy must not hide outside
|
|
28
|
+
* the default face. Deep-carrying idempotent touches move no rev and keep the edges;
|
|
29
|
+
* - the TARGET stands in the listing at exactly the edge's anchored rev (the input-side rev
|
|
30
|
+
* anchor, D-2a): a later legitimate edit of the superseded entry REVIVES it (new information
|
|
31
|
+
* is never suppressed by an edge frozen over old bytes); an absent/tombstoned target makes the
|
|
32
|
+
* edge inert (nothing to suppress — the partial-apply direction is safe by the self-containment
|
|
33
|
+
* law: an edge exists only on a landed product);
|
|
34
|
+
* - the target lives in the CARRIER'S OWN SCOPE (§3.3 内闭合, read side). The freeze and the retry
|
|
35
|
+
* enforce "one plan, one scope" on the WRITE side, but every read consumer here is fed a
|
|
36
|
+
* MULTI-scope listing (the tool face lists a plane's scopes, index rebuild and the collapse fuse
|
|
37
|
+
* list every mounted scope), and an edge frozen legally inside scope A can later face a target
|
|
38
|
+
* in scope B: `computeEntryRev` deliberately excludes scope/slug, so a supported cross-scope
|
|
39
|
+
* MOVE leaves the anchored rev intact, and an adopted carrier (sync/bundle) is validated for its
|
|
40
|
+
* OWN scope with its edge list checked for SHAPE only. Consolidation authority is per-scope
|
|
41
|
+
* (seat + lease), so a scope-A carrier must not pull a scope-B entry out of B's default face.
|
|
42
|
+
* A whole-scope migration moves carrier and target together and keeps this leg satisfied; only
|
|
43
|
+
* the split/partial case changes, and it degrades toward REVIVAL — this module's stated safe
|
|
44
|
+
* direction (a deployment shows MORE evidence, never less).
|
|
45
|
+
*
|
|
46
|
+
* CHAIN RULE (§1.4.3, explicit): a carrier that is ITSELF superseded (by a later product) keeps
|
|
47
|
+
* its edges — supersession is convergence, not discredit; the chain p2→p1→x keeps x suppressed.
|
|
48
|
+
* Only integrity questioning (the overlay) and the tombstone suspend a carrier's edges.
|
|
49
|
+
*
|
|
50
|
+
* NON-EMPTINESS (§1.4.3 定理, r2-1): edges only point at a product's own inputs, and inputs are
|
|
51
|
+
* committed before the product exists — the supersession relation is a DAG, its maximal elements
|
|
52
|
+
* are always active, so a non-empty library can never derive an empty active set (pinned by test
|
|
53
|
+
* with the dual-plan full-coverage construction).
|
|
54
|
+
*/
|
|
55
|
+
export declare function deriveSupersededSet(headers: readonly MemoryEntryHeader[], opts?: {
|
|
56
|
+
excluded?: (id: string) => boolean;
|
|
57
|
+
}): Map<string, string>;
|
|
58
|
+
/** memory_get head note for a superseded entry (delivery proceeds — disclosure, never refusal). */
|
|
59
|
+
export declare function memorySupersededNote(carrierId: string): string;
|
|
60
|
+
/** The tag suffixed to a search hit line when a superseded entry is retrieved explicitly. */
|
|
61
|
+
export declare const MEMORY_SEARCH_SUPERSEDED_TAG = "[superseded]";
|
|
62
|
+
/**
|
|
63
|
+
* §1.7/§2.3 — the host-injected GLOBAL lease seat (multi-node deployments). The protocol's SAFETY
|
|
64
|
+
* never depends on it (§1.7 four-independence table) — the lease is COST suppression (one
|
|
65
|
+
* distillation fleet-wide) plus the fencing token that shrinks a stale holder's overlap window to
|
|
66
|
+
* a single applyPatches batch. `acquire` answers `undefined` when the lease is NOT held; a held
|
|
67
|
+
* answer may carry a monotonic `token` whose CURRENT value is re-checked before every apply batch
|
|
68
|
+
* (a changed token aborts the batch — the plan stays replayable under the new holder).
|
|
69
|
+
*/
|
|
70
|
+
export interface ConsolidationLeaseSeat {
|
|
71
|
+
acquire(scope: string): Promise<{
|
|
72
|
+
token?: string;
|
|
73
|
+
} | undefined> | {
|
|
74
|
+
token?: string;
|
|
75
|
+
} | undefined;
|
|
76
|
+
renew?(scope: string): Promise<{
|
|
77
|
+
token?: string;
|
|
78
|
+
} | undefined> | {
|
|
79
|
+
token?: string;
|
|
80
|
+
} | undefined;
|
|
81
|
+
release?(scope: string): Promise<void> | void;
|
|
82
|
+
}
|
|
83
|
+
/** design/339 §6.1 — the `MemoryEngineOptions.consolidation` seat (absent = OFF, the v3 default). */
|
|
84
|
+
export interface MemoryConsolidationOptions {
|
|
85
|
+
/** Fuse ratio (0,1]: one plan's supersession targets ≤ min(N-1, max(floor, ⌊ratio×N⌋)). */
|
|
86
|
+
supersedeRatioCap?: number;
|
|
87
|
+
/** Small-library floor (positive integer), capped by the N-1 ceiling. */
|
|
88
|
+
supersedeAbsoluteFloor?: number;
|
|
89
|
+
maxProductsPerRun?: number;
|
|
90
|
+
maxInputsPerProduct?: number;
|
|
91
|
+
maxDirectedPatchesPerPlan?: number;
|
|
92
|
+
minRunIntervalMs?: number;
|
|
93
|
+
minSessionsBetweenRuns?: number;
|
|
94
|
+
/** D-13a — the multi-node POSTURE declaration: present ⇒ `lease` must be injected (constructor
|
|
95
|
+
* refusal otherwise). Multi-node without declaring is OUT OF CONTRACT (structurally
|
|
96
|
+
* undetectable — the declaration turns a silent omission into an explicit choice). */
|
|
97
|
+
multiNode?: true;
|
|
98
|
+
/** The global lease (§1.7). Absent ⇒ the single-machine plan-seat CAS is the only mutex. */
|
|
99
|
+
lease?: ConsolidationLeaseSeat;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* D-10 — factory defaults. CC-product-default transplants, NOT sema calibrations (178 ①-3): the
|
|
103
|
+
* 24h/5-session cadence mirrors the background-consolidation cadence CC-family products ship;
|
|
104
|
+
* 0.25/floor-4 mirror the mass-deletion fuse family's calibration posture (MASS_DELETION_FUSE_RATIO
|
|
105
|
+
* 0.5 halved — a LOGICAL delete rides on top of the physical fuse, so its单窗 ceiling is stricter);
|
|
106
|
+
* 64/32/32 are the "小 N" ruling's quantification. Hosts can configure every one.
|
|
107
|
+
*/
|
|
108
|
+
export declare const CONSOLIDATION_DEFAULTS: {
|
|
109
|
+
readonly supersedeRatioCap: 0.25;
|
|
110
|
+
readonly supersedeAbsoluteFloor: 4;
|
|
111
|
+
readonly maxProductsPerRun: 64;
|
|
112
|
+
readonly maxInputsPerProduct: 32;
|
|
113
|
+
readonly maxDirectedPatchesPerPlan: 32;
|
|
114
|
+
readonly minRunIntervalMs: number;
|
|
115
|
+
readonly minSessionsBetweenRuns: 5;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* §2.2 — the HARD throttle floor: even a `force` run cannot start a second same-scope run within
|
|
119
|
+
* this window of the previous one (cost floor + double-run suppression; the mutex covers overlap,
|
|
120
|
+
* this covers thrash). The window is `min(minRunIntervalMs, THIS)` — the knob is reused as the
|
|
121
|
+
* cap, never exceeded — because a full-`minRunIntervalMs` hard throttle would make `force`
|
|
122
|
+
* structurally dead (the doc's force-bypassable time gate and its unbypassable throttle share the
|
|
123
|
+
* knob; this floor form is the one reading that keeps both meaningful, disclosed).
|
|
124
|
+
*/
|
|
125
|
+
export declare const CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS = 60000;
|
|
126
|
+
/** The bounded distinct-session ring (§2.3 sessionsSince dedup). */
|
|
127
|
+
export declare const CONSOLIDATION_SESSION_RING_MAX = 64;
|
|
128
|
+
/**
|
|
129
|
+
* codex r2 — the seat's FREEZE GRACE: reconcile's orphan-release arm treats a seat whose plan file
|
|
130
|
+
* is absent as crashed ONLY once the claim is at least this old. Calibration: the freeze between
|
|
131
|
+
* the seat CAS and the plan file's durable write is committed READS only (headers + a bounded
|
|
132
|
+
* getByIds) — seconds on any healthy store; ten minutes is orders of magnitude past it, while a
|
|
133
|
+
* genuinely crashed claim still self-heals within one recommendation cadence. Inside the window
|
|
134
|
+
* reconcile reports the claim as in-flight instead of releasing it.
|
|
135
|
+
*/
|
|
136
|
+
export declare const CONSOLIDATION_SEAT_FREEZE_GRACE_MS: number;
|
|
137
|
+
export interface ScreenedConsolidationOptions {
|
|
138
|
+
supersedeRatioCap: number;
|
|
139
|
+
supersedeAbsoluteFloor: number;
|
|
140
|
+
maxProductsPerRun: number;
|
|
141
|
+
maxInputsPerProduct: number;
|
|
142
|
+
maxDirectedPatchesPerPlan: number;
|
|
143
|
+
minRunIntervalMs: number;
|
|
144
|
+
minSessionsBetweenRuns: number;
|
|
145
|
+
multiNode: boolean;
|
|
146
|
+
lease?: ConsolidationLeaseSeat;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Screen the consolidation options at engine construction (#123 bad-value loudness: every bad
|
|
150
|
+
* value refuses with a code, never folds to a default). Two structural refusals beyond values:
|
|
151
|
+
* - D-12a: `provenance: "off"` × consolidation present is a CONSTRUCTION refusal — off means "no
|
|
152
|
+
* origin is ever minted", while consolidation's fold law must be able to mint (a marked input's
|
|
153
|
+
* product would otherwise commit unmarked = the laundering window). Enable carry or disable
|
|
154
|
+
* consolidation.
|
|
155
|
+
* - D-13a: `multiNode: true` without an injected lease refuses — the declaration exists exactly
|
|
156
|
+
* to make that omission explicit.
|
|
157
|
+
*/
|
|
158
|
+
export declare function screenConsolidationOptions(raw: MemoryConsolidationOptions, provenance: "off" | "carry"): ScreenedConsolidationOptions;
|
|
159
|
+
/** §1.2-2 — the supersession fuse ceiling: min(N-1, max(floor, ⌊ratio×N⌋)); N ≤ 1 ⇒ 0 (one plan
|
|
160
|
+
* may NEVER empty the active set — the N-1 ceiling is unconditional, G21). */
|
|
161
|
+
export declare function supersessionFuseCeiling(activeSetSize: number, opts: Pick<ScreenedConsolidationOptions, "supersedeRatioCap" | "supersedeAbsoluteFloor">): number;
|
|
162
|
+
/** §3.1 — the type axis of the eligibility predicate: episodic entries are read-only evidence
|
|
163
|
+
* (never candidates, never supersession/directed targets, A-7); procedural promotion is a v3
|
|
164
|
+
* follow-on ticket. Unknown/absent types are ordinary semantic notes — eligible. */
|
|
165
|
+
export declare function consolidationTypeEligible(type: string | undefined): boolean;
|
|
166
|
+
export declare const CONSOLIDATION_GATE_FILE = "consolidation-gate.json";
|
|
167
|
+
export declare const CONSOLIDATION_PLANS_DIR = "consolidation-plans";
|
|
168
|
+
export declare const CONSOLIDATION_INTENTS_FILE = "consolidation-intents.json";
|
|
169
|
+
export interface ConsolidationGateRow {
|
|
170
|
+
/** Last COMPLETED run's settle time (ms epoch). */
|
|
171
|
+
lastRunAt?: number;
|
|
172
|
+
/** Distinct terminal-harvest sessions since the last run (count + bounded dedup ring). */
|
|
173
|
+
sessions: {
|
|
174
|
+
count: number;
|
|
175
|
+
ring: string[];
|
|
176
|
+
};
|
|
177
|
+
/** The plan-seat mutex: the one non-terminal (or conflict-parked) plan of this scope. */
|
|
178
|
+
openPlanId?: string;
|
|
179
|
+
/** When the seat was claimed (ms epoch) — the reconcile orphan-release arm's staleness anchor
|
|
180
|
+
* (codex r2: a freeze legitimately computes for a while between the seat CAS and the plan
|
|
181
|
+
* file's durable write; an absent file alone must never read as a crash). */
|
|
182
|
+
seatClaimedAt?: number;
|
|
183
|
+
/** §1.4.5 collapse-fuse baseline (last observed active-set size). */
|
|
184
|
+
activeSetBaseline?: number;
|
|
185
|
+
/** Once-per-crossing edge for the recommendation notice. */
|
|
186
|
+
lastRecommendedAt?: number;
|
|
187
|
+
/** Run watermark: increments on every completed/discarded settlement (the stale-proposal gate). */
|
|
188
|
+
epoch: number;
|
|
189
|
+
/** §1.2-1 — the last COMPLETED run's full-eligible-set fingerprint (id → committed rev):
|
|
190
|
+
* change-driven increments diff against THIS — a candidate is an eligible entry that is new
|
|
191
|
+
* to the map or whose rev moved (eligibility flips INTO the set read as "new": the map only
|
|
192
|
+
* ever holds eligible entries). Absent/lost ⇒ full candidates (conservative: 多看不少看). */
|
|
193
|
+
fingerprint?: Record<string, {
|
|
194
|
+
rev: string;
|
|
195
|
+
}>;
|
|
196
|
+
/** The last snapshot taken for this scope (the engine's visible-set account, §1.2-2 r3-1: the
|
|
197
|
+
* fold/instruction authority is what the engine actually SERVED — recorded here, keyed by the
|
|
198
|
+
* cycle token; a newer snapshot replaces it, and commit refuses a token that no longer
|
|
199
|
+
* matches). `candidates`: the served (incremental) id → rev set — the edge-target legality
|
|
200
|
+
* domain; `eligible`: the FULL eligible id → rev map at snapshot time (the next fingerprint on
|
|
201
|
+
* completion); `marked`: any served candidate carried a committed external-origin marker. */
|
|
202
|
+
snapshot?: {
|
|
203
|
+
token: string;
|
|
204
|
+
at: number;
|
|
205
|
+
epoch: number;
|
|
206
|
+
candidates: Record<string, string>;
|
|
207
|
+
eligible: Record<string, string>;
|
|
208
|
+
marked: boolean;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
interface ConsolidationGateFile {
|
|
212
|
+
v: 1;
|
|
213
|
+
scopes: Record<string, ConsolidationGateRow>;
|
|
214
|
+
}
|
|
215
|
+
/** Locked, journaled gate update (the strict family's exact discipline). */
|
|
216
|
+
export declare function updateConsolidationGate<T>(controlDir: string, fn: (file: ConsolidationGateFile) => {
|
|
217
|
+
next?: ConsolidationGateFile;
|
|
218
|
+
result: T;
|
|
219
|
+
}): T;
|
|
220
|
+
/** Lock-less journal-aware gate read (corrupt ⇒ throws — the verbs' fail-closed arm). */
|
|
221
|
+
export declare function readConsolidationGateFile(controlDir: string): ConsolidationGateFile;
|
|
222
|
+
/** True ⇔ the gate store file exists at all (G1's zero-file pin reads this negatively). */
|
|
223
|
+
export declare function consolidationGateFileExists(controlDir: string): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* §2.2 — the engine-minted session count (harvest-tail bookkeeping; ≤1 row write per terminal
|
|
226
|
+
* harvest; the OFF mode never calls this — D-9a zero-write). Returns the recommendation edge:
|
|
227
|
+
* `recommended` is true exactly ONCE per threshold crossing (once-per-越线沿; a completed run
|
|
228
|
+
* resets the edge).
|
|
229
|
+
*/
|
|
230
|
+
export declare function recordConsolidationSession(controlDir: string, scope: string, sessionId: string, opts: ScreenedConsolidationOptions, now: () => number): {
|
|
231
|
+
recommended: boolean;
|
|
232
|
+
sessionsSince: number;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* §1.4.5 — the active-set COLLAPSE fuse: record the freshly derived active-set size against the
|
|
236
|
+
* stored baseline; a single-window shrink beyond the ratio cap answers the collapse fact (the
|
|
237
|
+
* caller discloses via incident + announcement — the read face NEVER refuses service over it).
|
|
238
|
+
* Baseline loss ⇒ re-baseline silently (conservative: one extra disclosure at worst next window).
|
|
239
|
+
*/
|
|
240
|
+
export declare function recordActiveSetBaseline(controlDir: string, scope: string, size: number, ratioCap: number, _now: () => number): {
|
|
241
|
+
collapsed?: {
|
|
242
|
+
from: number;
|
|
243
|
+
to: number;
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
/** §1.5 — a directed-change intent (TERMINAL-STATE form: the intent names the target end state,
|
|
247
|
+
* never an increment — the precondition for replay/re-judgment converging at any interleaving). */
|
|
248
|
+
export type ConsolidationIntent = {
|
|
249
|
+
requestId: string;
|
|
250
|
+
kind: "forget";
|
|
251
|
+
entryId: string;
|
|
252
|
+
} | {
|
|
253
|
+
requestId: string;
|
|
254
|
+
kind: "rewrite";
|
|
255
|
+
entryId: string;
|
|
256
|
+
body: string;
|
|
257
|
+
name?: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
type?: string;
|
|
260
|
+
};
|
|
261
|
+
/** §1.1 — one proposed product (driver output = DATA, zero carriage authority: origin/distilled
|
|
262
|
+
* in a proposal do not exist as fields — the engine computes both at freeze). */
|
|
263
|
+
export interface ConsolidationProductProposal {
|
|
264
|
+
name?: string;
|
|
265
|
+
description?: string;
|
|
266
|
+
type?: string;
|
|
267
|
+
body: string;
|
|
268
|
+
/** The product's COMPLETE input attribution; `supersede: true` rows are the proposed edges. */
|
|
269
|
+
inputs: Array<{
|
|
270
|
+
id: string;
|
|
271
|
+
supersede?: boolean;
|
|
272
|
+
}>;
|
|
273
|
+
}
|
|
274
|
+
export interface ConsolidationProposal {
|
|
275
|
+
scope: string;
|
|
276
|
+
products: ConsolidationProductProposal[];
|
|
277
|
+
intents?: ConsolidationIntent[];
|
|
278
|
+
}
|
|
279
|
+
export type ConsolidationDirectedState = "pending" | "applied" | "satisfied" | "conflict";
|
|
280
|
+
export interface ConsolidationDirectedPatch {
|
|
281
|
+
intentRequestId: string;
|
|
282
|
+
op: "update" | "delete";
|
|
283
|
+
id: string;
|
|
284
|
+
baseRev: string;
|
|
285
|
+
/** update: computeEntryRev of the frozen terminal entry (the satisfied-judgment anchor, r2-4);
|
|
286
|
+
* absent for delete (the delete terminal is ABSENCE — probed, not hashed). */
|
|
287
|
+
plannedPostRev?: string;
|
|
288
|
+
entry?: MemoryEntry;
|
|
289
|
+
state: ConsolidationDirectedState;
|
|
290
|
+
/** delete only (§1.5-2): who reached the terminal — "self" when THIS plan's apply landed it,
|
|
291
|
+
* "unattributed" when a replay found the terminal already standing (the F9 crash micro-window:
|
|
292
|
+
* audit degrades, the judgment never does). */
|
|
293
|
+
attribution?: "self" | "unattributed";
|
|
294
|
+
}
|
|
295
|
+
export type ConsolidationPlanState = "open" | "applying" | "completed" | "conflict" | "discarded";
|
|
296
|
+
export interface ConsolidationPlanFile {
|
|
297
|
+
v: 1;
|
|
298
|
+
planId: string;
|
|
299
|
+
scope: string;
|
|
300
|
+
requestId: string;
|
|
301
|
+
createdAt: number;
|
|
302
|
+
cycleToken?: string;
|
|
303
|
+
epoch: number;
|
|
304
|
+
/** r3-1 — the ENGINE-VISIBLE-SET fold fact this run froze under (authority = what the engine
|
|
305
|
+
* served, never the proposal's declaration). */
|
|
306
|
+
visibleMarked: boolean;
|
|
307
|
+
foldedOrigin?: MemoryEntryOrigin;
|
|
308
|
+
/** Frozen products — FULL entry bytes (id/frontmatter incl. distilled + origin/body/rev). */
|
|
309
|
+
products: MemoryEntry[];
|
|
310
|
+
productStates: Record<string, "pending" | "applied" | "conflict">;
|
|
311
|
+
directed: ConsolidationDirectedPatch[];
|
|
312
|
+
intents: Array<{
|
|
313
|
+
requestId: string;
|
|
314
|
+
state: "pending" | "settled" | "abandoned";
|
|
315
|
+
}>;
|
|
316
|
+
state: ConsolidationPlanState;
|
|
317
|
+
audit: Array<{
|
|
318
|
+
at: number;
|
|
319
|
+
event: string;
|
|
320
|
+
requestId?: string;
|
|
321
|
+
detail?: string;
|
|
322
|
+
}>;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Plan ids are ENGINE-MINTED uuids, but the host-facing verbs (`resolveConsolidationPlan`,
|
|
326
|
+
* `reconcileConsolidation` via the gate row) echo them back as ARGUMENTS — and the id doubles as a
|
|
327
|
+
* control-plane FILE NAME, so a crafted spelling (`../…`, an absolute path) would escape the plans
|
|
328
|
+
* directory on the read AND on the corrupt-custody RENAME (an outside pass's security finding,
|
|
329
|
+
* adopted). One grammar (the entry-id family's shape), judged before any path join — the 幽灵行
|
|
330
|
+
* guard's refusal posture: an invalid spelling reads as absent/refused, never as a path.
|
|
331
|
+
*/
|
|
332
|
+
export declare function isValidConsolidationPlanId(value: string): boolean;
|
|
333
|
+
export declare function writeConsolidationPlan(controlDir: string, plan: ConsolidationPlanFile): void;
|
|
334
|
+
export type ConsolidationPlanRead = {
|
|
335
|
+
state: "ok";
|
|
336
|
+
plan: ConsolidationPlanFile;
|
|
337
|
+
} | {
|
|
338
|
+
state: "absent";
|
|
339
|
+
} | {
|
|
340
|
+
state: "corrupt";
|
|
341
|
+
detail: string;
|
|
342
|
+
};
|
|
343
|
+
export declare function readConsolidationPlan(controlDir: string, planId: string): ConsolidationPlanRead;
|
|
344
|
+
export declare function listConsolidationPlanIds(controlDir: string): string[];
|
|
345
|
+
/** F5 custody — a corrupt plan file the valve DISCARDS is moved aside (never deleted: the bytes
|
|
346
|
+
* are the only record of what the plan was). Returns the custody file name. */
|
|
347
|
+
export declare function quarantineCorruptPlan(controlDir: string, planId: string): string | undefined;
|
|
348
|
+
export interface ConsolidationIntentCredentialRow {
|
|
349
|
+
requestId: string;
|
|
350
|
+
state: "settled" | "abandoned";
|
|
351
|
+
planId: string;
|
|
352
|
+
at: number;
|
|
353
|
+
patches: Array<{
|
|
354
|
+
op: "update" | "delete";
|
|
355
|
+
id: string;
|
|
356
|
+
attribution?: "self" | "unattributed";
|
|
357
|
+
}>;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Record intent credentials — IDEMPOTENT on requestId (F10: two plans racing one intent cannot
|
|
361
|
+
* double-settle; the first credential row stands, a later contradictory write is dropped with the
|
|
362
|
+
* losing verdict returned to the caller for audit). Returns the requestIds actually written.
|
|
363
|
+
*/
|
|
364
|
+
export declare function recordIntentCredentials(controlDir: string, rows: readonly ConsolidationIntentCredentialRow[]): string[];
|
|
365
|
+
export declare function readIntentCredentials(controlDir: string): Record<string, ConsolidationIntentCredentialRow>;
|
|
366
|
+
/** Derive a product's slug from its proposed name (memory filename charset; collisions are the
|
|
367
|
+
* backend's deterministic -n suffix). */
|
|
368
|
+
export declare function deriveProductSlug(name: string | undefined, fallback: string): string;
|
|
369
|
+
/** The refusal error every consolidation verb throws (structured: stable `code` + the per-item
|
|
370
|
+
* disclosure list — G5/G7/G14's "结构化拒+披露" shape). */
|
|
371
|
+
export declare class ConsolidationRefusedError extends Error {
|
|
372
|
+
readonly code: string;
|
|
373
|
+
readonly reasons: string[];
|
|
374
|
+
constructor(code: string, message: string, reasons?: string[]);
|
|
375
|
+
}
|
|
376
|
+
/** A NotePatch list for one frozen product (the §1.3 add-only arm's one spelling). */
|
|
377
|
+
export declare function productAddPatch(product: MemoryEntry): NotePatch;
|
|
378
|
+
export {};
|