@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/verify.d.ts +27 -3
  7. package/dist/agents/verify.js +7 -2
  8. package/dist/core/governance-codes.js +14 -0
  9. package/dist/core/hooks.js +1 -1
  10. package/dist/core/lsp-diagnostics.d.ts +19 -17
  11. package/dist/core/lsp-diagnostics.js +11 -5
  12. package/dist/core/mcp.d.ts +46 -0
  13. package/dist/core/mcp.js +132 -6
  14. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  15. package/dist/core/memory-engine/consolidation.js +342 -0
  16. package/dist/core/memory-engine/dual-root.js +3 -0
  17. package/dist/core/memory-engine/engine.d.ts +237 -4
  18. package/dist/core/memory-engine/engine.js +1111 -4
  19. package/dist/core/memory-engine/export-bundle.js +9 -0
  20. package/dist/core/memory-engine/file-backend.js +27 -1
  21. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  22. package/dist/core/memory-engine/frontmatter.js +111 -0
  23. package/dist/core/memory-engine/index.d.ts +4 -2
  24. package/dist/core/memory-engine/index.js +3 -1
  25. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  26. package/dist/core/memory-engine/sync-client.js +26 -0
  27. package/dist/core/memory-engine/tools.d.ts +9 -0
  28. package/dist/core/memory-engine/tools.js +57 -13
  29. package/dist/core/memory-engine/types.d.ts +99 -0
  30. package/dist/core/memory-recall.js +4 -3
  31. package/dist/core/memory.d.ts +33 -3
  32. package/dist/core/memory.js +6 -4
  33. package/dist/core/permission-rules.d.ts +22 -0
  34. package/dist/core/permission-rules.js +60 -6
  35. package/dist/core/reminder-disclosure.d.ts +29 -4
  36. package/dist/core/reminder-disclosure.js +60 -12
  37. package/dist/core/runner/prepare-memory.js +7 -2
  38. package/dist/core/runner/prepare-task.d.ts +31 -1
  39. package/dist/core/runner/prepare-task.js +31 -14
  40. package/dist/core/runner/runtask.d.ts +8 -1
  41. package/dist/core/runner/runtask.js +12 -10
  42. package/dist/core/runner/session-rule-policy.js +5 -3
  43. package/dist/core/runner/synthetic-tools.js +4 -2
  44. package/dist/core/runner/turn-attachments.d.ts +16 -6
  45. package/dist/core/runner/turn-attachments.js +34 -20
  46. package/dist/core/tool-policy.d.ts +18 -0
  47. package/dist/core/tool-policy.js +19 -8
  48. package/dist/core/types.d.ts +89 -6
  49. package/dist/core/untrusted-egress.js +12 -2
  50. package/dist/core/untrusted-text.d.ts +189 -3
  51. package/dist/core/untrusted-text.js +416 -6
  52. package/dist/engine/loop/types.d.ts +7 -0
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/prompts/default.d.ts +12 -2
  56. package/dist/tools/fs/index.d.ts +3 -1
  57. package/package.json +1 -1
  58. 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
- const normalized = normalizeMcpToolSchema(t.inputSchema);
1427
- let effectiveInputSchema = t.inputSchema;
1428
- let effectiveDescription = t.description !== undefined ? sanitizeUntrustedText(t.description) : undefined;
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 {};