@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.
- package/CHANGELOG.md +103 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +48 -6
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +101 -1
- package/dist/core/hooks.js +116 -14
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +94 -1
- package/dist/core/memory-engine/engine.js +329 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +232 -0
- package/dist/core/memory-engine/layout.js +469 -1
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +93 -21
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +195 -0
- package/dist/core/permission-rule-org.js +234 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +314 -3
- package/dist/core/permission-rule-sync.d.ts +139 -0
- package/dist/core/permission-rule-sync.js +343 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +64 -3
- package/dist/core/runner/prepare-task.js +357 -32
- package/dist/core/runner/runtask.js +166 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +155 -4
- package/dist/core/tool-policy.js +148 -10
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +79 -1
- package/dist/core/types.d.ts +94 -3
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.js +12 -5
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +3 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
|
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
|
|
9
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
|
|
9
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
10
10
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
11
11
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
12
12
|
|
|
@@ -33,6 +33,7 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
|
|
|
33
33
|
return MEMORY_INSTRUCTION_TEMPLATE.replaceAll("{{MEMORY_DIR}}", dir).replaceAll("{{INSTRUCTION_FILE}}", instructionFileName ?? "CLAUDE.md");
|
|
34
34
|
}
|
|
35
35
|
export const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry — the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
|
|
36
|
+
export const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines — write them yourself; nothing fills them in for you.";
|
|
36
37
|
export const MEMORY_INDEX_MAX_LINES = 200;
|
|
37
38
|
export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
38
39
|
export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
|
|
@@ -87,6 +88,8 @@ export class MemoryEngine {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
recordRetrieved(ids) {
|
|
91
|
+
for (const id of ids)
|
|
92
|
+
this.retrievedThisSession.add(id);
|
|
90
93
|
try {
|
|
91
94
|
recordRetrievedAccount(this.controlDir, ids, this.now);
|
|
92
95
|
}
|
|
@@ -124,6 +127,28 @@ export class MemoryEngine {
|
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
}
|
|
130
|
+
try {
|
|
131
|
+
const rec = this.sessionPollution(sessionId) ?? { at: this.now(), reason };
|
|
132
|
+
const contributions = lineageContributionsOfSession(this.controlDir, sessionId);
|
|
133
|
+
this.challengeAndAnnounce(contributions.map((c) => ({
|
|
134
|
+
eventId: `pollution:${sessionId}:${rec.at}:${c.entryId}`,
|
|
135
|
+
entryId: c.entryId,
|
|
136
|
+
reason: `challenged: the contributing session was marked polluted`,
|
|
137
|
+
challengedRev: c.lastRev,
|
|
138
|
+
})));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const sink = this.onIncident;
|
|
142
|
+
if (sink !== undefined) {
|
|
143
|
+
try {
|
|
144
|
+
const e = new Error(`memory challenge sweep for a polluted session failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
e.code = "memory.challenge_sweep_failed";
|
|
146
|
+
sink(e);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
sessionPollution(sessionId) {
|
|
129
154
|
const inProcess = this.pollutedSessions.get(sessionId);
|
|
@@ -136,6 +161,162 @@ export class MemoryEngine {
|
|
|
136
161
|
return undefined;
|
|
137
162
|
}
|
|
138
163
|
}
|
|
164
|
+
retrievedThisSession = new Set();
|
|
165
|
+
readChallengeExclusions() {
|
|
166
|
+
const out = new Map();
|
|
167
|
+
for (const id of lineageLatchedIds(this.controlDir))
|
|
168
|
+
out.set(id, { code: "lineage_pending" });
|
|
169
|
+
for (const [id, g] of challengedEntryIds(this.controlDir))
|
|
170
|
+
out.set(id, { code: "challenged", generation: g.generation, at: g.at });
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
challengeAndAnnounce(events) {
|
|
174
|
+
if (events.length === 0)
|
|
175
|
+
return [];
|
|
176
|
+
const { assignments: assigned, eventCount } = appendChallengeEvents(this.controlDir, events, this.now);
|
|
177
|
+
if (eventCount !== undefined)
|
|
178
|
+
this.discloseChallengeLedgerSize(eventCount);
|
|
179
|
+
const fresh = assigned.filter((a) => !a.replayed);
|
|
180
|
+
if (fresh.length > 0) {
|
|
181
|
+
try {
|
|
182
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
183
|
+
kind: "gate",
|
|
184
|
+
at: this.now(),
|
|
185
|
+
items: fresh.map((a) => `memory entry challenged: id ${quoteId(a.entryId)} (generation ${a.generation}) — its content is withheld from memory tools and the injected index. If the fact is still needed, verify it with the user, record a fresh entry from their current statement, and tombstone the old one.`),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
this.discloseAnnounceFailure("challenge enqueue", err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return assigned;
|
|
193
|
+
}
|
|
194
|
+
challengeLedgerSizeDisclosed = false;
|
|
195
|
+
discloseChallengeLedgerSize(eventCount) {
|
|
196
|
+
if (this.challengeLedgerSizeDisclosed || eventCount <= CHALLENGE_LEDGER_MAX_EVENTS)
|
|
197
|
+
return;
|
|
198
|
+
this.challengeLedgerSizeDisclosed = true;
|
|
199
|
+
const sink = this.onIncident;
|
|
200
|
+
if (sink === undefined)
|
|
201
|
+
return;
|
|
202
|
+
try {
|
|
203
|
+
const err = new Error(`memory challenge ledger holds ${eventCount} events, past the calibrated bound of ${CHALLENGE_LEDGER_MAX_EVENTS}: ` +
|
|
204
|
+
`the whole file is rewritten on every challenge and parsed on every model-visible read (index rebuild, memory tools), ` +
|
|
205
|
+
`so this account now costs the session latency. It is NOT rotated — dropping events would un-withhold their entries — ` +
|
|
206
|
+
`so the deployment resolves the settled generations or archives the account deliberately.`);
|
|
207
|
+
err.code = "memory.challenge_ledger_oversize";
|
|
208
|
+
sink(err);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
rebuildCorruptControlPlaneLedger(ledger, reason) {
|
|
214
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
215
|
+
const e = new Error("rebuildCorruptControlPlaneLedger: a non-empty reason is required (this call drops taint evidence — it is recorded, never implicit)");
|
|
216
|
+
e.code = "config.memory_rebuild_reason";
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
219
|
+
if (!isStrictControlPlaneLedgerCorrupt(this.controlDir, ledger)) {
|
|
220
|
+
const e = new Error(`${ledger === "challenges" ? "memory challenge ledger" : "memory lineage ledger"} 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.`);
|
|
221
|
+
e.code = "memory.control_plane_not_corrupt";
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
225
|
+
kind: "gate",
|
|
226
|
+
at: this.now(),
|
|
227
|
+
items: [
|
|
228
|
+
ledger === "challenges"
|
|
229
|
+
? `memory control plane: the challenge ledger was found CORRUPT and a deployment-initiated reset to empty was requested — entries that were withheld by an unresolved challenge may no longer be withheld. Treat facts recalled from memory as unverified until confirmed with the user.`
|
|
230
|
+
: `memory control plane: the harvest lineage ledger was found CORRUPT and a deployment-initiated reset to empty was requested — which session contributed which entry may no longer be recorded for anything written before this point.`,
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
const receipt = rebuildStrictControlPlaneLedger(this.controlDir, ledger, this.now);
|
|
234
|
+
const sink = this.onIncident;
|
|
235
|
+
if (sink !== undefined) {
|
|
236
|
+
try {
|
|
237
|
+
const err = new Error(`memory control plane: ${ledger} ledger rebuilt after corruption (reason: ${reason}). ` +
|
|
238
|
+
`The corrupt bytes are quarantined at ${receipt.quarantinedTo.join(", ") || "(no file existed)"} — ` +
|
|
239
|
+
`they are the only remaining record of what this account held.`);
|
|
240
|
+
err.code = "memory.control_plane_rebuilt";
|
|
241
|
+
sink(err);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return receipt;
|
|
247
|
+
}
|
|
248
|
+
challengeEntries(ids, reason, requestId) {
|
|
249
|
+
if (typeof requestId !== "string" || requestId === "") {
|
|
250
|
+
const e = new Error("challengeEntries: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|
|
251
|
+
e.code = "config.memory_challenge_request";
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
|
|
255
|
+
}
|
|
256
|
+
challengeSession(sessionId, reason, requestId) {
|
|
257
|
+
if (typeof requestId !== "string" || requestId === "") {
|
|
258
|
+
const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|
|
259
|
+
e.code = "config.memory_challenge_request";
|
|
260
|
+
throw e;
|
|
261
|
+
}
|
|
262
|
+
const contributions = lineageContributionsOfSession(this.controlDir, sessionId);
|
|
263
|
+
return this.challengeAndAnnounce(contributions.map((c) => ({ eventId: `${requestId}:${c.entryId}`, entryId: c.entryId, reason, challengedRev: c.lastRev })));
|
|
264
|
+
}
|
|
265
|
+
resolveChallenge(entryId, generation, reason, requestId) {
|
|
266
|
+
const { resolved, eventCount } = resolveChallengeEvent(this.controlDir, entryId, generation, reason, this.now, requestId);
|
|
267
|
+
if (eventCount !== undefined)
|
|
268
|
+
this.discloseChallengeLedgerSize(eventCount);
|
|
269
|
+
return resolved;
|
|
270
|
+
}
|
|
271
|
+
adjudicatePendingLineage(txnId, action) {
|
|
272
|
+
const promoted = adjudicateLineagePending(this.controlDir, txnId, action, this.now);
|
|
273
|
+
this.settlePromotions(promoted);
|
|
274
|
+
return promoted;
|
|
275
|
+
}
|
|
276
|
+
listPendingLineage() {
|
|
277
|
+
return readLineageRecord(this.controlDir).pending;
|
|
278
|
+
}
|
|
279
|
+
readChallengeLedger() {
|
|
280
|
+
return readChallengeEvents(this.controlDir);
|
|
281
|
+
}
|
|
282
|
+
readChallengedHistoryAccount() {
|
|
283
|
+
try {
|
|
284
|
+
return readChallengedHistory(this.controlDir);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
settlePromotions(promoted) {
|
|
291
|
+
if (promoted.length === 0)
|
|
292
|
+
return;
|
|
293
|
+
const events = [];
|
|
294
|
+
for (const p of promoted) {
|
|
295
|
+
let rec;
|
|
296
|
+
try {
|
|
297
|
+
rec = readSessionPollution(this.controlDir, p.sessionId);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
rec = undefined;
|
|
301
|
+
}
|
|
302
|
+
if (rec === undefined)
|
|
303
|
+
rec = this.pollutedSessions.get(p.sessionId);
|
|
304
|
+
if (rec === undefined)
|
|
305
|
+
continue;
|
|
306
|
+
events.push({
|
|
307
|
+
eventId: `pollution:${p.sessionId}:${rec.at}:${p.entryId}`,
|
|
308
|
+
entryId: p.entryId,
|
|
309
|
+
reason: `challenged: the contributing session was marked polluted`,
|
|
310
|
+
challengedRev: p.rev,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
this.challengeAndAnnounce(events);
|
|
314
|
+
try {
|
|
315
|
+
appendLineageAudit(this.controlDir, promoted.map((p) => ({ entryId: p.entryId, sessionId: p.sessionId, rev: p.rev, at: this.now() })));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
139
320
|
async materialize(scopes, writeScope) {
|
|
140
321
|
ensureDirExists(this.memoryDir);
|
|
141
322
|
ensureDirExists(this.controlDir);
|
|
@@ -316,7 +497,8 @@ export class MemoryEngine {
|
|
|
316
497
|
return report;
|
|
317
498
|
}
|
|
318
499
|
async harvestCore(handle, opts) {
|
|
319
|
-
|
|
500
|
+
let pollutedReason = opts?.polluted?.reason ?? (opts?.sessionId !== undefined ? this.sessionPollution(opts.sessionId)?.reason : undefined);
|
|
501
|
+
const lineageSessionId = opts?.sessionId;
|
|
320
502
|
const startedAt = this.now();
|
|
321
503
|
const report = {
|
|
322
504
|
ok: true,
|
|
@@ -344,6 +526,30 @@ export class MemoryEngine {
|
|
|
344
526
|
report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${err instanceof Error ? err.message : String(err)}` };
|
|
345
527
|
return report;
|
|
346
528
|
}
|
|
529
|
+
try {
|
|
530
|
+
const rec = reconcileLineage(this.controlDir, this.now);
|
|
531
|
+
this.settlePromotions(rec.promoted);
|
|
532
|
+
for (const u of rec.undecidable) {
|
|
533
|
+
report.warnings.push(`memory lineage transaction ${u.txnId} is unsettled (a crash landed between commit and its durable credential) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the host adjudicates it (adjudicatePendingLineage)`);
|
|
534
|
+
}
|
|
535
|
+
if (pollutedReason !== undefined && lineageSessionId !== undefined) {
|
|
536
|
+
const rec2 = this.sessionPollution(lineageSessionId);
|
|
537
|
+
if (rec2 !== undefined) {
|
|
538
|
+
const contributions = lineageContributionsOfSession(this.controlDir, lineageSessionId);
|
|
539
|
+
this.challengeAndAnnounce(contributions.map((c) => ({
|
|
540
|
+
eventId: `pollution:${lineageSessionId}:${rec2.at}:${c.entryId}`,
|
|
541
|
+
entryId: c.entryId,
|
|
542
|
+
reason: `challenged: the contributing session was marked polluted`,
|
|
543
|
+
challengedRev: c.lastRev,
|
|
544
|
+
})));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
report.ok = false;
|
|
550
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory lineage/challenge ledger refused: ${err instanceof Error ? err.message : String(err)}` };
|
|
551
|
+
return report;
|
|
552
|
+
}
|
|
347
553
|
if (!existsSync(handle.writableRoot)) {
|
|
348
554
|
report.ok = false;
|
|
349
555
|
report.incident = { kind: "dir_missing", detail: `memory writable root is gone: ${handle.writableRoot}` };
|
|
@@ -693,13 +899,99 @@ export class MemoryEngine {
|
|
|
693
899
|
patchReport = { applied: [], conflicts: [] };
|
|
694
900
|
}
|
|
695
901
|
else {
|
|
696
|
-
|
|
697
|
-
|
|
902
|
+
const txnId = uuidv7();
|
|
903
|
+
const pendingRows = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev }));
|
|
904
|
+
const lineageArmed = lineageSessionId !== undefined && pendingRows.length > 0;
|
|
905
|
+
if (lineageArmed) {
|
|
906
|
+
try {
|
|
907
|
+
stageLineagePending(this.controlDir, txnId, lineageSessionId, pendingRows, this.now);
|
|
908
|
+
}
|
|
909
|
+
catch (err) {
|
|
910
|
+
report.ok = false;
|
|
911
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory lineage write-ahead refused: ${err instanceof Error ? err.message : String(err)}` };
|
|
912
|
+
return report;
|
|
913
|
+
}
|
|
698
914
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
915
|
+
const preCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
|
|
916
|
+
if (preCommit !== undefined) {
|
|
917
|
+
if (lineageArmed) {
|
|
918
|
+
try {
|
|
919
|
+
discardLineagePending(this.controlDir, txnId);
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
pollutedReason = preCommit.reason;
|
|
925
|
+
for (const f of records)
|
|
926
|
+
await containPollutedRecord(f);
|
|
927
|
+
patchReport = { applied: [], conflicts: [] };
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
try {
|
|
931
|
+
patchReport = await this.backend.applyPatches(patches);
|
|
932
|
+
}
|
|
933
|
+
catch (err) {
|
|
934
|
+
if (lineageArmed) {
|
|
935
|
+
try {
|
|
936
|
+
discardLineagePending(this.controlDir, txnId);
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
report.ok = false;
|
|
942
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${err instanceof Error ? err.message : String(err)}` };
|
|
943
|
+
return report;
|
|
944
|
+
}
|
|
945
|
+
if (lineageArmed) {
|
|
946
|
+
try {
|
|
947
|
+
recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
|
|
948
|
+
const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
|
|
949
|
+
this.settlePromotions(promoted);
|
|
950
|
+
}
|
|
951
|
+
catch (err) {
|
|
952
|
+
const detail = `memory lineage settlement failed after commit — the affected entries stay latched (memory reads refuse them) until a later harvest reconciles or the host adjudicates: ${err instanceof Error ? err.message : String(err)}`;
|
|
953
|
+
report.warnings.push(detail);
|
|
954
|
+
const sink = this.onIncident;
|
|
955
|
+
if (sink !== undefined) {
|
|
956
|
+
try {
|
|
957
|
+
const e = new Error(detail);
|
|
958
|
+
e.code = "memory.lineage_settle_failed";
|
|
959
|
+
sink(e);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const deletedIds = patchReport.applied.filter((a) => a.op === "delete").map((a) => a.id);
|
|
967
|
+
if (deletedIds.length > 0) {
|
|
968
|
+
try {
|
|
969
|
+
clearLineageForEntries(this.controlDir, deletedIds);
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
recordChallengedHistory(this.controlDir, patchReport.applied
|
|
976
|
+
.filter((a) => (a.op === "update" || a.op === "delete") && this.retrievedThisSession.has(a.id))
|
|
977
|
+
.map((a) => ({ entryId: a.id, op: a.op === "delete" ? "tombstone" : "update" })), this.now);
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
}
|
|
981
|
+
const postCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
|
|
982
|
+
if (postCommit !== undefined) {
|
|
983
|
+
const appliedNonDelete = patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id);
|
|
984
|
+
try {
|
|
985
|
+
this.challengeAndAnnounce(appliedNonDelete.map((id) => ({
|
|
986
|
+
eventId: `pollution:${lineageSessionId}:${postCommit.at}:${id}`,
|
|
987
|
+
entryId: id,
|
|
988
|
+
reason: `challenged: the contributing session was marked polluted`,
|
|
989
|
+
})));
|
|
990
|
+
}
|
|
991
|
+
catch (err) {
|
|
992
|
+
report.warnings.push(`memory challenge of this harvest's commits failed (session polluted post-commit): ${err instanceof Error ? err.message : String(err)}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
703
995
|
}
|
|
704
996
|
}
|
|
705
997
|
const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
|
|
@@ -798,18 +1090,39 @@ export class MemoryEngine {
|
|
|
798
1090
|
}
|
|
799
1091
|
rebuildIndex(handle, headers, opts, warnings) {
|
|
800
1092
|
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
1093
|
+
let excludedIds;
|
|
1094
|
+
try {
|
|
1095
|
+
excludedIds = this.readChallengeExclusions();
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
const detail = `memory index withheld: the challenge/lineage ledger cannot be trusted (${err instanceof Error ? err.message : String(err)}) — fail-closed, nothing injected`;
|
|
1099
|
+
warnings?.push(detail);
|
|
1100
|
+
handle.indexOnDiskUntrusted = true;
|
|
1101
|
+
const sink = this.onIncident;
|
|
1102
|
+
if (sink !== undefined) {
|
|
1103
|
+
try {
|
|
1104
|
+
const e = new Error(detail);
|
|
1105
|
+
e.code = "memory.challenge_ledger_corrupt";
|
|
1106
|
+
sink(e);
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return "";
|
|
1112
|
+
}
|
|
801
1113
|
const existing = opts.ignoreOnDisk === true ? "" : (readNoFollowSafe(indexPath) ?? "");
|
|
802
1114
|
const indexRevs = readIndexRevs(this.controlDir);
|
|
803
1115
|
const nextIndexRevs = {};
|
|
804
1116
|
const expected = new Map();
|
|
1117
|
+
const excludedTargets = new Set();
|
|
805
1118
|
for (const h of headers) {
|
|
806
1119
|
const repoTarget = handle.repoIndexTargets?.get(h.id);
|
|
807
|
-
|
|
808
|
-
|
|
1120
|
+
const target = repoTarget ?? relative(handle.writableRoot, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
|
|
1121
|
+
if (excludedIds.has(h.id)) {
|
|
1122
|
+
excludedTargets.add(target);
|
|
809
1123
|
continue;
|
|
810
1124
|
}
|
|
811
|
-
|
|
812
|
-
expected.set(relative(handle.writableRoot, canonicalize(file)), h);
|
|
1125
|
+
expected.set(target, h);
|
|
813
1126
|
}
|
|
814
1127
|
const covered = new Set();
|
|
815
1128
|
const keptByTarget = new Map();
|
|
@@ -821,6 +1134,10 @@ export class MemoryEngine {
|
|
|
821
1134
|
keptLines.push(line);
|
|
822
1135
|
continue;
|
|
823
1136
|
}
|
|
1137
|
+
if (excludedTargets.has(target)) {
|
|
1138
|
+
warnings?.push(`MEMORY.md index line removed: ${target} is withheld (challenged or unsettled) — its content stays off the injected index`);
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
824
1141
|
if (covered.has(target)) {
|
|
825
1142
|
if (line === keptByTarget.get(target))
|
|
826
1143
|
continue;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/180 裁决 C — the v2 header hints, read-only, over `frontmatter.extra`.
|
|
3
|
+
*
|
|
4
|
+
* The v2 header fields (pinned / triggers / last-confirmed / applies-when) are DELIBERATELY not
|
|
5
|
+
* typed structure positions: the parser's closed grammar keeps unknown lines VERBATIM in `extra`,
|
|
6
|
+
* `extra` participates in the entry rev, and the sync wire carries it — so a file with these lines
|
|
7
|
+
* computes the SAME rev on every client vintage (old and new), and no backend/sync twin owes an
|
|
8
|
+
* upgrade. Promoting them to typed fields would make the same bytes hash to two different revs
|
|
9
|
+
* across a mixed fleet (the provenance note in frontmatter.ts names that exact incident shape).
|
|
10
|
+
*
|
|
11
|
+
* This accessor is the ONLY consumer-side reading: pure, lenient, and closed over bad values —
|
|
12
|
+
* a malformed line reads as FIELD ABSENT, never an error and never a partial adoption. Nothing in
|
|
13
|
+
* the engine writes these lines; the model does (the preference-discipline paragraph teaches it),
|
|
14
|
+
* because an engine-written confirmation would forge the usage evidence it is supposed to record.
|
|
15
|
+
*/
|
|
16
|
+
/** The parsed v2 hints. Every field is absent unless a well-formed line was found. */
|
|
17
|
+
export interface V2HeaderHints {
|
|
18
|
+
/** `pinned: true` — only the exact literal counts (anything else = absent). */
|
|
19
|
+
pinned?: boolean;
|
|
20
|
+
/** `triggers: a, b, c` — comma-split, trimmed, empties dropped; an empty result = absent. */
|
|
21
|
+
triggers?: string[];
|
|
22
|
+
/** `last-confirmed: YYYY-MM-DD…` — kept as the raw string when it starts with an ISO-date shape;
|
|
23
|
+
* anything else = absent (bad values must not masquerade as confirmations). */
|
|
24
|
+
lastConfirmedAt?: string;
|
|
25
|
+
/** `applies-when: <free text>` — non-empty free text, trimmed. */
|
|
26
|
+
appliesWhen?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Read the v2 header hints out of an entry's preserved-verbatim `extra` lines. First well-formed
|
|
29
|
+
* occurrence of each key wins (duplicate lines are model bookkeeping noise, not a merge input). */
|
|
30
|
+
export declare function readV2HeaderHints(extra: readonly string[] | undefined): V2HeaderHints;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const LINE_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*):\s*(.*?)\s*$/;
|
|
2
|
+
const ISO_DATE_PREFIX_RE = /^\d{4}-\d{2}-\d{2}(?:$|[T\s])/;
|
|
3
|
+
export function readV2HeaderHints(extra) {
|
|
4
|
+
const out = {};
|
|
5
|
+
if (extra === undefined)
|
|
6
|
+
return out;
|
|
7
|
+
for (const line of extra) {
|
|
8
|
+
const m = LINE_RE.exec(line);
|
|
9
|
+
if (m === null)
|
|
10
|
+
continue;
|
|
11
|
+
const [, key, value] = m;
|
|
12
|
+
switch (key) {
|
|
13
|
+
case "pinned":
|
|
14
|
+
if (out.pinned === undefined && value === "true")
|
|
15
|
+
out.pinned = true;
|
|
16
|
+
break;
|
|
17
|
+
case "triggers": {
|
|
18
|
+
if (out.triggers !== undefined)
|
|
19
|
+
break;
|
|
20
|
+
const items = value
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter((s) => s !== "");
|
|
24
|
+
if (items.length > 0)
|
|
25
|
+
out.triggers = items;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case "last-confirmed":
|
|
29
|
+
if (out.lastConfirmedAt === undefined && ISO_DATE_PREFIX_RE.test(value))
|
|
30
|
+
out.lastConfirmedAt = value;
|
|
31
|
+
break;
|
|
32
|
+
case "applies-when":
|
|
33
|
+
if (out.appliesWhen === undefined && value !== "")
|
|
34
|
+
out.appliesWhen = value;
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
|
|
5
|
-
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, } from "./layout.js";
|
|
5
|
+
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";
|
|
6
|
+
export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
|
|
6
7
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
|
|
7
8
|
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
8
9
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
|
|
5
|
-
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, } from "./layout.js";
|
|
5
|
+
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";
|
|
6
|
+
export { readV2HeaderHints } from "./header-hints.js";
|
|
6
7
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
|
|
7
8
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
|
|
8
9
|
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";
|