@sema-agent/core 5.45.0 → 5.46.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 +56 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/hooks.d.ts +9 -2
- package/dist/core/hooks.js +6 -5
- package/dist/core/memory-engine/content-origin.d.ts +3 -1
- package/dist/core/memory-engine/engine.d.ts +50 -3
- package/dist/core/memory-engine/engine.js +194 -32
- package/dist/core/memory-engine/export-bundle.d.ts +10 -1
- package/dist/core/memory-engine/export-bundle.js +21 -0
- package/dist/core/memory-engine/file-backend.d.ts +33 -4
- package/dist/core/memory-engine/file-backend.js +165 -39
- package/dist/core/memory-engine/frontmatter.d.ts +42 -1
- package/dist/core/memory-engine/frontmatter.js +141 -1
- package/dist/core/memory-engine/header-hints.d.ts +17 -0
- package/dist/core/memory-engine/header-hints.js +6 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +25 -2
- package/dist/core/memory-engine/layout.js +25 -12
- package/dist/core/memory-engine/memory-backend-contract.js +65 -0
- package/dist/core/memory-engine/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +7 -0
- package/dist/core/memory-engine/tools.js +3 -0
- package/dist/core/memory-engine/types.d.ts +75 -1
- package/dist/core/memory-engine/types.js +1 -1
- package/dist/core/reminder-mint.d.ts +70 -0
- package/dist/core/reminder-mint.js +25 -0
- package/dist/core/runner/git-status-frame.d.ts +3 -14
- package/dist/core/runner/git-status-frame.js +39 -14
- package/dist/core/runner/prepare-config-doors.d.ts +4 -0
- package/dist/core/runner/prepare-config-doors.js +15 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
- package/dist/core/runner/prepare-hands-readface.js +26 -0
- package/dist/core/runner/prepare-memory.d.ts +11 -0
- package/dist/core/runner/prepare-memory.js +12 -10
- package/dist/core/runner/prepare-task.d.ts +22 -1
- package/dist/core/runner/prepare-task.js +48 -13
- package/dist/core/runner/runtask.js +62 -55
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/types.d.ts +47 -7
- package/dist/engine/harness/types.d.ts +46 -1
- package/dist/engine/harness/types.js +11 -0
- package/dist/engine/session/import-validate.js +6 -1
- package/dist/engine/session/session.d.ts +20 -0
- package/dist/engine/session/session.js +26 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/orchestration/run-workflow-tool.d.ts +16 -0
- package/dist/orchestration/run-workflow-tool.js +23 -3
- package/dist/orchestration/workflow-governance.d.ts +8 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/types.d.ts +6 -0
- package/dist/prompts/default.d.ts +13 -1
- package/dist/prompts/default.js +5 -1
- package/dist/tools/fs/fs-bash.d.ts +4 -0
- package/dist/tools/fs/fs-bash.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +8 -7
- package/dist/tools/fs/fs-shared.d.ts +10 -4
- package/dist/tools/fs/fs-shared.js +6 -3
- package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
- package/dist/tools/fs/gh-rate-limit.js +3 -2
- package/dist/tools/fs/index.d.ts +10 -2
- package/dist/tools/fs/index.js +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +12 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
2
3
|
const FM_FENCE = "---";
|
|
3
4
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/;
|
|
4
5
|
export function isValidEntryId(value) {
|
|
@@ -28,8 +29,12 @@ export function parseEntryFile(text) {
|
|
|
28
29
|
const provRaw = [];
|
|
29
30
|
const provUnknown = [];
|
|
30
31
|
const prov = {};
|
|
32
|
+
let inOrigin = false;
|
|
33
|
+
const originRaw = [];
|
|
34
|
+
const originUnknown = [];
|
|
35
|
+
const orig = {};
|
|
31
36
|
for (let i = 1; i < end; i++) {
|
|
32
|
-
const line = lines[i];
|
|
37
|
+
const line = lines[i] ?? "";
|
|
33
38
|
const trimmed = line.trim();
|
|
34
39
|
if (trimmed === "")
|
|
35
40
|
continue;
|
|
@@ -37,6 +42,7 @@ export function parseEntryFile(text) {
|
|
|
37
42
|
if (!indented) {
|
|
38
43
|
inMetadata = false;
|
|
39
44
|
inProvenance = false;
|
|
45
|
+
inOrigin = false;
|
|
40
46
|
}
|
|
41
47
|
if (inMetadata) {
|
|
42
48
|
const m = /^\s+type:\s*(.+?)\s*$/.exec(line);
|
|
@@ -72,11 +78,35 @@ export function parseEntryFile(text) {
|
|
|
72
78
|
inMetadata = true;
|
|
73
79
|
continue;
|
|
74
80
|
}
|
|
81
|
+
if (inOrigin) {
|
|
82
|
+
originRaw.push(line);
|
|
83
|
+
const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
|
|
84
|
+
if (m) {
|
|
85
|
+
const [, k = "", v = ""] = m;
|
|
86
|
+
if (k === "taint")
|
|
87
|
+
orig.taint = v;
|
|
88
|
+
else if (k === "cause")
|
|
89
|
+
orig.cause = v;
|
|
90
|
+
else if (k === "at")
|
|
91
|
+
orig.at = /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : Number.NaN;
|
|
92
|
+
else
|
|
93
|
+
originUnknown.push(line);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
originUnknown.push(line);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
75
100
|
if (/^provenance:\s*$/.test(trimmed)) {
|
|
76
101
|
inProvenance = true;
|
|
77
102
|
provRaw.push(line);
|
|
78
103
|
continue;
|
|
79
104
|
}
|
|
105
|
+
if (/^origin:\s*$/.test(trimmed) && originRaw.length === 0) {
|
|
106
|
+
inOrigin = true;
|
|
107
|
+
originRaw.push(line);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
80
110
|
const kv = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(trimmed);
|
|
81
111
|
if (!kv || indented) {
|
|
82
112
|
extra.push(line);
|
|
@@ -126,6 +156,15 @@ export function parseEntryFile(text) {
|
|
|
126
156
|
extra.push(...provRaw);
|
|
127
157
|
}
|
|
128
158
|
}
|
|
159
|
+
if (originRaw.length > 0) {
|
|
160
|
+
if (orig.taint === "external" && typeof orig.at === "number" && Number.isFinite(orig.at) && (orig.cause === undefined || MEMORY_ORIGIN_CAUSES.includes(orig.cause))) {
|
|
161
|
+
fm.origin = { taint: "external", ...(orig.cause !== undefined ? { cause: orig.cause } : {}), at: orig.at };
|
|
162
|
+
extra.push(...originUnknown);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
extra.push(...originRaw);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
129
168
|
if (extra.length > 0)
|
|
130
169
|
fm.extra = extra;
|
|
131
170
|
let body = lines.slice(end + 1).join("\n");
|
|
@@ -149,6 +188,9 @@ export function serializeEntryFile(entry) {
|
|
|
149
188
|
}
|
|
150
189
|
if (fm.trust !== undefined)
|
|
151
190
|
lines.push(`trust: ${fm.trust}`);
|
|
191
|
+
if (fm.origin !== undefined) {
|
|
192
|
+
lines.push("origin:", ` taint: ${fm.origin.taint}`, ...(fm.origin.cause !== undefined ? [` cause: ${fm.origin.cause}`] : []), ` at: ${fm.origin.at}`);
|
|
193
|
+
}
|
|
152
194
|
if (fm.extra)
|
|
153
195
|
lines.push(...fm.extra);
|
|
154
196
|
lines.push(FM_FENCE, "");
|
|
@@ -168,9 +210,107 @@ export function computeEntryRev(entry) {
|
|
|
168
210
|
...(fm.provenance !== undefined || fm.trust !== undefined
|
|
169
211
|
? [fm.trust ?? null, fm.provenance !== undefined ? [fm.provenance.kind, fm.provenance.path, fm.provenance.contentHash, fm.provenance.ingestedAt] : null]
|
|
170
212
|
: []),
|
|
213
|
+
...(fm.origin !== undefined ? [["origin", fm.origin.taint, fm.origin.cause ?? null, fm.origin.at]] : []),
|
|
171
214
|
]);
|
|
172
215
|
return createHash("sha256").update(canonical, "utf8").digest("hex").slice(0, 16);
|
|
173
216
|
}
|
|
217
|
+
const ORIGIN_FORM_LINE_RE = /^\s*origin\s*:/;
|
|
218
|
+
export function hasOriginFormExtra(fm) {
|
|
219
|
+
return fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line));
|
|
220
|
+
}
|
|
221
|
+
function walkExtraOrigin(extra) {
|
|
222
|
+
if (extra === undefined)
|
|
223
|
+
return { carriers: 0, first: undefined };
|
|
224
|
+
let carriers = 0;
|
|
225
|
+
let inBlock = false;
|
|
226
|
+
let blockIndex = 0;
|
|
227
|
+
let taint;
|
|
228
|
+
let cause;
|
|
229
|
+
let at;
|
|
230
|
+
for (const line of extra) {
|
|
231
|
+
const trimmed = line.trim();
|
|
232
|
+
if (/^origin\s*:\s*$/.test(trimmed)) {
|
|
233
|
+
carriers += 1;
|
|
234
|
+
inBlock = true;
|
|
235
|
+
blockIndex += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (ORIGIN_FORM_LINE_RE.test(line) && !inBlock) {
|
|
239
|
+
carriers += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!/^\s/.test(line))
|
|
243
|
+
inBlock = false;
|
|
244
|
+
if (!inBlock || blockIndex !== 1)
|
|
245
|
+
continue;
|
|
246
|
+
const m = /^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/.exec(line);
|
|
247
|
+
if (m === null)
|
|
248
|
+
continue;
|
|
249
|
+
const [, k = "", v = ""] = m;
|
|
250
|
+
if (k === "taint" && taint === undefined)
|
|
251
|
+
taint = v;
|
|
252
|
+
else if (k === "cause" && cause === undefined)
|
|
253
|
+
cause = v;
|
|
254
|
+
else if (k === "at" && at === undefined)
|
|
255
|
+
at = /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : Number.NaN;
|
|
256
|
+
}
|
|
257
|
+
if (carriers === 0)
|
|
258
|
+
return { carriers: 0, first: undefined };
|
|
259
|
+
if (taint === "external" && typeof at === "number" && Number.isFinite(at) && (cause === undefined || MEMORY_ORIGIN_CAUSES.includes(cause))) {
|
|
260
|
+
return { carriers, first: { taint: "external", ...(cause !== undefined ? { cause: cause } : {}), at } };
|
|
261
|
+
}
|
|
262
|
+
return { carriers, first: { taint: "external", at: 0 } };
|
|
263
|
+
}
|
|
264
|
+
export function committedOriginOf(fm) {
|
|
265
|
+
if (fm.origin !== undefined)
|
|
266
|
+
return { taint: fm.origin.taint, ...(fm.origin.cause !== undefined ? { cause: fm.origin.cause } : {}), at: fm.origin.at };
|
|
267
|
+
if (!hasOriginFormExtra(fm))
|
|
268
|
+
return undefined;
|
|
269
|
+
return walkExtraOrigin(fm.extra).first ?? { taint: "external", at: 0 };
|
|
270
|
+
}
|
|
271
|
+
export function originEquals(a, b) {
|
|
272
|
+
if (a === undefined || b === undefined)
|
|
273
|
+
return a === b;
|
|
274
|
+
return a.taint === b.taint && a.cause === b.cause && a.at === b.at;
|
|
275
|
+
}
|
|
276
|
+
export function ambiguousOriginRepresentation(fm) {
|
|
277
|
+
const { carriers, first } = walkExtraOrigin(fm.extra);
|
|
278
|
+
const total = carriers + (fm.origin !== undefined ? 1 : 0);
|
|
279
|
+
if (total <= 1)
|
|
280
|
+
return false;
|
|
281
|
+
if (total === 2 && fm.origin !== undefined && carriers === 1)
|
|
282
|
+
return !originEquals(fm.origin, first);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
export function stripModelWrittenOrigin(fm) {
|
|
286
|
+
let stripped = false;
|
|
287
|
+
if (fm.origin !== undefined) {
|
|
288
|
+
delete fm.origin;
|
|
289
|
+
stripped = true;
|
|
290
|
+
}
|
|
291
|
+
if (fm.extra !== undefined && fm.extra.some((line) => ORIGIN_FORM_LINE_RE.test(line))) {
|
|
292
|
+
const kept = [];
|
|
293
|
+
let inBlock = false;
|
|
294
|
+
for (const line of fm.extra) {
|
|
295
|
+
if (ORIGIN_FORM_LINE_RE.test(line)) {
|
|
296
|
+
inBlock = /^\s*origin\s*:\s*$/.test(line);
|
|
297
|
+
stripped = true;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (inBlock && /^\s/.test(line)) {
|
|
301
|
+
stripped = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
inBlock = false;
|
|
305
|
+
kept.push(line);
|
|
306
|
+
}
|
|
307
|
+
if (kept.length > 0)
|
|
308
|
+
fm.extra = kept;
|
|
309
|
+
else
|
|
310
|
+
delete fm.extra;
|
|
311
|
+
}
|
|
312
|
+
return stripped;
|
|
313
|
+
}
|
|
174
314
|
export function entryFromFile(text, id, slug, scope) {
|
|
175
315
|
const parsed = parseEntryFile(text);
|
|
176
316
|
const entry = { id, slug, frontmatter: parsed.frontmatter, body: parsed.body, rev: "", scope };
|
|
@@ -25,6 +25,23 @@ export interface V2HeaderHints {
|
|
|
25
25
|
/** `applies-when: <free text>` — non-empty free text, trimmed. */
|
|
26
26
|
appliesWhen?: string;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* design/336 §4.1 — the MECHANICAL instruction-form predicate (deliberately zero semantic judgment:
|
|
30
|
+
* no model, no content heuristics — SOTA R4's LLM-judge ban). An entry is instruction-form ⇔
|
|
31
|
+
* - `metadata.type === "feedback"` (the declared behavior-correction type), OR
|
|
32
|
+
* - its v2 header hints declare an injection/trigger privilege: `pinned` / `triggers` /
|
|
33
|
+
* `applies-when` (the three hints whose whole purpose is automatic or conditional injection;
|
|
34
|
+
* `last-confirmed` is bookkeeping and does not count).
|
|
35
|
+
* Escape analysis: an entry that declares none of these rides the ordinary-entry row — where the
|
|
36
|
+
* origin-marker floor governs — so NOT declaring buys no privilege; declaring buys injection
|
|
37
|
+
* privilege AND walks through the exposure hard gate with it. ONE exported predicate on purpose:
|
|
38
|
+
* the write side (harvest hard gate) and any read-side consumer must judge through the same
|
|
39
|
+
* function, never through re-implementations that can drift.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isInstructionEntry(fm: {
|
|
42
|
+
type?: string;
|
|
43
|
+
extra?: readonly string[];
|
|
44
|
+
}): boolean;
|
|
28
45
|
/** Read the v2 header hints out of an entry's preserved-verbatim `extra` lines. First well-formed
|
|
29
46
|
* occurrence of each key wins (duplicate lines are model bookkeeping noise, not a merge input). */
|
|
30
47
|
export declare function readV2HeaderHints(extra: readonly string[] | undefined): V2HeaderHints;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const LINE_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/;
|
|
2
2
|
const ISO_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}(?:$|[T\s])/;
|
|
3
|
+
export function isInstructionEntry(fm) {
|
|
4
|
+
if (fm.type === "feedback")
|
|
5
|
+
return true;
|
|
6
|
+
const hints = readV2HeaderHints(fm.extra);
|
|
7
|
+
return hints.pinned !== undefined || hints.triggers !== undefined || hints.appliesWhen !== undefined;
|
|
8
|
+
}
|
|
3
9
|
export function readV2HeaderHints(extra) {
|
|
4
10
|
const out = {};
|
|
5
11
|
if (extra === undefined)
|
|
@@ -4,9 +4,10 @@ export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_S
|
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, type MemoryExportSnapshot, } from "./file-backend.js";
|
|
5
5
|
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, type MemoryExportBundle, type MemoryImportReport, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, } from "./export-bundle.js";
|
|
6
6
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
|
|
7
|
-
export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
|
|
8
|
-
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
|
|
9
|
-
export
|
|
7
|
+
export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
|
|
8
|
+
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, type ParsedEntryFile } from "./frontmatter.js";
|
|
9
|
+
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
10
|
+
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
10
11
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
|
|
11
12
|
export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, } from "./scope-contract.js";
|
|
12
13
|
export { migrateScope, type MigrateScopeReport } from "./migrate.js";
|
|
@@ -4,8 +4,9 @@ export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_S
|
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, erasureSelectHash, } from "./file-backend.js";
|
|
5
5
|
export { computeBundleSectionHashes, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
6
6
|
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
|
|
7
|
-
export { readV2HeaderHints } from "./header-hints.js";
|
|
8
|
-
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
|
|
7
|
+
export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
|
|
8
|
+
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals } from "./frontmatter.js";
|
|
9
|
+
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
9
10
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
|
|
10
11
|
export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, } from "./scope-contract.js";
|
|
11
12
|
export { migrateScope } from "./migrate.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MemoryAnnouncement } from "./types.js";
|
|
2
2
|
/** Cursor sidecar for the design/84 Seam B pair on the FileBackend (`{ [scope]: cursor }`). B3: control plane. */
|
|
3
3
|
export declare const CURSORS_FILE = "cursors.json";
|
|
4
4
|
/** The control-plane subdir name under a key dir — ONE spelling for every derivation and for the
|
|
@@ -285,6 +285,11 @@ export declare const SESSION_POLLUTION_DIR = "session-pollution";
|
|
|
285
285
|
export interface SessionPollutionRecord {
|
|
286
286
|
at: number;
|
|
287
287
|
reason: string;
|
|
288
|
+
/** design/336 §2.2 — the structured mechanical cause of the exposure (the value an origin marker
|
|
289
|
+
* minted off this record carries). ADDITIVE: records written before the field existed read as
|
|
290
|
+
* honestly cause-less (the mint then falls back to `"observed"`); old readers narrow-take the
|
|
291
|
+
* two fields they know. */
|
|
292
|
+
cause?: import("./types.js").MemoryOriginCause;
|
|
288
293
|
}
|
|
289
294
|
/**
|
|
290
295
|
* Mark `sessionId` polluted (idempotent; the FIRST record wins — `wx` create refuses overwrite).
|
|
@@ -299,7 +304,7 @@ export interface SessionPollutionRecord {
|
|
|
299
304
|
* stays polluted for this process either way).
|
|
300
305
|
*/
|
|
301
306
|
export type SessionPollutionMarkOutcome = "created" | "existed" | "unpersisted";
|
|
302
|
-
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number): SessionPollutionMarkOutcome;
|
|
307
|
+
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number, cause?: import("./types.js").MemoryOriginCause): SessionPollutionMarkOutcome;
|
|
303
308
|
/** Read the durable pollution record for `sessionId` (undefined = no marker).
|
|
304
309
|
* A marker that EXISTS still reads as polluted whether or not its bytes can be read or parsed
|
|
305
310
|
* (fail-closed: neither corruption nor an unreadable node may launder the state) — a synthesized
|
|
@@ -360,6 +365,11 @@ export declare const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
|
|
|
360
365
|
export interface LineageContribution {
|
|
361
366
|
lastRev: string;
|
|
362
367
|
lastAt: number;
|
|
368
|
+
/** design/336 — carried from the staged row at promotion: the contribution's content (at
|
|
369
|
+
* `lastRev`) carries its external-origin marker, so the pollution retroaction sweeps owe it no
|
|
370
|
+
* challenge (the account travels with the entry). Absence = unmarked content — the sweeps'
|
|
371
|
+
* domain (older vintages read as unmarked, the conservative side). */
|
|
372
|
+
marked?: true;
|
|
363
373
|
}
|
|
364
374
|
/** One staged (pre-commit) row: an entry this transaction WOULD commit. `kind: "latch-only"` marks
|
|
365
375
|
* a bundle-import SYNTHETIC latch row (design/178 v2-c §1): it withholds the id on the model-visible
|
|
@@ -370,6 +380,12 @@ export interface LineagePendingRow {
|
|
|
370
380
|
entryId: string;
|
|
371
381
|
rev: string;
|
|
372
382
|
kind?: "latch-only";
|
|
383
|
+
/** design/336 — set ⇔ the staged content CARRIES its external-origin marker (`frontmatter.origin`
|
|
384
|
+
* in the very rev this row names): the exposure account travels WITH the entry, so promotion
|
|
385
|
+
* settlement owes it no pollution challenge (challenging it would re-quarantine a tag-admission
|
|
386
|
+
* at the read face). Absence = the row's content is unmarked (older vintages included) — the
|
|
387
|
+
* conservative side: an unmarked row of a marked session is challenged. */
|
|
388
|
+
marked?: true;
|
|
373
389
|
}
|
|
374
390
|
/** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
|
|
375
391
|
* succeeds; promotion consumes both. A pending txn without a credential is the crash window —
|
|
@@ -406,6 +422,10 @@ export interface LineagePromotion {
|
|
|
406
422
|
entryId: string;
|
|
407
423
|
sessionId: string;
|
|
408
424
|
rev: string;
|
|
425
|
+
/** design/336 — carried through from {@link LineagePendingRow.marked}: the promoted content
|
|
426
|
+
* already carries its external-origin marker, so promotion settlement skips the pollution
|
|
427
|
+
* challenge for it. */
|
|
428
|
+
marked?: true;
|
|
409
429
|
}
|
|
410
430
|
/** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
|
|
411
431
|
* committed contribution set (deduped by (entryId, sessionId) — a repeat pair refreshes
|
|
@@ -473,6 +493,8 @@ export declare function lineageLatchedIds(controlDir: string): Set<string>;
|
|
|
473
493
|
export declare function lineageContributionsOfSession(controlDir: string, sessionId: string): Array<{
|
|
474
494
|
entryId: string;
|
|
475
495
|
lastRev: string;
|
|
496
|
+
lastAt: number;
|
|
497
|
+
marked?: true;
|
|
476
498
|
}>;
|
|
477
499
|
/** design/178 v2-a §1.3 — the BY-ENTRY lineage account, from ONE parse of the ledger (the
|
|
478
500
|
* by-session read's transpose; the committed set is keyed by entryId, so the index is O(1) after
|
|
@@ -538,6 +560,7 @@ export declare function importLineageCommitted(controlDir: string, rows: Readonl
|
|
|
538
560
|
sessionId: string;
|
|
539
561
|
lastRev: string;
|
|
540
562
|
lastAt: number;
|
|
563
|
+
marked?: true;
|
|
541
564
|
}>): LineageImportDivergence[];
|
|
542
565
|
/** Full-ledger read (tests / host observability). Throws on corruption. */
|
|
543
566
|
export declare function readLineageRecord(controlDir: string): {
|
|
@@ -3,6 +3,7 @@ const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_EXCL } = fsConstants;
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
6
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
6
7
|
const SCOPES_FILE = "scopes.json";
|
|
7
8
|
export const CURSORS_FILE = "cursors.json";
|
|
8
9
|
export const CONTROL_PLANE_DIR = ".engine";
|
|
@@ -734,11 +735,11 @@ export const SESSION_POLLUTION_DIR = "session-pollution";
|
|
|
734
735
|
function pollutionPath(controlDir, sessionId) {
|
|
735
736
|
return join(controlDir, SESSION_POLLUTION_DIR, `${encodeURIComponent(sessionId)}.json`);
|
|
736
737
|
}
|
|
737
|
-
export function markSessionPolluted(controlDir, sessionId, reason, now) {
|
|
738
|
+
export function markSessionPolluted(controlDir, sessionId, reason, now, cause) {
|
|
738
739
|
const path = pollutionPath(controlDir, sessionId);
|
|
739
740
|
try {
|
|
740
741
|
ensureDirExists(dirname(path));
|
|
741
|
-
const record = { at: now(), reason };
|
|
742
|
+
const record = { at: now(), reason, ...(cause !== undefined ? { cause } : {}) };
|
|
742
743
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
743
744
|
return "created";
|
|
744
745
|
}
|
|
@@ -764,8 +765,10 @@ export function readSessionPollution(controlDir, sessionId) {
|
|
|
764
765
|
if (parsed !== null && typeof parsed === "object") {
|
|
765
766
|
const at = parsed.at;
|
|
766
767
|
const reason = parsed.reason;
|
|
768
|
+
const cause = parsed.cause;
|
|
769
|
+
const knownCause = typeof cause === "string" && MEMORY_ORIGIN_CAUSES.includes(cause) ? cause : undefined;
|
|
767
770
|
if (typeof at === "number" && typeof reason === "string")
|
|
768
|
-
return { at, reason };
|
|
771
|
+
return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}) };
|
|
769
772
|
}
|
|
770
773
|
}
|
|
771
774
|
catch {
|
|
@@ -804,7 +807,7 @@ export function importSessionPollution(controlDir, sessionId, record) {
|
|
|
804
807
|
const path = pollutionPath(controlDir, sessionId);
|
|
805
808
|
ensureDirExists(dirname(path));
|
|
806
809
|
try {
|
|
807
|
-
writeFileSync(path, `${JSON.stringify({ at: record.at, reason: record.reason }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
810
|
+
writeFileSync(path, `${JSON.stringify({ at: record.at, reason: record.reason, ...(record.cause !== undefined ? { cause: record.cause } : {}) }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
808
811
|
return "written";
|
|
809
812
|
}
|
|
810
813
|
catch (err) {
|
|
@@ -976,6 +979,8 @@ function coerceLineage(raw) {
|
|
|
976
979
|
if (!row || typeof row !== "object" || typeof row.lastRev !== "string" || typeof row.lastAt !== "number") {
|
|
977
980
|
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}]`);
|
|
978
981
|
}
|
|
982
|
+
if (row.marked !== undefined && row.marked !== true)
|
|
983
|
+
throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] marked`);
|
|
979
984
|
}
|
|
980
985
|
}
|
|
981
986
|
for (const [txnId, t] of Object.entries(r.pending)) {
|
|
@@ -991,6 +996,8 @@ function coerceLineage(raw) {
|
|
|
991
996
|
throw badShape(`pending[${JSON.stringify(txnId)}] row`);
|
|
992
997
|
if (p.kind !== undefined && p.kind !== "latch-only")
|
|
993
998
|
throw badShape(`pending[${JSON.stringify(txnId)}] row kind`);
|
|
999
|
+
if (p.marked !== undefined && p.marked !== true)
|
|
1000
|
+
throw badShape(`pending[${JSON.stringify(txnId)}] row marked`);
|
|
994
1001
|
}
|
|
995
1002
|
if (txn.credential !== undefined) {
|
|
996
1003
|
const c = txn.credential;
|
|
@@ -1049,8 +1056,8 @@ export function promoteLineagePending(controlDir, txnId, now) {
|
|
|
1049
1056
|
if (!applied.has(row.entryId))
|
|
1050
1057
|
continue;
|
|
1051
1058
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1052
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1053
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1059
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1060
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1054
1061
|
}
|
|
1055
1062
|
delete rec.pending[txnId];
|
|
1056
1063
|
return { next: rec, result: promoted };
|
|
@@ -1075,8 +1082,8 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
|
|
|
1075
1082
|
if (applied !== undefined && !applied.has(row.entryId))
|
|
1076
1083
|
continue;
|
|
1077
1084
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1078
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1079
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1085
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1086
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1080
1087
|
}
|
|
1081
1088
|
delete rec.pending[txnId];
|
|
1082
1089
|
return { next: rec, result: promoted };
|
|
@@ -1099,8 +1106,8 @@ export function reconcileLineage(controlDir, now) {
|
|
|
1099
1106
|
continue;
|
|
1100
1107
|
at ??= now();
|
|
1101
1108
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1102
|
-
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at };
|
|
1103
|
-
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev });
|
|
1109
|
+
sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
|
|
1110
|
+
promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
|
|
1104
1111
|
}
|
|
1105
1112
|
delete rec.pending[txnId];
|
|
1106
1113
|
changed = true;
|
|
@@ -1182,7 +1189,7 @@ export function lineageContributionsOfSession(controlDir, sessionId) {
|
|
|
1182
1189
|
for (const [entryId, sessions] of Object.entries(rec.committed)) {
|
|
1183
1190
|
const c = Object.prototype.hasOwnProperty.call(sessions, sessionId) ? sessions[sessionId] : undefined;
|
|
1184
1191
|
if (c !== undefined)
|
|
1185
|
-
out.push({ entryId, lastRev: c.lastRev });
|
|
1192
|
+
out.push({ entryId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
|
|
1186
1193
|
}
|
|
1187
1194
|
return out;
|
|
1188
1195
|
}
|
|
@@ -1262,12 +1269,18 @@ export function importLineageCommitted(controlDir, rows) {
|
|
|
1262
1269
|
const sessions = (rec.committed[row.entryId] ??= Object.create(null));
|
|
1263
1270
|
const standing = Object.prototype.hasOwnProperty.call(sessions, row.sessionId) ? sessions[row.sessionId] : undefined;
|
|
1264
1271
|
if (standing === undefined || row.lastAt > standing.lastAt) {
|
|
1265
|
-
sessions[row.sessionId] = { lastRev: row.lastRev, lastAt: row.lastAt };
|
|
1272
|
+
sessions[row.sessionId] = { lastRev: row.lastRev, lastAt: row.lastAt, ...(row.marked === true ? { marked: true } : {}) };
|
|
1266
1273
|
changed = true;
|
|
1267
1274
|
continue;
|
|
1268
1275
|
}
|
|
1269
1276
|
if (row.lastAt === standing.lastAt && row.lastRev !== standing.lastRev) {
|
|
1270
1277
|
divergence.push({ entryId: row.entryId, sessionId: row.sessionId, kept: "destination" });
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (row.lastAt === standing.lastAt && row.lastRev === standing.lastRev && row.marked === true && standing.marked !== true) {
|
|
1281
|
+
sessions[row.sessionId] = { ...standing, marked: true };
|
|
1282
|
+
changed = true;
|
|
1283
|
+
continue;
|
|
1271
1284
|
}
|
|
1272
1285
|
}
|
|
1273
1286
|
return { ...(changed ? { next: rec } : {}), result: divergence };
|
|
@@ -243,6 +243,71 @@ export async function memoryBackendContract(hooks) {
|
|
|
243
243
|
assert.strictEqual(after.frontmatter.trust, "untrusted");
|
|
244
244
|
assert.strictEqual(after.rev, next.rev, "provenance participates in the rev (computeEntryRev closure)");
|
|
245
245
|
});
|
|
246
|
+
defer("design/336: origin marker round-trips; strip/rewrite refused across update, re-add, guard add and same-batch delete+re-add; committed tombstone is the legal exit", async () => {
|
|
247
|
+
const b = await hooks.make();
|
|
248
|
+
const origin = { taint: "external", cause: "observed", at: 1700000000500 };
|
|
249
|
+
const base = {
|
|
250
|
+
id: "id-origin-001",
|
|
251
|
+
scope: "s1",
|
|
252
|
+
slug: "marked-note",
|
|
253
|
+
frontmatter: { name: "marked-note", description: "written by an exposed session", origin },
|
|
254
|
+
body: "marked body v1",
|
|
255
|
+
rev: "",
|
|
256
|
+
};
|
|
257
|
+
base.rev = computeEntryRev(base);
|
|
258
|
+
const repAdd = await b.applyPatches([{ op: "add", id: base.id, entry: base }]);
|
|
259
|
+
assert.deepStrictEqual(repAdd.conflicts, []);
|
|
260
|
+
const stored = (await b.getByIds([base.id]))[0];
|
|
261
|
+
assert.deepStrictEqual(stored.frontmatter.origin, origin, "origin must survive storage (round-trip)");
|
|
262
|
+
assert.strictEqual(stored.rev, base.rev, "origin participates in the rev (computeEntryRev closure)");
|
|
263
|
+
const mkStripped = () => {
|
|
264
|
+
const e = { ...base, frontmatter: { name: "marked-note", description: "written by an exposed session" }, rev: "" };
|
|
265
|
+
e.rev = computeEntryRev(e);
|
|
266
|
+
return e;
|
|
267
|
+
};
|
|
268
|
+
const rep1 = await b.applyPatches([{ op: "update", id: base.id, entry: mkStripped(), baseRev: base.rev }]);
|
|
269
|
+
assert.strictEqual(rep1.applied.length, 0, "an origin-stripping update must not apply");
|
|
270
|
+
assert.match(rep1.conflicts[0]?.reason ?? "", /malformed patch refused/);
|
|
271
|
+
const rewritten = { ...base, frontmatter: { ...base.frontmatter, origin: { taint: "external", cause: "static", at: 1 } }, rev: "" };
|
|
272
|
+
rewritten.rev = computeEntryRev(rewritten);
|
|
273
|
+
const rep2 = await b.applyPatches([{ op: "update", id: base.id, entry: rewritten, baseRev: base.rev }]);
|
|
274
|
+
assert.strictEqual(rep2.applied.length, 0, "an origin-rewriting update must not apply");
|
|
275
|
+
assert.match(rep2.conflicts[0]?.reason ?? "", /malformed patch refused/);
|
|
276
|
+
const rep3 = await b.applyPatches([{ op: "add", id: base.id, entry: mkStripped() }]);
|
|
277
|
+
assert.strictEqual(rep3.applied.length, 0, "an origin-stripping re-add must not apply");
|
|
278
|
+
assert.match(rep3.conflicts[0]?.reason ?? "", /malformed patch refused/);
|
|
279
|
+
const rep4 = await b.applyPatches([{ op: "add", id: base.id, entry: mkStripped(), guard: "absent" }]);
|
|
280
|
+
assert.strictEqual(rep4.applied.length, 0, "an origin-stripping guard add must not apply");
|
|
281
|
+
assert.match(rep4.conflicts[0]?.reason ?? "", /malformed patch refused/);
|
|
282
|
+
const rep5 = await b.applyPatches([
|
|
283
|
+
{ op: "delete", id: base.id, baseRev: base.rev },
|
|
284
|
+
{ op: "add", id: base.id, entry: mkStripped() },
|
|
285
|
+
]);
|
|
286
|
+
const readd = rep5.conflicts.find((c) => c.op === "add");
|
|
287
|
+
assert.ok(readd !== undefined, "the same-batch unmarked re-add must be refused");
|
|
288
|
+
assert.match(readd.reason, /malformed patch refused/);
|
|
289
|
+
const survivors = await b.getByIds([base.id]);
|
|
290
|
+
if (survivors.length > 0) {
|
|
291
|
+
assert.deepStrictEqual(survivors[0].frontmatter.origin, origin, "a surviving entry keeps its marker verbatim");
|
|
292
|
+
}
|
|
293
|
+
if (survivors.length > 0) {
|
|
294
|
+
const repDel = await b.applyPatches([{ op: "delete", id: base.id }]);
|
|
295
|
+
assert.deepStrictEqual(repDel.conflicts.filter((c) => c.op === "delete"), [], "the tombstone itself applies");
|
|
296
|
+
}
|
|
297
|
+
const fresh = mkStripped();
|
|
298
|
+
const rep6 = await b.applyPatches([{ op: "add", id: base.id, entry: fresh }]);
|
|
299
|
+
assert.deepStrictEqual(rep6.conflicts, [], "after a committed tombstone, the id starts an unmarked life");
|
|
300
|
+
assert.strictEqual((await b.getByIds([base.id]))[0]?.frontmatter.origin, undefined);
|
|
301
|
+
const b2 = await hooks.make();
|
|
302
|
+
const m0 = { ...base, id: "id-origin-002", rev: "" };
|
|
303
|
+
m0.rev = computeEntryRev(m0);
|
|
304
|
+
await b2.applyPatches([{ op: "add", id: m0.id, entry: m0 }]);
|
|
305
|
+
const m1 = { ...m0, body: "marked body v2", rev: "" };
|
|
306
|
+
m1.rev = computeEntryRev(m1);
|
|
307
|
+
const rep7 = await b2.applyPatches([{ op: "update", id: m0.id, entry: m1, baseRev: m0.rev }]);
|
|
308
|
+
assert.deepStrictEqual(rep7.conflicts, []);
|
|
309
|
+
assert.deepStrictEqual((await b2.getByIds([m0.id]))[0]?.frontmatter.origin, origin, "the conforming update carries the marker forward verbatim");
|
|
310
|
+
});
|
|
246
311
|
defer("projection authority: getByIds carries the OWNING scope; other scopes never list the entry", async () => {
|
|
247
312
|
const b = await hooks.make();
|
|
248
313
|
const e = entry("id-auth-0001", "s1", "authored", "authored body", { name: "Authored" });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MemorySyncCursor } from "./sync.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type MemoryBackend, type MemoryEntry, type PatchReport } from "./types.js";
|
|
3
3
|
/** The injected HTTP seam — core never bundles a fetch. The deployment maps `path` (e.g.
|
|
4
4
|
* `/v1/memory/sync/user%3Aalice`) onto its server base URL, attaches auth, and returns the
|
|
5
5
|
* DECODED JSON body of a 2xx response; any non-2xx / network failure should THROW (the round then
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { computeEntryRev, serializeEntryFile } from "./frontmatter.js";
|
|
1
|
+
import { ambiguousOriginRepresentation, computeEntryRev, serializeEntryFile } from "./frontmatter.js";
|
|
2
2
|
import { screenInboundEntries } from "./data-plane.js";
|
|
3
3
|
import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
|
|
4
4
|
import { MAX_MEMORY_BYTES } from "../memory.js";
|
|
5
5
|
import { reconcileMemoryEntries } from "./sync.js";
|
|
6
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
6
7
|
function memorySyncPath(scope) {
|
|
7
8
|
return `/v1/memory/sync/${encodeURIComponent(scope)}`;
|
|
8
9
|
}
|
|
@@ -79,6 +80,28 @@ function parseMemorySyncResponse(raw, scope, peer) {
|
|
|
79
80
|
!Number.isFinite(provWire["ingestedAt"]))) {
|
|
80
81
|
fail('entry.frontmatter.provenance must be { kind: "repo_file", path, contentHash, ingestedAt } when present');
|
|
81
82
|
}
|
|
83
|
+
const originWire = fm["origin"];
|
|
84
|
+
if (originWire !== undefined &&
|
|
85
|
+
(!isRecord(originWire) ||
|
|
86
|
+
originWire["taint"] !== "external" ||
|
|
87
|
+
typeof originWire["at"] !== "number" ||
|
|
88
|
+
!Number.isFinite(originWire["at"]) ||
|
|
89
|
+
(originWire["cause"] !== undefined && !(typeof originWire["cause"] === "string" && MEMORY_ORIGIN_CAUSES.includes(originWire["cause"]))))) {
|
|
90
|
+
fail('entry.frontmatter.origin must be { taint: "external", cause?, at } with a known cause when present');
|
|
91
|
+
}
|
|
92
|
+
const extraRaw = fm["extra"];
|
|
93
|
+
if (Array.isArray(extraRaw) && extraRaw.every((s) => typeof s === "string")) {
|
|
94
|
+
const pickedOrigin = originWire !== undefined
|
|
95
|
+
? {
|
|
96
|
+
taint: "external",
|
|
97
|
+
...(originWire["cause"] !== undefined ? { cause: originWire["cause"] } : {}),
|
|
98
|
+
at: originWire["at"],
|
|
99
|
+
}
|
|
100
|
+
: undefined;
|
|
101
|
+
if (ambiguousOriginRepresentation({ ...(pickedOrigin !== undefined ? { origin: pickedOrigin } : {}), extra: fm["extra"] })) {
|
|
102
|
+
fail("entry.frontmatter carries conflicting or duplicated origin representations (ambiguous marker representation refused)");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
82
105
|
serverEntries.push({
|
|
83
106
|
id: e["id"],
|
|
84
107
|
slug: e["slug"],
|
|
@@ -101,6 +124,15 @@ function parseMemorySyncResponse(raw, scope, peer) {
|
|
|
101
124
|
}
|
|
102
125
|
: {}),
|
|
103
126
|
...(fm["trust"] !== undefined ? { trust: "untrusted" } : {}),
|
|
127
|
+
...(originWire !== undefined
|
|
128
|
+
? {
|
|
129
|
+
origin: {
|
|
130
|
+
taint: "external",
|
|
131
|
+
...(originWire["cause"] !== undefined ? { cause: originWire["cause"] } : {}),
|
|
132
|
+
at: originWire["at"],
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
: {}),
|
|
104
136
|
...(fm["extra"] !== undefined ? { extra: [...fm["extra"]] } : {}),
|
|
105
137
|
},
|
|
106
138
|
});
|
|
@@ -78,6 +78,13 @@ export interface MemoryEngineToolsOptions {
|
|
|
78
78
|
sessionPollution?: () => {
|
|
79
79
|
reason: string;
|
|
80
80
|
} | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* design/336 §13-3 — the provenance mode the write engine runs under, so the pollution sentence
|
|
83
|
+
* states what the mark actually DOES: under `"carry"` writes commit with an origin marker (the
|
|
84
|
+
* pre-336 "not admitted" sentence would be false); absent ≡ `"off"` keeps that sentence
|
|
85
|
+
* byte-identical. Pure display input — nothing else branches on it here.
|
|
86
|
+
*/
|
|
87
|
+
provenance?: "off" | "carry";
|
|
81
88
|
}
|
|
82
89
|
export interface MemorySearchHit {
|
|
83
90
|
id: string;
|
|
@@ -101,6 +101,9 @@ export function createMemoryEngineTools(opts) {
|
|
|
101
101
|
}
|
|
102
102
|
if (reason === undefined)
|
|
103
103
|
return "";
|
|
104
|
+
if (opts.provenance === "carry") {
|
|
105
|
+
return ` Note: this session read external content, so memory entries it writes are saved with an external-origin marker; instruction-style entries (type: feedback, or pinned/triggers/applies-when lines) are withheld for host review instead.`;
|
|
106
|
+
}
|
|
104
107
|
return ` This session is marked polluted (${inlineUntrusted(reason, 200)}): the memory it writes is not admitted to the library at harvest, so writing this down now would not make it retrievable later either.`;
|
|
105
108
|
};
|
|
106
109
|
const noMatch = (query) => {
|