@sema-agent/core 5.49.0 → 5.51.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 +112 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/send-message-tool.js +5 -5
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +142 -5
- package/dist/agents/teacher.js +4 -1
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/auto-mode-prompt.js +9 -1
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +26 -4
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/assemble-result.d.ts +6 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +104 -44
- package/dist/core/runner/runtask.d.ts +5 -1
- package/dist/core/runner/runtask.js +14 -7
- package/dist/core/task-registry-agent.js +9 -3
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.js +4 -2
- package/dist/core/tool-policy.d.ts +37 -0
- package/dist/core/tool-policy.js +36 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/types.d.ts +53 -1
- package/dist/engine/loop/agent-loop.js +95 -30
- package/dist/engine/loop/types.d.ts +32 -0
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/package.json +1 -1
package/dist/core/mcp.js
CHANGED
|
@@ -650,7 +650,30 @@ export function mcpToolSchemaProblem(schema) {
|
|
|
650
650
|
}
|
|
651
651
|
return undefined;
|
|
652
652
|
}
|
|
653
|
-
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
653
|
+
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
|
|
654
|
+
let revocationProbeFailed = false;
|
|
655
|
+
const isServerRevoked = (serverName) => {
|
|
656
|
+
if (mcpRevocations === undefined)
|
|
657
|
+
return false;
|
|
658
|
+
try {
|
|
659
|
+
const r = mcpRevocations.isRevoked(serverName);
|
|
660
|
+
if (typeof r !== "boolean") {
|
|
661
|
+
throw new Error(`isRevoked returned ${r instanceof Promise ? "a Promise — the ledger seat is SYNCHRONOUS by contract" : `a non-boolean (${typeof r})`}`);
|
|
662
|
+
}
|
|
663
|
+
return r;
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
if (!revocationProbeFailed) {
|
|
667
|
+
revocationProbeFailed = true;
|
|
668
|
+
try {
|
|
669
|
+
mcpRevocations.onProbeFailure?.(e);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
654
677
|
const mcpDisclosure = reminderDisclosure?.reminderMark !== undefined
|
|
655
678
|
? { mark: reminderDisclosure.reminderMark, windows: new Map(), ...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}) }
|
|
656
679
|
: undefined;
|
|
@@ -665,7 +688,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
665
688
|
const droppedTools = [];
|
|
666
689
|
let disposing = false;
|
|
667
690
|
const serverHandles = [];
|
|
668
|
-
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure)));
|
|
691
|
+
const settled = await Promise.allSettled(specs.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
|
|
669
692
|
try {
|
|
670
693
|
for (let i = 0; i < specs.length; i++) {
|
|
671
694
|
const r = settled[i];
|
|
@@ -701,7 +724,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
701
724
|
statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
|
|
702
725
|
}
|
|
703
726
|
}
|
|
704
|
-
const resourceTools = buildResourceTools(resourceServers);
|
|
727
|
+
const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
|
|
705
728
|
tools.push(...resourceTools.tools);
|
|
706
729
|
toolAxes.push(...resourceTools.axes);
|
|
707
730
|
}
|
|
@@ -729,10 +752,14 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
729
752
|
results.push({ server: h.name, prefix: prefixOf(h.name), status: "not_connected", toolCount: 0, added: [], removed: [] });
|
|
730
753
|
continue;
|
|
731
754
|
}
|
|
755
|
+
if (isServerRevoked(h.name)) {
|
|
756
|
+
results.push({ server: h.name, prefix: prefixOf(h.name), status: "revoked", toolCount: 0, added: [], removed: [], error: "server revoked by the operator — the refresh did not contact it" });
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
732
759
|
try {
|
|
733
760
|
const listed = await listToolsLenient(h.client);
|
|
734
761
|
cacheMcpToolMetadata(h.client, listed.tools);
|
|
735
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure);
|
|
762
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
|
|
736
763
|
const newNames = serverTools.map((t) => t.name);
|
|
737
764
|
const added = newNames.filter((n) => !h.toolNames.includes(n));
|
|
738
765
|
const removed = h.toolNames.filter((n) => !newNames.includes(n));
|
|
@@ -882,7 +909,7 @@ function renderDirChildren(server, uri, children, flags) {
|
|
|
882
909
|
terminate: false,
|
|
883
910
|
};
|
|
884
911
|
}
|
|
885
|
-
function buildResourceTools(resourceServers) {
|
|
912
|
+
function buildResourceTools(resourceServers, isServerRevoked = () => false) {
|
|
886
913
|
const listable = resourceServers.filter((rs) => rs.listAllowed);
|
|
887
914
|
const readable = resourceServers.filter((rs) => rs.readAllowed);
|
|
888
915
|
if (listable.length === 0 && readable.length === 0)
|
|
@@ -910,6 +937,11 @@ function buildResourceTools(resourceServers) {
|
|
|
910
937
|
const all = [];
|
|
911
938
|
const errors = [];
|
|
912
939
|
for (const rs of targets) {
|
|
940
|
+
if (isServerRevoked(rs.server)) {
|
|
941
|
+
errors.push({ server: rs.server, error: "server revoked by the operator mid-session (request not sent)" });
|
|
942
|
+
sections.push(`[${rs.server}] Error: server revoked by the operator — its resources are unavailable this turn.`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
913
945
|
if (rs.health.dead) {
|
|
914
946
|
errors.push({ server: rs.server, error: "server disconnected (transport closed earlier in this task)" });
|
|
915
947
|
sections.push(`[${rs.server}] Error: server disconnected — its resources are unavailable.`);
|
|
@@ -970,6 +1002,14 @@ function buildResourceTools(resourceServers) {
|
|
|
970
1002
|
if (!rs)
|
|
971
1003
|
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with readable resources. Available: ${serverNames(readable)}.` }], details: undefined, terminate: false };
|
|
972
1004
|
const what = `The read of resource ${inlineUntrusted(uri)}`;
|
|
1005
|
+
if (isServerRevoked(server)) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
|
|
1008
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1009
|
+
terminate: false,
|
|
1010
|
+
isError: true,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
973
1013
|
if (rs.health.dead)
|
|
974
1014
|
throwDeadServer(server, what);
|
|
975
1015
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -1021,6 +1061,14 @@ function buildResourceTools(resourceServers) {
|
|
|
1021
1061
|
if (!rs)
|
|
1022
1062
|
return { content: [{ type: "text", text: `Error: no MCP server ${inlineUntrusted(server)} with listable resources. Available: ${serverNames(listable)}.` }], details: undefined, terminate: false };
|
|
1023
1063
|
const what = `The directory listing of ${inlineUntrusted(uri)}`;
|
|
1064
|
+
if (isServerRevoked(server)) {
|
|
1065
|
+
return {
|
|
1066
|
+
content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. The tool list updates at the next turn.` }],
|
|
1067
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
|
|
1068
|
+
terminate: false,
|
|
1069
|
+
isError: true,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1024
1072
|
if (rs.health.dead)
|
|
1025
1073
|
throwDeadServer(server, what);
|
|
1026
1074
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -1123,7 +1171,7 @@ const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient
|
|
|
1123
1171
|
async function listToolsLenient(client, options) {
|
|
1124
1172
|
return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
|
|
1125
1173
|
}
|
|
1126
|
-
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure) {
|
|
1174
|
+
async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1127
1175
|
const elicitOn = spec.elicitation === true && onElicit !== undefined;
|
|
1128
1176
|
const health = { dead: false, pendingElicitations: 0, lastElicitationClosedAt: 0 };
|
|
1129
1177
|
const client = new Client({ name: `sema-core/${spec.name}`, version: "0.1.0" }, { capabilities: elicitOn ? { elicitation: { form: {} } } : {} });
|
|
@@ -1159,7 +1207,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1159
1207
|
};
|
|
1160
1208
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1161
1209
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1162
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure);
|
|
1210
|
+
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1163
1211
|
const caps = client.getServerCapabilities();
|
|
1164
1212
|
const resourceInfo = caps?.resources
|
|
1165
1213
|
? {
|
|
@@ -1192,7 +1240,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1192
1240
|
throw err;
|
|
1193
1241
|
}
|
|
1194
1242
|
}
|
|
1195
|
-
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure) {
|
|
1243
|
+
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1196
1244
|
const serverTools = [];
|
|
1197
1245
|
const serverAxes = [];
|
|
1198
1246
|
const dropped = [];
|
|
@@ -1235,6 +1283,14 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1235
1283
|
...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
|
|
1236
1284
|
execute: async (_toolCallId, params, signal) => {
|
|
1237
1285
|
const what = `The call to tool ${inlineUntrusted(remoteName)}`;
|
|
1286
|
+
if (isServerRevoked?.(spec.name) === true) {
|
|
1287
|
+
return {
|
|
1288
|
+
content: [{ type: "text", text: `The call to MCP server "${spec.name}" was refused: the server was revoked by the operator mid-session. The call was NOT sent, so the server did not execute it. The tool list updates at the next turn.` }],
|
|
1289
|
+
details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server: spec.name },
|
|
1290
|
+
terminate: false,
|
|
1291
|
+
isError: true,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1238
1294
|
if (health.dead)
|
|
1239
1295
|
throwDeadServer(spec.name, what);
|
|
1240
1296
|
const timeoutMs = mcpToolTimeoutMs();
|
|
@@ -849,6 +849,11 @@ export declare class MemoryEngine {
|
|
|
849
849
|
* the deleted disk file WAS the backend's storage). Zero-copy skips getByIds: its read-side scan
|
|
850
850
|
* cannot see a deleted file, and calling it mid-harvest would sync-adopt in-session edits. */
|
|
851
851
|
private committedContentFor;
|
|
852
|
+
/** {@link committedContentFor} with the FAULT axis preserved: `fault: true` means the backend
|
|
853
|
+
* read THREW and no shadow answered — "could not read" — which consumers that make destructive
|
|
854
|
+
* decisions on absence (the projection-debt settlement clears a row on "entry gone") must
|
|
855
|
+
* distinguish from a clean miss. The 8 non-destructive read sites keep the folded face. */
|
|
856
|
+
private committedContentOrFault;
|
|
852
857
|
/**
|
|
853
858
|
* design/336 §2.2 (r4-9) — the COMMITTED frontmatter an origin carry-forward is computed against.
|
|
854
859
|
* Deliberately NOT {@link committedContentFor}'s non-zero-copy leg: that one calls the backend's
|
|
@@ -910,8 +915,32 @@ export declare class MemoryEngine {
|
|
|
910
915
|
/** Persist the canonical projection back to the session file (id minting / frontmatter completion).
|
|
911
916
|
* C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
|
|
912
917
|
* commit point. Skips the write when the disk already holds the canonical bytes (zero-copy: the
|
|
913
|
-
* journal's own execute step wrote them, making this an idempotent no-op).
|
|
918
|
+
* journal's own execute step wrote them, making this an idempotent no-op). Returns whether the
|
|
919
|
+
* disk now holds the canonical bytes — the ordinary completion family stays best-effort on a
|
|
920
|
+
* `false` (the next materialize re-projects), but the id-COMPLETION family's caller reads it to
|
|
921
|
+
* keep the #366 projection-debt row standing (an id-less plane file must re-bind to its
|
|
922
|
+
* committed id at the next harvest, never re-admit as a duplicate). */
|
|
914
923
|
private writeBackProjection;
|
|
924
|
+
/**
|
|
925
|
+
* #366 — repair an id-less-but-committed seat IN PLACE (the failed write-back's retry): write the
|
|
926
|
+
* committed canonical bytes over the seat and settle the debt row on success. Callers have
|
|
927
|
+
* already proven the seat carries no unadmitted edit (rev equality against its baseline), so the
|
|
928
|
+
* overwrite is content-preserving — it restores the id line, the completed frontmatter and any
|
|
929
|
+
* carried marker, nothing else. A refused read/write leaves the row standing (retried next pass;
|
|
930
|
+
* the next materialize's projection loop repairs and settles it too).
|
|
931
|
+
*/
|
|
932
|
+
private repairProjectionSeat;
|
|
933
|
+
/**
|
|
934
|
+
* #366 — validate a projection-debt row against the COMMITTED state (side-effect-free read: the
|
|
935
|
+
* retrievalView face in copy-out, never the File backend's adopting `getByIds`). Three-valued on
|
|
936
|
+
* purpose: `valid` binds, `stale` clears the row (the seat claim dissolved — entry gone, moved or
|
|
937
|
+
* renamed), and `unknown` (committed state unreadable) makes the caller DEFER the file fail-closed
|
|
938
|
+
* — treating a transient read fault as "stale" would clear the row and mint the very duplicate
|
|
939
|
+
* the account exists to close. Zero-copy answers `stale` by construction: there the backend's own
|
|
940
|
+
* commit writes the id into the plane file, so any standing row is a leftover, and the plain
|
|
941
|
+
* `getByIds` there would sync-adopt mid-harvest.
|
|
942
|
+
*/
|
|
943
|
+
private debtCommittedProjection;
|
|
915
944
|
/** Sibling scope subdir names under `dir` (excluded from a scope-tree walk when `dir` is the root —
|
|
916
945
|
* a root-owning layer's chmod/restore must never touch another scope's home). */
|
|
917
946
|
private siblingScopeDirNames;
|
|
@@ -14,7 +14,7 @@ import { noReplaceRestore } from "./delegation-settlement.js";
|
|
|
14
14
|
import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
15
15
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
16
16
|
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
17
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
17
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
|
|
18
18
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
19
19
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
20
20
|
|
|
@@ -876,6 +876,8 @@ export class MemoryEngine {
|
|
|
876
876
|
this.discloseSessionAccountIncident("the committed snapshot was contended/incomplete — the id-bearing residue arm is degraded to id-less for this materialize", undefined);
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
|
+
const debtByRel = new Map(readProjectionDebts(this.controlDir).map((r) => [r.relPath, r.entryId]));
|
|
880
|
+
const canonicalMemoryDir = canonicalize(this.memoryDir);
|
|
879
881
|
for (const f of scanEntryFiles(wroot, { maxDepth: this.maxDepth })) {
|
|
880
882
|
const canonical = canonicalize(f.path);
|
|
881
883
|
if (canonical !== wroot && !canonical.startsWith(`${wroot}${sep}`))
|
|
@@ -883,11 +885,20 @@ export class MemoryEngine {
|
|
|
883
885
|
const text = readSafe(canonical);
|
|
884
886
|
if (text === undefined)
|
|
885
887
|
continue;
|
|
886
|
-
const rel = relative(
|
|
888
|
+
const rel = relative(canonicalMemoryDir, canonical);
|
|
887
889
|
if (rel === MEMORY_INDEX_FILENAME || rel.endsWith(`${sep}${MEMORY_INDEX_FILENAME}`))
|
|
888
890
|
continue;
|
|
889
891
|
const parsed = parseEntryFile(text);
|
|
890
|
-
const
|
|
892
|
+
const debtEntryId = parsed.id === undefined ? debtByRel.get(relative(canonicalMemoryDir, canonical)) : undefined;
|
|
893
|
+
let debtAttributed = false;
|
|
894
|
+
if (debtEntryId !== undefined) {
|
|
895
|
+
const proj = await this.debtCommittedProjection(debtEntryId);
|
|
896
|
+
if (proj.state === "unknown") {
|
|
897
|
+
throw new Error(`the committed state for debt-bound file ${rel} could not be read during crash-residue attribution — refusing to classify on a fault`);
|
|
898
|
+
}
|
|
899
|
+
debtAttributed = proj.state === "valid" && proj.scope === writeScope && proj.slug === f.slug;
|
|
900
|
+
}
|
|
901
|
+
const uncommitted = (parsed.id === undefined && !debtAttributed) || (parsed.id !== undefined && committedRevs !== undefined && !committedRevs.has(parsed.id));
|
|
891
902
|
if (uncommitted) {
|
|
892
903
|
unattributed.push(rel);
|
|
893
904
|
continue;
|
|
@@ -1029,6 +1040,20 @@ export class MemoryEngine {
|
|
|
1029
1040
|
handle.baseIds.set(path, entry.id);
|
|
1030
1041
|
}
|
|
1031
1042
|
}
|
|
1043
|
+
{
|
|
1044
|
+
const debts = readProjectionDebts(this.controlDir);
|
|
1045
|
+
if (debts.length > 0) {
|
|
1046
|
+
const projectedIdByRel = new Map(handle.materialized.filter((m) => !m.stub).map((m) => [m.relPath, m.id]));
|
|
1047
|
+
const healed = debts.filter((r) => projectedIdByRel.get(r.relPath) === r.entryId).map((r) => ({ relPath: r.relPath, entryId: r.entryId }));
|
|
1048
|
+
if (healed.length > 0) {
|
|
1049
|
+
try {
|
|
1050
|
+
settleProjectionDebts(this.controlDir, healed);
|
|
1051
|
+
}
|
|
1052
|
+
catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1032
1057
|
const indexGate = this.gateDerivedIndex(handle);
|
|
1033
1058
|
if (indexGate !== undefined) {
|
|
1034
1059
|
if (!indexGate.contained)
|
|
@@ -1508,6 +1533,17 @@ export class MemoryEngine {
|
|
|
1508
1533
|
for (const [p, id] of idByBasePath)
|
|
1509
1534
|
if (!handle.fileToScope.get(p) || handle.fileToScope.get(p) === handle.writeScope)
|
|
1510
1535
|
basePathById.set(id, p);
|
|
1536
|
+
let projectionDebts;
|
|
1537
|
+
try {
|
|
1538
|
+
projectionDebts = new Map(readProjectionDebts(this.controlDir).map((r) => [r.relPath, { entryId: r.entryId, rev: r.rev }]));
|
|
1539
|
+
}
|
|
1540
|
+
catch (err) {
|
|
1541
|
+
report.ok = false;
|
|
1542
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory projection-debt ledger unreadable: ${err instanceof Error ? err.message : String(err)}` };
|
|
1543
|
+
return report;
|
|
1544
|
+
}
|
|
1545
|
+
const debtClears = [];
|
|
1546
|
+
const staleReplacedDebts = new Map();
|
|
1511
1547
|
const patches = [];
|
|
1512
1548
|
const pendingProjections = [];
|
|
1513
1549
|
const idsAddedThisHarvest = new Set();
|
|
@@ -1528,8 +1564,12 @@ export class MemoryEngine {
|
|
|
1528
1564
|
continue;
|
|
1529
1565
|
}
|
|
1530
1566
|
const fastBaseId = idByBasePath.get(f.canonical);
|
|
1531
|
-
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical))
|
|
1567
|
+
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical)) {
|
|
1568
|
+
const fastDebt = projectionDebts.get(rel);
|
|
1569
|
+
if (fastDebt !== undefined && fastDebt.entryId === fastBaseId)
|
|
1570
|
+
await this.repairProjectionSeat(rel, f.canonical, fastBaseId, debtClears);
|
|
1532
1571
|
continue;
|
|
1572
|
+
}
|
|
1533
1573
|
processed++;
|
|
1534
1574
|
if (pollutedReason !== undefined && !carry) {
|
|
1535
1575
|
await containPollutedRecord(f);
|
|
@@ -1585,8 +1625,35 @@ export class MemoryEngine {
|
|
|
1585
1625
|
continue;
|
|
1586
1626
|
}
|
|
1587
1627
|
const parsed = f.parsed;
|
|
1588
|
-
|
|
1628
|
+
let baseId = idByBasePath.get(f.canonical);
|
|
1589
1629
|
let id = baseId ?? parsed.id;
|
|
1630
|
+
let debtReconciled = false;
|
|
1631
|
+
if (id === undefined && projectionDebts.size > 0) {
|
|
1632
|
+
const debt = projectionDebts.get(rel);
|
|
1633
|
+
if (debt !== undefined) {
|
|
1634
|
+
const committed = await this.debtCommittedProjection(debt.entryId);
|
|
1635
|
+
if (committed.state === "unknown") {
|
|
1636
|
+
report.rejections.push({
|
|
1637
|
+
path: rel,
|
|
1638
|
+
code: "deferred",
|
|
1639
|
+
reason: "memory write deferred: this file's recorded committed id could not be validated (committed state unreadable) — retried next harvest (fail-closed: a fresh mint here could duplicate the committed entry)",
|
|
1640
|
+
});
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (committed.state === "valid" && committed.scope === writeScope && committed.slug === f.slug) {
|
|
1644
|
+
baseId = debt.entryId;
|
|
1645
|
+
id = debt.entryId;
|
|
1646
|
+
idByBasePath.set(f.canonical, debt.entryId);
|
|
1647
|
+
revByBasePath.set(f.canonical, debt.rev);
|
|
1648
|
+
debtReconciled = true;
|
|
1649
|
+
report.warnings.push(`${rel}: reconciled to its committed entry — a prior harvest committed this file but the id write-back did not land; the projection is repaired through the ordinary update lane, never re-admitted as a duplicate`);
|
|
1650
|
+
}
|
|
1651
|
+
else {
|
|
1652
|
+
debtClears.push({ relPath: rel, entryId: debt.entryId });
|
|
1653
|
+
staleReplacedDebts.set(rel, debt.entryId);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1590
1657
|
const minted = id === undefined;
|
|
1591
1658
|
if (id === undefined)
|
|
1592
1659
|
id = uuidv7();
|
|
@@ -1680,9 +1747,13 @@ export class MemoryEngine {
|
|
|
1680
1747
|
continue;
|
|
1681
1748
|
}
|
|
1682
1749
|
if (baseId !== undefined) {
|
|
1683
|
-
if (revOfText(f.text, id) === revByBasePath.get(f.canonical))
|
|
1750
|
+
if (revOfText(f.text, id) === revByBasePath.get(f.canonical)) {
|
|
1751
|
+
const seatDebt = projectionDebts.get(rel);
|
|
1752
|
+
if (seatDebt !== undefined && seatDebt.entryId === id)
|
|
1753
|
+
await this.repairProjectionSeat(rel, f.canonical, id, debtClears);
|
|
1684
1754
|
continue;
|
|
1685
|
-
|
|
1755
|
+
}
|
|
1756
|
+
pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm), ...(debtReconciled ? { idCompletion: true } : {}) });
|
|
1686
1757
|
patches.push({ op: "update", id, entry, ...(revByBasePath.get(f.canonical) !== undefined ? { baseRev: revByBasePath.get(f.canonical) } : {}) });
|
|
1687
1758
|
continue;
|
|
1688
1759
|
}
|
|
@@ -1732,7 +1803,7 @@ export class MemoryEngine {
|
|
|
1732
1803
|
entry.rev = computeEntryRev(entry);
|
|
1733
1804
|
report.warnings.push(`${rel}: the file's model-written id was not adopted for this external-origin commit — committed under an engine-minted id (a marked entry's index/search handles carry no model-authored bytes)`);
|
|
1734
1805
|
}
|
|
1735
|
-
pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || remintedId !== undefined || needsCompletion(parsed, fm) });
|
|
1806
|
+
pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || remintedId !== undefined || needsCompletion(parsed, fm), ...(minted || reboundId !== undefined || remintedId !== undefined ? { idCompletion: true } : {}) });
|
|
1736
1807
|
idsAddedThisHarvest.add(entry.id);
|
|
1737
1808
|
if (parsed.id !== undefined && entry.id === parsed.id)
|
|
1738
1809
|
modelIdAdds.add(entry.id);
|
|
@@ -1835,6 +1906,7 @@ export class MemoryEngine {
|
|
|
1835
1906
|
p.entry.id = uuidv7();
|
|
1836
1907
|
p.entry.rev = computeEntryRev(p.entry);
|
|
1837
1908
|
p.id = p.entry.id;
|
|
1909
|
+
projection.idCompletion = true;
|
|
1838
1910
|
report.warnings.push(`${flipRec.rel}: the file's model-written id was not adopted for this external-origin commit — committed under an engine-minted id (a marked entry's index/search handles carry no model-authored bytes)`);
|
|
1839
1911
|
}
|
|
1840
1912
|
}
|
|
@@ -1869,6 +1941,68 @@ export class MemoryEngine {
|
|
|
1869
1941
|
}
|
|
1870
1942
|
}
|
|
1871
1943
|
}
|
|
1944
|
+
const debtStage = pendingProjections
|
|
1945
|
+
.filter((p) => p.idCompletion === true)
|
|
1946
|
+
.map((p) => {
|
|
1947
|
+
const relPath = relative(handle.memoryDir, p.path);
|
|
1948
|
+
const replaces = staleReplacedDebts.get(relPath);
|
|
1949
|
+
return { relPath, entryId: p.entry.id, rev: p.entry.rev, ...(replaces !== undefined ? { replaces } : {}) };
|
|
1950
|
+
});
|
|
1951
|
+
if (debtStage.length > 0) {
|
|
1952
|
+
let refusedStage = [];
|
|
1953
|
+
try {
|
|
1954
|
+
refusedStage = stageProjectionDebts(this.controlDir, debtStage, this.now).refused;
|
|
1955
|
+
}
|
|
1956
|
+
catch (err) {
|
|
1957
|
+
report.warnings.push(`memory projection-debt staging failed — duplicate-admission protection for this harvest's id write-backs is degraded (a failed write-back could re-admit its file as a duplicate until the next materialize repairs the plane): ${err instanceof Error ? err.message : String(err)}`);
|
|
1958
|
+
}
|
|
1959
|
+
if (refusedStage.length > 0) {
|
|
1960
|
+
const refusedIds = new Set(refusedStage.map((r) => r.entryId));
|
|
1961
|
+
for (let i = patches.length - 1; i >= 0; i--) {
|
|
1962
|
+
const p = patches[i];
|
|
1963
|
+
if (p.op !== "delete" && refusedIds.has(p.id))
|
|
1964
|
+
patches.splice(i, 1);
|
|
1965
|
+
}
|
|
1966
|
+
for (let i = pendingProjections.length - 1; i >= 0; i--) {
|
|
1967
|
+
const proj = pendingProjections[i];
|
|
1968
|
+
if (refusedIds.has(proj.entry.id)) {
|
|
1969
|
+
idByBasePath.delete(proj.path);
|
|
1970
|
+
revByBasePath.delete(proj.path);
|
|
1971
|
+
pendingProjections.splice(i, 1);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
for (const r of refusedStage) {
|
|
1975
|
+
report.rejections.push({
|
|
1976
|
+
path: r.relPath,
|
|
1977
|
+
code: "deferred",
|
|
1978
|
+
reason: "memory write deferred: a concurrent writer's projection-debt claim stands at this seat — retried next harvest (committing without a protected id write-back could duplicate the concurrent entry)",
|
|
1979
|
+
});
|
|
1980
|
+
report.warnings.push(`${r.relPath}: earlier notes about this file committing under a fresh id are superseded — its commit was WITHDRAWN this harvest (deferred to the next)`);
|
|
1981
|
+
}
|
|
1982
|
+
if (lineageArmed) {
|
|
1983
|
+
const remaining = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev, ...(p.entry.frontmatter.origin !== undefined ? { marked: true } : {}) }));
|
|
1984
|
+
if (remaining.length > 0) {
|
|
1985
|
+
try {
|
|
1986
|
+
stageLineagePending(this.controlDir, txnId, lineageSessionId, remaining, this.now);
|
|
1987
|
+
}
|
|
1988
|
+
catch (err) {
|
|
1989
|
+
report.ok = false;
|
|
1990
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory lineage restage refused after a withheld projection-debt seat: ${err instanceof Error ? err.message : String(err)}` };
|
|
1991
|
+
return report;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
else {
|
|
1995
|
+
try {
|
|
1996
|
+
discardLineagePending(this.controlDir, txnId);
|
|
1997
|
+
}
|
|
1998
|
+
catch (err) {
|
|
1999
|
+
report.warnings.push(`memory lineage stage for withheld entries could not be discarded — their ids stay latched until the host adjudicates pending transaction ${txnId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2000
|
+
}
|
|
2001
|
+
lineageArmed = false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1872
2006
|
try {
|
|
1873
2007
|
patchReport = await this.backend.applyPatches(patches);
|
|
1874
2008
|
}
|
|
@@ -1938,18 +2072,44 @@ export class MemoryEngine {
|
|
|
1938
2072
|
}
|
|
1939
2073
|
const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
|
|
1940
2074
|
for (const p of pendingProjections) {
|
|
2075
|
+
const pRel = relative(handle.memoryDir, p.path);
|
|
1941
2076
|
if (appliedIds.has(p.entry.id)) {
|
|
1942
|
-
this.writeBackProjection(p.path, p.entry, p.needed);
|
|
2077
|
+
const wrote = this.writeBackProjection(p.path, p.entry, p.needed);
|
|
2078
|
+
if (p.idCompletion === true) {
|
|
2079
|
+
if (wrote)
|
|
2080
|
+
debtClears.push({ relPath: pRel, entryId: p.entry.id });
|
|
2081
|
+
else {
|
|
2082
|
+
const landed = patchReport.applied.find((a) => a.id === p.entry.id)?.slug;
|
|
2083
|
+
const suffixNote = landed !== undefined && landed !== p.entry.slug ? ` NOTE: the entry landed at slug ${JSON.stringify(landed)} (collision suffix) — the seat claim will not re-bind; host reconciliation may be needed.` : "";
|
|
2084
|
+
report.warnings.push(`${pRel}: the entry committed but its id write-back FAILED — the projection-debt row stands, so the file re-binds to its committed id at the next harvest (and the next materialize repairs the plane); it is never re-admitted as a duplicate.${suffixNote}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1943
2087
|
continue;
|
|
1944
2088
|
}
|
|
1945
|
-
const
|
|
2089
|
+
const committedRead = await this.committedContentOrFault(p.entry.id);
|
|
2090
|
+
const committed = committedRead.content;
|
|
2091
|
+
let planeHoldsCommitted = false;
|
|
1946
2092
|
if (committed !== undefined && readNoFollowSafe(p.path) !== committed) {
|
|
1947
2093
|
try {
|
|
1948
2094
|
writeFileNoFollow(p.path, committed);
|
|
2095
|
+
planeHoldsCommitted = true;
|
|
1949
2096
|
}
|
|
1950
2097
|
catch {
|
|
1951
2098
|
}
|
|
1952
2099
|
}
|
|
2100
|
+
else if (committed !== undefined) {
|
|
2101
|
+
planeHoldsCommitted = true;
|
|
2102
|
+
}
|
|
2103
|
+
if (p.idCompletion === true && ((committed === undefined && !committedRead.fault) || planeHoldsCommitted))
|
|
2104
|
+
debtClears.push({ relPath: pRel, entryId: p.entry.id });
|
|
2105
|
+
}
|
|
2106
|
+
if (debtClears.length > 0) {
|
|
2107
|
+
try {
|
|
2108
|
+
settleProjectionDebts(this.controlDir, debtClears);
|
|
2109
|
+
}
|
|
2110
|
+
catch (err) {
|
|
2111
|
+
report.warnings.push(`memory projection-debt settlement failed — settled rows stay on the ledger (harmless: every consumption re-validates a row against the committed state before binding): ${err instanceof Error ? err.message : String(err)}`);
|
|
2112
|
+
}
|
|
1953
2113
|
}
|
|
1954
2114
|
try {
|
|
1955
2115
|
clearScanFuse(this.controlDir, pendingProjections.filter((p) => appliedIds.has(p.entry.id)).map((p) => p.path));
|
|
@@ -2074,7 +2234,7 @@ export class MemoryEngine {
|
|
|
2074
2234
|
if (committedOrigin !== undefined)
|
|
2075
2235
|
fm.origin = committedOrigin;
|
|
2076
2236
|
else if (opts.valve)
|
|
2077
|
-
fm.origin = { taint: "external", cause: "static", at:
|
|
2237
|
+
fm.origin = { taint: "external", cause: "static", at: row.capturedAt };
|
|
2078
2238
|
}
|
|
2079
2239
|
if (fm.name === undefined)
|
|
2080
2240
|
fm.name = row.slug.split("/").pop();
|
|
@@ -2140,7 +2300,9 @@ export class MemoryEngine {
|
|
|
2140
2300
|
report.warnings.push(`released hold ${row.relPath}: lineage settlement failed after commit — the entry stays latched until a later harvest reconciles: ${err instanceof Error ? err.message : String(err)}`);
|
|
2141
2301
|
}
|
|
2142
2302
|
markHoldReleased(this.controlDir, { holdId: row.holdId, now: this.now });
|
|
2143
|
-
const
|
|
2303
|
+
const landedSlug = patchReport.applied.find((a) => a.id === patch.id)?.slug ?? row.slug;
|
|
2304
|
+
const landedRelPath = landedSlug === row.slug ? row.relPath : join(dirname(row.relPath), `${landedSlug.split("/").pop()}.md`);
|
|
2305
|
+
const abs = join(this.memoryDir, landedRelPath);
|
|
2144
2306
|
if (existsSync(abs)) {
|
|
2145
2307
|
const canonical = canonicalize(abs);
|
|
2146
2308
|
const text = readSafe(canonical);
|
|
@@ -2149,7 +2311,7 @@ export class MemoryEngine {
|
|
|
2149
2311
|
handle.baseRevs.set(canonical, computeEntryRev({ id: patch.id, frontmatter: parseEntryFile(text).frontmatter, body: parseEntryFile(text).body }));
|
|
2150
2312
|
handle.fileToScope.set(canonical, row.scope);
|
|
2151
2313
|
if (!handle.materialized.some((m) => m.path === canonical)) {
|
|
2152
|
-
handle.materialized.push({ path: canonical, relPath:
|
|
2314
|
+
handle.materialized.push({ path: canonical, relPath: landedRelPath, scope: row.scope, id: patch.id, slug: landedSlug, rev: handle.baseRevs.get(canonical), stub: false, readonly: false });
|
|
2153
2315
|
}
|
|
2154
2316
|
}
|
|
2155
2317
|
}
|
|
@@ -2576,20 +2738,28 @@ export class MemoryEngine {
|
|
|
2576
2738
|
return text;
|
|
2577
2739
|
}
|
|
2578
2740
|
async committedContentFor(id) {
|
|
2741
|
+
return (await this.committedContentOrFault(id)).content;
|
|
2742
|
+
}
|
|
2743
|
+
async committedContentOrFault(id) {
|
|
2744
|
+
let fault = false;
|
|
2579
2745
|
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
2580
2746
|
if (!zeroCopy) {
|
|
2581
2747
|
try {
|
|
2582
2748
|
const [entry] = await this.backend.getByIds([id]);
|
|
2583
2749
|
if (entry !== undefined && entry.id === id)
|
|
2584
|
-
return serializeEntryFile(entry);
|
|
2750
|
+
return { content: serializeEntryFile(entry), fault: false };
|
|
2585
2751
|
}
|
|
2586
2752
|
catch {
|
|
2753
|
+
fault = true;
|
|
2587
2754
|
}
|
|
2588
2755
|
}
|
|
2589
2756
|
const viaBackend = this.backend.readCommittedShadow?.(id);
|
|
2590
2757
|
if (viaBackend !== undefined)
|
|
2591
|
-
return viaBackend;
|
|
2592
|
-
|
|
2758
|
+
return { content: viaBackend, fault: false };
|
|
2759
|
+
const viaShadow = readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
2760
|
+
if (viaShadow !== undefined)
|
|
2761
|
+
return { content: viaShadow, fault: false };
|
|
2762
|
+
return { fault };
|
|
2593
2763
|
}
|
|
2594
2764
|
async committedFrontmatterFor(id) {
|
|
2595
2765
|
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
@@ -2682,14 +2852,45 @@ export class MemoryEngine {
|
|
|
2682
2852
|
}
|
|
2683
2853
|
writeBackProjection(path, entry, needed) {
|
|
2684
2854
|
if (!needed)
|
|
2685
|
-
return;
|
|
2855
|
+
return true;
|
|
2686
2856
|
const text = serializeEntryFile(entry);
|
|
2687
2857
|
if (readNoFollowSafe(path) === text)
|
|
2688
|
-
return;
|
|
2858
|
+
return true;
|
|
2689
2859
|
try {
|
|
2690
2860
|
writeFileNoFollow(path, text);
|
|
2861
|
+
return true;
|
|
2862
|
+
}
|
|
2863
|
+
catch {
|
|
2864
|
+
return false;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
async repairProjectionSeat(rel, canonical, entryId, debtClears) {
|
|
2868
|
+
const committed = await this.committedContentFor(entryId);
|
|
2869
|
+
if (committed === undefined)
|
|
2870
|
+
return;
|
|
2871
|
+
if (readNoFollowSafe(canonical) !== committed) {
|
|
2872
|
+
try {
|
|
2873
|
+
writeFileNoFollow(canonical, committed);
|
|
2874
|
+
}
|
|
2875
|
+
catch {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
debtClears.push({ relPath: rel, entryId });
|
|
2880
|
+
}
|
|
2881
|
+
async debtCommittedProjection(entryId) {
|
|
2882
|
+
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
2883
|
+
if (zeroCopy)
|
|
2884
|
+
return { state: "stale" };
|
|
2885
|
+
const face = this.backend.retrievalView?.() ?? this.backend;
|
|
2886
|
+
try {
|
|
2887
|
+
const [entry] = await face.getByIds([entryId]);
|
|
2888
|
+
if (entry !== undefined && entry.id === entryId)
|
|
2889
|
+
return { state: "valid", scope: entry.scope, slug: entry.slug };
|
|
2890
|
+
return { state: "stale" };
|
|
2691
2891
|
}
|
|
2692
2892
|
catch {
|
|
2893
|
+
return { state: "unknown" };
|
|
2693
2894
|
}
|
|
2694
2895
|
}
|
|
2695
2896
|
siblingScopeDirNames(dir, scope) {
|
|
@@ -721,6 +721,49 @@ export declare function recordChallengedHistory(controlDir: string, rows: Readon
|
|
|
721
721
|
}>, now: () => number): void;
|
|
722
722
|
/** Journal-aware read (observability/tests only — no engine consumer exists, on purpose). */
|
|
723
723
|
export declare function readChallengedHistory(controlDir: string): Record<string, ChallengedHistoryRow>;
|
|
724
|
+
export declare const PROJECTION_DEBTS_FILE = "projection-debts.json";
|
|
725
|
+
/** One standing debt: the plane file at `relPath` (memory-dir-relative, canonical base) belongs to
|
|
726
|
+
* committed entry `entryId`, whose id write-back has not landed; `rev` is the rev the projection
|
|
727
|
+
* was staged against (the CAS baseline a reconciling harvest hands its update). */
|
|
728
|
+
export interface ProjectionDebtRow {
|
|
729
|
+
relPath: string;
|
|
730
|
+
entryId: string;
|
|
731
|
+
rev: string;
|
|
732
|
+
at: number;
|
|
733
|
+
}
|
|
734
|
+
/** WRITE-AHEAD staging. Upsert discipline (adversarial round 2): a stage lands only when the seat
|
|
735
|
+
* has NO standing row, the standing row is the stager's OWN entry (a rev refresh), or the
|
|
736
|
+
* standing row is the entry the stager itself just judged stale (`replaces` — the same-harvest
|
|
737
|
+
* stale-then-remint lane). It never blindly replaces ANOTHER writer's protection: a lagging
|
|
738
|
+
* process that validated an old row before pausing must not overwrite the row a faster sibling
|
|
739
|
+
* staged at the same path (its own commit then CAS-conflicts and its tuple-keyed clear misses
|
|
740
|
+
* the survivor — the account converges instead of emptying). Called before the backend
|
|
741
|
+
* transaction; the caller degrades LOUDLY (report warning) on a refused stage rather than
|
|
742
|
+
* refusing the harvest — the ledger is duplicate-admission protection, not the commit's
|
|
743
|
+
* integrity. */
|
|
744
|
+
export declare function stageProjectionDebts(controlDir: string, rows: ReadonlyArray<{
|
|
745
|
+
relPath: string;
|
|
746
|
+
entryId: string;
|
|
747
|
+
rev: string;
|
|
748
|
+
replaces?: string;
|
|
749
|
+
}>, now: () => number): {
|
|
750
|
+
refused: Array<{
|
|
751
|
+
relPath: string;
|
|
752
|
+
entryId: string;
|
|
753
|
+
}>;
|
|
754
|
+
};
|
|
755
|
+
/** Settle (remove) rows by ROW IDENTITY — (relPath, entryId), never the bare path: the write-back
|
|
756
|
+
* landed, the staged claim turned out stale, or a materialize re-projected the seat. Identity
|
|
757
|
+
* matters (adversarial review F1): a stale-clear judged against an OLD row must not delete the
|
|
758
|
+
* NEWER row a later staging upserted at the same path (same harvest: stale X cleared while fresh
|
|
759
|
+
* Y's write-back failed — a path-keyed drop would erase Y and re-open the duplicate window; same
|
|
760
|
+
* shape across processes for a lagging sibling's clear). Missing rows are a no-op (idempotent). */
|
|
761
|
+
export declare function settleProjectionDebts(controlDir: string, rows: ReadonlyArray<{
|
|
762
|
+
relPath: string;
|
|
763
|
+
entryId: string;
|
|
764
|
+
}>): void;
|
|
765
|
+
/** Strict read (ENOENT ⇒ empty; corrupt ⇒ throws — the caller's fail-closed arm owns the refusal). */
|
|
766
|
+
export declare function readProjectionDebts(controlDir: string): ProjectionDebtRow[];
|
|
724
767
|
/**
|
|
725
768
|
* REF-C6 — write EVERY byte of `data` to `fd`, looping until the OS has taken all of them.
|
|
726
769
|
*
|