@myagentroam/agent 0.11.55 → 0.11.57

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.
@@ -1,4 +1,8 @@
1
1
  import { completedNativeThinkingIdentity } from '../model/native-context.js';
2
+ export const CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS = 10;
3
+ export function contextGcProtectionMature(count) {
4
+ return count.rollouts >= CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS && count.thinking >= 3;
5
+ }
2
6
  /** One immutable boundary index, shared by all candidate anchors in this evaluation. */
3
7
  export function buildContextGcProtection(records, modelId, contextWindowTokens) {
4
8
  const configurations = new Map();
@@ -24,7 +24,7 @@ export function contextGcCodeModeInputReplacement(input, retainedInputs = []) {
24
24
  originalBytes: Buffer.byteLength(input.code),
25
25
  lines: lineCount(input.code),
26
26
  inputMode: input.inputMode,
27
- contextGc: 'Completed Code Mode source was reclaimed after six successful rollouts. The paired output, images, completion evidence, and nested tool side effects remain in history.',
27
+ contextGc: 'Completed Code Mode source was reclaimed after the protection period. The paired output, images, completion evidence, and nested tool side effects remain in history.',
28
28
  retainedInputs
29
29
  });
30
30
  return input.inputMode === 'freeform' ? content : { code: content };
@@ -37,7 +37,7 @@ export function contextGcCodeModeOutputReplacement(originalContent, truncated, v
37
37
  truncated,
38
38
  preview,
39
39
  previewComplete: !truncated && preview === originalContent,
40
- contextGc: 'Detailed Code Mode output was reclaimed after six successful rollouts. An incomplete preview is not the complete cell output. The source was not replayed and nested tool side effects were not changed.'
40
+ contextGc: 'Detailed Code Mode output was reclaimed after the protection period. An incomplete preview is not the complete cell output. The source was not replayed and nested tool side effects were not changed.'
41
41
  });
42
42
  }
43
43
  export function contextGcPatchResultFromCompletedPayload(payload) {
@@ -111,7 +111,7 @@ export function contextGcWebOutputReplacement(toolName, originalContent, truncat
111
111
  truncated,
112
112
  preview,
113
113
  previewComplete: preview === originalContent,
114
- contextGc: 'Detailed web tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete search result or page, and GC did not access the network.'
114
+ contextGc: 'Detailed web tool output was reclaimed after the protection period. An incomplete preview is not the complete search result or page, and GC did not access the network.'
115
115
  });
116
116
  }
117
117
  /** Tools with their own evidence and lifecycle policies are never handled as generic output. */
@@ -142,7 +142,7 @@ export function contextGcToolOutputReplacement(toolName, originalContent, trunca
142
142
  truncated,
143
143
  preview,
144
144
  previewComplete: !truncated && preview === originalContent,
145
- contextGc: 'Detailed tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete result; the tool was not replayed.'
145
+ contextGc: 'Detailed tool output was reclaimed after the protection period. An incomplete preview is not the complete result; the tool was not replayed.'
146
146
  });
147
147
  }
148
148
  export function contextGcSkillOutputReplacement(name, originalContent, successorCallId) {
@@ -3,7 +3,7 @@ import { mkdir, rename, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
5
  import { inlineMediaHistory } from '../../inline-media.js';
6
- import { copyToolResultResources } from '../../tool-result-resources.js';
6
+ import { prepareToolResultResources, writeToolResultResourceIndex } from '../../tool-result-resources.js';
7
7
  import { syncJsonlDirectory } from './fs-durability.js';
8
8
  import { forkedPayload, stageSessionSegments } from './history.js';
9
9
  import { copyGeneratedImages, generatedImageFileNames } from './images.js';
@@ -63,14 +63,49 @@ export async function forkSessionOperation(source, records, input, sourceRoot, s
63
63
  }
64
64
  ];
65
65
  const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
66
+ const resourcesByOwner = new Map();
67
+ for (const [resourceRef, { owner, bytes }] of await prepareToolResultResources(sourceDirectory, retained)) {
68
+ const resources = resourcesByOwner.get(owner) ?? [];
69
+ resources.push({ resourceRef, bytes });
70
+ resourcesByOwner.set(owner, resources);
71
+ }
72
+ const resourceLocators = new Map();
66
73
  const inlineState = inlineMediaHistory(retained).state;
67
74
  const stateAnchor = inlineState &&
68
75
  retained.findLast((record) => record.type === 'resource.window' || record.type === 'compact.completed');
69
76
  const forkIdentities = new Map();
70
77
  for (const record of retained) {
71
- if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
78
+ if (record.type === 'context.gc.completed' ||
79
+ record.type === 'responses.continuation' ||
80
+ record.type === 'tool.resource')
72
81
  continue;
82
+ const pendingLocators = [];
83
+ for (const { resourceRef, bytes } of resourcesByOwner.get(record.recordId) ?? []) {
84
+ const bodySequences = [];
85
+ // Small private records share normal segment durability and rollback boundaries.
86
+ for (let offset = 0; offset < Math.max(1, bytes.length); offset += 1024 * 1024) {
87
+ const sequence = copied.length + 1;
88
+ bodySequences.push(sequence);
89
+ copied.push({
90
+ recordId: randomUUID(),
91
+ sequence,
92
+ sessionId: target.id,
93
+ type: 'tool.resource',
94
+ payload: {
95
+ resourceRef,
96
+ dataBase64: bytes.subarray(offset, offset + 1024 * 1024).toString('base64')
97
+ },
98
+ ...(record.executionId === undefined ? {} : { executionId: record.executionId }),
99
+ ...(record.turnId === undefined ? {} : { turnId: record.turnId }),
100
+ timestamp: record.timestamp
101
+ });
102
+ }
103
+ pendingLocators.push({ resourceRef, bodySequences });
104
+ }
105
+ resourcesByOwner.delete(record.recordId);
73
106
  const identity = { recordId: randomUUID(), sequence: copied.length + 1 };
107
+ for (const { resourceRef, bodySequences } of pendingLocators)
108
+ resourceLocators.set(resourceRef, { ...identity, bodySequences });
74
109
  forkIdentities.set(record.recordId, identity);
75
110
  copied.push({
76
111
  ...identity,
@@ -95,7 +130,7 @@ export async function forkSessionOperation(source, records, input, sourceRoot, s
95
130
  await rename(file.temporaryPath, file.path);
96
131
  target.segments = staged.segments;
97
132
  target.nextSequence = copied.length + 1;
98
- await copyToolResultResources(sourceDirectory, targetDirectory, copied);
133
+ await writeToolResultResourceIndex(targetDirectory, resourceLocators);
99
134
  await copyGeneratedImages(sourceRoot, publication.directory, generatedImageFileNames(retained));
100
135
  await copyResources(sourceDirectory, targetDirectory, retained);
101
136
  for (const path of [
@@ -39,8 +39,9 @@ export type HistoryIndexOutcome = {
39
39
  readonly error: unknown;
40
40
  };
41
41
  /** Execute one rollback/clear transaction while its caller holds maintenance. */
42
- export declare function replaceSessionHistory(meta: SessionMeta, retained: readonly SessionRecord[], operation: {
42
+ export declare function replaceSessionHistory(meta: SessionMeta, prefix: readonly SessionRecord[], operation: {
43
43
  kind: 'rollback';
44
+ reuseSegments: boolean;
44
45
  } | {
45
46
  kind: 'clear';
46
47
  clearedAt: string;
@@ -4,17 +4,26 @@ import { basename, join } from 'node:path';
4
4
  import { toolResultResourceFiles } from '../../tool-result-resources.js';
5
5
  import { beginSessionStorageTransaction, retiredSessionTrashPath, recoverSessionStorageTransaction } from './transaction-recovery.js';
6
6
  import { deriveExecutionState } from '../execution-state.js';
7
- import { renameIfExists, retainedSessionTitle, stageSessionSegments, stageUnreferencedImages } from './history.js';
7
+ import { renameIfExists, retainedSessionTitle, stageRollbackSegments, stageSessionSegments, stageUnreferencedImages } from './history.js';
8
8
  import { generatedImageFileNames } from './images.js';
9
9
  import { CONTEXTUAL_WORLD_STATE_FILE } from './open-recovery.js';
10
10
  import { resourceFiles } from './resources.js';
11
11
  import { syncJsonlDirectory } from './fs-durability.js';
12
12
  /** Execute one rollback/clear transaction while its caller holds maintenance. */
13
- export async function replaceSessionHistory(meta, retained, operation, context) {
13
+ export async function replaceSessionHistory(meta, prefix, operation, context) {
14
+ const retained = operation.kind === 'rollback'
15
+ ? prefix.filter((record) => record.type !== 'responses.continuation')
16
+ : prefix;
14
17
  const targetDirectory = context.targetGeneration === undefined
15
18
  ? context.directory
16
19
  : join(context.sessionDirectory, 'v2', context.targetGeneration);
17
- const staged = await stageSessionSegments(targetDirectory, retained);
20
+ const incremental = operation.kind === 'rollback' &&
21
+ operation.reuseSegments &&
22
+ meta.storage?.encoding === 2 &&
23
+ meta.segments !== undefined;
24
+ const staged = incremental
25
+ ? await stageRollbackSegments(targetDirectory, prefix, meta.segments)
26
+ : await stageSessionSegments(targetDirectory, retained);
18
27
  const transactionId = randomUUID();
19
28
  const targetNextSequence = operation.kind === 'clear' ? 2 : (retained.at(-1)?.sequence ?? 0) + 1;
20
29
  const contextualWorldStatePath = join(context.directory, CONTEXTUAL_WORLD_STATE_FILE);
@@ -22,7 +31,9 @@ export async function replaceSessionHistory(meta, retained, operation, context)
22
31
  const originalTransactionId = meta.storageTransactionId;
23
32
  const originalPaths = await context.segmentPaths();
24
33
  const retiredPaths = await retiredSessionPaths(context.sessionDirectory, context.directory, targetDirectory);
25
- const backups = originalPaths.map((path) => ({
34
+ const backups = originalPaths
35
+ .filter((path) => !staged.reused?.has(basename(path)))
36
+ .map((path) => ({
26
37
  path,
27
38
  backup: `${path}.${transactionId}.${operation.kind}-backup`
28
39
  }));
@@ -30,7 +41,7 @@ export async function replaceSessionHistory(meta, retained, operation, context)
30
41
  version: 1,
31
42
  id: transactionId,
32
43
  kind: operation.kind,
33
- originalSegments: originalPaths.map((path) => basename(path)),
44
+ originalSegments: backups.map((item) => basename(item.path)),
34
45
  stagedSegments: staged.files.map((item) => ({
35
46
  path: basename(item.path),
36
47
  temporary: basename(item.temporaryPath)
@@ -6,8 +6,13 @@ interface StagedSessionSegments {
6
6
  readonly temporaryPath: string;
7
7
  }[];
8
8
  readonly segments: NonNullable<SessionMeta['segments']>;
9
+ readonly reused?: ReadonlySet<string>;
9
10
  }
10
11
  export declare function stageSessionSegments(sessionDirectory: string, records: readonly SessionRecord[], operation?: 'rollback' | 'fork'): Promise<StagedSessionSegments>;
12
+ /** Reuse intact files in ordered v2 histories; journal only replacements and removals. */
13
+ export declare function stageRollbackSegments(directory: string, prefix: readonly SessionRecord[], originals: NonNullable<SessionMeta['segments']>): Promise<StagedSessionSegments & {
14
+ readonly reused: ReadonlySet<string>;
15
+ }>;
11
16
  export declare function stageUnreferencedImages(sessionDirectory: string, directoryName: 'generated_images' | 'tool_results' | 'resources', retained: ReadonlySet<string>, transactionId: string): Promise<Array<{
12
17
  from: string;
13
18
  to: string;
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
5
  import { parseInlineMediaState } from '../../inline-media.js';
6
6
  import { MAX_SEGMENT_BYTES } from './records.js';
7
+ import { forEachTransactionFile } from './transaction-files.js';
7
8
  export async function renameIfExists(from, to) {
8
9
  try {
9
10
  await rename(from, to);
@@ -26,23 +27,9 @@ export async function stageSessionSegments(sessionDirectory, records, operation
26
27
  if (group === undefined)
27
28
  return;
28
29
  const number = segments.length + 1;
29
- const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
30
- const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
31
- const handle = await open(temporaryPath, 'wx', 0o600);
32
- files.push({ path, temporaryPath });
33
- try {
34
- await handle.writeFile(group.lines.join(''));
35
- await handle.sync();
36
- }
37
- finally {
38
- await handle.close();
39
- }
40
- segments.push({
41
- number,
42
- firstSequence: group.firstSequence,
43
- lastSequence: group.lastSequence,
44
- bytes: group.bytes
45
- });
30
+ const staged = await writeStagedSegment(sessionDirectory, group, number, transactionId, operation);
31
+ files.push(staged.file);
32
+ segments.push(staged.segment);
46
33
  group = undefined;
47
34
  };
48
35
  try {
@@ -67,10 +54,86 @@ export async function stageSessionSegments(sessionDirectory, records, operation
67
54
  return { files, segments };
68
55
  }
69
56
  catch (error) {
70
- await Promise.all(files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
57
+ await forEachTransactionFile(files, (file) => rm(file.temporaryPath, { force: true }).catch(() => undefined));
58
+ throw error;
59
+ }
60
+ }
61
+ /** Reuse intact files in ordered v2 histories; journal only replacements and removals. */
62
+ export async function stageRollbackSegments(directory, prefix, originals) {
63
+ const files = [];
64
+ const segments = [];
65
+ const reused = new Set();
66
+ const transactionId = randomUUID();
67
+ let offset = 0;
68
+ try {
69
+ for (const original of originals) {
70
+ const start = offset;
71
+ while (offset < prefix.length && prefix[offset].sequence <= original.lastSequence)
72
+ offset++;
73
+ const source = prefix.slice(start, offset);
74
+ if (source.length === 0)
75
+ break;
76
+ const retained = source.filter((record) => record.type !== 'responses.continuation');
77
+ if (retained.length === source.length && source.at(-1).sequence === original.lastSequence) {
78
+ reused.add(`events-${String(original.number).padStart(6, '0')}.jsonl`);
79
+ segments.push({ ...original });
80
+ }
81
+ else if (retained.length > 0) {
82
+ const lines = retained.map((record) => `${JSON.stringify(record)}\n`);
83
+ const staged = await writeStagedSegment(directory, {
84
+ lines,
85
+ bytes: lines.reduce((total, line) => total + Buffer.byteLength(line), 0),
86
+ firstSequence: retained[0].sequence,
87
+ lastSequence: retained.at(-1).sequence
88
+ }, original.number, transactionId, 'rollback');
89
+ files.push(staged.file);
90
+ segments.push(staged.segment);
91
+ }
92
+ }
93
+ return { files, segments, reused };
94
+ }
95
+ catch (error) {
96
+ await forEachTransactionFile(files, (file) => rm(file.temporaryPath, { force: true }).catch(() => undefined));
71
97
  throw error;
72
98
  }
73
99
  }
100
+ async function writeStagedSegment(directory, group, number, transactionId, operation) {
101
+ const path = join(directory, `events-${String(number).padStart(6, '0')}.jsonl`);
102
+ const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
103
+ const handle = await open(temporaryPath, 'wx', 0o600);
104
+ let failed = false;
105
+ let failure;
106
+ try {
107
+ await handle.writeFile(group.lines.join(''));
108
+ await handle.sync();
109
+ }
110
+ catch (error) {
111
+ failed = true;
112
+ failure = error;
113
+ }
114
+ try {
115
+ await handle.close();
116
+ }
117
+ catch (error) {
118
+ if (!failed) {
119
+ failed = true;
120
+ failure = error;
121
+ }
122
+ }
123
+ if (failed) {
124
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
125
+ throw failure;
126
+ }
127
+ return {
128
+ file: { path, temporaryPath },
129
+ segment: {
130
+ number,
131
+ firstSequence: group.firstSequence,
132
+ lastSequence: group.lastSequence,
133
+ bytes: group.bytes
134
+ }
135
+ };
136
+ }
74
137
  export async function stageUnreferencedImages(sessionDirectory, directoryName, retained, transactionId) {
75
138
  const directory = join(sessionDirectory, directoryName);
76
139
  let names;
@@ -84,14 +147,12 @@ export async function stageUnreferencedImages(sessionDirectory, directoryName, r
84
147
  }
85
148
  const moved = [];
86
149
  try {
87
- for (const name of names) {
88
- if (retained.has(name))
89
- continue;
150
+ await forEachTransactionFile(names.filter((name) => !retained.has(name)), async (name) => {
90
151
  const from = join(directory, name);
91
152
  const trash = join(directory, `.${name}.${transactionId}.rollback-trash`);
92
153
  await rename(from, trash);
93
154
  moved.push({ from: trash, to: from, trash });
94
- }
155
+ });
95
156
  return moved;
96
157
  }
97
158
  catch (error) {
@@ -100,8 +161,7 @@ export async function stageUnreferencedImages(sessionDirectory, directoryName, r
100
161
  }
101
162
  }
102
163
  export async function restoreRenamedFiles(files) {
103
- for (const file of files.toReversed())
104
- await rename(file.from, file.to).catch(() => undefined);
164
+ await forEachTransactionFile(files, (file) => rename(file.from, file.to).catch(() => undefined));
105
165
  }
106
166
  export function retainedSessionTitle(records, fallback) {
107
167
  let title = fallback;
@@ -698,14 +698,28 @@ export class JsonlSessionStore {
698
698
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
699
699
  if (!meta)
700
700
  throw new AgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
701
- const records = await this.read(sessionId);
701
+ const indexedSegments = meta.segments;
702
+ let segmentIndex = 0;
703
+ let matchingSegments = indexedSegments !== undefined;
704
+ // Check optional cache bounds during the history read already required by rollback.
705
+ const records = await this.#serializeRecordOperation(sessionId, () => this.#readRecords(sessionId, undefined, (path, segment) => {
706
+ const indexed = indexedSegments?.[segmentIndex];
707
+ segmentIndex++;
708
+ matchingSegments &&=
709
+ indexed !== undefined &&
710
+ path === this.#segmentPath(sessionId, indexed.number, meta) &&
711
+ segment[0]?.sequence === indexed.firstSequence &&
712
+ segment.at(-1)?.sequence === indexed.lastSequence;
713
+ }));
702
714
  if (records[0]?.type !== 'session.created')
703
715
  throw new AgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
704
716
  const boundaryIndex = rollbackBoundaryIndex(records, input);
705
- const retained = records
706
- .slice(0, boundaryIndex + 1)
707
- .filter((record) => record.type !== 'responses.continuation');
708
- return await this.#replaceHistory(meta, retained, { kind: 'rollback' });
717
+ const retained = records.slice(0, boundaryIndex + 1);
718
+ // Migrated v1 histories may preserve duplicate or non-increasing sequences.
719
+ const reuseSegments = matchingSegments &&
720
+ segmentIndex === indexedSegments?.length &&
721
+ records.every((record, index) => index === 0 || record.sequence > records[index - 1].sequence);
722
+ return await this.#replaceHistory(meta, retained, { kind: 'rollback', reuseSegments });
709
723
  }
710
724
  catch (error) {
711
725
  failed = true;
@@ -1110,7 +1124,7 @@ export class JsonlSessionStore {
1110
1124
  async read(sessionId) {
1111
1125
  return this.#serializeRecordOperation(sessionId, () => this.#readRecords(sessionId));
1112
1126
  }
1113
- async #readRecords(sessionId, selectedMeta) {
1127
+ async #readRecords(sessionId, selectedMeta, onSegment) {
1114
1128
  return withSessionReadPin(this.#sessionRoot(sessionId), async () => {
1115
1129
  this.#assertOpen();
1116
1130
  if (!selectedMeta)
@@ -1125,8 +1139,11 @@ export class JsonlSessionStore {
1125
1139
  const records = [];
1126
1140
  const meta = selectedMeta ?? this.#shared.index.sessions.find((item) => item.id === sessionId);
1127
1141
  const encoding = meta?.storage?.encoding ?? 1;
1128
- for (const path of await this.#segmentPaths(sessionId, meta))
1129
- records.push(...(await readRecords(path, encoding)));
1142
+ for (const path of await this.#segmentPaths(sessionId, meta)) {
1143
+ const segment = await readRecords(path, encoding);
1144
+ onSegment?.(path, segment);
1145
+ records.push(...segment);
1146
+ }
1130
1147
  return records;
1131
1148
  }
1132
1149
  catch (cause) {
@@ -0,0 +1,2 @@
1
+ /** Stop scheduling on the first failure, then drain all in-flight filesystem operations. */
2
+ export declare function forEachTransactionFile<T>(files: readonly T[], operation: (file: T) => Promise<unknown>): Promise<void>;
@@ -0,0 +1,22 @@
1
+ /** Stop scheduling on the first failure, then drain all in-flight filesystem operations. */
2
+ export async function forEachTransactionFile(files, operation) {
3
+ let next = 0;
4
+ let failed = false;
5
+ let failure;
6
+ await Promise.all(Array.from({ length: Math.min(8, files.length) }, async () => {
7
+ while (!failed && next < files.length) {
8
+ const file = files[next++];
9
+ try {
10
+ await operation(file);
11
+ }
12
+ catch (error) {
13
+ if (!failed) {
14
+ failed = true;
15
+ failure = error;
16
+ }
17
+ }
18
+ }
19
+ }));
20
+ if (failed)
21
+ throw failure;
22
+ }
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
+ import { forEachTransactionFile } from './transaction-files.js';
5
6
  const JOURNAL = 'storage-transaction.json';
6
7
  const SEGMENT = /^events-\d{6}\.jsonl$/u;
7
8
  const TRANSACTION_ID = /^[0-9a-f-]{36}$/iu;
@@ -291,24 +292,24 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
291
292
  ? directory
292
293
  : join(sessionDirectory, 'v2', transaction.targetGeneration);
293
294
  const originals = new Set(transaction.originalSegments);
294
- for (const name of transaction.originalSegments) {
295
+ await forEachTransactionFile([...originals], async (name) => {
295
296
  const canonical = join(directory, name);
296
297
  const backup = `${canonical}.${transaction.id}.${transaction.kind}-backup`;
297
- if (!(await exists(backup)))
298
- continue;
299
298
  if (committed)
300
299
  await rm(backup, { force: true });
301
300
  else {
301
+ if (!(await exists(backup)))
302
+ return;
302
303
  await rm(canonical, { force: true });
303
304
  await rename(backup, canonical);
304
305
  }
305
- }
306
+ });
306
307
  if (!committed)
307
- for (const entry of transaction.stagedSegments)
308
+ await forEachTransactionFile(transaction.stagedSegments, async (entry) => {
308
309
  if (stagedDirectory !== directory || !originals.has(entry.path))
309
310
  await rm(join(stagedDirectory, entry.path), { force: true });
310
- for (const entry of transaction.stagedSegments)
311
- await rm(join(stagedDirectory, entry.temporary), { force: true });
311
+ });
312
+ await forEachTransactionFile(transaction.stagedSegments, (entry) => rm(join(stagedDirectory, entry.temporary), { force: true }));
312
313
  if (!committed && transaction.targetGeneration !== undefined)
313
314
  await rm(stagedDirectory, { recursive: true, force: true });
314
315
  const sidecar = join(directory, 'context-world-state.json');
@@ -333,15 +334,13 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
333
334
  throw error;
334
335
  }
335
336
  const suffix = `.${transaction.id}.rollback-trash`;
336
- for (const name of names) {
337
- if (!name.startsWith('.') || !name.endsWith(suffix))
338
- continue;
337
+ await forEachTransactionFile(names.filter((name) => name.startsWith('.') && name.endsWith(suffix)), async (name) => {
339
338
  const trash = join(path, name);
340
339
  if (committed)
341
340
  await rm(trash, { force: true });
342
341
  else
343
342
  await rename(trash, join(path, name.slice(1, -suffix.length)));
344
- }
343
+ });
345
344
  }
346
345
  for (const path of transaction.retiredPaths ?? []) {
347
346
  const source = join(sessionDirectory, path);
@@ -357,9 +356,7 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
357
356
  await removeUnjournaledTransactionFiles(directory);
358
357
  }
359
358
  async function removeUnjournaledTransactionFiles(directory) {
360
- for (const name of await readdir(directory))
361
- if (isUnjournaledTransactionFile(name))
362
- await rm(join(directory, name), { force: true });
359
+ await forEachTransactionFile((await readdir(directory)).filter(isUnjournaledTransactionFile), (name) => rm(join(directory, name), { force: true }));
363
360
  }
364
361
  async function exists(path) {
365
362
  try {
@@ -20,7 +20,16 @@ export declare function recordToolResources(record: Pick<SessionRecord, 'type' |
20
20
  /** A locator is only a hint: reads must verify its exact committed record identity. */
21
21
  export declare function bindToolResultResources(directory: string, record: SessionRecord, readRecord?: (sequence: number) => Promise<SessionRecord | undefined>, degrade?: boolean): Promise<void>;
22
22
  export declare function toolResultResourceFiles(records: readonly SessionRecord[]): Set<string>;
23
- export declare function copyToolResultResources(source: string, target: string, records: readonly SessionRecord[]): Promise<void>;
23
+ export interface ToolResourceLocator {
24
+ sequence: number;
25
+ recordId: string;
26
+ bodySequences?: number[];
27
+ }
28
+ export declare function prepareToolResultResources(source: string, records: readonly SessionRecord[]): Promise<Map<string, {
29
+ owner: string;
30
+ bytes: Buffer;
31
+ }>>;
32
+ export declare function writeToolResultResourceIndex(target: string, locators: ReadonlyMap<string, ToolResourceLocator>): Promise<void>;
24
33
  export declare function completeToolResultResources(directory: string, record: SessionRecord): Promise<void>;
25
34
  export declare function cleanupUnreferencedToolResources(directory: string, readRecords: () => Promise<readonly SessionRecord[]>): Promise<void>;
26
35
  /** Caller must hold a stable history and resource generation for the entire read. */