@sema-agent/core 5.45.0 → 5.47.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 (80) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/agents/subagent.js +130 -3
  3. package/dist/core/checkpoint-store.d.ts +12 -0
  4. package/dist/core/governance-codes.d.ts +13 -0
  5. package/dist/core/governance-codes.js +33 -0
  6. package/dist/core/hooks.d.ts +9 -2
  7. package/dist/core/hooks.js +6 -5
  8. package/dist/core/memory-engine/content-origin.d.ts +3 -1
  9. package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
  10. package/dist/core/memory-engine/delegation-settlement.js +661 -0
  11. package/dist/core/memory-engine/engine.d.ts +209 -4
  12. package/dist/core/memory-engine/engine.js +885 -39
  13. package/dist/core/memory-engine/export-bundle.d.ts +10 -1
  14. package/dist/core/memory-engine/export-bundle.js +21 -0
  15. package/dist/core/memory-engine/file-backend.d.ts +34 -4
  16. package/dist/core/memory-engine/file-backend.js +168 -40
  17. package/dist/core/memory-engine/frontmatter.d.ts +69 -1
  18. package/dist/core/memory-engine/frontmatter.js +156 -2
  19. package/dist/core/memory-engine/header-hints.d.ts +17 -0
  20. package/dist/core/memory-engine/header-hints.js +6 -0
  21. package/dist/core/memory-engine/index.d.ts +7 -5
  22. package/dist/core/memory-engine/index.js +5 -3
  23. package/dist/core/memory-engine/layout.d.ts +39 -2
  24. package/dist/core/memory-engine/layout.js +27 -14
  25. package/dist/core/memory-engine/memory-backend-contract.js +108 -0
  26. package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
  27. package/dist/core/memory-engine/origin-clearance.js +84 -0
  28. package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
  29. package/dist/core/memory-engine/provenance-wording.js +15 -0
  30. package/dist/core/memory-engine/sync-client.d.ts +1 -1
  31. package/dist/core/memory-engine/sync-client.js +33 -1
  32. package/dist/core/memory-engine/tools.d.ts +64 -3
  33. package/dist/core/memory-engine/tools.js +37 -9
  34. package/dist/core/memory-engine/types.d.ts +145 -3
  35. package/dist/core/memory-engine/types.js +1 -1
  36. package/dist/core/reminder-mint.d.ts +70 -0
  37. package/dist/core/reminder-mint.js +25 -0
  38. package/dist/core/runner/git-status-frame.d.ts +3 -14
  39. package/dist/core/runner/git-status-frame.js +39 -14
  40. package/dist/core/runner/prepare-config-doors.d.ts +4 -0
  41. package/dist/core/runner/prepare-config-doors.js +15 -0
  42. package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
  43. package/dist/core/runner/prepare-hands-readface.js +26 -0
  44. package/dist/core/runner/prepare-memory.d.ts +11 -0
  45. package/dist/core/runner/prepare-memory.js +61 -24
  46. package/dist/core/runner/prepare-task.d.ts +46 -1
  47. package/dist/core/runner/prepare-task.js +128 -23
  48. package/dist/core/runner/runtask.js +62 -55
  49. package/dist/core/session-reconcile.js +3 -2
  50. package/dist/core/side-query.d.ts +11 -1
  51. package/dist/core/side-query.js +3 -0
  52. package/dist/core/types.d.ts +85 -10
  53. package/dist/core/types.js +3 -0
  54. package/dist/engine/harness/types.d.ts +46 -1
  55. package/dist/engine/harness/types.js +11 -0
  56. package/dist/engine/session/import-validate.js +6 -1
  57. package/dist/engine/session/session.d.ts +20 -0
  58. package/dist/engine/session/session.js +26 -1
  59. package/dist/index.d.ts +3 -1
  60. package/dist/index.js +3 -1
  61. package/dist/orchestration/run-workflow-tool.d.ts +16 -0
  62. package/dist/orchestration/run-workflow-tool.js +23 -3
  63. package/dist/orchestration/workflow-governance.d.ts +8 -1
  64. package/dist/prompt-assembly/epoch.js +2 -0
  65. package/dist/prompt-assembly/types.d.ts +6 -0
  66. package/dist/prompts/default.d.ts +13 -1
  67. package/dist/prompts/default.js +5 -1
  68. package/dist/tools/fs/fs-bash.d.ts +4 -0
  69. package/dist/tools/fs/fs-bash.js +1 -1
  70. package/dist/tools/fs/fs-read.d.ts +1 -1
  71. package/dist/tools/fs/fs-read.js +8 -7
  72. package/dist/tools/fs/fs-shared.d.ts +10 -4
  73. package/dist/tools/fs/fs-shared.js +6 -3
  74. package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
  75. package/dist/tools/fs/gh-rate-limit.js +3 -2
  76. package/dist/tools/fs/index.d.ts +10 -2
  77. package/dist/tools/fs/index.js +2 -1
  78. package/dist/tools/task-list.d.ts +5 -1
  79. package/package.json +1 -1
  80. package/test/export-surface.snapshot.json +21 -2
@@ -0,0 +1,661 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, linkSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { QUARANTINE_DIR, appendChallengeEvents, enqueueMemoryAnnouncement, ensureDirExists, lineageContributionsOfSession, lockedStrictUpdate, markSessionPolluted, readSessionPollution, readStrictSidecar, ControlPlaneCorruptError } from "./layout.js";
5
+ export const DELEGATION_SETTLEMENT_FILE = "delegation-settlement.json";
6
+ function reqStr(v) {
7
+ return typeof v === "string" && v.length > 0;
8
+ }
9
+ function safeBasename(v) {
10
+ return typeof v === "string" && v.length > 0 && !v.includes("/") && !v.includes("\\") && v !== "." && v !== "..";
11
+ }
12
+ function safeRelPath(v) {
13
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("/") || v.startsWith("\\") || /^[A-Za-z]:/.test(v))
14
+ return false;
15
+ return v.split(/[\/\\]/).every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
16
+ }
17
+ const SETTLEMENT_EVENT_STATUSES = new Set(["clean", "external", "expired", "void", "unattestable"]);
18
+ function coerceSettlement(raw) {
19
+ if (raw === undefined)
20
+ return { version: 1, rows: [], events: [], effects: {} };
21
+ const rec = raw;
22
+ if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows) || !Array.isArray(rec.events) || typeof rec.effects !== "object" || rec.effects === null || Array.isArray(rec.effects)) {
23
+ throw new ControlPlaneCorruptError("memory delegation-settlement ledger has the wrong shape");
24
+ }
25
+ if (rec.version !== 1)
26
+ throw new ControlPlaneCorruptError(`memory delegation-settlement ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
27
+ for (const r of rec.rows) {
28
+ const row = r;
29
+ if (typeof row !== "object" || row === null || !reqStr(row.settleId) || !reqStr(row.sessionId) || typeof row.launchedAt !== "number" || !Number.isFinite(row.launchedAt) || (row.lane !== "background" && row.lane !== "sync") || (row.status !== "pending" && row.status !== "unattestable") || (row.toolUseId !== undefined && !reqStr(row.toolUseId))) {
30
+ throw new ControlPlaneCorruptError("memory delegation-settlement ledger: a row is malformed (unknown status/lane, missing id, or a non-finite timestamp) — refusing (fail-closed; an unevaluable row must not fold as clean)");
31
+ }
32
+ }
33
+ for (const e of rec.events) {
34
+ const ev = e;
35
+ const kindOk = ev?.kind === "settle" || ev?.kind === "admin-resolve" || ev?.kind === "annotation";
36
+ const statusOk = ev?.status === undefined || SETTLEMENT_EVENT_STATUSES.has(ev.status);
37
+ if (typeof ev !== "object" || ev === null || !reqStr(ev.settleId) || !reqStr(ev.eventId) || typeof ev.at !== "number" || !Number.isFinite(ev.at) || !kindOk || !statusOk || (ev.kind === "settle" && ev.status === undefined) || (ev.kind === "admin-resolve" && (ev.status === undefined || !reqStr(ev.requestId)))) {
38
+ throw new ControlPlaneCorruptError("memory delegation-settlement ledger: an event is malformed (unknown kind/status, missing ids, an unattributed admin ruling, or a non-finite timestamp) — refusing (fail-closed)");
39
+ }
40
+ }
41
+ for (const [k, v] of Object.entries(rec.effects)) {
42
+ if (!reqStr(k) || !reqStr(v))
43
+ throw new ControlPlaneCorruptError("memory delegation-settlement ledger: an effects watermark entry is malformed — refusing (fail-closed)");
44
+ }
45
+ return rec;
46
+ }
47
+ export function syncSettleId(sessionId, toolCallId) {
48
+ return `sync:${encodeURIComponent(sessionId)}:${encodeURIComponent(toolCallId)}`;
49
+ }
50
+ export function registerDelegationLaunch(controlDir, input) {
51
+ lockedStrictUpdate(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger", coerceSettlement, (rec) => {
52
+ if (rec.rows.some((r) => r.settleId === input.settleId))
53
+ return { result: undefined };
54
+ rec.rows.push({ settleId: input.settleId, sessionId: input.sessionId, launchedAt: input.now(), lane: "background", status: "pending", ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}) });
55
+ return { next: rec, result: undefined };
56
+ });
57
+ }
58
+ export function recordSyncUnattestable(controlDir, input) {
59
+ const settleId = syncSettleId(input.sessionId, input.toolCallId);
60
+ lockedStrictUpdate(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger", coerceSettlement, (rec) => {
61
+ if (rec.rows.some((r) => r.settleId === settleId))
62
+ return { result: undefined };
63
+ rec.rows.push({ settleId, sessionId: input.sessionId, launchedAt: input.now(), lane: "sync", status: "unattestable" });
64
+ return { next: rec, result: undefined };
65
+ });
66
+ }
67
+ export function settleDelegation(controlDir, input) {
68
+ const eventId = input.eventId ?? `settle:${input.settleId}:${input.status}`;
69
+ lockedStrictUpdate(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger", coerceSettlement, (rec) => {
70
+ if (rec.events.some((e) => e.eventId === eventId))
71
+ return { result: undefined };
72
+ rec.events.push({ settleId: input.settleId, eventId, at: input.now(), kind: "settle", status: input.status, ...(input.note !== undefined ? { note: input.note } : {}) });
73
+ return { next: rec, result: undefined };
74
+ });
75
+ }
76
+ export function resolveSettlementRecord(controlDir, input) {
77
+ if (typeof input.requestId !== "string" || input.requestId.length === 0) {
78
+ const e = new Error("resolveSettlement requires a non-empty requestId (the audit attribution of who resolved) — refused, never defaulted.");
79
+ e.code = "memory.settlement_resolve_unattributed";
80
+ throw e;
81
+ }
82
+ if (input.to !== "clean" && input.to !== "external") {
83
+ const e = new Error(`resolveSettlement verdict must be "clean" or "external" (got ${JSON.stringify(input.to)}) — refused loudly, never folded.`);
84
+ e.code = "memory.settlement_resolve_invalid";
85
+ throw e;
86
+ }
87
+ const eventId = `admin:${input.requestId}:${input.settleId}:${input.to}`;
88
+ lockedStrictUpdate(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger", coerceSettlement, (rec) => {
89
+ if (!rec.rows.some((r) => r.settleId === input.settleId)) {
90
+ const e = new Error(`resolveSettlement: unknown settleId ${JSON.stringify(input.settleId)}`);
91
+ e.code = "memory.settlement_resolve_unknown";
92
+ throw e;
93
+ }
94
+ if (rec.events.some((e2) => e2.eventId === eventId))
95
+ return { result: undefined };
96
+ rec.events.push({ settleId: input.settleId, eventId, at: input.now(), kind: "admin-resolve", status: input.to, requestId: input.requestId });
97
+ return { next: rec, result: undefined };
98
+ });
99
+ }
100
+ function foldOne(row, events) {
101
+ let effective = row.status;
102
+ let everExternal = false;
103
+ let terminalEventId;
104
+ let adminRuled = false;
105
+ for (const e of events) {
106
+ const s = e.status;
107
+ if (s === undefined)
108
+ continue;
109
+ if (e.kind === "settle") {
110
+ if (s === "external") {
111
+ everExternal = true;
112
+ if (effective !== "external" && (effective !== "clean" || adminRuled)) {
113
+ effective = "external";
114
+ terminalEventId = e.eventId;
115
+ }
116
+ }
117
+ else if (s === "clean" || s === "void") {
118
+ if (effective === "pending") {
119
+ effective = s;
120
+ terminalEventId = e.eventId;
121
+ }
122
+ }
123
+ else if (s === "expired" || s === "unattestable") {
124
+ if (effective === "pending") {
125
+ effective = s;
126
+ terminalEventId = e.eventId;
127
+ }
128
+ }
129
+ }
130
+ else if (e.kind === "admin-resolve") {
131
+ adminRuled = true;
132
+ if (s === "external") {
133
+ everExternal = true;
134
+ effective = "external";
135
+ terminalEventId = e.eventId;
136
+ }
137
+ else if (s === "clean") {
138
+ effective = "clean";
139
+ terminalEventId = e.eventId;
140
+ }
141
+ }
142
+ }
143
+ return { row, effective, everExternal, ...(terminalEventId !== undefined ? { terminalEventId } : {}) };
144
+ }
145
+ export function effectiveSettlements(controlDir) {
146
+ const rec = coerceSettlement(readStrictSidecar(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger"));
147
+ const bySettle = new Map();
148
+ for (const e of rec.events) {
149
+ const arr = bySettle.get(e.settleId);
150
+ if (arr === undefined)
151
+ bySettle.set(e.settleId, [e]);
152
+ else
153
+ arr.push(e);
154
+ }
155
+ return rec.rows.map((row) => foldOne(row, bySettle.get(row.settleId) ?? []));
156
+ }
157
+ export function sessionSettlements(controlDir, sessionId) {
158
+ return effectiveSettlements(controlDir).filter((s) => s.row.sessionId === sessionId);
159
+ }
160
+ export function classifySessionSettlements(rows) {
161
+ let sawPending = false;
162
+ let exposedCause;
163
+ let reason;
164
+ for (const s of rows) {
165
+ if (s.effective === "external") {
166
+ exposedCause = "observed";
167
+ reason = "a delegated child of this session settled with an observed external-content attestation";
168
+ break;
169
+ }
170
+ if (s.effective === "unattestable" || s.effective === "expired") {
171
+ if (exposedCause === undefined) {
172
+ exposedCause = "static";
173
+ reason = s.effective === "unattestable" ? "a delegation of this session delivered without a provable attestation (permanently unprovable is not clean)" : "a delegation of this session never settled inside its evidence window (expired unprovable is not clean)";
174
+ }
175
+ continue;
176
+ }
177
+ if (s.effective === "pending")
178
+ sawPending = true;
179
+ }
180
+ if (exposedCause !== undefined)
181
+ return { state: "exposed", cause: exposedCause, ...(reason !== undefined ? { reason } : {}) };
182
+ if (sawPending)
183
+ return { state: "pending" };
184
+ return { state: "clean" };
185
+ }
186
+ export function expireOverdueSettlements(controlDir, input) {
187
+ const overdue = effectiveSettlements(controlDir).filter((s) => s.effective === "pending" && input.now() - s.row.launchedAt > input.timeoutMs);
188
+ for (const s of overdue)
189
+ settleDelegation(controlDir, { settleId: s.row.settleId, status: "expired", now: input.now });
190
+ return overdue.map((s) => s.row.settleId);
191
+ }
192
+ export function replayExternalSettlementEffects(controlDir, input) {
193
+ const applied = [];
194
+ const folded = effectiveSettlements(controlDir).filter((s) => s.effective === "external" && s.terminalEventId !== undefined);
195
+ if (folded.length === 0)
196
+ return applied;
197
+ const rec = coerceSettlement(readStrictSidecar(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger"));
198
+ for (const s of folded) {
199
+ if (rec.effects[s.row.settleId] === s.terminalEventId)
200
+ continue;
201
+ const sessionId = s.row.sessionId;
202
+ const outcome = markSessionPolluted(controlDir, sessionId, "a delegated child of this session settled external after launch (delegation settlement account)", input.now, "observed");
203
+ if (outcome === "unpersisted")
204
+ continue;
205
+ try {
206
+ const swept = lineageContributionsOfSession(controlDir, sessionId);
207
+ const contributions = input.carry ? swept.filter((c) => c.marked !== true) : swept;
208
+ appendChallengeEvents(controlDir, contributions.map((c) => ({
209
+ eventId: `settlement:${s.row.settleId}:${s.terminalEventId}:${c.entryId}`,
210
+ entryId: c.entryId,
211
+ reason: "challenged: the contributing session's delegation settled external",
212
+ challengedRev: c.lastRev,
213
+ })), input.now);
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ try {
219
+ enqueueMemoryAnnouncement(controlDir, {
220
+ kind: "gate",
221
+ at: input.now(),
222
+ items: [`delegation settlement: a background delegation of session ${JSON.stringify(sessionId)} settled EXTERNAL after launch — the session is marked and its unmarked committed contributions are challenged`],
223
+ });
224
+ }
225
+ catch {
226
+ }
227
+ lockedStrictUpdate(controlDir, DELEGATION_SETTLEMENT_FILE, "memory delegation-settlement ledger", coerceSettlement, (r) => {
228
+ r.effects[s.row.settleId] = s.terminalEventId;
229
+ return { next: r, result: undefined };
230
+ });
231
+ applied.push({ settleId: s.row.settleId, sessionId });
232
+ }
233
+ return applied;
234
+ }
235
+ export const SESSION_ACCOUNTS_FILE = "session-accounts.json";
236
+ function coerceSessionAccounts(raw) {
237
+ if (raw === undefined)
238
+ return { version: 1, rows: [] };
239
+ const rec = raw;
240
+ if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
241
+ throw new ControlPlaneCorruptError("memory session-account ledger has the wrong shape");
242
+ if (rec.version !== 1)
243
+ throw new ControlPlaneCorruptError(`memory session-account ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
244
+ for (const r of rec.rows) {
245
+ const row = r;
246
+ if (typeof row !== "object" ||
247
+ row === null ||
248
+ !reqStr(row.sessionId) ||
249
+ typeof row.openedAt !== "number" ||
250
+ !Number.isFinite(row.openedAt) ||
251
+ !Array.isArray(row.unattributed) ||
252
+ row.unattributed.some((x) => typeof x !== "string") ||
253
+ (row.closedAt !== undefined && (typeof row.closedAt !== "number" || !Number.isFinite(row.closedAt))) ||
254
+ (row.unadjudicated !== undefined && row.unadjudicated !== true)) {
255
+ throw new ControlPlaneCorruptError("memory session-account ledger: a row is malformed — refusing (fail-closed; an unevaluable row must not read as adjudicated)");
256
+ }
257
+ }
258
+ return rec;
259
+ }
260
+ export function foreignDanglingSessionAccounts(controlDir, selfSessionId) {
261
+ const rec = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
262
+ return rec.rows.filter((r) => r.sessionId !== selfSessionId && (r.closedAt === undefined || r.unadjudicated === true));
263
+ }
264
+ export function openSessionAccount(controlDir, input) {
265
+ lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
266
+ const row = rec.rows.find((r) => r.sessionId === input.sessionId);
267
+ if (row === undefined) {
268
+ rec.rows.push({ sessionId: input.sessionId, openedAt: input.now(), unattributed: [...input.unattributed] });
269
+ }
270
+ else if (row.closedAt !== undefined && row.unadjudicated === undefined) {
271
+ row.openedAt = input.now();
272
+ delete row.closedAt;
273
+ row.unattributed = [...input.unattributed];
274
+ }
275
+ else {
276
+ row.openedAt = input.now();
277
+ delete row.closedAt;
278
+ delete row.unadjudicated;
279
+ row.unattributed = [...new Set([...row.unattributed, ...input.unattributed])];
280
+ }
281
+ return { next: rec, result: undefined };
282
+ });
283
+ }
284
+ export function sessionUnattributedSet(controlDir, sessionId) {
285
+ const rec = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
286
+ return new Set(rec.rows.find((r) => r.sessionId === sessionId)?.unattributed ?? []);
287
+ }
288
+ export function closeSessionAccount(controlDir, input) {
289
+ lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
290
+ const row = rec.rows.find((r) => r.sessionId === input.sessionId);
291
+ if (row === undefined)
292
+ return { result: undefined };
293
+ row.closedAt = input.now();
294
+ delete row.unadjudicated;
295
+ for (const r of rec.rows)
296
+ delete r.unadjudicated;
297
+ return { next: rec, result: undefined };
298
+ });
299
+ }
300
+ export function resolveSessionAccountRecord(controlDir, input) {
301
+ if (typeof input.requestId !== "string" || input.requestId.length === 0) {
302
+ const e = new Error("resolveSessionAccount requires a non-empty requestId (the audit attribution of who closed) — refused, never defaulted.");
303
+ e.code = "memory.session_account_resolve_unattributed";
304
+ throw e;
305
+ }
306
+ lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
307
+ const row = rec.rows.find((r) => r.sessionId === input.sessionId);
308
+ if (row === undefined) {
309
+ const e = new Error(`resolveSessionAccount: unknown sessionId ${JSON.stringify(input.sessionId)}`);
310
+ e.code = "memory.session_account_resolve_unknown";
311
+ throw e;
312
+ }
313
+ row.closedAt = input.now();
314
+ row.unadjudicated = true;
315
+ return { next: rec, result: undefined };
316
+ });
317
+ }
318
+ export const HOLDS_FILE = "holds.json";
319
+ export const HOLD_DIR = "hold";
320
+ const HOLD_STATUSES = new Set(["capturing", "held", "released", "disposed"]);
321
+ const HOLD_TERMINALS = new Set(["dirty", "expired", "conflict", "discarded", "capture_lost"]);
322
+ function coerceHolds(raw) {
323
+ if (raw === undefined)
324
+ return { version: 1, rows: [] };
325
+ const rec = raw;
326
+ if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
327
+ throw new ControlPlaneCorruptError("memory instruction-hold ledger has the wrong shape");
328
+ if (rec.version !== 1)
329
+ throw new ControlPlaneCorruptError(`memory instruction-hold ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
330
+ for (const r of rec.rows) {
331
+ const row = r;
332
+ if (typeof row !== "object" ||
333
+ row === null ||
334
+ !reqStr(row.holdId) ||
335
+ !reqStr(row.sessionId) ||
336
+ (row.op !== "add" && row.op !== "update") ||
337
+ !reqStr(row.relPath) ||
338
+ !reqStr(row.slug) ||
339
+ !reqStr(row.scope) ||
340
+ !reqStr(row.captureName) ||
341
+ !reqStr(row.contentDigest) ||
342
+ typeof row.capturedAt !== "number" ||
343
+ !Number.isFinite(row.capturedAt) ||
344
+ !Array.isArray(row.settleIds) ||
345
+ row.settleIds.some((x) => typeof x !== "string") ||
346
+ !HOLD_STATUSES.has(row.status) ||
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
+ (row.resolved !== undefined && row.resolved !== "release" && row.resolved !== "discard") ||
349
+ (row.entryId !== undefined && typeof row.entryId !== "string") ||
350
+ (row.baseRev !== undefined && typeof row.baseRev !== "string") ||
351
+ (row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
352
+ !safeRelPath(row.relPath) ||
353
+ !safeBasename(row.captureName)) {
354
+ throw new ControlPlaneCorruptError("memory instruction-hold ledger: a row is malformed — refusing (fail-closed; an unevaluable hold must not read as settled)");
355
+ }
356
+ }
357
+ return rec;
358
+ }
359
+ export function readHolds(controlDir) {
360
+ return coerceHolds(readStrictSidecar(controlDir, HOLDS_FILE, "memory instruction-hold ledger")).rows;
361
+ }
362
+ function sha256(buf) {
363
+ return createHash("sha256").update(buf).digest("hex");
364
+ }
365
+ let stagingSeq = 0;
366
+ function uniqueStagingName(holdId, now) {
367
+ stagingSeq += 1;
368
+ return `${holdId}.staging-${now()}-${process.pid}-${stagingSeq}`;
369
+ }
370
+ export function noReplaceRestore(stagingPath, destPath) {
371
+ try {
372
+ linkSync(stagingPath, destPath);
373
+ }
374
+ catch (err) {
375
+ if (err.code !== "EEXIST") {
376
+ try {
377
+ writeFileSync(destPath, readFileSync(stagingPath), { flag: "wx" });
378
+ }
379
+ catch (err2) {
380
+ return err2.code === "EEXIST" ? "kept" : "stranded";
381
+ }
382
+ try {
383
+ unlinkSync(stagingPath);
384
+ }
385
+ catch {
386
+ }
387
+ return "restored";
388
+ }
389
+ return "kept";
390
+ }
391
+ try {
392
+ unlinkSync(stagingPath);
393
+ }
394
+ catch {
395
+ }
396
+ return "restored";
397
+ }
398
+ export function openInstructionHold(controlDir, input) {
399
+ const holdId = `hold-${input.now()}-${createHash("sha256").update(`${input.sessionId}\n${input.relPath}\n${input.content}`).digest("hex").slice(0, 12)}`;
400
+ const buf = Buffer.from(input.content, "utf8");
401
+ const digest = sha256(buf);
402
+ const captureName = `${holdId}.md`;
403
+ const holdDir = join(controlDir, HOLD_DIR);
404
+ try {
405
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
406
+ if (rec.rows.some((r) => r.holdId === holdId))
407
+ return { result: undefined };
408
+ rec.rows.push({
409
+ holdId,
410
+ sessionId: input.sessionId,
411
+ ...(input.entryId !== undefined ? { entryId: input.entryId } : {}),
412
+ op: input.op,
413
+ ...(input.baseRev !== undefined ? { baseRev: input.baseRev } : {}),
414
+ relPath: input.relPath,
415
+ slug: input.slug,
416
+ scope: input.scope,
417
+ captureName,
418
+ contentDigest: digest,
419
+ capturedAt: input.now(),
420
+ settleIds: [...input.settleIds],
421
+ status: "capturing",
422
+ });
423
+ return { next: rec, result: undefined };
424
+ });
425
+ }
426
+ catch (err) {
427
+ return { ok: false, reason: `hold ledger refused: ${err instanceof Error ? err.message : String(err)}` };
428
+ }
429
+ try {
430
+ ensureDirExists(holdDir);
431
+ writeFileSync(join(holdDir, captureName), buf, { flag: "wx" });
432
+ const back = readFileSync(join(holdDir, captureName));
433
+ if (sha256(back) !== digest)
434
+ return { ok: false, holdId, reason: "hold capture verification failed (written bytes do not hash to the captured digest)" };
435
+ }
436
+ catch (err) {
437
+ return { ok: false, holdId, reason: `hold capture failed: ${err instanceof Error ? err.message : String(err)}` };
438
+ }
439
+ const stagingName = uniqueStagingName(holdId, input.now);
440
+ const stagingPath = join(holdDir, stagingName);
441
+ let thirdWriterStranded;
442
+ try {
443
+ renameSync(input.absPath, stagingPath);
444
+ }
445
+ catch (err) {
446
+ const code = err.code;
447
+ if (code === "ENOENT") {
448
+ }
449
+ else {
450
+ return { ok: false, holdId, reason: `hold removal failed (${code ?? "rename error"}): the plane file was not moved` };
451
+ }
452
+ }
453
+ if (existsSync(stagingPath)) {
454
+ const moved = readFileSync(stagingPath);
455
+ if (sha256(moved) === digest) {
456
+ try {
457
+ unlinkSync(stagingPath);
458
+ }
459
+ catch {
460
+ }
461
+ }
462
+ else {
463
+ if (noReplaceRestore(stagingPath, input.absPath) === "stranded") {
464
+ thirdWriterStranded = stagingName;
465
+ }
466
+ }
467
+ }
468
+ try {
469
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
470
+ const row = rec.rows.find((r) => r.holdId === holdId);
471
+ if (row === undefined || row.status !== "capturing")
472
+ return { result: undefined };
473
+ row.status = "held";
474
+ return { next: rec, result: undefined };
475
+ });
476
+ }
477
+ catch (err) {
478
+ return { ok: false, holdId, reason: `hold ledger flip refused: ${err instanceof Error ? err.message : String(err)}` };
479
+ }
480
+ return { ok: true, holdId, ...(thirdWriterStranded !== undefined ? { thirdWriterStranded } : {}) };
481
+ }
482
+ export function reconcileHolds(controlDir, input) {
483
+ const holdDir = join(controlDir, HOLD_DIR);
484
+ const discloseStranded = (holdId, relPath, stagingName) => {
485
+ try {
486
+ enqueueMemoryAnnouncement(controlDir, {
487
+ kind: "gate",
488
+ at: input.now(),
489
+ items: [`instruction hold ${holdId}: a concurrent writer's bytes for ${JSON.stringify(relPath)} could not be restored onto the plane — they are preserved at hold/${stagingName} in the memory control plane for host recovery`],
490
+ });
491
+ }
492
+ catch {
493
+ console.warn(`[sema] instruction hold ${holdId}: concurrent-writer bytes preserved at hold/${stagingName} could not be restored or announced`);
494
+ }
495
+ };
496
+ const rows = readHolds(controlDir).filter((r) => r.status === "capturing");
497
+ for (const row of rows) {
498
+ const custody = join(holdDir, row.captureName);
499
+ let terminalLost = false;
500
+ if (existsSync(custody)) {
501
+ const bytes = readFileSync(custody);
502
+ if (sha256(bytes) !== row.contentDigest) {
503
+ terminalLost = true;
504
+ }
505
+ else {
506
+ const plane = join(input.memoryDir, row.relPath);
507
+ if (existsSync(plane)) {
508
+ try {
509
+ const stagingName = uniqueStagingName(row.holdId, input.now);
510
+ const staging = join(holdDir, stagingName);
511
+ renameSync(plane, staging);
512
+ if (sha256(readFileSync(staging)) === row.contentDigest) {
513
+ unlinkSync(staging);
514
+ }
515
+ else if (noReplaceRestore(staging, plane) === "stranded") {
516
+ discloseStranded(row.holdId, row.relPath, stagingName);
517
+ }
518
+ }
519
+ catch {
520
+ }
521
+ }
522
+ }
523
+ }
524
+ else {
525
+ const plane = join(input.memoryDir, row.relPath);
526
+ if (existsSync(plane)) {
527
+ const bytes = readFileSync(plane);
528
+ if (sha256(bytes) === row.contentDigest) {
529
+ try {
530
+ ensureDirExists(holdDir);
531
+ writeFileSync(custody, bytes, { flag: "wx" });
532
+ const stagingName = uniqueStagingName(row.holdId, input.now);
533
+ const staging = join(holdDir, stagingName);
534
+ renameSync(plane, staging);
535
+ if (sha256(readFileSync(staging)) === row.contentDigest) {
536
+ try {
537
+ unlinkSync(staging);
538
+ }
539
+ catch {
540
+ }
541
+ }
542
+ else if (noReplaceRestore(staging, plane) === "stranded") {
543
+ discloseStranded(row.holdId, row.relPath, stagingName);
544
+ }
545
+ }
546
+ catch {
547
+ terminalLost = true;
548
+ }
549
+ }
550
+ else {
551
+ terminalLost = true;
552
+ }
553
+ }
554
+ else {
555
+ terminalLost = true;
556
+ }
557
+ }
558
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
559
+ const r = rec.rows.find((x) => x.holdId === row.holdId);
560
+ if (r === undefined || r.status !== "capturing")
561
+ return { result: undefined };
562
+ if (terminalLost) {
563
+ r.status = "disposed";
564
+ r.disposition = { terminal: "capture_lost" };
565
+ }
566
+ else {
567
+ r.status = "held";
568
+ }
569
+ return { next: rec, result: undefined };
570
+ });
571
+ }
572
+ }
573
+ export function disposeHold(controlDir, input) {
574
+ const rows = readHolds(controlDir);
575
+ const row = rows.find((r) => r.holdId === input.holdId);
576
+ if (row === undefined || row.status === "disposed" || row.status === "released")
577
+ return { ok: false };
578
+ const custody = join(controlDir, HOLD_DIR, row.captureName);
579
+ const quarantineName = `${input.now()}-hold-${row.captureName}`;
580
+ const qDir = join(controlDir, QUARANTINE_DIR);
581
+ let moved = false;
582
+ try {
583
+ ensureDirExists(qDir);
584
+ renameSync(custody, join(qDir, quarantineName));
585
+ moved = true;
586
+ }
587
+ catch {
588
+ moved = existsSync(join(qDir, quarantineName));
589
+ }
590
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
591
+ const r = rec.rows.find((x) => x.holdId === input.holdId);
592
+ if (r === undefined || r.status === "disposed" || r.status === "released")
593
+ return { result: undefined };
594
+ r.status = "disposed";
595
+ r.disposition = { terminal: input.terminal, ...(moved ? { quarantineName } : {}) };
596
+ return { next: rec, result: undefined };
597
+ });
598
+ return { ok: true, ...(moved ? { quarantineName } : {}) };
599
+ }
600
+ export function markHoldReleased(controlDir, input) {
601
+ const captureName = lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
602
+ const r = rec.rows.find((x) => x.holdId === input.holdId);
603
+ if (r === undefined || (r.status !== "held" && !(r.status === "disposed" && r.disposition?.terminal === "expired")))
604
+ return { result: undefined };
605
+ r.status = "released";
606
+ delete r.resolved;
607
+ return { next: rec, result: r.captureName };
608
+ });
609
+ if (captureName === undefined)
610
+ return;
611
+ try {
612
+ unlinkSync(join(controlDir, HOLD_DIR, captureName));
613
+ }
614
+ catch {
615
+ }
616
+ }
617
+ export function resolveHoldRecord(controlDir, input) {
618
+ if (typeof input.requestId !== "string" || input.requestId.length === 0) {
619
+ const e = new Error("resolveHold requires a non-empty requestId (the audit attribution of who resolved) — refused, never defaulted.");
620
+ e.code = "memory.hold_resolve_unattributed";
621
+ throw e;
622
+ }
623
+ if (input.action !== "release" && input.action !== "discard") {
624
+ const e = new Error(`resolveHold action must be "release" or "discard" (got ${JSON.stringify(input.action)}) — refused loudly, never folded.`);
625
+ e.code = "memory.hold_resolve_invalid";
626
+ throw e;
627
+ }
628
+ lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (rec) => {
629
+ const r = rec.rows.find((x) => x.holdId === input.holdId);
630
+ if (r === undefined) {
631
+ const e = new Error(`resolveHold: unknown holdId ${JSON.stringify(input.holdId)}`);
632
+ e.code = "memory.hold_resolve_unknown";
633
+ throw e;
634
+ }
635
+ if (r.status !== "held" && !(r.status === "disposed" && r.disposition?.terminal === "expired" && input.action === "release")) {
636
+ const e = new Error(`resolveHold: hold ${JSON.stringify(input.holdId)} is ${r.status}${r.disposition !== undefined ? ` (${r.disposition.terminal})` : ""} — only a held row, or an expired disposition (release), can be resolved`);
637
+ e.code = "memory.hold_resolve_invalid_state";
638
+ throw e;
639
+ }
640
+ r.resolved = input.action;
641
+ return { next: rec, result: undefined };
642
+ });
643
+ }
644
+ export function readHoldCustody(controlDir, row) {
645
+ const seats = [join(controlDir, HOLD_DIR, row.captureName), ...(row.disposition?.quarantineName !== undefined ? [join(controlDir, QUARANTINE_DIR, row.disposition.quarantineName)] : [])];
646
+ for (const seat of seats) {
647
+ try {
648
+ const bytes = readFileSync(seat);
649
+ if (sha256(bytes) === row.contentDigest)
650
+ return bytes.toString("utf8");
651
+ }
652
+ catch {
653
+ }
654
+ }
655
+ return undefined;
656
+ }
657
+ export function sessionBlocksCleanRelease(controlDir, sessionId) {
658
+ if (readSessionPollution(controlDir, sessionId) !== undefined)
659
+ return true;
660
+ return sessionSettlements(controlDir, sessionId).some((s) => s.effective !== "clean" && s.effective !== "void");
661
+ }