@sema-agent/core 5.17.0 → 5.18.1
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 +121 -0
- package/dist/agents/subagent.js +24 -0
- package/dist/core/auto-compaction.d.ts +6 -0
- package/dist/core/auto-compaction.js +15 -1
- package/dist/core/checkpoint-store.d.ts +4 -2
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +9 -0
- package/dist/core/hooks.js +21 -0
- package/dist/core/mcp.js +3 -0
- package/dist/core/memory-engine/content-origin.d.ts +27 -0
- package/dist/core/memory-engine/content-origin.js +38 -0
- package/dist/core/memory-engine/engine.d.ts +12 -2
- package/dist/core/memory-engine/engine.js +172 -12
- package/dist/core/memory-engine/file-backend.d.ts +4 -0
- package/dist/core/memory-engine/file-backend.js +25 -3
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +16 -0
- package/dist/core/memory-engine/layout.js +90 -2
- package/dist/core/memory-engine/sync-client.d.ts +1 -0
- package/dist/core/memory-engine/sync-client.js +23 -5
- package/dist/core/memory-engine/tools.d.ts +55 -0
- package/dist/core/memory-engine/tools.js +307 -0
- package/dist/core/memory-engine/types.d.ts +1 -1
- package/dist/core/memory.d.ts +4 -0
- package/dist/core/memory.js +15 -2
- package/dist/core/permission-rule-consent.d.ts +138 -0
- package/dist/core/permission-rule-consent.js +318 -0
- package/dist/core/permission-rule-model.d.ts +66 -0
- package/dist/core/permission-rule-model.js +135 -0
- package/dist/core/permission-rule-store.d.ts +89 -0
- package/dist/core/permission-rule-store.js +145 -0
- package/dist/core/permission-rules.d.ts +3 -2
- package/dist/core/permission-rules.js +9 -4
- package/dist/core/runner/prepare-memory.d.ts +3 -1
- package/dist/core/runner/prepare-memory.js +54 -14
- package/dist/core/runner/prepare-task.d.ts +12 -0
- package/dist/core/runner/prepare-task.js +206 -12
- package/dist/core/runner/runtask.d.ts +3 -1
- package/dist/core/runner/runtask.js +47 -5
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/tool-policy.d.ts +13 -1
- package/dist/core/tool-policy.js +93 -12
- package/dist/core/tools.js +1 -0
- package/dist/core/trace.d.ts +20 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/wiring-manifest.d.ts +5 -1
- package/dist/core/wiring-manifest.js +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- package/dist/stores/file/permission-rule-store.d.ts +32 -0
- package/dist/stores/file/permission-rule-store.js +213 -0
- package/dist/tools/fs/fs-bash.js +12 -5
- package/dist/tools/fs/fs-shared.d.ts +12 -0
- package/dist/tools/fs/fs-shared.js +65 -1
- package/dist/tools/web.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { canonicalize } from "./canonical-json.js";
|
|
2
|
+
import { isRuleLive } from "./permission-rule-model.js";
|
|
3
|
+
export const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
|
|
4
|
+
export function writerOf(store) {
|
|
5
|
+
const w = store[PERMISSION_RULE_WRITER];
|
|
6
|
+
return w !== undefined && typeof w.apply === "function" && typeof w.nextDot === "function" ? w : undefined;
|
|
7
|
+
}
|
|
8
|
+
export function sameScope(a, b) {
|
|
9
|
+
return a.kind === "global" ? b.kind === "global" : b.kind === "project" && a.root === b.root;
|
|
10
|
+
}
|
|
11
|
+
function sameDot(a, b) {
|
|
12
|
+
return a.actor === b.actor && a.counter === b.counter;
|
|
13
|
+
}
|
|
14
|
+
export function applyTombstones(rules, tombstones) {
|
|
15
|
+
return rules
|
|
16
|
+
.map((r) => {
|
|
17
|
+
const removed = tombstones.filter((t) => t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots);
|
|
18
|
+
return removed.length === 0 ? r : { ...r, adds: r.adds.filter((a) => !removed.some((d) => sameDot(d, a.dot))) };
|
|
19
|
+
})
|
|
20
|
+
.filter(isRuleLive);
|
|
21
|
+
}
|
|
22
|
+
export function foldDelta(rules, delta) {
|
|
23
|
+
const next = rules.map((r) => ({ ...r, adds: [...r.adds] }));
|
|
24
|
+
const existing = next.find((r) => r.rule === delta.rule && sameScope(r.scope, delta.scope));
|
|
25
|
+
if (existing === undefined) {
|
|
26
|
+
next.push({ rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, scope: delta.scope, adds: [delta.add] });
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
if (!existing.adds.some((a) => sameDot(a.dot, delta.add.dot)))
|
|
30
|
+
existing.adds.push(delta.add);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
export function addDotsOf(rules) {
|
|
34
|
+
return rules.flatMap((r) => r.adds.map((a) => a.dot));
|
|
35
|
+
}
|
|
36
|
+
const ADD_ONLY_KEYS = ["add", "redemption", "rule", "command", "match", "tool"];
|
|
37
|
+
export function assertDeleteDeltaCarriesNoAdd(delta) {
|
|
38
|
+
const smuggled = ADD_ONLY_KEYS.filter((k) => Object.hasOwn(delta, k));
|
|
39
|
+
if (smuggled.length > 0) {
|
|
40
|
+
throw new Error(`a tighten-delete may not introduce a new add (it carried: ${smuggled.join(", ")})`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(delta.tombstone?.removedDots) || delta.tombstone.removedDots.length === 0) {
|
|
43
|
+
throw new Error("a tighten-delete must carry a tombstone naming at least one observed add dot");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function ruleStoreChecksum(payload) {
|
|
47
|
+
const { createHash } = await import("node:crypto");
|
|
48
|
+
return `sha256:${createHash("sha256").update(canonicalize(payload), "utf8").digest("hex")}`;
|
|
49
|
+
}
|
|
50
|
+
const REMOVE_MAX_ATTEMPTS = 8;
|
|
51
|
+
export async function removePersistedRule(opts) {
|
|
52
|
+
const store = opts.provider.forPrincipal(opts.principal);
|
|
53
|
+
const writer = writerOf(store);
|
|
54
|
+
if (writer === undefined) {
|
|
55
|
+
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
|
|
56
|
+
}
|
|
57
|
+
let snapshot;
|
|
58
|
+
try {
|
|
59
|
+
snapshot = await store.list();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}` };
|
|
63
|
+
}
|
|
64
|
+
const live = snapshot.rules.filter((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope));
|
|
65
|
+
const removedDots = live.flatMap((r) => r.adds.map((a) => a.dot));
|
|
66
|
+
if (removedDots.length === 0)
|
|
67
|
+
return { status: "no-op", rev: snapshot.rev };
|
|
68
|
+
const tombstone = { rule: opts.rule, scope: opts.scope, removedDots, deletedBy: await writer.nextDot() };
|
|
69
|
+
let expectedRev = snapshot.rev;
|
|
70
|
+
let lastError = "";
|
|
71
|
+
for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev });
|
|
74
|
+
if (!("conflict" in res)) {
|
|
75
|
+
let stillLive = false;
|
|
76
|
+
try {
|
|
77
|
+
const after = await store.list();
|
|
78
|
+
stillLive = after.rules.some((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope) && isRuleLive(r));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
stillLive = false;
|
|
82
|
+
}
|
|
83
|
+
return { status: "removed", rev: res.rev, stillLive };
|
|
84
|
+
}
|
|
85
|
+
expectedRev = res.rev;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
lastError = errText(err);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const reason = lastError !== "" ? lastError : `optimistic-concurrency retries exhausted after ${REMOVE_MAX_ATTEMPTS} attempts`;
|
|
93
|
+
try {
|
|
94
|
+
const after = await store.list();
|
|
95
|
+
const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
|
|
96
|
+
return landed
|
|
97
|
+
? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule === opts.rule && sameScope(r.scope, opts.scope) && isRuleLive(r)) }
|
|
98
|
+
: { status: "failed", error: `${reason} (read-back confirms no tombstone was written)` };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return { status: "failed", error: `${reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function errText(err) {
|
|
105
|
+
return err instanceof Error ? err.message : String(err);
|
|
106
|
+
}
|
|
107
|
+
export const EMPTY_RULE_STORE = {
|
|
108
|
+
list: async () => ({ rules: [], tombstones: [], rev: 0 }),
|
|
109
|
+
durability: "process-local",
|
|
110
|
+
};
|
|
111
|
+
export class InMemoryPermissionRuleStore {
|
|
112
|
+
actor;
|
|
113
|
+
durability = "process-local";
|
|
114
|
+
fidelity = "structured-clone";
|
|
115
|
+
rules = [];
|
|
116
|
+
tombstones = [];
|
|
117
|
+
rev = 0;
|
|
118
|
+
counter = 0;
|
|
119
|
+
constructor(actor = `mem-${Math.random().toString(36).slice(2, 10)}`) {
|
|
120
|
+
this.actor = actor;
|
|
121
|
+
}
|
|
122
|
+
async list() {
|
|
123
|
+
return {
|
|
124
|
+
rules: applyTombstones(this.rules, this.tombstones).map((r) => ({ ...r, adds: r.adds.map((a) => ({ ...a })) })),
|
|
125
|
+
tombstones: this.tombstones.map((t) => ({ ...t })),
|
|
126
|
+
rev: this.rev,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
[PERMISSION_RULE_WRITER] = {
|
|
130
|
+
nextDot: async () => ({ actor: this.actor, counter: ++this.counter }),
|
|
131
|
+
apply: async (delta, opts) => {
|
|
132
|
+
if (opts.expectedRev !== this.rev)
|
|
133
|
+
return { conflict: true, rev: this.rev };
|
|
134
|
+
if (delta.kind === "redemption-add") {
|
|
135
|
+
this.rules = foldDelta(this.rules, delta);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
assertDeleteDeltaCarriesNoAdd(delta);
|
|
139
|
+
this.tombstones = [...this.tombstones, delta.tombstone];
|
|
140
|
+
}
|
|
141
|
+
this.rev += 1;
|
|
142
|
+
return { rev: this.rev };
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -11,7 +11,7 @@ export interface ParsedPermissionRule {
|
|
|
11
11
|
}
|
|
12
12
|
export interface PermissionRuleIssue {
|
|
13
13
|
rule: string;
|
|
14
|
-
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren" | "unsupported.unknown_param";
|
|
14
|
+
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.bash_prefix_rules_store" | "unsupported.file_glob" | "unsupported.mcp_paren" | "unsupported.unknown_param";
|
|
15
15
|
message: string;
|
|
16
16
|
}
|
|
17
17
|
export interface PermissionRuleCaps {
|
|
@@ -26,10 +26,11 @@ export interface PermissionRulePolicyOptions {
|
|
|
26
26
|
onIssue?: (issue: PermissionRuleIssue) => void;
|
|
27
27
|
defaultAction?: "allow" | "ask" | "deny";
|
|
28
28
|
primaryFieldGeneric?: "reject" | "allow-lexical";
|
|
29
|
+
bashPrefixLane?: "reject" | "rules-store";
|
|
29
30
|
caps?: Partial<PermissionRuleCaps>;
|
|
30
31
|
}
|
|
31
32
|
export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
|
|
32
33
|
export declare function wildcardMatch(pattern: string, value: string): boolean;
|
|
33
34
|
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
34
|
-
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
|
|
35
|
+
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "bashPrefixLane">): PermissionRuleIssue[];
|
|
35
36
|
export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
|
|
@@ -109,7 +109,7 @@ function countStars(s) {
|
|
|
109
109
|
n++;
|
|
110
110
|
return n;
|
|
111
111
|
}
|
|
112
|
-
function compile(rules, caps, primaryFieldGeneric) {
|
|
112
|
+
function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject") {
|
|
113
113
|
const byTool = new Map();
|
|
114
114
|
const issues = [];
|
|
115
115
|
const bad = (rule, code, message) => {
|
|
@@ -159,7 +159,12 @@ function compile(rules, caps, primaryFieldGeneric) {
|
|
|
159
159
|
const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
|
|
160
160
|
const looksLikeParamRule = colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && BASH_GENERIC_PARAMS.has(paramPart);
|
|
161
161
|
if (!looksLikeParamRule || content.endsWith(":*")) {
|
|
162
|
-
|
|
162
|
+
if (bashPrefixLane === "rules-store") {
|
|
163
|
+
bad(text, "unsupported.bash_prefix_rules_store", "Bash prefix rules (`Bash(npm run:*)`, `Bash(git *)`) belong to the persisted allow-rule lane this deployment declared — not to the generic param DSL; the rule is not compiled here");
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
bad(text, "unsupported.bash_prefix", "Bash prefix/legacy rules (`Bash(npm run:*)`, `Bash(git *)`) are the tool-specific lane — not supported by the generic param DSL (use createCoarseCommandNamePolicy or a dedicated batch)");
|
|
167
|
+
}
|
|
163
168
|
continue;
|
|
164
169
|
}
|
|
165
170
|
}
|
|
@@ -205,7 +210,7 @@ function compile(rules, caps, primaryFieldGeneric) {
|
|
|
205
210
|
}
|
|
206
211
|
export function validatePermissionRules(rules, opts) {
|
|
207
212
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
208
|
-
return compile(rules, caps, opts?.primaryFieldGeneric ?? "reject").issues;
|
|
213
|
+
return compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject").issues;
|
|
209
214
|
}
|
|
210
215
|
function isPlainObjectArgs(args) {
|
|
211
216
|
return typeof args === "object" && args !== null && !Array.isArray(args);
|
|
@@ -235,7 +240,7 @@ function ruleMessage(kind, ruleText, source) {
|
|
|
235
240
|
}
|
|
236
241
|
export function createPermissionRulePolicy(rules, opts) {
|
|
237
242
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
238
|
-
const { byTool, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject");
|
|
243
|
+
const { byTool, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject");
|
|
239
244
|
const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
|
|
240
245
|
if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
|
|
241
246
|
const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BeforeWriteHook, RunnerDeps, TaskSpec } from "../types.js";
|
|
1
|
+
import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
|
|
2
2
|
import type { Prepared } from "./prepare-task.js";
|
|
3
3
|
export interface PrepareMemoryInput {
|
|
4
4
|
spec: TaskSpec;
|
|
@@ -9,6 +9,7 @@ export interface PrepareMemoryInput {
|
|
|
9
9
|
current?: BeforeWriteHook;
|
|
10
10
|
};
|
|
11
11
|
writeToolsMounted: boolean;
|
|
12
|
+
memorySearchToolsPlanned: boolean;
|
|
12
13
|
admissionCtx: {
|
|
13
14
|
orgMemoryDenied: boolean;
|
|
14
15
|
complianceDegraded: boolean;
|
|
@@ -20,6 +21,7 @@ export interface PrepareMemoryInput {
|
|
|
20
21
|
export interface PrepareMemoryResult {
|
|
21
22
|
memoryEngineSession: Prepared["memoryEngineSession"];
|
|
22
23
|
memoryBlock: string | undefined;
|
|
24
|
+
memoryTools?: ToolSpec[];
|
|
23
25
|
seedFiles?: ReadonlyArray<{
|
|
24
26
|
path: string;
|
|
25
27
|
content: string;
|
|
@@ -2,7 +2,8 @@ import { admitMemoryScopes } from "../memory-admission.js";
|
|
|
2
2
|
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
3
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
4
4
|
import { normalizeMemorySpec } from "../memory.js";
|
|
5
|
-
import { MemoryEngine } from "../memory-engine/engine.js";
|
|
5
|
+
import { MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
|
+
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
6
7
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
7
8
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
8
9
|
export async function prepareMemory(input) {
|
|
@@ -35,6 +36,7 @@ export async function prepareMemory(input) {
|
|
|
35
36
|
}
|
|
36
37
|
const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
|
|
37
38
|
let memoryEngineSession;
|
|
39
|
+
let memoryTools;
|
|
38
40
|
if (useMemoryEngine && memorySpec) {
|
|
39
41
|
try {
|
|
40
42
|
const backend = deps.memoryBackend;
|
|
@@ -111,24 +113,36 @@ export async function prepareMemory(input) {
|
|
|
111
113
|
}
|
|
112
114
|
const personalMemoryDir = derivePersonalMemoryDir(engineRoot);
|
|
113
115
|
const personalControlDir = derivePersonalControlDir(engineRoot);
|
|
114
|
-
const createPersonalEngine = () =>
|
|
115
|
-
|
|
116
|
+
const createPersonalEngine = () => {
|
|
117
|
+
const personalBackend = typeof pinned === "string" && pinned
|
|
116
118
|
? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir })
|
|
117
|
-
: backend
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
: backend;
|
|
120
|
+
return {
|
|
121
|
+
engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident }),
|
|
122
|
+
backend: personalBackend,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const onEngineIncident = (err) => deps.onError?.(err, { phase: "memory", sessionId });
|
|
126
|
+
const retrievalBackend = (b) => b.retrievalView?.() ?? b;
|
|
127
|
+
const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
|
|
128
|
+
const pollutedOpts = (engine) => {
|
|
129
|
+
const rec = engine.sessionPollution(sessionId);
|
|
130
|
+
return rec !== undefined ? { polluted: { reason: rec.reason } } : {};
|
|
131
|
+
};
|
|
121
132
|
let writeEngine;
|
|
122
133
|
let writeHandle;
|
|
123
134
|
let injectFn;
|
|
124
135
|
let harvestBoth;
|
|
136
|
+
let toolPlanes;
|
|
125
137
|
if (dual) {
|
|
126
138
|
const projectEngine = new MemoryEngine({
|
|
127
139
|
backend,
|
|
128
140
|
memoryDir,
|
|
129
141
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
142
|
+
onIncident: onEngineIncident,
|
|
130
143
|
});
|
|
131
|
-
const
|
|
144
|
+
const personal = createPersonalEngine();
|
|
145
|
+
const personalEngine = personal.engine;
|
|
132
146
|
const p = planes;
|
|
133
147
|
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
|
|
134
148
|
const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null);
|
|
@@ -136,10 +150,14 @@ export async function prepareMemory(input) {
|
|
|
136
150
|
writeEngine = writeIsPersonal ? personalEngine : projectEngine;
|
|
137
151
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
138
152
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
153
|
+
toolPlanes = [
|
|
154
|
+
{ backend: retrievalBackend(backend), scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null), recordRetrieved: (ids) => projectEngine.recordRetrieved(ids) },
|
|
155
|
+
{ backend: retrievalBackend(personal.backend), scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null), recordRetrieved: (ids) => personalEngine.recordRetrieved(ids) },
|
|
156
|
+
];
|
|
139
157
|
harvestBoth = async () => {
|
|
140
158
|
const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
|
|
141
159
|
const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
|
|
142
|
-
const writeReport = await writeFirst[0].harvest(writeFirst[1]);
|
|
160
|
+
const writeReport = await writeFirst[0].harvest(writeFirst[1], pollutedOpts(writeFirst[0]));
|
|
143
161
|
let readReport;
|
|
144
162
|
let readFailure;
|
|
145
163
|
try {
|
|
@@ -155,24 +173,28 @@ export async function prepareMemory(input) {
|
|
|
155
173
|
};
|
|
156
174
|
}
|
|
157
175
|
else if (personalOnly) {
|
|
158
|
-
const
|
|
176
|
+
const personal = createPersonalEngine();
|
|
177
|
+
const personalEngine = personal.engine;
|
|
159
178
|
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
160
179
|
writeEngine = personalEngine;
|
|
161
180
|
writeHandle = handle;
|
|
162
181
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
163
|
-
harvestBoth = () => personalEngine.harvest(handle);
|
|
182
|
+
harvestBoth = () => personalEngine.harvest(handle, pollutedOpts(personalEngine));
|
|
183
|
+
toolPlanes = [{ backend: retrievalBackend(personal.backend), scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope), recordRetrieved: (ids) => personalEngine.recordRetrieved(ids) }];
|
|
164
184
|
}
|
|
165
185
|
else {
|
|
166
186
|
const engine = new MemoryEngine({
|
|
167
187
|
backend,
|
|
168
188
|
memoryDir,
|
|
169
189
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
190
|
+
onIncident: onEngineIncident,
|
|
170
191
|
});
|
|
171
192
|
const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
172
193
|
writeEngine = engine;
|
|
173
194
|
writeHandle = handle;
|
|
174
195
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
175
|
-
harvestBoth = () => engine.harvest(handle);
|
|
196
|
+
harvestBoth = () => engine.harvest(handle, pollutedOpts(engine));
|
|
197
|
+
toolPlanes = [{ backend: retrievalBackend(backend), scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope), recordRetrieved: (ids) => engine.recordRetrieved(ids) }];
|
|
176
198
|
}
|
|
177
199
|
memoryWriteGateRef.current = (w) => writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
178
200
|
const harvestSafe = async (phase = "terminal") => {
|
|
@@ -197,7 +219,22 @@ export async function prepareMemory(input) {
|
|
|
197
219
|
return undefined;
|
|
198
220
|
}
|
|
199
221
|
};
|
|
200
|
-
memoryEngineSession = {
|
|
222
|
+
memoryEngineSession = {
|
|
223
|
+
engine: writeEngine,
|
|
224
|
+
handle: writeHandle,
|
|
225
|
+
inject: injectFn,
|
|
226
|
+
harvest: harvestSafe,
|
|
227
|
+
pollution: {
|
|
228
|
+
polluted: () => writeEngine.sessionPollution(sessionId),
|
|
229
|
+
markPolluted: (reason) => writeEngine.markSessionPolluted(sessionId, reason),
|
|
230
|
+
},
|
|
231
|
+
contentSafety: {
|
|
232
|
+
trustedTools: new Set(memorySpec.trustedTools ?? []),
|
|
233
|
+
execIsExternalContent: memorySpec.execIsExternalContent === true,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
if (input.memorySearchToolsPlanned)
|
|
237
|
+
memoryTools = createMemoryEngineTools({ planes: toolPlanes });
|
|
201
238
|
}
|
|
202
239
|
catch (err) {
|
|
203
240
|
if (typeof err.code === "string" && (err.code.startsWith("config.memory_scope") || err.code.startsWith("config.memory_project"))) {
|
|
@@ -216,8 +253,11 @@ export async function prepareMemory(input) {
|
|
|
216
253
|
const injection = memoryEngineSession.inject();
|
|
217
254
|
if (injection.block.trim())
|
|
218
255
|
memoryBlock = injection.block;
|
|
256
|
+
if (memoryTools !== undefined) {
|
|
257
|
+
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
|
|
258
|
+
}
|
|
219
259
|
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
220
260
|
seedFiles = [injection.indexSeed];
|
|
221
261
|
}
|
|
222
|
-
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
262
|
+
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
223
263
|
}
|
|
@@ -64,6 +64,7 @@ export interface Prepared {
|
|
|
64
64
|
hit: boolean;
|
|
65
65
|
};
|
|
66
66
|
blockedToolCalls: Set<string>;
|
|
67
|
+
approvalSettledBy: Map<string, import("../tool-policy.js").ApprovalSettledBy>;
|
|
67
68
|
nestedStats: NestedUsageAccum;
|
|
68
69
|
rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
|
|
69
70
|
cwdRef?: CwdRef;
|
|
@@ -134,6 +135,17 @@ export interface Prepared {
|
|
|
134
135
|
handle: MemorySessionHandle;
|
|
135
136
|
inject: () => import("../memory-engine/engine.js").MemoryInjection;
|
|
136
137
|
harvest: (phase?: "checkpoint" | "terminal") => Promise<HarvestReport | undefined>;
|
|
138
|
+
pollution: {
|
|
139
|
+
polluted: () => {
|
|
140
|
+
at: number;
|
|
141
|
+
reason: string;
|
|
142
|
+
} | undefined;
|
|
143
|
+
markPolluted: (reason: string) => void;
|
|
144
|
+
};
|
|
145
|
+
contentSafety: {
|
|
146
|
+
trustedTools: ReadonlySet<string>;
|
|
147
|
+
execIsExternalContent: boolean;
|
|
148
|
+
};
|
|
137
149
|
};
|
|
138
150
|
ownedEnv?: ExecutionEnv;
|
|
139
151
|
suspendRef: {
|