@sema-agent/core 5.17.0 → 5.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/agents/subagent.js +24 -0
  3. package/dist/core/auto-compaction.d.ts +6 -0
  4. package/dist/core/auto-compaction.js +15 -1
  5. package/dist/core/checkpoint-store.d.ts +1 -0
  6. package/dist/core/governance-codes.js +1 -0
  7. package/dist/core/hooks.d.ts +8 -0
  8. package/dist/core/hooks.js +17 -0
  9. package/dist/core/mcp.js +3 -0
  10. package/dist/core/memory-engine/content-origin.d.ts +27 -0
  11. package/dist/core/memory-engine/content-origin.js +38 -0
  12. package/dist/core/memory-engine/engine.d.ts +12 -2
  13. package/dist/core/memory-engine/engine.js +172 -12
  14. package/dist/core/memory-engine/file-backend.d.ts +4 -0
  15. package/dist/core/memory-engine/file-backend.js +25 -3
  16. package/dist/core/memory-engine/index.d.ts +2 -1
  17. package/dist/core/memory-engine/index.js +2 -1
  18. package/dist/core/memory-engine/layout.d.ts +16 -0
  19. package/dist/core/memory-engine/layout.js +90 -2
  20. package/dist/core/memory-engine/sync-client.d.ts +1 -0
  21. package/dist/core/memory-engine/sync-client.js +23 -5
  22. package/dist/core/memory-engine/tools.d.ts +55 -0
  23. package/dist/core/memory-engine/tools.js +307 -0
  24. package/dist/core/memory-engine/types.d.ts +1 -1
  25. package/dist/core/memory.d.ts +4 -0
  26. package/dist/core/memory.js +15 -2
  27. package/dist/core/permission-rule-consent.d.ts +131 -0
  28. package/dist/core/permission-rule-consent.js +307 -0
  29. package/dist/core/permission-rule-model.d.ts +66 -0
  30. package/dist/core/permission-rule-model.js +135 -0
  31. package/dist/core/permission-rule-store.d.ts +89 -0
  32. package/dist/core/permission-rule-store.js +145 -0
  33. package/dist/core/permission-rules.d.ts +3 -2
  34. package/dist/core/permission-rules.js +9 -4
  35. package/dist/core/runner/prepare-memory.d.ts +3 -1
  36. package/dist/core/runner/prepare-memory.js +54 -14
  37. package/dist/core/runner/prepare-task.d.ts +11 -0
  38. package/dist/core/runner/prepare-task.js +192 -10
  39. package/dist/core/runner/runtask.js +24 -0
  40. package/dist/core/runner/tool-output-projection.js +1 -1
  41. package/dist/core/tool-policy.d.ts +8 -1
  42. package/dist/core/tool-policy.js +36 -0
  43. package/dist/core/tools.js +1 -0
  44. package/dist/core/trace.d.ts +20 -0
  45. package/dist/core/types.d.ts +14 -0
  46. package/dist/core/wiring-manifest.d.ts +5 -1
  47. package/dist/core/wiring-manifest.js +2 -0
  48. package/dist/index.d.ts +6 -2
  49. package/dist/index.js +5 -1
  50. package/dist/stores/file/permission-rule-store.d.ts +32 -0
  51. package/dist/stores/file/permission-rule-store.js +213 -0
  52. package/dist/tools/fs/fs-bash.js +12 -5
  53. package/dist/tools/fs/fs-shared.d.ts +12 -0
  54. package/dist/tools/fs/fs-shared.js +65 -1
  55. package/dist/tools/web.js +2 -0
  56. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ import type { PersistedAllowRule, RuleAdd, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
2
+ import type { StoreDurability, StoreFidelity } from "./checkpoint-store.js";
3
+ export interface StoredAllowRules {
4
+ rules: PersistedAllowRule[];
5
+ tombstones: RuleTombstone[];
6
+ rev: number;
7
+ checksum?: string;
8
+ }
9
+ export interface PermissionRuleStore {
10
+ list(): Promise<StoredAllowRules>;
11
+ readonly durability?: StoreDurability;
12
+ readonly fidelity?: StoreFidelity;
13
+ }
14
+ export interface PermissionRuleStoreProvider {
15
+ forPrincipal(principal: string | undefined): PermissionRuleStore;
16
+ }
17
+ export interface PutResult {
18
+ rev: number;
19
+ }
20
+ export interface RedemptionAuthorization {
21
+ recordId: string;
22
+ principal: string;
23
+ }
24
+ export interface RuleAddDelta {
25
+ kind: "redemption-add";
26
+ rule: string;
27
+ scope: RuleScope;
28
+ tool: PersistedAllowRule["tool"];
29
+ match: PersistedAllowRule["match"];
30
+ command: string;
31
+ add: RuleAdd;
32
+ redemption: RedemptionAuthorization;
33
+ }
34
+ export interface RuleDeleteDelta {
35
+ kind: "tighten-delete";
36
+ tombstone: RuleTombstone;
37
+ }
38
+ export type RuleWriteDelta = RuleAddDelta | RuleDeleteDelta;
39
+ export interface PermissionRuleWriter {
40
+ nextDot(): Promise<RuleDot>;
41
+ apply(delta: RuleWriteDelta, opts: {
42
+ expectedRev: number;
43
+ }): Promise<PutResult | {
44
+ conflict: true;
45
+ rev: number;
46
+ }>;
47
+ }
48
+ export declare const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
49
+ export interface WritablePermissionRuleStore extends PermissionRuleStore {
50
+ readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
51
+ }
52
+ export declare function writerOf(store: PermissionRuleStore): PermissionRuleWriter | undefined;
53
+ export declare function sameScope(a: RuleScope, b: RuleScope): boolean;
54
+ export declare function applyTombstones(rules: readonly PersistedAllowRule[], tombstones: readonly RuleTombstone[]): PersistedAllowRule[];
55
+ export declare function foldDelta(rules: readonly PersistedAllowRule[], delta: RuleAddDelta): PersistedAllowRule[];
56
+ export declare function addDotsOf(rules: readonly PersistedAllowRule[]): RuleDot[];
57
+ export declare function assertDeleteDeltaCarriesNoAdd(delta: RuleDeleteDelta): void;
58
+ export declare function ruleStoreChecksum(payload: unknown): Promise<string>;
59
+ export type RemoveResult = {
60
+ status: "removed";
61
+ rev: number;
62
+ stillLive: boolean;
63
+ } | {
64
+ status: "no-op";
65
+ rev: number;
66
+ } | {
67
+ status: "failed";
68
+ error: string;
69
+ };
70
+ export declare function removePersistedRule(opts: {
71
+ rule: string;
72
+ scope: RuleScope;
73
+ principal: string;
74
+ provider: PermissionRuleStoreProvider;
75
+ }): Promise<RemoveResult>;
76
+ export declare function errText(err: unknown): string;
77
+ export declare const EMPTY_RULE_STORE: PermissionRuleStore;
78
+ export declare class InMemoryPermissionRuleStore implements WritablePermissionRuleStore {
79
+ private readonly actor;
80
+ readonly durability: StoreDurability;
81
+ readonly fidelity: StoreFidelity;
82
+ private rules;
83
+ private tombstones;
84
+ private rev;
85
+ private counter;
86
+ constructor(actor?: string);
87
+ list(): Promise<StoredAllowRules>;
88
+ readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
89
+ }
@@ -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
- 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)");
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 = () => new MemoryEngine({
115
- backend: typeof pinned === "string" && pinned
116
+ const createPersonalEngine = () => {
117
+ const personalBackend = typeof pinned === "string" && pinned
116
118
  ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir })
117
- : backend,
118
- memoryDir: personalMemoryDir,
119
- controlDir: personalControlDir,
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 personalEngine = createPersonalEngine();
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 personalEngine = createPersonalEngine();
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 = { engine: writeEngine, handle: writeHandle, inject: injectFn, harvest: harvestSafe };
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
  }
@@ -134,6 +134,17 @@ export interface Prepared {
134
134
  handle: MemorySessionHandle;
135
135
  inject: () => import("../memory-engine/engine.js").MemoryInjection;
136
136
  harvest: (phase?: "checkpoint" | "terminal") => Promise<HarvestReport | undefined>;
137
+ pollution: {
138
+ polluted: () => {
139
+ at: number;
140
+ reason: string;
141
+ } | undefined;
142
+ markPolluted: (reason: string) => void;
143
+ };
144
+ contentSafety: {
145
+ trustedTools: ReadonlySet<string>;
146
+ execIsExternalContent: boolean;
147
+ };
137
148
  };
138
149
  ownedEnv?: ExecutionEnv;
139
150
  suspendRef: {