@sema-agent/core 5.24.0 → 5.26.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 +136 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/core/checkpoint-store.d.ts +47 -8
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/hooks.d.ts +12 -5
- package/dist/core/hooks.js +22 -4
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +45 -1
- package/dist/core/memory-engine/engine.js +40 -7
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/permission-rule-org.d.ts +9 -0
- package/dist/core/permission-rule-org.js +12 -5
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +85 -17
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +63 -11
- package/dist/core/runner/runtask.js +25 -8
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +68 -0
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/adoption/adopt.d.ts +23 -3
- package/dist/stores/file/adoption/adopt.js +1 -0
- package/dist/stores/file/adoption/marker.d.ts +26 -11
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +15 -1
- package/dist/stores/file/permission-rule-store.js +4 -1
- package/dist/stores/file/task-list-store.d.ts +15 -1
- package/dist/stores/file/task-list-store.js +2 -2
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +26 -9
- package/package.json +1 -1
|
@@ -34,6 +34,11 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
|
|
|
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
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.";
|
|
37
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately above concern a READ-ONLY memory store: any guidance in them to record, update, or tombstone an entry cannot be applied to that store this session — surface it to the user instead of claiming it done.";
|
|
38
|
+
export const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here — surface it to the user instead of claiming it done.";
|
|
39
|
+
export const MEMORY_READONLY_NOTICE = `# Memory
|
|
40
|
+
|
|
41
|
+
You have READ-ONLY access to persistent memory in this session: stored notes are available below, but this session has no memory write channel — the engine will not accept writes into the memory store. If the user asks you to remember something for later, say plainly that you cannot save it in this session — never claim to have noted or remembered it.`;
|
|
37
42
|
export const MEMORY_INDEX_MAX_LINES = 200;
|
|
38
43
|
export const MEMORY_INDEX_MAX_BYTES = 25 * 1024;
|
|
39
44
|
export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listing the slug in memory/.hydrate]";
|
|
@@ -41,6 +46,7 @@ export const DEFAULT_MAX_MEMORY_FILES = 500;
|
|
|
41
46
|
export const DEFAULT_HARVEST_DEADLINE_MS = 5_000;
|
|
42
47
|
export const DEFAULT_HARVEST_FILE_BUDGET = 2_000;
|
|
43
48
|
export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
49
|
+
let indexCaptureSeq = 0;
|
|
44
50
|
export class MemoryEngine {
|
|
45
51
|
backend;
|
|
46
52
|
memoryDir;
|
|
@@ -425,6 +431,7 @@ export class MemoryEngine {
|
|
|
425
431
|
inject(handle, opts) {
|
|
426
432
|
const writeChannel = handle.writeScope !== null && opts?.writeToolMounted !== false;
|
|
427
433
|
const instruction = writeChannel ? buildMemoryInstruction(handle.writableRoot) : "";
|
|
434
|
+
const readOnlyNotice = handle.writeScope === null ? MEMORY_READONLY_NOTICE : undefined;
|
|
428
435
|
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
429
436
|
const onDisk = handle.indexOnDiskUntrusted === true ? undefined : readSafe(indexPath);
|
|
430
437
|
const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
|
|
@@ -440,14 +447,17 @@ export class MemoryEngine {
|
|
|
440
447
|
if (drained.queue.length > 0 || drained.folded > 0) {
|
|
441
448
|
announcements = drained.queue;
|
|
442
449
|
announceBlock = renderAnnouncements(drained.queue, drained.folded);
|
|
450
|
+
if (handle.writeScope === null)
|
|
451
|
+
announceBlock = `${announceBlock}\n\n${MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA}`;
|
|
443
452
|
}
|
|
444
453
|
}
|
|
445
454
|
catch (err) {
|
|
446
455
|
this.discloseAnnounceFailure("inject drain", err);
|
|
447
456
|
}
|
|
448
|
-
const block = [instruction, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
457
|
+
const block = [instruction || readOnlyNotice, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
449
458
|
return {
|
|
450
459
|
instruction,
|
|
460
|
+
...(readOnlyNotice !== undefined ? { readOnlyNotice } : {}),
|
|
451
461
|
...(index !== undefined ? { index } : {}),
|
|
452
462
|
...(announcements !== undefined ? { announcements } : {}),
|
|
453
463
|
...(announceBlock !== undefined ? { announceBlock } : {}),
|
|
@@ -459,8 +469,14 @@ export class MemoryEngine {
|
|
|
459
469
|
const root = handle.writableRoot;
|
|
460
470
|
if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`))
|
|
461
471
|
return { ok: true };
|
|
462
|
-
if (handle.writeScope === null)
|
|
463
|
-
return {
|
|
472
|
+
if (handle.writeScope === null) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
code: "read_only_layering",
|
|
476
|
+
reason: "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written.",
|
|
477
|
+
muted: false,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
464
480
|
const findings = [];
|
|
465
481
|
const nameFinding = scanMemoryFileName(relative(root, canonicalPath));
|
|
466
482
|
if (nameFinding !== undefined)
|
|
@@ -512,10 +528,12 @@ export class MemoryEngine {
|
|
|
512
528
|
warnings: [],
|
|
513
529
|
};
|
|
514
530
|
const writeScope = handle.writeScope;
|
|
515
|
-
if (writeScope === null) {
|
|
531
|
+
if (writeScope === null || opts?.admitNothing !== undefined) {
|
|
516
532
|
const roFindings = this.backend.drainInboundFindings?.();
|
|
517
533
|
if (roFindings !== undefined && roFindings.length > 0)
|
|
518
534
|
report.inboundFindings = roFindings;
|
|
535
|
+
if (opts?.admitNothing !== undefined)
|
|
536
|
+
report.warnings.push(opts.admitNothing.reason);
|
|
519
537
|
return report;
|
|
520
538
|
}
|
|
521
539
|
try {
|
|
@@ -656,16 +674,18 @@ export class MemoryEngine {
|
|
|
656
674
|
const indexNow = readSafe(pollutedIndexPath);
|
|
657
675
|
if (indexNow === undefined || indexNow === handle.indexText)
|
|
658
676
|
return;
|
|
677
|
+
let indexCaptureLanded = false;
|
|
659
678
|
try {
|
|
660
679
|
const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-polluted-${MEMORY_INDEX_FILENAME}`);
|
|
661
680
|
ensureDirExists(dirname(dest));
|
|
662
|
-
writeFileSync(dest, indexNow, "utf8");
|
|
681
|
+
writeFileSync(dest, indexNow, { encoding: "utf8", flag: "wx" });
|
|
682
|
+
indexCaptureLanded = true;
|
|
663
683
|
}
|
|
664
684
|
catch {
|
|
665
685
|
}
|
|
666
686
|
try {
|
|
667
687
|
writeFileNoFollow(pollutedIndexPath, handle.indexText);
|
|
668
|
-
report.warnings.push(
|
|
688
|
+
report.warnings.push(`memory index restored to its pre-session state — this session's index additions were not retained (session polluted; ${indexCaptureLanded ? "the removed text was captured to quarantine" : "the quarantine capture did NOT land — the removed text is gone"})`);
|
|
669
689
|
}
|
|
670
690
|
catch (err) {
|
|
671
691
|
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1225,7 +1245,20 @@ export class MemoryEngine {
|
|
|
1225
1245
|
try {
|
|
1226
1246
|
const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-${MEMORY_INDEX_FILENAME}`);
|
|
1227
1247
|
ensureDirExists(dirname(dest));
|
|
1228
|
-
|
|
1248
|
+
let landed = false;
|
|
1249
|
+
for (let attempt = 0; !landed && attempt < 10; attempt++) {
|
|
1250
|
+
const candidate = attempt === 0 ? dest : `${dest}.${attempt}`;
|
|
1251
|
+
try {
|
|
1252
|
+
writeFileSync(candidate, text, { encoding: "utf8", flag: "wx" });
|
|
1253
|
+
landed = true;
|
|
1254
|
+
}
|
|
1255
|
+
catch (err) {
|
|
1256
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
|
|
1257
|
+
throw err;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (!landed)
|
|
1261
|
+
writeFileSync(`${dest}.${process.pid}.${indexCaptureSeq++}`, text, { encoding: "utf8", flag: "wx" });
|
|
1229
1262
|
captured = true;
|
|
1230
1263
|
}
|
|
1231
1264
|
catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, 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";
|
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, 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";
|
|
@@ -281,8 +281,15 @@ export async function prepareCcImport(opts) {
|
|
|
281
281
|
skipped.push({ rule: String(entry), reason: "settings entry is not a string" });
|
|
282
282
|
continue;
|
|
283
283
|
}
|
|
284
|
-
if (!entry.startsWith("Bash("))
|
|
284
|
+
if (!entry.startsWith("Bash(")) {
|
|
285
|
+
const reason = /^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)
|
|
286
|
+
? "unsupported.tool: only Bash(...) command rules import in v1 — this entry stays in the settings file, unimported"
|
|
287
|
+
: /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
288
|
+
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
289
|
+
: "unsupported.form: not a Bash(...) command rule — it stays in the settings file, unimported";
|
|
290
|
+
skipped.push({ rule: entry, reason });
|
|
285
291
|
continue;
|
|
292
|
+
}
|
|
286
293
|
const parsed = parseAllowRuleText(entry);
|
|
287
294
|
if ("reject" in parsed) {
|
|
288
295
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
@@ -112,6 +112,15 @@ export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
|
|
|
112
112
|
export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
|
|
113
113
|
signal?: AbortSignal;
|
|
114
114
|
timeoutMs: number;
|
|
115
|
+
/**
|
|
116
|
+
* backlog #136④ — which arm WON, reported at the moment it won. A consumer that wants to tell a
|
|
117
|
+
* cancelled wait from an unreadable provider cannot get that by sampling `signal.aborted` after
|
|
118
|
+
* the await: a cancellation queued between the settlement and the continuation reads identically
|
|
119
|
+
* to one that actually ended the wait, and the operator-facing account would name the wrong
|
|
120
|
+
* cause. Called at most once, before `p` resolves, and only for the two non-provider arms —
|
|
121
|
+
* absent call ⇒ the awaited promise itself settled.
|
|
122
|
+
*/
|
|
123
|
+
onFallback?: (cause: "aborted" | "timeout") => void;
|
|
115
124
|
}): Promise<T>;
|
|
116
125
|
/** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
|
|
117
126
|
* single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
|
|
@@ -11,22 +11,29 @@ export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
|
11
11
|
export function settleOrgVerdictWithin(p, fallback, opts) {
|
|
12
12
|
return new Promise((resolve) => {
|
|
13
13
|
let settled = false;
|
|
14
|
-
const finish = (v) => {
|
|
14
|
+
const finish = (v, cause) => {
|
|
15
15
|
if (settled)
|
|
16
16
|
return;
|
|
17
17
|
settled = true;
|
|
18
18
|
clearTimeout(timer);
|
|
19
19
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
20
|
+
if (cause !== undefined) {
|
|
21
|
+
try {
|
|
22
|
+
opts.onFallback?.(cause);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
20
27
|
resolve(v);
|
|
21
28
|
};
|
|
22
|
-
const onAbort = () => finish(fallback);
|
|
23
|
-
const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
|
|
29
|
+
const onAbort = () => finish(fallback, "aborted");
|
|
30
|
+
const timer = setTimeout(() => finish(fallback, "timeout"), opts.timeoutMs);
|
|
24
31
|
if (opts.signal?.aborted === true) {
|
|
25
|
-
finish(fallback);
|
|
32
|
+
finish(fallback, "aborted");
|
|
26
33
|
return;
|
|
27
34
|
}
|
|
28
35
|
opts.signal?.addEventListener("abort", onAbort);
|
|
29
|
-
p.then(finish, () => finish(fallback));
|
|
36
|
+
p.then((v) => finish(v), () => finish(fallback));
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
39
|
export const ORG_RULE_DECISION_REASON = "org_rule";
|
|
@@ -95,10 +95,10 @@ export declare function buildStaleOffloadPointer(toolName: string, ref: string,
|
|
|
95
95
|
* The session transcript is NEVER touched — this runs on the outgoing {@link Context} only.
|
|
96
96
|
* Error results, image/document-bearing blocks' non-text parts, already-offloaded previews, and
|
|
97
97
|
* replacements that would save < `minSavingsChars` are left verbatim. The full text is persisted
|
|
98
|
-
* under a deterministic content-digested ref (
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* readable back via `read_tool_result`.
|
|
98
|
+
* under a deterministic content-digested ref (`buildToolResultRef(sessionId, toolCallId,
|
|
99
|
+
* toolResultContentSegment(text))` — a `~`-separated three-segment mint whose digest is its OWN
|
|
100
|
+
* segment; write-once, so re-projection on every turn re-puts a no-op; the digest exists because
|
|
101
|
+
* tool-call ids carry no cross-turn uniqueness contract), readable back via `read_tool_result`.
|
|
102
102
|
*/
|
|
103
103
|
export declare function projectStaleToolResults(context: Context, cfg: ResolvedStaleToolResultOffload, store: ToolResultStore, sessionId: string,
|
|
104
104
|
/** Run-scoped cache of refs already persisted by THIS run (独立复审 MED,已修): without it the
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { emitTrace } from "../trace.js";
|
|
3
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "../tool-result-store.js";
|
|
2
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "../tool-result-store.js";
|
|
4
3
|
export function buildWorkingFileAttachments(spec, prepared) {
|
|
5
4
|
if (spec.compaction?.attachWorkingFiles === false || !prepared.readTaskFile)
|
|
6
5
|
return undefined;
|
|
@@ -73,13 +72,13 @@ export async function projectStaleToolResults(context, cfg, store, sessionId, wr
|
|
|
73
72
|
const text = toolResultText(msg);
|
|
74
73
|
if (text.startsWith(PERSISTED_OUTPUT_PREFIX))
|
|
75
74
|
continue;
|
|
76
|
-
const ref = buildToolResultRef(sessionId,
|
|
75
|
+
const ref = buildToolResultRef(sessionId, msg.toolCallId, toolResultContentSegment(text));
|
|
77
76
|
const pointer = buildStaleOffloadPointer(toolName, ref, text.length);
|
|
78
77
|
if (text.length - pointer.length < cfg.minSavingsChars)
|
|
79
78
|
continue;
|
|
80
79
|
if (!writtenRefs.has(ref)) {
|
|
81
80
|
try {
|
|
82
|
-
await store.put(ref, text);
|
|
81
|
+
await store.put(ref, text, toolResultProvenanceOf(sessionId));
|
|
83
82
|
writtenRefs.add(ref);
|
|
84
83
|
}
|
|
85
84
|
catch {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/157 B15 一期 (P6 相位抽取) — prepareTask's long-term-memory phase, verbatim. The ONLY inputs
|
|
3
|
-
* are the five fields below (measured, not assumed: spec.memory + deps.* + the three ambient values);
|
|
4
|
-
* the ONLY outputs are the memory-engine session and the composed injection block. `deps.onError`
|
|
5
|
-
* call order and payloads are part of the contract (event-sequence snapshot pin recorded across the
|
|
6
|
-
* move). Throws pass through unchanged: a `config.memory_*`-coded violation is a DELIBERATE refusal
|
|
7
|
-
* (design/142 S1 硬门) and must keep failing prepare loudly.
|
|
8
|
-
*/
|
|
9
1
|
import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
|
|
10
2
|
import type { Prepared } from "./prepare-task.js";
|
|
11
3
|
export interface PrepareMemoryInput {
|
|
@@ -18,15 +10,42 @@ export interface PrepareMemoryInput {
|
|
|
18
10
|
};
|
|
19
11
|
/**
|
|
20
12
|
* #181-F5 — whether a tool NAMED `Write` (the tool the CC `# Memory` instruction names) is on the
|
|
21
|
-
* ASSEMBLED roster this run
|
|
22
|
-
* `tools.some(t => t.name === "Write") && !exclude.includes("Write")
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
13
|
+
* ASSEMBLED roster this run, not excluded, AND can actually REACH the host-side memory root:
|
|
14
|
+
* prepare-task's `tools.some(t => t.name === "Write") && !exclude.includes("Write")` plus the
|
|
15
|
+
* remote-env conjunct (a hand-band Write on a remote ExecutionEnv writes the sandbox filesystem,
|
|
16
|
+
* not the host memory root — it does not count unless the deployment declares the mount shared
|
|
17
|
+
* via `memoryPersistenceCapable: true`). Name occupancy IS the channel declaration for the local
|
|
18
|
+
* arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
|
|
19
|
+
* run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
|
|
20
|
+
* a writable memory scope but no write channel is not instructed to call a tool that is not on
|
|
21
|
+
* its roster (or cannot reach the store); the RB-276 index seed follows the same gate.
|
|
28
22
|
*/
|
|
29
23
|
writeToolsMounted: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The session-wide persistence verdict: `TaskSpec.memoryPersistenceCapable` (the deployment's own
|
|
26
|
+
* statement — the only honest channel for a custom memory writer persisting through its closure,
|
|
27
|
+
* which no inference can see) when set, else the KNOWN-store-path inference: a mounted,
|
|
28
|
+
* non-excluded file-write tool (Write/Edit/NotebookEdit) or a write-capable shell. A generic
|
|
29
|
+
* write-effect tool does NOT count — a mail sender's side effect is not a memory store, and
|
|
30
|
+
* counting it re-opened the silent-confabulation hole. `false` ⇒ this phase mounts
|
|
31
|
+
* {@link MEMORY_READONLY_NOTICE} where the engine is silent.
|
|
32
|
+
*/
|
|
33
|
+
rosterCanPersist: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
|
|
36
|
+
* {@link rosterCanPersist} verdict because the two drive different arms:
|
|
37
|
+
* - `true` (declared) RETRACTS the engine's own read-only notice — the deployment vouches for a
|
|
38
|
+
* persistence channel the engine cannot see (a custom writer persisting through its closure),
|
|
39
|
+
* so "you cannot save" beside it would be a false claim. An INFERRED-true roster must NOT
|
|
40
|
+
* retract: over a writeScope-null layering the engine will refuse those very file writes
|
|
41
|
+
* (`read_only_layering`), so the roster's write tools prove nothing about THIS store and
|
|
42
|
+
* stripping the notice re-opens the silent-confabulation hole for exactly the state the
|
|
43
|
+
* notice was built for.
|
|
44
|
+
* - `false` (declared) also CLOSES the file-tool write channel into the writable memory root
|
|
45
|
+
* (the write gate refuses), so the mounted notice's "the engine will not accept writes into
|
|
46
|
+
* the memory store" stays a true statement instead of a disclosure the store then contradicts.
|
|
47
|
+
*/
|
|
48
|
+
memoryPersistenceDeclared?: boolean;
|
|
30
49
|
/**
|
|
31
50
|
* design/178 ②-1 — whether the `memory_search`/`memory_get` pair PASSED its early mount conjuncts
|
|
32
51
|
* (exclusion + name occupancy, decided in prepare-task BEFORE this phase). True ⇒ this phase builds
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { sep } from "node:path";
|
|
1
2
|
import { admitMemoryScopes } from "../memory-admission.js";
|
|
2
|
-
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
|
+
import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
4
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
4
5
|
import { normalizeMemorySpec } from "../memory.js";
|
|
5
|
-
import { MEMORY_PREFERENCE_DISCIPLINE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
|
+
import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
7
|
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
7
8
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
8
9
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
@@ -113,10 +114,12 @@ export async function prepareMemory(input) {
|
|
|
113
114
|
}
|
|
114
115
|
const personalMemoryDir = derivePersonalMemoryDir(engineRoot);
|
|
115
116
|
const personalControlDir = derivePersonalControlDir(engineRoot);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const effectiveControlDir = (b, fallback) => {
|
|
118
|
+
const pinnedCtl = b.controlPlaneRoot;
|
|
119
|
+
return typeof pinnedCtl === "string" && pinnedCtl ? pinnedCtl : fallback;
|
|
120
|
+
};
|
|
121
|
+
const choosePersonalBackend = () => typeof pinned === "string" && pinned ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir }) : backend;
|
|
122
|
+
const createPersonalEngine = (personalBackend) => {
|
|
120
123
|
return {
|
|
121
124
|
engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident }),
|
|
122
125
|
backend: personalBackend,
|
|
@@ -131,17 +134,43 @@ export async function prepareMemory(input) {
|
|
|
131
134
|
};
|
|
132
135
|
let writeEngine;
|
|
133
136
|
let writeHandle;
|
|
137
|
+
let readOnlyEngine;
|
|
138
|
+
let readOnlyHandle;
|
|
134
139
|
let injectFn;
|
|
135
140
|
let harvestBoth;
|
|
136
141
|
let toolPlanes;
|
|
142
|
+
const admitNothingOpts = input.memoryPersistenceDeclared === false
|
|
143
|
+
? { admitNothing: { reason: "harvest admitted nothing: memory persistence is declared unavailable for this session (memoryPersistenceCapable: false)" } }
|
|
144
|
+
: {};
|
|
137
145
|
if (dual) {
|
|
146
|
+
const personalBackendChosen = choosePersonalBackend();
|
|
147
|
+
{
|
|
148
|
+
const projCtl = effectiveControlDir(backend, identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot));
|
|
149
|
+
const persCtl = effectiveControlDir(personalBackendChosen, personalControlDir);
|
|
150
|
+
const overlap = (a, b) => isContainedIn(a, b) || isContainedIn(b, a);
|
|
151
|
+
const refuse = (what, a, b) => {
|
|
152
|
+
const e = new Error(`memory dual-root configuration error: ${what} overlap (${canonicalize(a)} vs ${canonicalize(b)}) — the planes' data roots and control planes must all be disjoint.`);
|
|
153
|
+
e.code = "config.memory_dual_root_overlap";
|
|
154
|
+
throw e;
|
|
155
|
+
};
|
|
156
|
+
if (overlap(memoryDir, personalMemoryDir))
|
|
157
|
+
refuse("the project and personal memory roots", memoryDir, personalMemoryDir);
|
|
158
|
+
if (overlap(projCtl, persCtl))
|
|
159
|
+
refuse("the project and personal control planes", projCtl, persCtl);
|
|
160
|
+
for (const [ctlName, ctl] of [["project control plane", projCtl], ["personal control plane", persCtl]]) {
|
|
161
|
+
for (const [rootName, dataRoot] of [["project memory root", memoryDir], ["personal memory mount", personalMemoryDir]]) {
|
|
162
|
+
if (overlap(ctl, dataRoot))
|
|
163
|
+
refuse(`the ${ctlName} and the ${rootName}`, ctl, dataRoot);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
138
167
|
const projectEngine = new MemoryEngine({
|
|
139
168
|
backend,
|
|
140
169
|
memoryDir,
|
|
141
170
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
142
171
|
onIncident: onEngineIncident,
|
|
143
172
|
});
|
|
144
|
-
const personal = createPersonalEngine();
|
|
173
|
+
const personal = createPersonalEngine(personalBackendChosen);
|
|
145
174
|
const personalEngine = personal.engine;
|
|
146
175
|
const p = planes;
|
|
147
176
|
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
|
|
@@ -149,6 +178,8 @@ export async function prepareMemory(input) {
|
|
|
149
178
|
const writeIsPersonal = p.writePlane === "personal";
|
|
150
179
|
writeEngine = writeIsPersonal ? personalEngine : projectEngine;
|
|
151
180
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
181
|
+
readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
|
|
182
|
+
readOnlyHandle = writeIsPersonal ? projectHandle : personalHandle;
|
|
152
183
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
153
184
|
toolPlanes = [
|
|
154
185
|
{
|
|
@@ -167,7 +198,7 @@ export async function prepareMemory(input) {
|
|
|
167
198
|
harvestBoth = async () => {
|
|
168
199
|
const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
|
|
169
200
|
const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
|
|
170
|
-
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId });
|
|
201
|
+
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId, ...admitNothingOpts });
|
|
171
202
|
let readReport;
|
|
172
203
|
let readFailure;
|
|
173
204
|
try {
|
|
@@ -183,13 +214,13 @@ export async function prepareMemory(input) {
|
|
|
183
214
|
};
|
|
184
215
|
}
|
|
185
216
|
else if (personalOnly) {
|
|
186
|
-
const personal = createPersonalEngine();
|
|
217
|
+
const personal = createPersonalEngine(choosePersonalBackend());
|
|
187
218
|
const personalEngine = personal.engine;
|
|
188
219
|
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
189
220
|
writeEngine = personalEngine;
|
|
190
221
|
writeHandle = handle;
|
|
191
222
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
192
|
-
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId });
|
|
223
|
+
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
|
|
193
224
|
toolPlanes = [
|
|
194
225
|
{
|
|
195
226
|
backend: retrievalBackend(personal.backend),
|
|
@@ -210,7 +241,7 @@ export async function prepareMemory(input) {
|
|
|
210
241
|
writeEngine = engine;
|
|
211
242
|
writeHandle = handle;
|
|
212
243
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
213
|
-
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId });
|
|
244
|
+
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
|
|
214
245
|
toolPlanes = [
|
|
215
246
|
{
|
|
216
247
|
backend: retrievalBackend(backend),
|
|
@@ -220,7 +251,25 @@ export async function prepareMemory(input) {
|
|
|
220
251
|
},
|
|
221
252
|
];
|
|
222
253
|
}
|
|
223
|
-
memoryWriteGateRef.current = (w) =>
|
|
254
|
+
memoryWriteGateRef.current = (w) => {
|
|
255
|
+
if (readOnlyEngine !== undefined && readOnlyHandle !== undefined) {
|
|
256
|
+
const ro = readOnlyEngine.gateWrite(readOnlyHandle, w.key, w.content);
|
|
257
|
+
if (!ro.ok)
|
|
258
|
+
return ro;
|
|
259
|
+
}
|
|
260
|
+
if (input.memoryPersistenceDeclared === false) {
|
|
261
|
+
const root = writeHandle.writableRoot;
|
|
262
|
+
if (w.key === root || w.key.startsWith(`${root}${sep}`)) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
code: "read_only_layering",
|
|
266
|
+
reason: `memory persistence is declared unavailable for this session (memoryPersistenceCapable: false) — writes into the memory store are refused. Nothing was written.`,
|
|
267
|
+
muted: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
272
|
+
};
|
|
224
273
|
const harvestSafe = async (phase = "terminal") => {
|
|
225
274
|
try {
|
|
226
275
|
const report = await harvestBoth();
|
|
@@ -261,7 +310,10 @@ export async function prepareMemory(input) {
|
|
|
261
310
|
memoryTools = createMemoryEngineTools({ planes: toolPlanes });
|
|
262
311
|
}
|
|
263
312
|
catch (err) {
|
|
264
|
-
if (typeof err.code === "string" &&
|
|
313
|
+
if (typeof err.code === "string" &&
|
|
314
|
+
(err.code.startsWith("config.memory_scope") ||
|
|
315
|
+
err.code.startsWith("config.memory_project") ||
|
|
316
|
+
err.code === "config.memory_dual_root_overlap")) {
|
|
265
317
|
throw err;
|
|
266
318
|
}
|
|
267
319
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
|
|
@@ -275,15 +327,31 @@ export async function prepareMemory(input) {
|
|
|
275
327
|
}
|
|
276
328
|
if (memoryEngineSession) {
|
|
277
329
|
const injection = memoryEngineSession.inject();
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
let blockBody = injection.block;
|
|
331
|
+
if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
|
|
332
|
+
blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
|
|
333
|
+
}
|
|
334
|
+
else if (!input.rosterCanPersist && injection.instruction !== "") {
|
|
335
|
+
blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
336
|
+
}
|
|
337
|
+
else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
|
|
338
|
+
blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
339
|
+
}
|
|
340
|
+
if (!input.rosterCanPersist &&
|
|
341
|
+
(injection.announceBlock?.trim() ?? "") !== "" &&
|
|
342
|
+
blockBody.includes(injection.announceBlock) &&
|
|
343
|
+
!blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
|
|
344
|
+
blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
|
|
345
|
+
}
|
|
346
|
+
if (blockBody.trim())
|
|
347
|
+
memoryBlock = blockBody;
|
|
280
348
|
if (memoryTools !== undefined) {
|
|
281
349
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
|
|
282
350
|
}
|
|
283
|
-
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted) {
|
|
351
|
+
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist) {
|
|
284
352
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
|
|
285
353
|
}
|
|
286
|
-
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
354
|
+
if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
|
|
287
355
|
seedFiles = [injection.indexSeed];
|
|
288
356
|
}
|
|
289
357
|
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
@@ -28,6 +28,8 @@ import { type WiringManifest } from "../wiring-manifest.js";
|
|
|
28
28
|
import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
|
|
29
29
|
import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
|
|
30
30
|
import type { RepairBundle } from "../../agents/repair-loop.js";
|
|
31
|
+
/** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
|
|
32
|
+
export declare function __resetMaterializeEnvAnnouncements(): void;
|
|
31
33
|
/**
|
|
32
34
|
* design/164 — validate `TaskSpec.limits` at the door and return it unchanged.
|
|
33
35
|
*
|