@sema-agent/core 5.21.1 → 5.23.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 (96) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +48 -6
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +14 -0
  18. package/dist/core/hooks.d.ts +101 -1
  19. package/dist/core/hooks.js +116 -14
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +94 -1
  25. package/dist/core/memory-engine/engine.js +329 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +232 -0
  31. package/dist/core/memory-engine/layout.js +469 -1
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +93 -21
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +195 -0
  39. package/dist/core/permission-rule-org.js +234 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +314 -3
  42. package/dist/core/permission-rule-sync.d.ts +139 -0
  43. package/dist/core/permission-rule-sync.js +343 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +64 -3
  46. package/dist/core/runner/prepare-task.js +357 -32
  47. package/dist/core/runner/runtask.js +166 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +155 -4
  61. package/dist/core/tool-policy.js +148 -10
  62. package/dist/core/tool-result-store.d.ts +9 -1
  63. package/dist/core/tool-result-store.js +2 -1
  64. package/dist/core/trace.d.ts +79 -1
  65. package/dist/core/types.d.ts +94 -3
  66. package/dist/core/wiring-manifest.d.ts +16 -1
  67. package/dist/core/wiring-manifest.js +7 -1
  68. package/dist/index.d.ts +15 -8
  69. package/dist/index.js +12 -5
  70. package/dist/orchestration/goal.d.ts +10 -0
  71. package/dist/orchestration/goal.js +6 -5
  72. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  73. package/dist/stores/file/adoption/adopt.js +616 -0
  74. package/dist/stores/file/adoption/marker.d.ts +194 -0
  75. package/dist/stores/file/adoption/marker.js +198 -0
  76. package/dist/stores/file/background-agent-store.js +2 -0
  77. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  78. package/dist/stores/file/checkpoint-store.js +3 -0
  79. package/dist/stores/file/file-snapshot-store.js +2 -0
  80. package/dist/stores/file/index.d.ts +2 -0
  81. package/dist/stores/file/index.js +4 -0
  82. package/dist/stores/file/mailbox-store.js +2 -0
  83. package/dist/stores/file/memory-store.js +2 -0
  84. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  85. package/dist/stores/file/permission-rule-adopt.js +95 -0
  86. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  87. package/dist/stores/file/permission-rule-store.js +189 -46
  88. package/dist/stores/file/session-policy-store.js +2 -0
  89. package/dist/stores/file/session-store.js +2 -0
  90. package/dist/stores/file/task-list-store.js +2 -0
  91. package/dist/stores/file/tool-result-store.js +2 -0
  92. package/dist/stores/file/usage-window-store.js +2 -0
  93. package/dist/stores/file/workflow-journal-store.js +2 -0
  94. package/dist/stores/file/workflow-run-store.js +2 -0
  95. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  96. package/package.json +3 -2
@@ -1,4 +1,4 @@
1
- import { closeSync, constants as fsConstants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { closeSync, constants as fsConstants, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
2
  const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW } = fsConstants;
3
3
  import { homedir } from "node:os";
4
4
  import { createHash } from "node:crypto";
@@ -696,6 +696,474 @@ export function clearScanFuse(controlDir, keys) {
696
696
  return changed ? counts : undefined;
697
697
  });
698
698
  }
699
+ export const LINEAGE_FILE = "lineage.json";
700
+ export const CHALLENGES_FILE = "challenges.json";
701
+ export const LINEAGE_AUDIT_FILE = "lineage-audit.json";
702
+ export const LINEAGE_AUDIT_MAX_ROWS = 2048;
703
+ export const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
704
+ function rollForwardStrictSidecar(file, journal) {
705
+ let raw;
706
+ try {
707
+ raw = readFileSync(journal, "utf8");
708
+ }
709
+ catch {
710
+ return;
711
+ }
712
+ try {
713
+ JSON.parse(raw);
714
+ }
715
+ catch {
716
+ throw new ControlPlaneCorruptError(`control-plane journal is unparseable (fail-closed sidecar): ${journal}`);
717
+ }
718
+ atomicWriteFileSync(file, raw);
719
+ rmSync(journal, { force: true });
720
+ }
721
+ function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
722
+ ensureDirExists(controlDir);
723
+ const file = join(controlDir, fileName);
724
+ const journal = `${file}.journal`;
725
+ const lock = `${file}.lock`;
726
+ const token = acquireSidecarLock(lock, { onDeadline: "throw" });
727
+ try {
728
+ rollForwardStrictSidecar(file, journal);
729
+ const current = coerce(readStrictSidecarRaw(file, what));
730
+ const { next, result } = fn(current);
731
+ if (next !== undefined) {
732
+ const data = `${JSON.stringify(next, null, 2)}\n`;
733
+ assertSidecarLockOwnership(lock, token, what);
734
+ atomicWriteFileSync(journal, data);
735
+ atomicWriteFileSync(file, data);
736
+ rmSync(journal, { force: true });
737
+ }
738
+ return result;
739
+ }
740
+ finally {
741
+ releaseSidecarLock(lock, token);
742
+ }
743
+ }
744
+ function readStrictSidecarRaw(file, what) {
745
+ let raw;
746
+ try {
747
+ raw = readFileSync(file, "utf8");
748
+ }
749
+ catch (err) {
750
+ if (err.code === "ENOENT")
751
+ return undefined;
752
+ throw new ControlPlaneCorruptError(`${what} unreadable (${err.code ?? "io error"}): ${file}`, { cause: err });
753
+ }
754
+ try {
755
+ return JSON.parse(raw);
756
+ }
757
+ catch {
758
+ throw new ControlPlaneCorruptError(`${what} is unparseable: ${file}`);
759
+ }
760
+ }
761
+ function readStrictSidecar(controlDir, fileName, what) {
762
+ const file = join(controlDir, fileName);
763
+ const journal = `${file}.journal`;
764
+ let journalRaw;
765
+ try {
766
+ journalRaw = readFileSync(journal, "utf8");
767
+ }
768
+ catch {
769
+ journalRaw = undefined;
770
+ }
771
+ if (journalRaw !== undefined) {
772
+ try {
773
+ return JSON.parse(journalRaw);
774
+ }
775
+ catch {
776
+ throw new ControlPlaneCorruptError(`control-plane journal is unparseable (fail-closed sidecar): ${journal}`);
777
+ }
778
+ }
779
+ return readStrictSidecarRaw(file, what);
780
+ }
781
+ function coerceLineage(raw) {
782
+ if (raw === undefined)
783
+ return { version: 1, committed: {}, pending: {} };
784
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
785
+ throw new ControlPlaneCorruptError("memory lineage ledger has the wrong shape");
786
+ const r = raw;
787
+ if (r.version !== 1)
788
+ throw new ControlPlaneCorruptError(`memory lineage ledger has an unrecognized version (${String(r.version)}) — refusing (fail-closed)`);
789
+ const badShape = (part) => new ControlPlaneCorruptError(`memory lineage ledger ${part} has the wrong shape`);
790
+ if (!r.committed || typeof r.committed !== "object" || Array.isArray(r.committed))
791
+ throw badShape("committed set");
792
+ if (!r.pending || typeof r.pending !== "object" || Array.isArray(r.pending))
793
+ throw badShape("pending set");
794
+ for (const [entryId, sessions] of Object.entries(r.committed)) {
795
+ if (!sessions || typeof sessions !== "object" || Array.isArray(sessions))
796
+ throw badShape(`committed[${JSON.stringify(entryId)}]`);
797
+ for (const [sid, c] of Object.entries(sessions)) {
798
+ const row = c;
799
+ if (!row || typeof row !== "object" || typeof row.lastRev !== "string" || typeof row.lastAt !== "number") {
800
+ throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}]`);
801
+ }
802
+ }
803
+ }
804
+ for (const [txnId, t] of Object.entries(r.pending)) {
805
+ const txn = t;
806
+ if (!txn || typeof txn !== "object" || typeof txn.sessionId !== "string" || typeof txn.at !== "number" || !Array.isArray(txn.rows)) {
807
+ throw badShape(`pending[${JSON.stringify(txnId)}]`);
808
+ }
809
+ for (const row of txn.rows) {
810
+ const p = row;
811
+ if (!p || typeof p !== "object" || typeof p.entryId !== "string" || typeof p.rev !== "string")
812
+ throw badShape(`pending[${JSON.stringify(txnId)}] row`);
813
+ }
814
+ if (txn.credential !== undefined) {
815
+ const c = txn.credential;
816
+ if (!c || typeof c !== "object" || !Array.isArray(c.appliedIds) || c.appliedIds.some((x) => typeof x !== "string") || typeof c.at !== "number") {
817
+ throw badShape(`pending[${JSON.stringify(txnId)}] credential`);
818
+ }
819
+ }
820
+ }
821
+ return raw;
822
+ }
823
+ export function stageLineagePending(controlDir, txnId, sessionId, rows, now) {
824
+ lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
825
+ rec.pending[txnId] = { sessionId, at: now(), rows: [...rows] };
826
+ return { next: rec, result: undefined };
827
+ });
828
+ }
829
+ export function discardLineagePending(controlDir, txnId) {
830
+ lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
831
+ if (rec.pending[txnId] === undefined)
832
+ return { result: undefined };
833
+ delete rec.pending[txnId];
834
+ return { next: rec, result: undefined };
835
+ });
836
+ }
837
+ export function recordLineageCredential(controlDir, txnId, appliedIds, now) {
838
+ lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
839
+ const txn = rec.pending[txnId];
840
+ if (txn === undefined)
841
+ throw new ControlPlaneCorruptError(`memory lineage ledger: credential for unknown pending transaction ${txnId}`);
842
+ txn.credential = { appliedIds: [...appliedIds], at: now() };
843
+ return { next: rec, result: undefined };
844
+ });
845
+ }
846
+ export function promoteLineagePending(controlDir, txnId, now) {
847
+ return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
848
+ const txn = rec.pending[txnId];
849
+ if (txn === undefined)
850
+ return { result: [] };
851
+ if (txn.credential === undefined) {
852
+ throw new ControlPlaneCorruptError(`memory lineage ledger: refusing to promote uncredentialed pending transaction ${txnId} (the commit outcome is not durably known)`);
853
+ }
854
+ const applied = new Set(txn.credential.appliedIds);
855
+ const at = now();
856
+ const promoted = [];
857
+ for (const row of txn.rows) {
858
+ if (!applied.has(row.entryId))
859
+ continue;
860
+ const sessions = (rec.committed[row.entryId] ??= {});
861
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
862
+ promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
863
+ }
864
+ delete rec.pending[txnId];
865
+ return { next: rec, result: promoted };
866
+ });
867
+ }
868
+ export function adjudicateLineagePending(controlDir, txnId, action, now) {
869
+ return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
870
+ const txn = rec.pending[txnId];
871
+ if (txn === undefined)
872
+ return { result: [] };
873
+ if (action === "discard") {
874
+ delete rec.pending[txnId];
875
+ return { next: rec, result: [] };
876
+ }
877
+ const applied = txn.credential !== undefined ? new Set(txn.credential.appliedIds) : undefined;
878
+ const at = now();
879
+ const promoted = [];
880
+ for (const row of txn.rows) {
881
+ if (applied !== undefined && !applied.has(row.entryId))
882
+ continue;
883
+ const sessions = (rec.committed[row.entryId] ??= {});
884
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
885
+ promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
886
+ }
887
+ delete rec.pending[txnId];
888
+ return { next: rec, result: promoted };
889
+ });
890
+ }
891
+ export function reconcileLineage(controlDir, now) {
892
+ return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
893
+ const promoted = [];
894
+ const undecidable = [];
895
+ let at;
896
+ let changed = false;
897
+ for (const [txnId, txn] of Object.entries(rec.pending)) {
898
+ if (txn.credential === undefined) {
899
+ undecidable.push({ txnId, sessionId: txn.sessionId, entryIds: txn.rows.map((r) => r.entryId) });
900
+ continue;
901
+ }
902
+ const applied = new Set(txn.credential.appliedIds);
903
+ for (const row of txn.rows) {
904
+ if (!applied.has(row.entryId))
905
+ continue;
906
+ at ??= now();
907
+ const sessions = (rec.committed[row.entryId] ??= {});
908
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
909
+ promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
910
+ }
911
+ delete rec.pending[txnId];
912
+ changed = true;
913
+ }
914
+ return { ...(changed ? { next: rec } : {}), result: { promoted, undecidable } };
915
+ });
916
+ }
917
+ export function lineageLatchedIds(controlDir) {
918
+ const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
919
+ const out = new Set();
920
+ for (const txn of Object.values(rec.pending))
921
+ for (const row of txn.rows)
922
+ out.add(row.entryId);
923
+ return out;
924
+ }
925
+ export function lineageContributionsOfSession(controlDir, sessionId) {
926
+ const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
927
+ const out = [];
928
+ for (const [entryId, sessions] of Object.entries(rec.committed)) {
929
+ const c = sessions[sessionId];
930
+ if (c !== undefined)
931
+ out.push({ entryId, lastRev: c.lastRev });
932
+ }
933
+ return out;
934
+ }
935
+ export function clearLineageForEntries(controlDir, entryIds) {
936
+ if (entryIds.length === 0)
937
+ return;
938
+ lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
939
+ let changed = false;
940
+ for (const id of entryIds) {
941
+ if (rec.committed[id] !== undefined) {
942
+ delete rec.committed[id];
943
+ changed = true;
944
+ }
945
+ }
946
+ return { ...(changed ? { next: rec } : {}), result: undefined };
947
+ });
948
+ }
949
+ export function readLineageRecord(controlDir) {
950
+ const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
951
+ return { committed: rec.committed, pending: rec.pending };
952
+ }
953
+ export function appendLineageAudit(controlDir, rows) {
954
+ if (rows.length === 0)
955
+ return;
956
+ lockedJournaledUpdate(controlDir, LINEAGE_AUDIT_FILE, (current) => {
957
+ const list = Array.isArray(current) ? current.filter((r) => r && typeof r === "object") : [];
958
+ list.push(...rows);
959
+ return list.length > LINEAGE_AUDIT_MAX_ROWS ? list.slice(-LINEAGE_AUDIT_MAX_ROWS) : list;
960
+ });
961
+ }
962
+ function coerceChallenges(raw) {
963
+ if (raw === undefined)
964
+ return { version: 1, events: [] };
965
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
966
+ throw new ControlPlaneCorruptError("memory challenge ledger has the wrong shape");
967
+ const r = raw;
968
+ if (r.version !== 1)
969
+ throw new ControlPlaneCorruptError(`memory challenge ledger has an unrecognized version (${String(r.version)}) — refusing (fail-closed)`);
970
+ if (!Array.isArray(r.events))
971
+ throw new ControlPlaneCorruptError("memory challenge ledger events list has the wrong shape");
972
+ for (const e of r.events) {
973
+ const ev = e;
974
+ if (!ev ||
975
+ typeof ev !== "object" ||
976
+ typeof ev.eventId !== "string" ||
977
+ typeof ev.entryId !== "string" ||
978
+ (ev.kind !== "challenge" && ev.kind !== "resolve") ||
979
+ typeof ev.generation !== "number" ||
980
+ typeof ev.at !== "number" ||
981
+ typeof ev.reason !== "string") {
982
+ throw new ControlPlaneCorruptError("memory challenge ledger event has the wrong shape");
983
+ }
984
+ }
985
+ return raw;
986
+ }
987
+ export const CHALLENGE_LEDGER_MAX_EVENTS = 10_000;
988
+ export function appendChallengeEvents(controlDir, events, now) {
989
+ if (events.length === 0)
990
+ return { assignments: [], eventCount: undefined };
991
+ return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
992
+ const byEventId = new Map(rec.events.map((e) => [e.eventId, e]));
993
+ const maxGen = new Map();
994
+ for (const e of rec.events) {
995
+ if (e.kind !== "challenge")
996
+ continue;
997
+ maxGen.set(e.entryId, Math.max(maxGen.get(e.entryId) ?? 0, e.generation));
998
+ }
999
+ const out = [];
1000
+ let changed = false;
1001
+ for (const req of events) {
1002
+ const existing = byEventId.get(req.eventId);
1003
+ if (existing !== undefined) {
1004
+ out.push({ entryId: existing.entryId, eventId: existing.eventId, generation: existing.generation, at: existing.at, replayed: true });
1005
+ continue;
1006
+ }
1007
+ const generation = (maxGen.get(req.entryId) ?? 0) + 1;
1008
+ maxGen.set(req.entryId, generation);
1009
+ const ev = {
1010
+ eventId: req.eventId,
1011
+ entryId: req.entryId,
1012
+ kind: "challenge",
1013
+ generation,
1014
+ at: now(),
1015
+ reason: req.reason,
1016
+ grade: "S",
1017
+ ...(req.challengedRev !== undefined ? { challengedRev: req.challengedRev } : {}),
1018
+ };
1019
+ rec.events.push(ev);
1020
+ byEventId.set(ev.eventId, ev);
1021
+ out.push({ entryId: ev.entryId, eventId: ev.eventId, generation, at: ev.at, replayed: false });
1022
+ changed = true;
1023
+ }
1024
+ return { ...(changed ? { next: rec } : {}), result: { assignments: out, eventCount: rec.events.length } };
1025
+ });
1026
+ }
1027
+ export function resolveChallengeEvent(controlDir, entryId, generation, reason, now, requestId) {
1028
+ return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
1029
+ const opened = rec.events.some((e) => e.kind === "challenge" && e.entryId === entryId && e.generation === generation);
1030
+ if (!opened)
1031
+ return { result: { resolved: false, eventCount: undefined } };
1032
+ const alreadyResolved = rec.events.some((e) => e.kind === "resolve" && e.entryId === entryId && e.generation === generation);
1033
+ if (alreadyResolved)
1034
+ return { result: { resolved: true, eventCount: undefined } };
1035
+ rec.events.push({
1036
+ eventId: requestId !== undefined && requestId !== "" ? `${requestId}:${entryId}` : `resolve:${entryId}:${generation}`,
1037
+ entryId,
1038
+ kind: "resolve",
1039
+ generation,
1040
+ at: now(),
1041
+ reason,
1042
+ });
1043
+ return { next: rec, result: { resolved: true, eventCount: rec.events.length } };
1044
+ });
1045
+ }
1046
+ export function challengedEntryIds(controlDir) {
1047
+ const rec = coerceChallenges(readStrictSidecar(controlDir, CHALLENGES_FILE, "memory challenge ledger"));
1048
+ const resolved = new Set();
1049
+ for (const e of rec.events)
1050
+ if (e.kind === "resolve")
1051
+ resolved.add(`${e.entryId}#${e.generation}`);
1052
+ const out = new Map();
1053
+ for (const e of rec.events) {
1054
+ if (e.kind !== "challenge" || resolved.has(`${e.entryId}#${e.generation}`))
1055
+ continue;
1056
+ const prior = out.get(e.entryId);
1057
+ if (prior === undefined || e.generation > prior.generation)
1058
+ out.set(e.entryId, { generation: e.generation, at: e.at });
1059
+ }
1060
+ return out;
1061
+ }
1062
+ export function readChallengeEvents(controlDir) {
1063
+ return coerceChallenges(readStrictSidecar(controlDir, CHALLENGES_FILE, "memory challenge ledger")).events;
1064
+ }
1065
+ export function isStrictControlPlaneLedgerCorrupt(controlDir, ledger) {
1066
+ const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
1067
+ const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
1068
+ const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
1069
+ try {
1070
+ coerce(readStrictSidecar(controlDir, fileName, what));
1071
+ return false;
1072
+ }
1073
+ catch (err) {
1074
+ if (err instanceof ControlPlaneCorruptError)
1075
+ return true;
1076
+ throw err;
1077
+ }
1078
+ }
1079
+ export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
1080
+ const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
1081
+ const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
1082
+ const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
1083
+ const empty = ledger === "lineage" ? { version: 1, committed: {}, pending: {} } : { version: 1, events: [] };
1084
+ ensureDirExists(controlDir);
1085
+ const file = join(controlDir, fileName);
1086
+ const journal = `${file}.journal`;
1087
+ const lock = `${file}.lock`;
1088
+ const token = acquireSidecarLock(lock, { onDeadline: "throw" });
1089
+ try {
1090
+ let healthy = false;
1091
+ try {
1092
+ coerce(readStrictSidecar(controlDir, fileName, what));
1093
+ healthy = true;
1094
+ }
1095
+ catch (err) {
1096
+ if (!(err instanceof ControlPlaneCorruptError))
1097
+ throw err;
1098
+ }
1099
+ if (healthy) {
1100
+ const e = new Error(`${what} reads cleanly — refusing to rebuild it. This call exists to recover a ledger whose corruption already refuses every read; resetting a healthy one would drop taint evidence that is still doing its job.`);
1101
+ e.code = "memory.control_plane_not_corrupt";
1102
+ throw e;
1103
+ }
1104
+ const at = now();
1105
+ const quarantinedTo = [];
1106
+ assertSidecarLockOwnership(lock, token, what);
1107
+ for (const path of [file, journal]) {
1108
+ if (!existsSync(path))
1109
+ continue;
1110
+ let dest = `${path}.corrupt-${at}`;
1111
+ for (let n = 2;; n++) {
1112
+ try {
1113
+ copyFileSync(path, dest, fsConstants.COPYFILE_EXCL);
1114
+ break;
1115
+ }
1116
+ catch (err) {
1117
+ if (err.code !== "EEXIST")
1118
+ throw err;
1119
+ dest = `${path}.corrupt-${at}-${n}`;
1120
+ }
1121
+ }
1122
+ quarantinedTo.push(dest);
1123
+ }
1124
+ assertSidecarLockOwnership(lock, token, what);
1125
+ atomicWriteFileSync(file, `${JSON.stringify(empty, null, 2)}\n`);
1126
+ rmSync(journal, { force: true });
1127
+ return { ledger, quarantinedTo, at };
1128
+ }
1129
+ finally {
1130
+ releaseSidecarLock(lock, token);
1131
+ }
1132
+ }
1133
+ function coerceChallengedHistory(raw) {
1134
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1135
+ return {};
1136
+ const out = {};
1137
+ for (const [k, v] of Object.entries(raw)) {
1138
+ const row = v;
1139
+ if (row &&
1140
+ typeof row === "object" &&
1141
+ typeof row.count === "number" &&
1142
+ Number.isFinite(row.count) &&
1143
+ row.count > 0 &&
1144
+ typeof row.lastAt === "number" &&
1145
+ (row.lastOp === "update" || row.lastOp === "tombstone")) {
1146
+ out[k] = { count: Math.floor(row.count), lastAt: row.lastAt, lastOp: row.lastOp };
1147
+ }
1148
+ }
1149
+ return out;
1150
+ }
1151
+ export function recordChallengedHistory(controlDir, rows, now) {
1152
+ if (rows.length === 0)
1153
+ return;
1154
+ const at = now();
1155
+ lockedJournaledUpdate(controlDir, CHALLENGED_HISTORY_FILE, (current) => {
1156
+ const acc = coerceChallengedHistory(current);
1157
+ for (const r of rows) {
1158
+ const prior = acc[r.entryId];
1159
+ acc[r.entryId] = { count: (prior?.count ?? 0) + 1, lastAt: at, lastOp: r.op };
1160
+ }
1161
+ return acc;
1162
+ });
1163
+ }
1164
+ export function readChallengedHistory(controlDir) {
1165
+ return coerceChallengedHistory(readSidecarJson(controlDir, CHALLENGED_HISTORY_FILE));
1166
+ }
699
1167
  export function writeAllSync(fd, data) {
700
1168
  const buf = Buffer.from(data, "utf8");
701
1169
  let written = 0;
@@ -48,6 +48,19 @@ export interface MemoryEnginePlane {
48
48
  backend: MemoryBackend;
49
49
  scopes: readonly string[];
50
50
  recordRetrieved: (ids: readonly string[]) => void;
51
+ /**
52
+ * design/180 B-2' — the plane's EXCLUSION view: entry id → why its content is withheld from the
53
+ * model face (`challenged` = an unresolved S-grade challenge generation; `lineage_pending` = the
54
+ * dirty latch, its commit bookkeeping is unsettled). THROWS when the underlying ledger's
55
+ * integrity is unknowable — both tools then REFUSE SERVICE on that plane (fail-closed: a corrupt
56
+ * ledger must never read as "nothing withheld"). Absent ⇒ a plane with no control plane (direct
57
+ * hosts / tests) — no exclusion, v1 behavior byte-identical.
58
+ */
59
+ challengeExclusions?: () => ReadonlyMap<string, {
60
+ code: "challenged" | "lineage_pending";
61
+ generation?: number;
62
+ at?: number;
63
+ }>;
51
64
  }
52
65
  export interface MemoryEngineToolsOptions {
53
66
  planes: ReadonlyArray<MemoryEnginePlane>;
@@ -85,6 +98,16 @@ export interface MemoryGetDetails {
85
98
  offset?: number;
86
99
  lines?: number;
87
100
  totalLines?: number;
101
+ /** design/180 — the OPAQUE withheld refusal's structured half (`reason: "challenged" |
102
+ * "lineage_pending"`): the unresolved challenge generation and its timestamp. Deliberately the
103
+ * ONLY fields beyond the id — name/description/free reason text are content positions and live
104
+ * on the host audit face exclusively. */
105
+ generation?: number;
106
+ challengedAt?: number;
107
+ /** design/180 (intra-line cursor) — byte offset into the (neutralized) single line at `offset`
108
+ * this page continued from / where the next page should continue. */
109
+ lineCursor?: number;
110
+ nextLineCursor?: number;
88
111
  }
89
112
  /** Cut `text` to at most `maxBytes` UTF-8 bytes on a CODE POINT boundary (a byte-wise slice would
90
113
  * strand half a character), reporting how many bytes were dropped. Unchanged text reports 0.
@@ -93,4 +116,11 @@ export declare function cutToBytes(text: string, maxBytes: number): {
93
116
  text: string;
94
117
  omittedBytes: number;
95
118
  };
119
+ /** Skip `startBytes` UTF-8 bytes of `text` on a code-point boundary (the intra-line cursor's seek
120
+ * half — {@link cutToBytes} is its cut half). A cursor landing mid-character (never minted by this
121
+ * module) rounds FORWARD to the character's end, so no half-character is ever emitted. */
122
+ export declare function skipBytes(text: string, startBytes: number): {
123
+ text: string;
124
+ skippedBytes: number;
125
+ };
96
126
  export declare function createMemoryEngineTools(opts: MemoryEngineToolsOptions): ToolSpec[];