@sema-agent/core 5.25.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agents/agent-definition.js +5 -0
  3. package/dist/agents/send-message-tool.js +1 -0
  4. package/dist/agents/subagent.d.ts +1 -0
  5. package/dist/agents/subagent.js +5 -0
  6. package/dist/core/hooks.js +3 -2
  7. package/dist/core/memory-engine/dual-root.js +3 -1
  8. package/dist/core/memory-engine/engine.d.ts +45 -1
  9. package/dist/core/memory-engine/engine.js +23 -5
  10. package/dist/core/memory-engine/index.d.ts +1 -1
  11. package/dist/core/memory-engine/index.js +1 -1
  12. package/dist/core/permission-rule-consent.js +8 -1
  13. package/dist/core/runner/compaction-call-options.d.ts +4 -4
  14. package/dist/core/runner/compaction-call-options.js +3 -4
  15. package/dist/core/runner/prepare-memory.d.ts +34 -15
  16. package/dist/core/runner/prepare-memory.js +85 -17
  17. package/dist/core/runner/prepare-task.js +31 -7
  18. package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
  19. package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
  20. package/dist/core/task-registry-agent.js +3 -3
  21. package/dist/core/task-registry-monitor.js +6 -5
  22. package/dist/core/tool-result-budget.d.ts +1 -1
  23. package/dist/core/tool-result-budget.js +3 -3
  24. package/dist/core/tool-result-store.d.ts +164 -9
  25. package/dist/core/tool-result-store.js +82 -23
  26. package/dist/core/types.d.ts +68 -0
  27. package/dist/core/untrusted-text.d.ts +6 -2
  28. package/dist/core/untrusted-text.js +1 -1
  29. package/dist/engine/session/import-validate.js +2 -1
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.js +3 -3
  32. package/dist/orchestration/workflow.js +2 -0
  33. package/dist/prompts/default.d.ts +11 -0
  34. package/dist/prompts/default.js +3 -0
  35. package/dist/stores/file/fs-atomic.d.ts +1 -1
  36. package/dist/stores/file/tool-result-store.d.ts +45 -9
  37. package/dist/stores/file/tool-result-store.js +76 -9
  38. package/dist/tools/fs/fs-shared.js +5 -4
  39. package/package.json +1 -1
@@ -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 createPersonalEngine = () => {
117
- const personalBackend = typeof pinned === "string" && pinned
118
- ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir })
119
- : backend;
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) => writeEngine.gateWrite(writeHandle, w.key, w.content);
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" && (err.code.startsWith("config.memory_scope") || err.code.startsWith("config.memory_project"))) {
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
- if (injection.block.trim())
279
- memoryBlock = injection.block;
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 } : {}) };
@@ -352,6 +352,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
352
352
  e.code = "config.tool_materialize_invalid";
353
353
  throw e;
354
354
  }
355
+ if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
356
+ const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
357
+ e.code = "config.memory_persistence_invalid";
358
+ throw e;
359
+ }
355
360
  if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
356
361
  const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
357
362
  `lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
@@ -1154,6 +1159,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1154
1159
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
1155
1160
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
1156
1161
  ...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
1162
+ ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
1157
1163
  getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
1158
1164
  parentCwd: taskRootPath,
1159
1165
  ...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
@@ -1687,20 +1693,24 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1687
1693
  `Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
1688
1694
  }
1689
1695
  if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
1690
- shellGatedBash = !egressTools.has("Bash") && !irreversibilityTier.has("Bash");
1691
- irreversibilityTier.set("Bash", shellGate === "always" ? "always" : "maybe");
1696
+ const bashTierBefore = irreversibilityTier.get("Bash");
1697
+ shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
1698
+ const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
1699
+ irreversibilityTier.set("Bash", bashEffectiveTier);
1692
1700
  irreversibleTools.add("Bash");
1693
1701
  const shellReadBoundary = () => ({
1694
1702
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
1695
1703
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
1696
1704
  });
1697
- if (shellGate === "classify")
1705
+ if (shellGate === "classify" && shellGatedBash)
1698
1706
  reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
1699
1707
  if (backgroundTaskToolsActive) {
1700
- shellGatedMonitor = !egressTools.has("Monitor") && !irreversibilityTier.has("Monitor");
1701
- irreversibilityTier.set("Monitor", shellGate === "always" ? "always" : "maybe");
1708
+ const monitorTierBefore = irreversibilityTier.get("Monitor");
1709
+ shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
1710
+ const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
1711
+ irreversibilityTier.set("Monitor", monitorEffectiveTier);
1702
1712
  irreversibleTools.add("Monitor");
1703
- if (shellGate === "classify")
1713
+ if (shellGate === "classify" && shellGatedMonitor)
1704
1714
  reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
1705
1715
  }
1706
1716
  }
@@ -1936,7 +1946,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1936
1946
  sessionId,
1937
1947
  taskRootPath,
1938
1948
  memoryWriteGateRef,
1939
- writeToolsMounted: tools.some((t) => t.name === "Write") && !(toolFaceSnapshot.exclude?.includes("Write") ?? false),
1949
+ writeToolsMounted: tools.some((t) => t.name === "Write") &&
1950
+ !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
1951
+ !(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
1952
+ memoryPersistenceDeclared: spec.memoryPersistenceCapable,
1953
+ rosterCanPersist: spec.memoryPersistenceCapable ??
1954
+ tools.some((t) => {
1955
+ if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
1956
+ return false;
1957
+ const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
1958
+ if (effect === "read")
1959
+ return false;
1960
+ if (t.name !== "Write" && t.name !== "Edit" && t.name !== "NotebookEdit" && t.name !== "Bash")
1961
+ return false;
1962
+ return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
1963
+ }),
1940
1964
  memorySearchToolsPlanned,
1941
1965
  admissionCtx: {
1942
1966
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
@@ -12,5 +12,11 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
12
12
  * with a narrower native key space owns an injective encoding, not a rejection). Before 2.1.0 the
13
13
  * bundled backends genuinely diverged on both halves (dev-green / deployment-red), so running this
14
14
  * kit against a pre-2.1.0-modeled backend is expected to go red on those two entries.
15
+ *
16
+ * Backlog #119 adds the provenance leg: `put`'s third argument and the `ownerOf` read face. `ownerOf`
17
+ * is typed OPTIONAL on the interface (source compatibility for a backend written against an older
18
+ * engine), but this kit REQUIRES it — a backend that cannot say who owns a ref cannot authorize a
19
+ * host-side read face, and an optional parameter one store honors while another drops it silently is
20
+ * exactly the divergence the two legs above exist to prevent.
15
21
  */
16
22
  export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -31,5 +31,29 @@ export async function toolResultStoreContract(make, runAssertion) {
31
31
  }
32
32
  assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
33
33
  });
34
+ run("#119 provenance: content+owner are one write; ownerOf answers; a different owner is a TYPED refusal; unowned rows stay unowned", async () => {
35
+ const store = make();
36
+ assert.equal(typeof store.ownerOf, "function", "a backend must implement ownerOf — a store that cannot say who owns a ref cannot back a host read face");
37
+ const ownerOf = (ref) => Promise.resolve(store.ownerOf(ref));
38
+ const ref = "tr_s1~c1";
39
+ await store.put(ref, "0123456789", { sessionId: "sess-A", taskId: "task-1" });
40
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "the winning write's owner round-trips");
41
+ await store.put(ref, "IGNORED", { sessionId: "sess-A", taskId: "task-1" });
42
+ assert.equal((await store.get(ref)).content, "0123456789");
43
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" });
44
+ await store.put(ref, "IGNORED");
45
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "an ownerless put must not clear the owner");
46
+ for (const other of [{ sessionId: "sess-B" }, { sessionId: "sess-A", taskId: "task-2" }]) {
47
+ await assert.rejects((async () => store.put(ref, "other tenant's bytes", other))(), (err) => err.code === "tool_result.ref_conflict", `put(${JSON.stringify(other)}) on an occupied ref must reject with code tool_result.ref_conflict`);
48
+ }
49
+ assert.equal((await store.get(ref)).content, "0123456789", "a refused put must not have overwritten anything");
50
+ const unowned = "tr_no_owner~c";
51
+ await store.put(unowned, "bytes from a write site that stated no owner");
52
+ assert.equal(await ownerOf(unowned), undefined, "a row stored without provenance is UNOWNED");
53
+ await store.put(unowned, "bytes from a write site that stated no owner", { sessionId: "sess-A" });
54
+ assert.equal(await ownerOf(unowned), undefined, "put must not back-fill an owner onto an unowned row");
55
+ assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
56
+ assert.equal(await ownerOf("tr_never~written"), undefined);
57
+ });
34
58
  await settle();
35
59
  }
@@ -6,7 +6,7 @@ import { shutdownDebug } from "./shutdown-debug.js";
6
6
  import { delimitUntrusted } from "./untrusted-text.js";
7
7
  import { boundedRedactedSummary } from "./untrusted-egress.js";
8
8
  import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
9
- import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
9
+ import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
10
10
  export function ensureDurableHeartbeatLane(core) {
11
11
  if (core.durableHeartbeatTimer !== undefined)
12
12
  return;
@@ -1232,8 +1232,8 @@ export async function spillClippedAgentResult(handle, full, clipped, store, sess
1232
1232
  if (store === undefined)
1233
1233
  return clipped;
1234
1234
  if (handle.spillRef === undefined) {
1235
- const ref = buildToolResultRef(sessionId ?? "no-session", `${handle.id}_c${handle.reviveCycle ?? 0}`);
1236
- await store.put(ref, full);
1235
+ const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${handle.reviveCycle ?? 0}`);
1236
+ await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId, handle.id));
1237
1237
  handle.spillRef = ref;
1238
1238
  }
1239
1239
  return `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${handle.spillRef}" to read it back.]`;
@@ -1,6 +1,6 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
2
  import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
- import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
3
+ import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
4
4
  export function registerMonitorLane(core, input) {
5
5
  assertOwnership(input, "registerMonitor");
6
6
  const id = core.mintTaskId("monitor");
@@ -61,14 +61,15 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
61
61
  return;
62
62
  }
63
63
  const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
64
- const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", `${handle.id}_${stream}_seg${n}`);
64
+ const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", handle.id, `${stream}_seg${n}`);
65
65
  handle.spillCharsUsed = used + dropped.length;
66
66
  if (stream === "out")
67
67
  handle.spillSegCount = n + 1;
68
68
  else
69
69
  handle.spillErrSegCount = n + 1;
70
70
  try {
71
- void Promise.resolve(store.put(ref, dropped)).catch(() => {
71
+ const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId, handle.id);
72
+ void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
72
73
  handle.spillFailed = true;
73
74
  });
74
75
  }
@@ -83,8 +84,8 @@ function monitorSpillNote(handle) {
83
84
  return "";
84
85
  const sid = handle.spillSessionId ?? "no-session";
85
86
  const segLabel = (stream, n) => {
86
- const first = buildToolResultRef(sid, `${handle.id}_${stream}_seg0`);
87
- return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, `${handle.id}_${stream}_seg${n - 1}`)}"`;
87
+ const first = buildToolResultRef(sid, handle.id, `${stream}_seg0`);
88
+ return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, handle.id, `${stream}_seg${n - 1}`)}"`;
88
89
  };
89
90
  const clauses = [];
90
91
  if (outN > 0)
@@ -10,7 +10,7 @@ import { type ToolResultStore } from "./tool-result-store.js";
10
10
  * **Simpler than CC by construction (§24.2):** a request-only, non-destructive transform (returns a new
11
11
  * array; the durable session keeps full results) applied in the existing `harness.on("context")` hook —
12
12
  * the same per-query point as `clearStaleToolResults` (the mirror of CC's query.ts:379). It is DETERMINISTIC
13
- * (stable `ref = tr_<sessionId>_<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
13
+ * (stable `ref = tr_<sessionId>~<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
14
14
  * query yields byte-identical previews → prompt-cache safe **without** CC's ContentReplacementState freeze
15
15
  * machine (the determinism IS the freeze), and resume-free (the transcript holds originals; this re-applies
16
16
  * on replay). Reuses design/30's offload store + preview format; with no store it falls back to a
@@ -1,5 +1,5 @@
1
1
  import { isToolResult } from "./message-utils.js";
2
- import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "./tool-result-store.js";
2
+ import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "./tool-result-store.js";
3
3
  export const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200_000;
4
4
  const HEAD = 1_000;
5
5
  const TAIL = 1_000;
@@ -61,13 +61,13 @@ export async function capAggregateToolResults(messages, opts) {
61
61
  const previewOne = async (k, before, head, tail, fromOriginal) => {
62
62
  const m = out[k];
63
63
  const full = textOf((fromOriginal ? messages[k] : m).content);
64
- const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`);
64
+ const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`, toolResultContentSegment(full));
65
65
  const sizes = head === HEAD && tail === TAIL ? undefined : { head, tail };
66
66
  let previewText;
67
67
  let storeFallback;
68
68
  if (opts.store) {
69
69
  try {
70
- await opts.store.put(ref, full);
70
+ await opts.store.put(ref, full, toolResultProvenanceOf(opts.sessionId));
71
71
  previewText = buildPreview(full, ref, sizes);
72
72
  storeFallback = false;
73
73
  }