@sema-agent/core 5.48.0 → 5.50.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/agent-transcript-tool.d.ts +1 -1
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/send-message-tool.js +2 -2
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +89 -13
- 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/background-agent-store.d.ts +1 -1
- package/dist/core/background-agent-store.js +5 -4
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +36 -2
- package/dist/core/memory-engine/engine.js +354 -38
- 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 +120 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +22 -8
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +18 -2
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +24 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- 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/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +8 -0
- package/dist/stores/file/index.js +12 -0
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +4 -1
- package/dist/stores/file/session-store.js +7 -1
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +8 -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();
|
|
@@ -221,6 +221,11 @@ export interface HoldRow {
|
|
|
221
221
|
};
|
|
222
222
|
/** Host valve verdict awaiting the next harvest ("release" commits with cause "static"). */
|
|
223
223
|
resolved?: "release" | "discard";
|
|
224
|
+
/** The verdict's GENERATION (strictly increases on every re-resolution): the settle leg's
|
|
225
|
+
* consumed-verdict exit clears only the exact generation it read, so a host re-resolution
|
|
226
|
+
* racing a failing release attempt is never erased by the stale attempt's cleanup. Absent on
|
|
227
|
+
* rows resolved before the member existed (a generation-less clear matches those). */
|
|
228
|
+
resolvedAt?: number;
|
|
224
229
|
}
|
|
225
230
|
export declare function readHolds(controlDir: string): HoldRow[];
|
|
226
231
|
/** ATOMIC no-replace restore of a staged file onto a plane path (adversarial round 2: an
|
|
@@ -308,6 +313,28 @@ export declare function resolveHoldRecord(controlDir: string, input: {
|
|
|
308
313
|
requestId: string;
|
|
309
314
|
now: () => number;
|
|
310
315
|
}): void;
|
|
316
|
+
/**
|
|
317
|
+
* Consume a standing valve verdict WITHOUT any other transition — the settle leg's exit for a
|
|
318
|
+
* valve release that FAILED on an already-disposed row (an expired hold whose release hit a
|
|
319
|
+
* judgment refusal: lost custody, re-scan refusal, CAS conflict). `disposeHold` is a no-op there
|
|
320
|
+
* (the row is already terminal), so leaving `resolved` standing would re-run the identical failing
|
|
321
|
+
* release — and re-announce a disposal that never happened — on every subsequent harvest, forever.
|
|
322
|
+
* Clearing it returns the row to its plain disposed/expired state: the failure is disclosed by the
|
|
323
|
+
* caller, custody/quarantine bytes stay addressable, and `resolveHold(holdId, "release")` remains
|
|
324
|
+
* admissible after the host reconciles. GENERATION-KEYED: only the exact verdict generation the
|
|
325
|
+
* caller read is cleared — a host re-resolution that landed while the failing attempt was in
|
|
326
|
+
* flight is a NEWER generation and stands (the stale attempt's cleanup must not erase a live host
|
|
327
|
+
* action). A generation-less verdict (a row written before the member existed) is consumable
|
|
328
|
+
* against a generation-less read — every CURRENT writer mints a generation, so the only verdict
|
|
329
|
+
* such a clear can erase is one written by a PRE-generation writer racing on the same control
|
|
330
|
+
* plane: the standing mixed-version-writer class the upgrade-order duty prices, kept consumable
|
|
331
|
+
* on purpose (refusing it would revive the forever-re-firing valve loop for every verdict minted
|
|
332
|
+
* before the upgrade). Idempotent; unknown/verdict-less/other-generation rows are a no-op.
|
|
333
|
+
*/
|
|
334
|
+
export declare function clearHoldResolution(controlDir: string, input: {
|
|
335
|
+
holdId: string;
|
|
336
|
+
ifResolvedAt: number | undefined;
|
|
337
|
+
}): void;
|
|
311
338
|
/** Read one hold's custody bytes (release leg + expired-release valve). Returns undefined when
|
|
312
339
|
* the custody file is gone or fails its digest — the caller records capture_lost, never commits
|
|
313
340
|
* unverified bytes. For an expired-released row the bytes may already sit in quarantine
|
|
@@ -346,6 +346,7 @@ function coerceHolds(raw) {
|
|
|
346
346
|
!HOLD_STATUSES.has(row.status) ||
|
|
347
347
|
(row.disposition !== undefined && (typeof row.disposition !== "object" || row.disposition === null || !HOLD_TERMINALS.has(row.disposition.terminal) || (row.disposition.quarantineName !== undefined && !safeBasename(row.disposition.quarantineName)))) ||
|
|
348
348
|
(row.resolved !== undefined && row.resolved !== "release" && row.resolved !== "discard") ||
|
|
349
|
+
(row.resolvedAt !== undefined && (typeof row.resolvedAt !== "number" || !Number.isSafeInteger(row.resolvedAt) || row.resolvedAt < 0 || row.resolvedAt >= 2 ** 50 || row.resolved === undefined)) ||
|
|
349
350
|
(row.entryId !== undefined && typeof row.entryId !== "string") ||
|
|
350
351
|
(row.baseRev !== undefined && typeof row.baseRev !== "string") ||
|
|
351
352
|
(row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
|
|
@@ -401,6 +402,14 @@ export function openInstructionHold(controlDir, input) {
|
|
|
401
402
|
const digest = sha256(buf);
|
|
402
403
|
const captureName = `${holdId}.md`;
|
|
403
404
|
const holdDir = join(controlDir, HOLD_DIR);
|
|
405
|
+
const refuse = (reason, terminal) => {
|
|
406
|
+
try {
|
|
407
|
+
disposeHold(controlDir, { holdId, terminal, now: input.now });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
}
|
|
411
|
+
return { ok: false, holdId, reason };
|
|
412
|
+
};
|
|
404
413
|
try {
|
|
405
414
|
lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
|
|
406
415
|
if (rec.rows.some((r) => r.holdId === holdId))
|
|
@@ -431,10 +440,10 @@ export function openInstructionHold(controlDir, input) {
|
|
|
431
440
|
writeFileSync(join(holdDir, captureName), buf, { flag: "wx" });
|
|
432
441
|
const back = readFileSync(join(holdDir, captureName));
|
|
433
442
|
if (sha256(back) !== digest)
|
|
434
|
-
return
|
|
443
|
+
return refuse("hold capture verification failed (written bytes do not hash to the captured digest)", "capture_lost");
|
|
435
444
|
}
|
|
436
445
|
catch (err) {
|
|
437
|
-
return
|
|
446
|
+
return refuse(`hold capture failed: ${err instanceof Error ? err.message : String(err)}`, "capture_lost");
|
|
438
447
|
}
|
|
439
448
|
const stagingName = uniqueStagingName(holdId, input.now);
|
|
440
449
|
const stagingPath = join(holdDir, stagingName);
|
|
@@ -447,7 +456,7 @@ export function openInstructionHold(controlDir, input) {
|
|
|
447
456
|
if (code === "ENOENT") {
|
|
448
457
|
}
|
|
449
458
|
else {
|
|
450
|
-
return
|
|
459
|
+
return refuse(`hold removal failed (${code ?? "rename error"}): the plane file was not moved`, "discarded");
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
462
|
if (existsSync(stagingPath)) {
|
|
@@ -475,7 +484,7 @@ export function openInstructionHold(controlDir, input) {
|
|
|
475
484
|
});
|
|
476
485
|
}
|
|
477
486
|
catch (err) {
|
|
478
|
-
return
|
|
487
|
+
return refuse(`hold ledger flip refused: ${err instanceof Error ? err.message : String(err)}`, "discarded");
|
|
479
488
|
}
|
|
480
489
|
return { ok: true, holdId, ...(thirdWriterStranded !== undefined ? { thirdWriterStranded } : {}) };
|
|
481
490
|
}
|
|
@@ -604,6 +613,7 @@ export function markHoldReleased(controlDir, input) {
|
|
|
604
613
|
return { result: undefined };
|
|
605
614
|
r.status = "released";
|
|
606
615
|
delete r.resolved;
|
|
616
|
+
delete r.resolvedAt;
|
|
607
617
|
return { next: rec, result: r.captureName };
|
|
608
618
|
});
|
|
609
619
|
if (captureName === undefined)
|
|
@@ -638,6 +648,23 @@ export function resolveHoldRecord(controlDir, input) {
|
|
|
638
648
|
throw e;
|
|
639
649
|
}
|
|
640
650
|
r.resolved = input.action;
|
|
651
|
+
const nextGeneration = Math.max(input.now(), 0, (r.resolvedAt ?? 0) + 1);
|
|
652
|
+
if (!Number.isSafeInteger(nextGeneration) || nextGeneration < 0 || nextGeneration >= 2 ** 50) {
|
|
653
|
+
const e = new Error(`resolveHold: hold ${JSON.stringify(input.holdId)} cannot mint a fresh verdict generation (${String(nextGeneration)} — an exhausted window, a deranged clock, or a corrupt ledger row) — refused, never written.`);
|
|
654
|
+
e.code = "memory.hold_resolve_invalid_state";
|
|
655
|
+
throw e;
|
|
656
|
+
}
|
|
657
|
+
r.resolvedAt = nextGeneration;
|
|
658
|
+
return { next: rec, result: undefined };
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
export function clearHoldResolution(controlDir, input) {
|
|
662
|
+
lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
|
|
663
|
+
const r = rec.rows.find((x) => x.holdId === input.holdId);
|
|
664
|
+
if (r === undefined || r.resolved === undefined || r.resolvedAt !== input.ifResolvedAt)
|
|
665
|
+
return { result: undefined };
|
|
666
|
+
delete r.resolved;
|
|
667
|
+
delete r.resolvedAt;
|
|
641
668
|
return { next: rec, result: undefined };
|
|
642
669
|
});
|
|
643
670
|
}
|
|
@@ -75,5 +75,16 @@ export function mergeHarvestReports(a, b) {
|
|
|
75
75
|
const qf = [...(a.quarantineFailures ?? []), ...(b.quarantineFailures ?? [])];
|
|
76
76
|
if (qf.length > 0)
|
|
77
77
|
merged.quarantineFailures = qf;
|
|
78
|
+
const ac = a.containment;
|
|
79
|
+
const bc = b.containment;
|
|
80
|
+
if (ac !== undefined || bc !== undefined) {
|
|
81
|
+
merged.containment = {
|
|
82
|
+
indexRolledBack: ac?.indexRolledBack === true || bc?.indexRolledBack === true,
|
|
83
|
+
quarantinedInstruction: [...(ac?.quarantinedInstruction ?? []), ...(bc?.quarantinedInstruction ?? [])],
|
|
84
|
+
heldInstruction: [...(ac?.heldInstruction ?? []), ...(bc?.heldInstruction ?? [])],
|
|
85
|
+
releasedHolds: [...(ac?.releasedHolds ?? []), ...(bc?.releasedHolds ?? [])],
|
|
86
|
+
disposedHolds: [...(ac?.disposedHolds ?? []), ...(bc?.disposedHolds ?? [])],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
78
89
|
return merged;
|
|
79
90
|
}
|
|
@@ -803,7 +803,12 @@ export declare class MemoryEngine {
|
|
|
803
803
|
* and a re-call RESUMES it idempotently. Refusals (#123 coded, caller-shape):
|
|
804
804
|
* unattributed/invalid input, unknown/unmarked entry, and a CHALLENGED entry — the clear valve
|
|
805
805
|
* is not a challenge exit (adjudicate the challenge first; clearing a challenged entry would
|
|
806
|
-
* launder the exclusion through the weaker credential)
|
|
806
|
+
* launder the exclusion through the weaker credential); the challenged refusal re-judges on the
|
|
807
|
+
* RESUME path too while the marked entry stands. Two effect-half refusals (terminal, both
|
|
808
|
+
* states untouched): an on-disk projection diverging from the committed state (the clear never
|
|
809
|
+
* overwrites unadopted plane bytes — adopt/reconcile first), and a resumed row whose entry is
|
|
810
|
+
* ABSENT without a recorded committed tombstone (that absence is an independent deletion the
|
|
811
|
+
* clear never resurrects; the custody bytes stay disclosed on the terminal row).
|
|
807
812
|
*/
|
|
808
813
|
clearEntryOrigin(entryId: string, input: {
|
|
809
814
|
requestId: string;
|
|
@@ -844,6 +849,11 @@ export declare class MemoryEngine {
|
|
|
844
849
|
* the deleted disk file WAS the backend's storage). Zero-copy skips getByIds: its read-side scan
|
|
845
850
|
* cannot see a deleted file, and calling it mid-harvest would sync-adopt in-session edits. */
|
|
846
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;
|
|
847
857
|
/**
|
|
848
858
|
* design/336 §2.2 (r4-9) — the COMMITTED frontmatter an origin carry-forward is computed against.
|
|
849
859
|
* Deliberately NOT {@link committedContentFor}'s non-zero-copy leg: that one calls the backend's
|
|
@@ -905,8 +915,32 @@ export declare class MemoryEngine {
|
|
|
905
915
|
/** Persist the canonical projection back to the session file (id minting / frontmatter completion).
|
|
906
916
|
* C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
|
|
907
917
|
* commit point. Skips the write when the disk already holds the canonical bytes (zero-copy: the
|
|
908
|
-
* 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). */
|
|
909
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;
|
|
910
944
|
/** Sibling scope subdir names under `dir` (excluded from a scope-tree walk when `dir` is the root —
|
|
911
945
|
* a root-owning layer's chmod/restore must never touch another scope's home). */
|
|
912
946
|
private siblingScopeDirNames;
|