@sema-agent/core 5.21.0 → 5.22.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 (73) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +45 -4
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +11 -0
  18. package/dist/core/hooks.d.ts +39 -0
  19. package/dist/core/hooks.js +26 -2
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +67 -1
  25. package/dist/core/memory-engine/engine.js +270 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +166 -0
  31. package/dist/core/memory-engine/layout.js +399 -0
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +91 -20
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +161 -0
  39. package/dist/core/permission-rule-org.js +211 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +313 -3
  42. package/dist/core/permission-rule-sync.d.ts +131 -0
  43. package/dist/core/permission-rule-sync.js +314 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +54 -1
  46. package/dist/core/runner/prepare-task.js +246 -27
  47. package/dist/core/runner/runtask.js +147 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +120 -2
  61. package/dist/core/tool-policy.js +116 -6
  62. package/dist/core/trace.d.ts +32 -1
  63. package/dist/core/types.d.ts +56 -3
  64. package/dist/index.d.ts +12 -7
  65. package/dist/index.js +10 -5
  66. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  69. package/dist/stores/file/permission-rule-adopt.js +95 -0
  70. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  71. package/dist/stores/file/permission-rule-store.js +189 -46
  72. package/dist/tools/fs/fs-search-tools.js +0 -1
  73. package/package.json +1 -1
@@ -0,0 +1,314 @@
1
+ import { parseAllowRuleText } from "./permission-rule-model.js";
2
+ import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
3
+ export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
4
+ export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
5
+ function refuseLocalOwnerSync() {
6
+ const e = new Error("a local-owner rule bucket cannot sync — syncing is an authenticated act, and this bucket has no cloud home until it is adopted (adoptFilePermissionRuleStore); this is NOT an authentication failure");
7
+ e.code = LOCAL_OWNER_UNSYNCABLE_CODE;
8
+ throw e;
9
+ }
10
+ const SYNC_MAX_ATTEMPTS = 8;
11
+ export async function syncPermissionRules(opts) {
12
+ if (opts.owner !== undefined) {
13
+ if (opts.owner.kind === "local-owner")
14
+ refuseLocalOwnerSync();
15
+ if (opts.owner.principal !== opts.principal) {
16
+ throw new Error(`contradictory identity: principal "${opts.principal}" and owner principal "${opts.owner.principal}" disagree`);
17
+ }
18
+ }
19
+ if (typeof opts.principal !== "string" || opts.principal === "") {
20
+ throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
21
+ }
22
+ const store = opts.provider.forPrincipal(opts.principal);
23
+ const writer = writerOf(store);
24
+ if (writer === undefined) {
25
+ throw new Error("the resolved permission-rule store has no write face — a sync round cannot land through it");
26
+ }
27
+ const raw = await writer.readRaw();
28
+ const request = {
29
+ principal: opts.principal,
30
+ replica: raw.actor,
31
+ state: { rules: raw.rules, tombstones: raw.tombstones },
32
+ ...(raw.observedVector !== undefined ? { observedVector: raw.observedVector } : {}),
33
+ };
34
+ const rawResponse = await opts.transport(PERMISSION_RULE_SYNC_PATH, request);
35
+ const response = parseRuleSyncResponse(rawResponse, opts.principal);
36
+ const warnings = [...response.warnings];
37
+ const dropped = [];
38
+ const inboundTombstones = [];
39
+ let tombstoneWithholding = false;
40
+ const forgedDot = (d) => d.actor === raw.actor && d.counter > raw.counter;
41
+ for (const t of response.merged.tombstones) {
42
+ if (t === undefined) {
43
+ tombstoneWithholding = true;
44
+ continue;
45
+ }
46
+ const forged = forgedDot(t.deletedBy) ? t.deletedBy : t.removedDots.find(forgedDot);
47
+ if (forged !== undefined) {
48
+ dropped.push({ rule: t.rule, scope: t.scope, dot: forged, reason: "own_actor_forged" });
49
+ tombstoneWithholding = true;
50
+ continue;
51
+ }
52
+ inboundTombstones.push(t);
53
+ }
54
+ if (tombstoneWithholding) {
55
+ warnings.push("the response carried at least one malformed tombstone — every inbound add is withheld this round (legal tombstones land; the adds re-arrive once the peer repairs the tombstone: full-state exchange has no missed-forever window)");
56
+ }
57
+ const inboundRules = [];
58
+ if (!tombstoneWithholding) {
59
+ for (const r of response.merged.rules) {
60
+ const n = normalizePersistedRule(r);
61
+ if ("reject" in n) {
62
+ for (const a of r.adds)
63
+ dropped.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: n.reject });
64
+ continue;
65
+ }
66
+ const kept = r.adds.filter((a) => {
67
+ if (a.dot.actor === raw.actor && a.dot.counter > raw.counter) {
68
+ dropped.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: "own_actor_forged" });
69
+ return false;
70
+ }
71
+ return true;
72
+ });
73
+ if (kept.length > 0)
74
+ inboundRules.push({ ...r, adds: kept });
75
+ }
76
+ }
77
+ const inbound = { rules: inboundRules, tombstones: inboundTombstones };
78
+ const quarantine = [];
79
+ const localScreen = screenRuleSyncState({ rules: raw.rules, tombstones: raw.tombstones });
80
+ for (const rej of localScreen.rejected) {
81
+ if (rej.reason === "invalid_rule_text" || rej.reason === "metadata_mismatch") {
82
+ quarantine.push({ rule: rej.rule, scope: rej.scope, dots: [rej.dot], reason: rej.reason });
83
+ dropped.push(rej);
84
+ }
85
+ }
86
+ const localDots = new Set(raw.rules.flatMap((r) => r.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
87
+ for (const d of response.dropped) {
88
+ if (localDots.has(JSON.stringify([d.dot.actor, d.dot.counter]))) {
89
+ const reason = d.reason === "below_gc_frontier" ? "below_gc_frontier" : "server_rejected";
90
+ quarantine.push({ rule: d.rule, scope: d.scope, dots: [d.dot], reason });
91
+ dropped.push({ rule: d.rule, scope: d.scope, dot: d.dot, reason });
92
+ }
93
+ else {
94
+ warnings.push(`the server dropped an inbound-side record (${d.rule}, dot ${d.dot.actor}#${d.dot.counter}): ${d.reason}`);
95
+ }
96
+ }
97
+ const clean = !tombstoneWithholding;
98
+ let current = raw;
99
+ for (let attempt = 0; attempt < SYNC_MAX_ATTEMPTS; attempt++) {
100
+ const preLive = applyTombstones(current.rules, current.tombstones);
101
+ const prospective = joinRuleStates({ rules: current.rules, tombstones: current.tombstones }, inbound);
102
+ const delta = {
103
+ kind: "sync-join",
104
+ inbound,
105
+ ...(response.gcFrontier !== undefined ? { gcFrontier: response.gcFrontier } : {}),
106
+ ...(clean ? { observedVector: joinFrontiers(current.observedVector ?? {}, ruleSyncVector(prospective)) } : {}),
107
+ ...(quarantine.length > 0 ? { quarantine } : {}),
108
+ };
109
+ let res;
110
+ try {
111
+ res = await writer.apply(delta, { expectedRev: current.rev });
112
+ }
113
+ catch (err) {
114
+ return {
115
+ ok: false,
116
+ pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
117
+ landed: { newAdds: 0, newTombstones: 0 },
118
+ resurrected: [],
119
+ dropped,
120
+ rev: current.rev,
121
+ warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
122
+ };
123
+ }
124
+ if ("conflict" in res) {
125
+ current = await writer.readRaw();
126
+ continue;
127
+ }
128
+ const landedRaw = await writer.readRaw();
129
+ const postLive = applyTombstones(landedRaw.rules, landedRaw.tombstones);
130
+ const resurrected = [];
131
+ for (const r of postLive) {
132
+ const pre = preLive.find((p) => p.rule === r.rule && sameScope(p.scope, r.scope));
133
+ if (pre !== undefined)
134
+ continue;
135
+ const hadAdds = current.rules.some((p) => p.rule === r.rule && sameScope(p.scope, r.scope));
136
+ const covered = current.tombstones.some((t) => t.rule === r.rule && sameScope(t.scope, r.scope));
137
+ if (hadAdds && covered) {
138
+ const preRuleDots = new Set(current.rules.filter((p) => p.rule === r.rule && sameScope(p.scope, r.scope)).flatMap((p) => p.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
139
+ const by = r.adds.find((a) => !preRuleDots.has(JSON.stringify([a.dot.actor, a.dot.counter]))) ?? r.adds[0];
140
+ if (by !== undefined)
141
+ resurrected.push({ rule: r.rule, scope: r.scope, byDot: by.dot });
142
+ }
143
+ }
144
+ const dotKeyOf = (d) => JSON.stringify([d.actor, d.counter]);
145
+ const preDots = new Set(current.rules.flatMap((r) => r.adds.map((a) => dotKeyOf(a.dot))));
146
+ const newAdds = landedRaw.rules.flatMap((r) => r.adds).filter((a) => !preDots.has(dotKeyOf(a.dot))).length;
147
+ const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind === "global" ? null : t.scope.root, dotKeyOf(t.deletedBy)]);
148
+ const preTombs = new Set(current.tombstones.map(tombKeyOf));
149
+ const newTombstones = landedRaw.tombstones.filter((t) => !preTombs.has(tombKeyOf(t))).length;
150
+ const landedReport = res.sync;
151
+ for (const d of landedReport?.droppedInbound ?? []) {
152
+ if (!dropped.some((x) => x.dot.actor === d.dot.actor && x.dot.counter === d.dot.counter && x.reason === d.reason))
153
+ dropped.push(d);
154
+ }
155
+ for (const q of landedReport?.quarantined ?? []) {
156
+ if (!dropped.some((x) => x.dot.actor === q.dot.actor && x.dot.counter === q.dot.counter && x.reason === q.reason))
157
+ dropped.push(q);
158
+ }
159
+ return {
160
+ ok: dropped.length === 0 && warnings.length === 0,
161
+ pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
162
+ landed: { newAdds, newTombstones },
163
+ resurrected,
164
+ dropped,
165
+ rev: landedRaw.rev,
166
+ ...(warnings.length > 0 ? { warnings } : {}),
167
+ };
168
+ }
169
+ return {
170
+ ok: false,
171
+ pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
172
+ landed: { newAdds: 0, newTombstones: 0 },
173
+ resurrected: [],
174
+ dropped,
175
+ rev: current.rev,
176
+ warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
177
+ };
178
+ }
179
+ function isDot(v) {
180
+ const d = v;
181
+ return typeof d?.actor === "string" && d.actor !== "" && typeof d.counter === "number" && Number.isFinite(d.counter);
182
+ }
183
+ function pickScope(v) {
184
+ const s = v;
185
+ if (s?.kind === "global")
186
+ return { kind: "global" };
187
+ if (s?.kind === "project" && typeof s.root === "string" && s.root !== "")
188
+ return { kind: "project", root: s.root };
189
+ return undefined;
190
+ }
191
+ function pickAdd(v) {
192
+ const a = v;
193
+ if (!isDot(a?.dot))
194
+ return undefined;
195
+ if (a.origin !== "user" && a.origin !== "imported-cc" && a.origin !== "starter")
196
+ return undefined;
197
+ if (typeof a.createdAt !== "string")
198
+ return undefined;
199
+ return { dot: { actor: a.dot.actor, counter: a.dot.counter }, origin: a.origin, createdAt: a.createdAt };
200
+ }
201
+ function pickFrontier(v) {
202
+ if (v === null || typeof v !== "object" || Array.isArray(v))
203
+ return undefined;
204
+ const out = {};
205
+ for (const [actor, counter] of Object.entries(v)) {
206
+ if (typeof counter !== "number" || !Number.isFinite(counter))
207
+ return undefined;
208
+ out[actor] = counter;
209
+ }
210
+ return out;
211
+ }
212
+ export function parseRuleSyncResponse(raw, expectedPrincipal) {
213
+ if (raw === null || typeof raw !== "object") {
214
+ throw new Error("the sync response is not an object — protocol error, nothing landed");
215
+ }
216
+ const r = raw;
217
+ if (typeof r.principal !== "string") {
218
+ throw new Error("the sync response carries no principal echo — protocol error, nothing landed");
219
+ }
220
+ if (r.principal !== expectedPrincipal) {
221
+ throw new Error(`the sync response echoes principal "${r.principal}" but this round synced "${expectedPrincipal}" — the transport is wired to the wrong bucket; refusing the whole round`);
222
+ }
223
+ const merged = r.merged;
224
+ if (merged === null || typeof merged !== "object" || !Array.isArray(merged.rules) || !Array.isArray(merged.tombstones)) {
225
+ throw new Error("the sync response carries no merged state — protocol error, nothing landed");
226
+ }
227
+ let gcFrontier;
228
+ if (r.gcFrontier !== undefined) {
229
+ gcFrontier = pickFrontier(r.gcFrontier);
230
+ if (gcFrontier === undefined) {
231
+ throw new Error("the sync response carries an unparseable gcFrontier — refusing the whole round (a garbled frontier must not drive the fence arm)");
232
+ }
233
+ }
234
+ const warnings = [];
235
+ const rules = [];
236
+ for (const entry of merged.rules) {
237
+ const e = entry;
238
+ const scope = pickScope(e?.scope);
239
+ if (typeof e?.rule !== "string" || scope === undefined || !Array.isArray(e.adds) || typeof e.command !== "string" || typeof e.tool !== "string" || typeof e.match !== "string") {
240
+ warnings.push("the response carried an unshapeable rule entry — skipped (it never reaches the local store)");
241
+ continue;
242
+ }
243
+ const adds = [];
244
+ let addDamaged = false;
245
+ for (const a of e.adds) {
246
+ const picked = pickAdd(a);
247
+ if (picked === undefined) {
248
+ addDamaged = true;
249
+ continue;
250
+ }
251
+ adds.push(picked);
252
+ }
253
+ if (addDamaged)
254
+ warnings.push(`rule "${e.rule}" carried at least one unshapeable add — skipped those adds`);
255
+ if (adds.length === 0)
256
+ continue;
257
+ rules.push({
258
+ rule: e.rule,
259
+ tool: e.tool,
260
+ match: e.match,
261
+ command: e.command,
262
+ scope,
263
+ adds,
264
+ });
265
+ }
266
+ const tombstones = [];
267
+ for (const entry of merged.tombstones) {
268
+ const t = entry;
269
+ const scope = pickScope(t?.scope);
270
+ const canonicalTomb = typeof t?.rule === "string" ? parseAllowRuleText(t.rule) : undefined;
271
+ if (typeof t?.rule !== "string" ||
272
+ canonicalTomb === undefined ||
273
+ "reject" in canonicalTomb ||
274
+ canonicalTomb.rule.rule !== t.rule ||
275
+ scope === undefined ||
276
+ !isDot(t.deletedBy) ||
277
+ !Array.isArray(t.removedDots) ||
278
+ t.removedDots.length === 0 ||
279
+ !t.removedDots.every(isDot)) {
280
+ tombstones.push(undefined);
281
+ continue;
282
+ }
283
+ tombstones.push({
284
+ rule: t.rule,
285
+ scope,
286
+ removedDots: t.removedDots.map((d) => ({ actor: d.actor, counter: d.counter })),
287
+ deletedBy: { actor: t.deletedBy.actor, counter: t.deletedBy.counter },
288
+ });
289
+ }
290
+ const dropped = [];
291
+ if (r.dropped !== undefined) {
292
+ if (!Array.isArray(r.dropped)) {
293
+ warnings.push("the response carried a non-array dropped field — ignored");
294
+ }
295
+ else {
296
+ for (const entry of r.dropped) {
297
+ const d = entry;
298
+ const scope = pickScope(d?.scope);
299
+ if (typeof d?.rule !== "string" || scope === undefined || !isDot(d.dot) || typeof d.reason !== "string") {
300
+ warnings.push("the response carried an unshapeable dropped entry — ignored (the server's own drop discipline is its contract obligation)");
301
+ continue;
302
+ }
303
+ dropped.push({ rule: d.rule, scope, dot: { actor: d.dot.actor, counter: d.dot.counter }, reason: d.reason });
304
+ }
305
+ }
306
+ }
307
+ return {
308
+ principal: r.principal,
309
+ merged: { rules, tombstones },
310
+ ...(gcFrontier !== undefined ? { gcFrontier } : {}),
311
+ dropped,
312
+ warnings,
313
+ };
314
+ }
@@ -2,7 +2,7 @@ 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 { MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
5
+ import { MEMORY_PREFERENCE_DISCIPLINE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
6
6
  import { createMemoryEngineTools } from "../memory-engine/tools.js";
7
7
  import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
8
8
  import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
@@ -151,13 +151,23 @@ export async function prepareMemory(input) {
151
151
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
152
152
  injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
153
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) },
154
+ {
155
+ backend: retrievalBackend(backend),
156
+ scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
157
+ recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
158
+ challengeExclusions: () => projectEngine.readChallengeExclusions(),
159
+ },
160
+ {
161
+ backend: retrievalBackend(personal.backend),
162
+ scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
163
+ recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
164
+ challengeExclusions: () => personalEngine.readChallengeExclusions(),
165
+ },
156
166
  ];
157
167
  harvestBoth = async () => {
158
168
  const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
159
169
  const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
160
- const writeReport = await writeFirst[0].harvest(writeFirst[1], pollutedOpts(writeFirst[0]));
170
+ const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId });
161
171
  let readReport;
162
172
  let readFailure;
163
173
  try {
@@ -179,8 +189,15 @@ export async function prepareMemory(input) {
179
189
  writeEngine = personalEngine;
180
190
  writeHandle = handle;
181
191
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
182
- harvestBoth = () => personalEngine.harvest(handle, pollutedOpts(personalEngine));
183
- toolPlanes = [{ backend: retrievalBackend(personal.backend), scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope), recordRetrieved: (ids) => personalEngine.recordRetrieved(ids) }];
192
+ harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId });
193
+ toolPlanes = [
194
+ {
195
+ backend: retrievalBackend(personal.backend),
196
+ scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
197
+ recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
198
+ challengeExclusions: () => personalEngine.readChallengeExclusions(),
199
+ },
200
+ ];
184
201
  }
185
202
  else {
186
203
  const engine = new MemoryEngine({
@@ -193,8 +210,15 @@ export async function prepareMemory(input) {
193
210
  writeEngine = engine;
194
211
  writeHandle = handle;
195
212
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
196
- harvestBoth = () => engine.harvest(handle, pollutedOpts(engine));
197
- toolPlanes = [{ backend: retrievalBackend(backend), scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope), recordRetrieved: (ids) => engine.recordRetrieved(ids) }];
213
+ harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId });
214
+ toolPlanes = [
215
+ {
216
+ backend: retrievalBackend(backend),
217
+ scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
218
+ recordRetrieved: (ids) => engine.recordRetrieved(ids),
219
+ challengeExclusions: () => engine.readChallengeExclusions(),
220
+ },
221
+ ];
198
222
  }
199
223
  memoryWriteGateRef.current = (w) => writeEngine.gateWrite(writeHandle, w.key, w.content);
200
224
  const harvestSafe = async (phase = "terminal") => {
@@ -256,6 +280,9 @@ export async function prepareMemory(input) {
256
280
  if (memoryTools !== undefined) {
257
281
  memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
258
282
  }
283
+ if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted) {
284
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
285
+ }
259
286
  if (memoryBlock !== undefined && injection.indexSeed !== undefined)
260
287
  seedFiles = [injection.indexSeed];
261
288
  }
@@ -1,6 +1,7 @@
1
1
  import { AgentHarness, type ThinkingLevel } from "../../internal/harness.js";
2
2
  import type { Model } from "../../internal/llm.js";
3
3
  import { type CompactionForkContext } from "../auto-compaction.js";
4
+ import { type AutoModeDecider } from "../auto-mode.js";
4
5
  import { type MaterializedMcp } from "../mcp.js";
5
6
  import { type MaterializedA2a } from "../a2a.js";
6
7
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
@@ -869,7 +870,20 @@ export interface InheritedGate {
869
870
  orgAdmissionGoverned?: true;
870
871
  /** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
871
872
  * RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
872
- * enforces, so a deps-level baseline is inherited too). `onAsk` is the ancestor's frozen
873
+ * enforces, so a deps-level baseline is inherited too).
874
+ *
875
+ * ⚖️ A-005.13 (ruled with the F-012 family, 2026-08-09): the ancestor's per-run BUDGET LEDGER does
876
+ * NOT travel this chain — by design, not by omission. An inherited policy's `check` receives the
877
+ * CHILD run's own `ToolCallRequest.budget` snapshot (the child's durable resource ledger), so a
878
+ * SELF-LIMITING ancestor policy ("allow N escalations, then ask") sees a fresh ledger in each
879
+ * delegated child rather than a continuation of the ancestor's counts. Freezing/snapshotting the
880
+ * ancestor ledger was considered and rejected: a frozen ledger goes stale the moment the ancestor
881
+ * keeps running (a stale ledger is a NEW wrongness surface, not a fix), and the durable resource
882
+ * ledger's identity is per-run by contract. A deployment wanting cross-delegation budget coherence
883
+ * carries it in its own policy state (the closure travels the chain intact — a closure-counter
884
+ * policy DOES aggregate across the tree, since every layer evaluates the same instance).
885
+ *
886
+ * `onAsk` is the ancestor's frozen
873
887
  * `spec.onAsk ?? deps.onAsk` — an ancestor `ask` resolves at that ancestor's own approver
874
888
  * (headless ⇒ deny), never at the child's. `durableMandate` is frozen at chain-assembly time when the
875
889
  * ancestor ran under a durable-approval regime that would actually PARK its own plain asks — a
@@ -883,6 +897,29 @@ export interface InheritedGate {
883
897
  policy: ToolPolicy;
884
898
  onAsk?: OnAsk;
885
899
  durableMandate?: boolean;
900
+ /** The CONTENT-ask twin of `durableMandate`, frozen from the QUESTION seat (`spec.onQuestion ??
901
+ * deps.onQuestion`) on the same rule: a `forceDurableGate` entitlement, or `durableApproval` with no
902
+ * LIVE question face frozen (absent, or the reserved resume placeholder — neither can answer). A
903
+ * wrapper seeing an `ask` on the question tool consults THIS bit, never the permission one: the two
904
+ * asks are answered by two different faces, and a run with a live `onQuestion` and no permission
905
+ * approver can answer a delegated question perfectly well. When it IS set the delegated question is
906
+ * marked unresolvable and takes the child gate's park / honest-refusal leg, exactly as an inherited
907
+ * permission ask does under `durableMandate`. */
908
+ contentMandate?: boolean;
909
+ /**
910
+ * F-012 (#106) — the ancestor's FROZEN auto-mode classifier: the same decider instance the
911
+ * ancestor's own gate consults for a surviving ask (`ToolGateInput.autoMode`). Without it the
912
+ * ancestor's ask DECISION CHAIN did not travel — the wrappers resolved an inherited ask straight
913
+ * at the frozen approver, so an ask the ancestor's classifier would have BLOCKED executed in the
914
+ * child on the frozen approver's allow. The wrapper runs it BEFORE the frozen approver, in the
915
+ * ancestor's own gate order (classifier → approver): `block` ⇒ deny (`decisionReason:"classifier"`),
916
+ * `allow` ⇒ the classifier's auto-allow (the frozen approver is not consulted), anything else
917
+ * falls through to the approver chain. Never consulted for the reserved question tool nor under a
918
+ * durable mandate (a marked call must reach the park with no synchronous decision-maker between).
919
+ */
920
+ autoMode?: {
921
+ decider: AutoModeDecider;
922
+ };
886
923
  /**
887
924
  * Set ONLY on an entry whose `policy` is the folded form of an ancestor's PreToolUse screening face
888
925
  * (issue #33 — the face used to stop at the task that installed it, so a call the ancestor had
@@ -1022,6 +1059,22 @@ export interface RunInternals {
1022
1059
  * Absent ⇒ no inheritance (a top-level task; fully backward-compatible).
1023
1060
  */
1024
1061
  inheritedGate?: InheritedGate;
1062
+ /**
1063
+ * design/180 half A — the delegation RUNTIME-PROVENANCE channel. Minted by the spawning delegation
1064
+ * tool when the parent chain is armed (the parent mounts a memory session, or is itself recording
1065
+ * for ITS parent): `ref` is the child's monotonic aggregate — the child's recorder writes into it
1066
+ * as the run progresses (live faces read the current bits; the terminal attestation reduces from
1067
+ * it); `contentSafety` is the chain's FROZEN classification snapshot — the child may narrow it
1068
+ * with its own config, never widen (design/180 A-2). Trusted internals chain only, same posture as
1069
+ * {@link inheritedGate}. Absent ⇒ the child records nothing (its deliveries then read `unknown`,
1070
+ * and every judgment falls back to the static floor — fail-closed by construction).
1071
+ */
1072
+ delegationProvenance?: {
1073
+ ref: {
1074
+ current: import("../memory-engine/delegation-provenance.js").DelegationProvenanceAggregate;
1075
+ };
1076
+ contentSafety: import("../memory-engine/delegation-provenance.js").DelegationContentSafety;
1077
+ };
1025
1078
  /**
1026
1079
  * #22 (ruled 2026-08-05) — the SESSION-scoped org-admission freeze for a SAME-SESSION continuation
1027
1080
  * that is NOT a checkpoint resume: a retained background child revived through the in-process resume