@myagentroam/agent 0.10.59 → 0.10.61

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 (58) hide show
  1. package/dist/agent-core/runtime/compaction/history.d.ts +1 -1
  2. package/dist/agent-core/runtime/compaction/retained-context.d.ts +1 -1
  3. package/dist/agent-core/runtime/compaction/retained-context.js +1 -1
  4. package/dist/agent-core/runtime/compaction/smart.d.ts +1 -1
  5. package/dist/agent-core/runtime/compaction/smart.js +1 -1
  6. package/dist/agent-core/runtime/context/gc-runtime.d.ts +1 -1
  7. package/dist/agent-core/runtime/context/gc-runtime.js +20 -14
  8. package/dist/agent-core/runtime/context/gc.d.ts +1 -1
  9. package/dist/agent-core/runtime/context/gc.js +4 -10
  10. package/dist/agent-core/runtime/context/projection.d.ts +1 -1
  11. package/dist/agent-core/runtime/context/projection.js +3 -5
  12. package/dist/agent-core/runtime/context/restore.d.ts +4 -4
  13. package/dist/agent-core/runtime/context/restore.js +1 -1
  14. package/dist/agent-core/runtime/context/usage.d.ts +2 -2
  15. package/dist/agent-core/runtime/context/usage.js +1 -1
  16. package/dist/agent-core/runtime/context/world-state.d.ts +2 -2
  17. package/dist/agent-core/runtime/context/world-state.js +1 -1
  18. package/dist/agent-core/sdk/agent.js +2 -2
  19. package/dist/agent-core/sdk/extension-agents.js +5 -1
  20. package/dist/agent-core/sdk/native-steer.d.ts +1 -1
  21. package/dist/agent-core/sdk/session.d.ts +19 -13
  22. package/dist/agent-core/sdk/session.js +35 -40
  23. package/dist/agent-core/sdk/subagent-backend.d.ts +1 -1
  24. package/dist/agent-core/sdk/subagent-backend.js +20 -17
  25. package/dist/agent-core/session/catalog.js +1 -1
  26. package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -8
  27. package/dist/agent-core/session/context-gc-checkpoint.js +82 -137
  28. package/dist/agent-core/session/context-gc-observations.d.ts +1 -1
  29. package/dist/agent-core/session/context-gc-replacements.d.ts +4 -7
  30. package/dist/agent-core/session/context-gc-replacements.js +25 -59
  31. package/dist/agent-core/session/context-gc-subagent.d.ts +0 -3
  32. package/dist/agent-core/session/context-gc-subagent.js +2 -16
  33. package/dist/agent-core/session/deferred-input.d.ts +1 -1
  34. package/dist/agent-core/session/store/current-state.d.ts +17 -0
  35. package/dist/agent-core/session/store/current-state.js +53 -0
  36. package/dist/agent-core/session/store/directory-index.d.ts +18 -0
  37. package/dist/agent-core/session/store/directory-index.js +169 -0
  38. package/dist/agent-core/session/store/execution-state.d.ts +18 -0
  39. package/dist/agent-core/session/store/execution-state.js +187 -0
  40. package/dist/agent-core/session/store/history.d.ts +25 -0
  41. package/dist/agent-core/session/store/history.js +157 -0
  42. package/dist/agent-core/session/store/images.d.ts +21 -0
  43. package/dist/agent-core/session/store/images.js +248 -0
  44. package/dist/agent-core/session/store/index.d.ts +4 -0
  45. package/dist/agent-core/session/store/index.js +2 -0
  46. package/dist/agent-core/session/{jsonl-store.d.ts → store/jsonl-store.d.ts} +14 -94
  47. package/dist/agent-core/session/{jsonl-store.js → store/jsonl-store.js} +323 -1269
  48. package/dist/agent-core/session/store/records.d.ts +23 -0
  49. package/dist/agent-core/session/store/records.js +249 -0
  50. package/dist/agent-core/session/store/types.d.ts +84 -0
  51. package/dist/agent-core/session/store/types.js +1 -0
  52. package/dist/agent-core/session/tool-result-resources.d.ts +1 -1
  53. package/dist/agent-core/subagent/scheduler.js +1 -1
  54. package/dist/agent-core/subagent/session-controller.d.ts +5 -2
  55. package/dist/agent-core/subagent/session-controller.js +99 -15
  56. package/dist/agent-core/tools/code-mode/state.d.ts +1 -1
  57. package/dist/agent-core/tools/code-mode/state.js +2 -2
  58. package/package.json +1 -1
@@ -1,26 +1,23 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
- import { copyFile, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, open, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
3
  import { basename, join } from 'node:path';
5
4
  import { isDeepStrictEqual } from 'node:util';
6
- import { MarAgentError } from '../error.js';
7
- import { completeToolResultResources, cleanupUnreferencedToolResources, bindToolResultResources, copyToolResultResources, readToolResultResource, toolResultResourceFiles, writeToolResultResource } from './tool-result-resources.js';
8
- import { acquireFileLock } from '../storage/file-lock.js';
9
- import { cloneCoreJson } from '../extensions/data.js';
10
- import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../tools/code-mode/state.js';
11
- import { todoItemsFromSnapshot } from '../tools/agent/todo.js';
12
- import { contextGcBaseFromRecords, contextGcBaseSequence, contextGcMinimumUsageGrowth, contextGcSourceProjectionHash, latestValidContextGcCheckpoint, normalizeContextGcReplacements, parseContextGcCheckpoint, sameContextGcBase, validateContextGcCheckpoint } from './context-gc-checkpoint.js';
13
- import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from './transaction-recovery.js';
14
- const MAX_SEGMENT_BYTES = 16 * 1024 * 1024;
15
- const REVERSE_READ_CHUNK_BYTES = 64 * 1024;
16
- const MAX_HISTORY_PAGE_FILES = 32;
17
- const MAX_HISTORY_PAGE_OFFSETS = 128;
18
- const MAX_INPUT_IMAGES = 5;
19
- const MAX_INPUT_IMAGE_BYTES = 5 * 1024 * 1024;
20
- const MAX_INPUT_IMAGES_BYTES = 10 * 1024 * 1024;
5
+ import { MarAgentError } from '../../error.js';
6
+ import { cloneCoreJson } from '../../extensions/data.js';
7
+ import { acquireFileLock } from '../../storage/file-lock.js';
8
+ import { todoItemsFromSnapshot } from '../../tools/agent/todo.js';
9
+ import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../../tools/code-mode/state.js';
10
+ import { contextGcBaseFromRecords, contextGcBaseSequence, contextGcMinimumUsageGrowth, latestValidContextGcCheckpoint, normalizeContextGcReplacements, parseContextGcCheckpoint, sameContextGcBase, validateContextGcCheckpoint } from '../context-gc-checkpoint.js';
11
+ import { bindToolResultResources, cleanupUnreferencedToolResources, completeToolResultResources, copyToolResultResources, readToolResultResource, toolResultResourceFiles, writeToolResultResource } from '../tool-result-resources.js';
12
+ import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from '../transaction-recovery.js';
13
+ import { extensionCheckpointFromPayload, indexExtensionCheckpoint, restoreExtensionCheckpoint, updateSessionCurrentState } from './current-state.js';
14
+ import { decodeSessionListCursor, encodeSessionListCursor, mergeSessionDerivedCache, normalizeSessionSource, persistSessionIndex, readSessionIndex, sessionListFilter, sessionMatchesFilter, sessionTreeIds } from './directory-index.js';
15
+ import { deriveExecutionState, executionModelFromRecord, isCompactCheckpoint, isNativeUserTurn, sameResumeIndex, userMessagePreviewFromRecord } from './execution-state.js';
16
+ import { forkedPayload, renameIfExists, restoreRenamedFiles, retainedSessionTitle, stageSessionSegments, stageUnreferencedImages } from './history.js';
17
+ import { MAX_INPUT_IMAGES, copyGeneratedImages, copyInputImages, generatedImageFileNames, inputImageReference, inputImageReferences, inputImageReferencesFromRecords, readInputImageBytes, recoverPendingInputImages, validInputImageFileName, writeInputImages } from './images.js';
18
+ import { MAX_SEGMENT_BYTES, appendRecordBytes, readRecordPageFromTail, readRecords, readRecordsFromHead, readRecordsFromTail, repairSegmentTail, segmentNumberFromPath } from './records.js';
21
19
  const MAX_CONTEXTUAL_WORLD_STATE_BYTES = 8 * 1024 * 1024;
22
20
  const CONTEXTUAL_WORLD_STATE_FILE = 'context-world-state.json';
23
- const INPUT_IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
24
21
  /** SDK-owned JSONL history with per-HOME in-process sharing and cross-process file locks. */
25
22
  export class JsonlSessionStore {
26
23
  static #instances = new Map();
@@ -161,7 +158,7 @@ export class JsonlSessionStore {
161
158
  closed: false,
162
159
  indexDirty: false,
163
160
  dirtySessionVersions: new Map(),
164
- previewOnlyDirtySessions: new Set(),
161
+ derivedOnlyDirtySessions: new Set(),
165
162
  pendingPublishIds: new Set(),
166
163
  codeStateWrites: new Map(),
167
164
  recordWrites: new Map(),
@@ -188,15 +185,15 @@ export class JsonlSessionStore {
188
185
  if (!disk)
189
186
  throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is missing.');
190
187
  const dirty = new Map(this.#shared.dirtySessionVersions);
191
- const previewOnly = new Set(this.#shared.previewOnlyDirtySessions);
188
+ const derivedOnly = new Set(this.#shared.derivedOnlyDirtySessions);
192
189
  const sessions = new Map(disk.sessions.map((session) => [session.id, session]));
193
190
  const created = new Set(createdIds);
194
191
  for (const session of this.#shared.index.sessions) {
195
192
  const current = sessions.get(session.id);
196
193
  // Disk membership is authoritative: a stale in-memory snapshot cannot revive a deletion.
197
194
  if (created.has(session.id) || (current && dirty.has(session.id)))
198
- sessions.set(session.id, current && previewOnly.has(session.id)
199
- ? mergeFirstUserMessagePreviewCache(current, session)
195
+ sessions.set(session.id, current && derivedOnly.has(session.id)
196
+ ? mergeSessionDerivedCache(current, session)
200
197
  : structuredClone(session));
201
198
  }
202
199
  for (const deletedId of typeof deletedIds === 'string' ? [deletedIds] : (deletedIds ?? []))
@@ -205,7 +202,7 @@ export class JsonlSessionStore {
205
202
  version: 1,
206
203
  revision: createdIds.length > 0 ||
207
204
  deletedIds !== undefined ||
208
- [...dirty.keys()].some((id) => !previewOnly.has(id))
205
+ [...dirty.keys()].some((id) => !derivedOnly.has(id))
209
206
  ? Math.max(disk.revision, this.#shared.index.revision) + 1
210
207
  : disk.revision,
211
208
  sessions: [...sessions.values()]
@@ -217,7 +214,7 @@ export class JsonlSessionStore {
217
214
  continue;
218
215
  if (this.#shared.dirtySessionVersions.get(id) === version) {
219
216
  this.#shared.dirtySessionVersions.delete(id);
220
- this.#shared.previewOnlyDirtySessions.delete(id);
217
+ this.#shared.derivedOnlyDirtySessions.delete(id);
221
218
  }
222
219
  }
223
220
  for (const id of created)
@@ -260,8 +257,8 @@ export class JsonlSessionStore {
260
257
  const current = local.get(session.id);
261
258
  if (!current || !this.#shared.dirtySessionVersions.has(session.id))
262
259
  return session;
263
- return this.#shared.previewOnlyDirtySessions.has(session.id)
264
- ? mergeFirstUserMessagePreviewCache(session, current)
260
+ return this.#shared.derivedOnlyDirtySessions.has(session.id)
261
+ ? mergeSessionDerivedCache(session, current)
265
262
  : current;
266
263
  })
267
264
  .concat(this.#shared.index.sessions.filter((session) => this.#shared.pendingPublishIds.has(session.id) && !published.has(session.id)))
@@ -302,6 +299,7 @@ export class JsonlSessionStore {
302
299
  automaticTitlePending: input.title === undefined,
303
300
  resume: {
304
301
  version: 1,
302
+ userTurns: 0,
305
303
  todoRevision: 0,
306
304
  executionModelIndexed: true,
307
305
  firstUserMessageIndexed: true
@@ -369,7 +367,7 @@ export class JsonlSessionStore {
369
367
  }
370
368
  }
371
369
  }
372
- sessionDirectory(sessionId) {
370
+ #sessionDirectory(sessionId) {
373
371
  this.#assertOpen();
374
372
  if (!this.#shared.index.sessions.some((session) => session.id === sessionId) &&
375
373
  !this.#shared.pendingSessions.has(sessionId))
@@ -377,7 +375,7 @@ export class JsonlSessionStore {
377
375
  return join(this.#shared.root, 'sessions', sessionId);
378
376
  }
379
377
  async readContextualWorldState(sessionId) {
380
- const path = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
378
+ const path = join(this.#sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
381
379
  try {
382
380
  const pathInfo = await lstat(path);
383
381
  if (!pathInfo.isFile())
@@ -406,7 +404,7 @@ export class JsonlSessionStore {
406
404
  }
407
405
  }
408
406
  async writeContextualWorldState(sessionId, state) {
409
- const directory = this.sessionDirectory(sessionId);
407
+ const directory = this.#sessionDirectory(sessionId);
410
408
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
411
409
  if (!meta)
412
410
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
@@ -434,69 +432,21 @@ export class JsonlSessionStore {
434
432
  return [];
435
433
  if (images.length > MAX_INPUT_IMAGES)
436
434
  throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Too many image inputs were provided.');
437
- const directory = join(this.sessionDirectory(sessionId), 'input_images');
438
- await mkdir(directory, { recursive: true, mode: 0o700 });
439
- const written = [];
440
- const pending = [];
441
- let temporary;
442
- let totalBytes = 0;
443
- try {
444
- const metadata = [];
445
- for (const image of images) {
446
- if (!INPUT_IMAGE_MIME_TYPES.has(image.mimeType))
447
- throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Image input MIME type is unsupported.');
448
- const data = Buffer.from(image.dataBase64, 'base64');
449
- totalBytes += data.byteLength;
450
- if (data.byteLength === 0 ||
451
- data.byteLength > MAX_INPUT_IMAGE_BYTES ||
452
- totalBytes > MAX_INPUT_IMAGES_BYTES)
453
- throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Image input exceeds the supported size limit.');
454
- const fileName = `${randomUUID()}.bin`;
455
- temporary = join(directory, `.${fileName}.${randomUUID()}.tmp`);
456
- const handle = await open(temporary, 'wx', 0o600);
457
- try {
458
- await handle.writeFile(data);
459
- await handle.sync();
460
- }
461
- finally {
462
- await handle.close();
463
- }
464
- const marker = join(directory, `.${fileName}.pending`);
465
- await writeFile(marker, '', { mode: 0o600, flag: 'wx', flush: true });
466
- pending.push(marker);
467
- await rename(temporary, join(directory, fileName));
468
- temporary = undefined;
469
- written.push(fileName);
470
- metadata.push({
471
- name: image.name,
472
- mimeType: image.mimeType,
473
- byteSize: data.byteLength,
474
- fileName,
475
- sha256: createHash('sha256').update(data).digest('hex')
476
- });
477
- }
478
- return metadata;
479
- }
480
- catch (error) {
481
- if (temporary !== undefined)
482
- await rm(temporary, { force: true }).catch(() => undefined);
483
- await Promise.all(pending.map((marker) => rm(marker, { force: true })));
484
- await Promise.all(written.map((fileName) => rm(join(directory, fileName), { force: true }).catch(() => undefined)));
485
- throw error;
486
- }
435
+ const directory = join(this.#sessionDirectory(sessionId), 'input_images');
436
+ return writeInputImages(directory, images);
487
437
  }
488
438
  async writeToolResultResource(sessionId, text) {
489
439
  this.#assertOpen();
490
440
  if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
491
441
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
492
- return writeToolResultResource(this.sessionDirectory(sessionId), text);
442
+ return writeToolResultResource(this.#sessionDirectory(sessionId), text);
493
443
  }
494
444
  async readToolResultResource(sessionId, input) {
495
445
  this.#assertOpen();
496
446
  await this.#refreshIndex();
497
447
  if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
498
448
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
499
- return readToolResultResource(this.sessionDirectory(sessionId), sessionId, input, async (sequence) => {
449
+ return readToolResultResource(this.#sessionDirectory(sessionId), sessionId, input, async (sequence) => {
500
450
  await this.#refreshIndex();
501
451
  return (await this.readPage(sessionId, { beforeSequence: sequence + 1, limit: 1 })).records.find((record) => record.sequence === sequence);
502
452
  });
@@ -506,7 +456,7 @@ export class JsonlSessionStore {
506
456
  const references = inputImageReferences(value);
507
457
  if (references.length === 0)
508
458
  return [];
509
- const directory = join(this.sessionDirectory(sessionId), 'input_images');
459
+ const directory = join(this.#sessionDirectory(sessionId), 'input_images');
510
460
  const images = [];
511
461
  for (const reference of references) {
512
462
  const data = await readInputImageBytes(directory, reference);
@@ -536,7 +486,7 @@ export class JsonlSessionStore {
536
486
  const reference = inputImageReference(record.payload.images[input.imageIndex]);
537
487
  if (reference === undefined)
538
488
  return undefined;
539
- const data = await readInputImageBytes(join(this.sessionDirectory(sessionId), 'input_images'), reference);
489
+ const data = await readInputImageBytes(join(this.#sessionDirectory(sessionId), 'input_images'), reference);
540
490
  return {
541
491
  name: reference.name,
542
492
  mimeType: reference.mimeType,
@@ -571,6 +521,7 @@ export class JsonlSessionStore {
571
521
  automaticTitlePending: false,
572
522
  resume: {
573
523
  version: 1,
524
+ userTurns: 0,
574
525
  todoRevision: 0,
575
526
  executionModelIndexed: true,
576
527
  firstUserMessageIndexed: true
@@ -617,7 +568,7 @@ export class JsonlSessionStore {
617
568
  });
618
569
  }
619
570
  // A fork is unpublished until every segment and referenced image is durable.
620
- const staged = await stageSessionSegments(this.sessionDirectory(target.id), copied, 'fork');
571
+ const staged = await stageSessionSegments(this.#sessionDirectory(target.id), copied, 'fork');
621
572
  for (const file of staged.files)
622
573
  await rename(file.temporaryPath, file.path);
623
574
  target.segments = staged.segments;
@@ -625,9 +576,9 @@ export class JsonlSessionStore {
625
576
  this.#shared.segments.set(target.id, staged.segments.length);
626
577
  for (const record of copied)
627
578
  this.#applyRecordToResumeIndex(target, record);
628
- await copyToolResultResources(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), copied);
629
- await copyGeneratedImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), generatedImageFileNames(retained));
630
- await copyInputImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), inputImageReferencesFromRecords(retained));
579
+ await copyToolResultResources(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), copied);
580
+ await copyGeneratedImages(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), generatedImageFileNames(retained));
581
+ await copyInputImages(this.#sessionDirectory(sourceSessionId), this.#sessionDirectory(target.id), inputImageReferencesFromRecords(retained));
631
582
  target.lastActivityAt = target.createdAt;
632
583
  this.#shared.pendingSessions.delete(target.id);
633
584
  this.#shared.index = {
@@ -705,102 +656,7 @@ export class JsonlSessionStore {
705
656
  const retained = records
706
657
  .slice(0, boundaryIndex + 1)
707
658
  .filter((record) => record.type !== 'responses.continuation');
708
- const staged = await stageSessionSegments(this.sessionDirectory(sessionId), retained);
709
- const transactionId = randomUUID();
710
- const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
711
- const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
712
- const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.rollback-backup`;
713
- let contextualWorldStateStaged = false;
714
- let generatedTrash = [];
715
- let inputTrash = [];
716
- const originalMeta = structuredClone(meta);
717
- const originalIndexRevision = this.#shared.index.revision;
718
- const originalSegment = this.#shared.segments.get(sessionId);
719
- const originalPaths = await this.#segmentPaths(sessionId);
720
- const backups = originalPaths.map((path) => ({
721
- path,
722
- backup: `${path}.${transactionId}.rollback-backup`
723
- }));
724
- const transaction = {
725
- version: 1,
726
- id: transactionId,
727
- kind: 'rollback',
728
- originalSegments: originalPaths.map((path) => basename(path)),
729
- stagedSegments: staged.files.map((item) => ({
730
- path: basename(item.path),
731
- temporary: basename(item.temporaryPath)
732
- })),
733
- sidecarBackup: basename(contextualWorldStateBackup)
734
- };
735
- const installedPaths = [];
736
- let committed = false;
737
- try {
738
- await beginSessionStorageTransaction(this.sessionDirectory(sessionId), transaction);
739
- contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
740
- generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
741
- inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
742
- inputTrash.push(...(await stageUnreferencedImages(this.sessionDirectory(sessionId), 'tool_results', toolResultResourceFiles(retained), transactionId)));
743
- for (const item of backups)
744
- await rename(item.path, item.backup);
745
- for (const item of staged.files) {
746
- await rename(item.temporaryPath, item.path);
747
- installedPaths.push(item.path);
748
- }
749
- const state = deriveExecutionState(retained);
750
- const completedAt = new Date().toISOString();
751
- meta.title = retainedSessionTitle(retained, meta.title);
752
- meta.lastActivityAt = completedAt;
753
- meta.nextSequence = targetNextSequence;
754
- meta.storageTransactionId = transactionId;
755
- meta.automaticTitlePending = state.automaticTitlePending;
756
- meta.resume = state.resume;
757
- meta.segments = staged.segments;
758
- this.#shared.segments.set(sessionId, staged.segments.at(-1)?.number ?? 1);
759
- this.#shared.index.revision++;
760
- this.#markIndexDirty(sessionId);
761
- await this.#saveIndex();
762
- committed = true;
763
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
764
- return {
765
- id: meta.id,
766
- title: meta.title,
767
- workspace: meta.workspace,
768
- createdAt: meta.createdAt,
769
- lastActivityAt: meta.lastActivityAt,
770
- source: normalizeSessionSource(meta.source)
771
- };
772
- }
773
- catch (error) {
774
- if (!committed) {
775
- committed = await this.#storageTransactionCommitted(sessionId, transactionId);
776
- if (committed) {
777
- this.#shared.dirtySessionVersions.delete(sessionId);
778
- this.#shared.indexDirty = this.#shared.dirtySessionVersions.size > 0;
779
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
780
- }
781
- }
782
- if (!committed) {
783
- await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
784
- await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
785
- if (contextualWorldStateStaged)
786
- await restoreRenamedFiles([
787
- { from: contextualWorldStateBackup, to: contextualWorldStatePath }
788
- ]);
789
- await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
790
- restoreSessionMeta(meta, originalMeta);
791
- this.#shared.index.revision = originalIndexRevision;
792
- if (originalSegment === undefined)
793
- this.#shared.segments.delete(sessionId);
794
- else
795
- this.#shared.segments.set(sessionId, originalSegment);
796
- this.#markIndexDirty(sessionId);
797
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), originalMeta.storageTransactionId);
798
- }
799
- throw error;
800
- }
801
- finally {
802
- await Promise.all(staged.files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
803
- }
659
+ return await this.#replaceHistory(meta, retained, { kind: 'rollback' });
804
660
  }
805
661
  finally {
806
662
  await release();
@@ -868,100 +724,120 @@ export class JsonlSessionStore {
868
724
  }
869
725
  }
870
726
  ];
871
- const staged = await stageSessionSegments(this.sessionDirectory(sessionId), retained);
872
- const transactionId = randomUUID();
873
- const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
874
- const contextualWorldStateBackup = `${contextualWorldStatePath}.2.${transactionId}.clear-backup`;
875
- let contextualWorldStateStaged = false;
876
- let generatedTrash = [];
877
- let inputTrash = [];
878
- const originalMeta = structuredClone(meta);
879
- const originalIndexRevision = this.#shared.index.revision;
880
- const originalSegment = this.#shared.segments.get(sessionId);
881
- const originalPaths = await this.#segmentPaths(sessionId);
882
- const backups = originalPaths.map((path) => ({
883
- path,
884
- backup: `${path}.${transactionId}.clear-backup`
885
- }));
886
- const transaction = {
887
- version: 1,
888
- id: transactionId,
889
- kind: 'clear',
890
- originalSegments: originalPaths.map((path) => basename(path)),
891
- stagedSegments: staged.files.map((item) => ({
892
- path: basename(item.path),
893
- temporary: basename(item.temporaryPath)
894
- })),
895
- sidecarBackup: basename(contextualWorldStateBackup)
896
- };
897
- const installedPaths = [];
898
- let committed = false;
899
- try {
900
- await beginSessionStorageTransaction(this.sessionDirectory(sessionId), transaction);
901
- contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
902
- generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(), transactionId);
903
- inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(), transactionId);
904
- inputTrash.push(...(await stageUnreferencedImages(this.sessionDirectory(sessionId), 'tool_results', new Set(), transactionId)));
905
- for (const item of backups)
906
- await rename(item.path, item.backup);
907
- for (const item of staged.files) {
908
- await rename(item.temporaryPath, item.path);
909
- installedPaths.push(item.path);
910
- }
911
- meta.lastActivityAt = clearedAt;
912
- meta.nextSequence = retained.length + 1;
727
+ return await this.#replaceHistory(meta, retained, { kind: 'clear', clearedAt });
728
+ }
729
+ finally {
730
+ await release();
731
+ }
732
+ }
733
+ /** Called only while rollback or clear holds the maintenance lease. */
734
+ async #replaceHistory(meta, retained, operation) {
735
+ const sessionId = meta.id;
736
+ const staged = await stageSessionSegments(this.#sessionDirectory(sessionId), retained);
737
+ const transactionId = randomUUID();
738
+ const targetNextSequence = operation.kind === 'clear' ? 2 : (retained.at(-1)?.sequence ?? 0) + 1;
739
+ const contextualWorldStatePath = join(this.#sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
740
+ const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.${operation.kind}-backup`;
741
+ let contextualWorldStateStaged = false;
742
+ let generatedTrash = [];
743
+ let inputTrash = [];
744
+ const originalMeta = structuredClone(meta);
745
+ const originalIndexRevision = this.#shared.index.revision;
746
+ const originalSegment = this.#shared.segments.get(sessionId);
747
+ const originalPaths = await this.#segmentPaths(sessionId);
748
+ const backups = originalPaths.map((path) => ({
749
+ path,
750
+ backup: `${path}.${transactionId}.${operation.kind}-backup`
751
+ }));
752
+ const transaction = {
753
+ version: 1,
754
+ id: transactionId,
755
+ kind: operation.kind,
756
+ originalSegments: originalPaths.map((path) => basename(path)),
757
+ stagedSegments: staged.files.map((item) => ({
758
+ path: basename(item.path),
759
+ temporary: basename(item.temporaryPath)
760
+ })),
761
+ sidecarBackup: basename(contextualWorldStateBackup)
762
+ };
763
+ const installedPaths = [];
764
+ let committed = false;
765
+ try {
766
+ await beginSessionStorageTransaction(this.#sessionDirectory(sessionId), transaction);
767
+ contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
768
+ generatedTrash = await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'generated_images', operation.kind === 'rollback' ? new Set(generatedImageFileNames(retained)) : new Set(), transactionId);
769
+ inputTrash = await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'input_images', operation.kind === 'rollback'
770
+ ? new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName))
771
+ : new Set(), transactionId);
772
+ inputTrash.push(...(await stageUnreferencedImages(this.#sessionDirectory(sessionId), 'tool_results', operation.kind === 'rollback' ? toolResultResourceFiles(retained) : new Set(), transactionId)));
773
+ for (const item of backups)
774
+ await rename(item.path, item.backup);
775
+ for (const item of staged.files) {
776
+ await rename(item.temporaryPath, item.path);
777
+ installedPaths.push(item.path);
778
+ }
779
+ if (operation.kind === 'rollback') {
780
+ const state = deriveExecutionState(retained);
781
+ const completedAt = new Date().toISOString();
782
+ meta.title = retainedSessionTitle(retained, meta.title);
783
+ meta.lastActivityAt = completedAt;
784
+ meta.nextSequence = targetNextSequence;
913
785
  meta.storageTransactionId = transactionId;
914
- meta.resume = { version: 1, todoRevision: 0, firstUserMessageIndexed: true };
915
- meta.segments = staged.segments;
916
- this.#shared.segments.set(sessionId, 1);
917
- this.#shared.index.revision++;
918
- this.#markIndexDirty(sessionId);
919
- await this.#saveIndex();
920
- committed = true;
921
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
922
- return {
923
- id: meta.id,
924
- title: meta.title,
925
- workspace: meta.workspace,
926
- createdAt: meta.createdAt,
927
- lastActivityAt: meta.lastActivityAt,
928
- source: normalizeSessionSource(meta.source)
929
- };
786
+ meta.automaticTitlePending = state.automaticTitlePending;
787
+ meta.resume = state.resume;
930
788
  }
931
- catch (error) {
932
- if (!committed) {
933
- committed = await this.#storageTransactionCommitted(sessionId, transactionId);
934
- if (committed) {
935
- this.#shared.dirtySessionVersions.delete(sessionId);
936
- this.#shared.indexDirty = this.#shared.dirtySessionVersions.size > 0;
937
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
938
- }
939
- }
940
- if (!committed) {
941
- await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
942
- await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
943
- if (contextualWorldStateStaged)
944
- await restoreRenamedFiles([
945
- { from: contextualWorldStateBackup, to: contextualWorldStatePath }
946
- ]);
947
- await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
948
- restoreSessionMeta(meta, originalMeta);
949
- this.#shared.index.revision = originalIndexRevision;
950
- if (originalSegment === undefined)
951
- this.#shared.segments.delete(sessionId);
952
- else
953
- this.#shared.segments.set(sessionId, originalSegment);
954
- this.#markIndexDirty(sessionId);
955
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), originalMeta.storageTransactionId);
789
+ else {
790
+ meta.lastActivityAt = operation.clearedAt;
791
+ meta.nextSequence = targetNextSequence;
792
+ meta.storageTransactionId = transactionId;
793
+ meta.resume = { version: 1, userTurns: 0, todoRevision: 0, firstUserMessageIndexed: true };
794
+ }
795
+ meta.segments = staged.segments;
796
+ this.#shared.segments.set(sessionId, operation.kind === 'clear' ? 1 : (staged.segments.at(-1)?.number ?? 1));
797
+ this.#shared.index.revision++;
798
+ this.#markIndexDirty(sessionId);
799
+ await this.#saveIndex();
800
+ committed = true;
801
+ await recoverSessionStorageTransaction(this.#sessionDirectory(sessionId), transactionId);
802
+ return {
803
+ id: meta.id,
804
+ title: meta.title,
805
+ workspace: meta.workspace,
806
+ createdAt: meta.createdAt,
807
+ lastActivityAt: meta.lastActivityAt,
808
+ source: normalizeSessionSource(meta.source)
809
+ };
810
+ }
811
+ catch (error) {
812
+ if (!committed) {
813
+ committed = await this.#storageTransactionCommitted(sessionId, transactionId);
814
+ if (committed) {
815
+ this.#shared.dirtySessionVersions.delete(sessionId);
816
+ this.#shared.indexDirty = this.#shared.dirtySessionVersions.size > 0;
817
+ await recoverSessionStorageTransaction(this.#sessionDirectory(sessionId), transactionId);
956
818
  }
957
- throw error;
958
819
  }
959
- finally {
960
- await Promise.all(staged.files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
820
+ if (!committed) {
821
+ await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
822
+ await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
823
+ if (contextualWorldStateStaged)
824
+ await restoreRenamedFiles([
825
+ { from: contextualWorldStateBackup, to: contextualWorldStatePath }
826
+ ]);
827
+ await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
828
+ restoreSessionMeta(meta, originalMeta);
829
+ this.#shared.index.revision = originalIndexRevision;
830
+ if (originalSegment === undefined)
831
+ this.#shared.segments.delete(sessionId);
832
+ else
833
+ this.#shared.segments.set(sessionId, originalSegment);
834
+ this.#markIndexDirty(sessionId);
835
+ await recoverSessionStorageTransaction(this.#sessionDirectory(sessionId), originalMeta.storageTransactionId);
961
836
  }
837
+ throw error;
962
838
  }
963
839
  finally {
964
- await release();
840
+ await Promise.all(staged.files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
965
841
  }
966
842
  }
967
843
  async getSession(sessionId) {
@@ -1021,7 +897,7 @@ export class JsonlSessionStore {
1021
897
  payload: input.payload
1022
898
  };
1023
899
  JSON.stringify(record); // Reject malformed payloads before binding resources.
1024
- await bindToolResultResources(this.sessionDirectory(sessionId), record, (sequence) => this.#readRecordAtSequence(before, sequence), true);
900
+ await bindToolResultResources(this.#sessionDirectory(sessionId), record, (sequence) => this.#readRecordAtSequence(before, sequence), true);
1025
901
  const serialized = `${JSON.stringify(record)}\n`;
1026
902
  let segment = await this.#currentSegment(sessionId);
1027
903
  let path = this.#segmentPath(sessionId, segment);
@@ -1036,20 +912,23 @@ export class JsonlSessionStore {
1036
912
  catch {
1037
913
  // The first append creates the first segment.
1038
914
  }
1039
- const handle = await open(path, 'a', 0o600);
1040
915
  try {
1041
- const bytes = Buffer.from(serialized);
1042
- let offset = 0;
1043
- while (offset < bytes.length) {
1044
- const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
1045
- if (bytesWritten === 0)
1046
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session append is incomplete.');
1047
- offset += bytesWritten;
1048
- }
1049
- await handle.sync();
916
+ await appendRecordBytes(path, serialized);
1050
917
  }
1051
- finally {
1052
- await handle.close();
918
+ catch (error) {
919
+ // A failed write/sync/close can leave a complete record on disk.
920
+ const current = this.#shared.index.sessions.find((item) => item.id === sessionId) ?? before;
921
+ if (current.resume?.userTurns !== undefined) {
922
+ delete current.resume.userTurns;
923
+ if (published)
924
+ this.#markIndexDirty(sessionId);
925
+ }
926
+ const lease = this.#shared.writerLeases.get(sessionId);
927
+ if (lease) {
928
+ delete lease.codeState;
929
+ delete lease.extensionCheckpoints;
930
+ }
931
+ throw error;
1053
932
  }
1054
933
  // Index refreshes and commits can replace metadata during file I/O. Publish all
1055
934
  // durable-record metadata onto the current entry without another await boundary.
@@ -1077,7 +956,8 @@ export class JsonlSessionStore {
1077
956
  this.#shared.index.revision++;
1078
957
  this.#markIndexDirty(sessionId);
1079
958
  }
1080
- await completeToolResultResources(this.sessionDirectory(sessionId), record);
959
+ updateSessionCurrentState(this.#shared.writerLeases.get(sessionId), record.type, serialized);
960
+ await completeToolResultResources(this.#sessionDirectory(sessionId), record);
1081
961
  if (record.type === 'turn.user' &&
1082
962
  isObject(record.payload) &&
1083
963
  Array.isArray(record.payload.images))
@@ -1085,7 +965,7 @@ export class JsonlSessionStore {
1085
965
  if (isObject(image) &&
1086
966
  typeof image.fileName === 'string' &&
1087
967
  validInputImageFileName(image.fileName))
1088
- await rm(join(this.sessionDirectory(sessionId), 'input_images', `.${image.fileName}.pending`), { force: true }).catch(() => undefined);
968
+ await rm(join(this.#sessionDirectory(sessionId), 'input_images', `.${image.fileName}.pending`), { force: true }).catch(() => undefined);
1089
969
  return record;
1090
970
  }
1091
971
  async read(sessionId) {
@@ -1106,10 +986,32 @@ export class JsonlSessionStore {
1106
986
  }
1107
987
  }
1108
988
  async readCodeModeState(sessionId) {
1109
- return restoreCodeModeState(await this.read(sessionId));
989
+ return this.#serializeRecordOperation(sessionId, () => this.#readCodeModeState(sessionId));
990
+ }
991
+ async #readCodeModeState(sessionId) {
992
+ this.#assertOpen();
993
+ const lease = this.#shared.writerLeases.get(sessionId);
994
+ if (lease?.owner === undefined)
995
+ return restoreCodeModeState(await this.#readRecords(sessionId));
996
+ lease.codeState ??= restoreCodeModeState(await this.#readRecords(sessionId));
997
+ return applyCodeModeStateMutations(lease.codeState, []);
1110
998
  }
1111
999
  async readExtensionCheckpoint(sessionId, extensionId, key) {
1112
- return restoreExtensionCheckpoint(await this.read(sessionId), extensionId, key);
1000
+ return this.#serializeRecordOperation(sessionId, () => this.#readExtensionCheckpoint(sessionId, extensionId, key));
1001
+ }
1002
+ async #readExtensionCheckpoint(sessionId, extensionId, key) {
1003
+ this.#assertOpen();
1004
+ const lease = this.#shared.writerLeases.get(sessionId);
1005
+ if (lease?.owner === undefined)
1006
+ return restoreExtensionCheckpoint(await this.#readRecords(sessionId), extensionId, key);
1007
+ if (!lease.extensionCheckpoints) {
1008
+ const index = new Map();
1009
+ for (const record of await this.#readRecords(sessionId))
1010
+ if (record.type === 'extension.checkpoint.updated')
1011
+ indexExtensionCheckpoint(index, record.payload);
1012
+ lease.extensionCheckpoints = index;
1013
+ }
1014
+ return extensionCheckpointFromPayload(lease.extensionCheckpoints.get(extensionId)?.get(key));
1113
1015
  }
1114
1016
  async writeExtensionCheckpoint(sessionId, extensionId, key, value, expectedRevision, input) {
1115
1017
  if (typeof key !== 'string' ||
@@ -1121,7 +1023,7 @@ export class JsonlSessionStore {
1121
1023
  const release = await this.acquireSessionWriteLease(sessionId, input.owner);
1122
1024
  try {
1123
1025
  return await this.#serializeRecordOperation(sessionId, async () => {
1124
- const current = restoreExtensionCheckpoint(await this.#readRecords(sessionId), extensionId, key);
1026
+ const current = await this.#readExtensionCheckpoint(sessionId, extensionId, key);
1125
1027
  if (expectedRevision !== undefined && expectedRevision !== current.revision)
1126
1028
  throw new MarAgentError('MAR_AGENT_EXTENSION_DATA_CONFLICT', 'Checkpoint revision changed before commit.');
1127
1029
  input.signal.throwIfAborted();
@@ -1200,73 +1102,61 @@ export class JsonlSessionStore {
1200
1102
  return this.#rebuildExecutionState(meta);
1201
1103
  }
1202
1104
  }
1203
- async readContext(sessionId) {
1204
- return (await this.readExecutionState(sessionId)).contextRecords;
1205
- }
1206
- async contextGcProjectionHash(sessionId, boundarySequence) {
1207
- this.#assertOpen();
1208
- const hash = contextGcSourceProjectionHash((await this.readExecutionState(sessionId)).contextRecords, boundarySequence);
1209
- if (!hash)
1210
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC projection boundary is invalid.');
1211
- return hash;
1212
- }
1213
- /** Recheck a planned GC checkpoint against authoritative records before appending it. */
1214
- async commitContextGc(sessionId, draft) {
1105
+ /** Compare the planned sequence inside the existing record writer queue. */
1106
+ async commitContextGc(sessionId, draft, planningRecords) {
1215
1107
  this.#assertOpen();
1216
- const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
1217
- if (!meta)
1218
- throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1219
- const records = (await this.readExecutionState(sessionId)).contextRecords;
1220
- const boundarySequence = records.at(-1)?.sequence;
1221
- if (boundarySequence === undefined || boundarySequence !== draft.boundarySequence)
1222
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC projection changed before checkpoint commit.');
1223
- const base = contextGcBaseFromRecords(records, boundarySequence);
1224
- const sourceProjectionHash = contextGcSourceProjectionHash(records, boundarySequence, draft.base);
1225
- if (!sameContextGcBase(base, draft.base) ||
1226
- sourceProjectionHash === undefined ||
1227
- sourceProjectionHash !== draft.sourceProjectionHash)
1228
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC source projection validation failed.');
1229
- const previous = latestValidContextGcCheckpoint(records, boundarySequence, meta.workspace);
1230
- if (previous && !sameContextGcBase(previous.payload.base, draft.base))
1231
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint base changed before commit.');
1232
- if (previous) {
1233
- const minimumUsageGrowth = contextGcMinimumUsageGrowth(draft.trigger.contextWindowTokens);
1234
- if (draft.trigger.normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal < 2 ||
1235
- draft.trigger.contextInputTokens - previous.payload.trigger.contextInputTokens <
1236
- minimumUsageGrowth)
1237
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
1238
- }
1239
- const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
1240
- if (!plannedReplacements || plannedReplacements.length === 0)
1241
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
1242
- const replacements = normalizeContextGcReplacements([
1243
- ...(previous?.payload.replacements ?? []),
1244
- ...plannedReplacements
1245
- ]);
1246
- if (!replacements ||
1247
- (previous && isDeepStrictEqual(replacements, previous.payload.replacements)))
1248
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC replacement map is invalid.');
1249
- const payload = {
1250
- ...structuredClone(draft),
1251
- checkpointId: randomUUID(),
1252
- generation: (previous?.payload.generation ?? 0) + 1,
1253
- replacements
1254
- };
1255
- const candidate = {
1256
- schemaVersion: 1,
1257
- recordId: randomUUID(),
1258
- sequence: boundarySequence + 1,
1259
- timestamp: new Date().toISOString(),
1260
- sessionId,
1261
- type: 'context.gc.completed',
1262
- payload
1263
- };
1264
- if (Buffer.byteLength(JSON.stringify(payload)) >= MAX_SEGMENT_BYTES ||
1265
- validateContextGcCheckpoint([...records, candidate], candidate, meta.workspace) === undefined)
1266
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint validation failed.');
1267
- return this.append(sessionId, {
1268
- type: 'context.gc.completed',
1269
- payload
1108
+ const records = planningRecords ?? (await this.readExecutionState(sessionId)).contextRecords;
1109
+ return this.#serializeRecordOperation(sessionId, async () => {
1110
+ const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
1111
+ if (!meta)
1112
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1113
+ const boundarySequence = meta.nextSequence - 1;
1114
+ if (boundarySequence !== draft.boundarySequence)
1115
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC projection changed before checkpoint commit.');
1116
+ if (records.at(-1)?.sequence !== boundarySequence ||
1117
+ records.at(-1)?.sessionId !== sessionId ||
1118
+ !sameContextGcBase(contextGcBaseFromRecords(records, boundarySequence), draft.base))
1119
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC planning boundary is invalid.');
1120
+ const previous = latestValidContextGcCheckpoint(records, boundarySequence, meta.workspace);
1121
+ if (previous) {
1122
+ const minimumUsageGrowth = contextGcMinimumUsageGrowth(draft.trigger.contextWindowTokens);
1123
+ if (draft.trigger.normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal < 2 ||
1124
+ draft.trigger.contextInputTokens - previous.payload.trigger.contextInputTokens <
1125
+ minimumUsageGrowth)
1126
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
1127
+ }
1128
+ const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
1129
+ if (!plannedReplacements || plannedReplacements.length === 0)
1130
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
1131
+ const replacements = normalizeContextGcReplacements([
1132
+ ...(previous?.payload.replacements ?? []),
1133
+ ...plannedReplacements
1134
+ ]);
1135
+ if (!replacements ||
1136
+ (previous && isDeepStrictEqual(replacements, previous.payload.replacements)))
1137
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC replacement map is invalid.');
1138
+ const payload = {
1139
+ ...structuredClone(draft),
1140
+ checkpointId: randomUUID(),
1141
+ generation: (previous?.payload.generation ?? 0) + 1,
1142
+ replacements
1143
+ };
1144
+ const candidate = {
1145
+ schemaVersion: 1,
1146
+ recordId: randomUUID(),
1147
+ sequence: boundarySequence + 1,
1148
+ timestamp: new Date().toISOString(),
1149
+ sessionId,
1150
+ type: 'context.gc.completed',
1151
+ payload
1152
+ };
1153
+ if (Buffer.byteLength(JSON.stringify(payload)) >= MAX_SEGMENT_BYTES ||
1154
+ validateContextGcCheckpoint(records, candidate, meta.workspace, previous?.payload.replacements) === undefined)
1155
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint validation failed.');
1156
+ return this.#appendRecord(sessionId, {
1157
+ type: 'context.gc.completed',
1158
+ payload
1159
+ });
1270
1160
  });
1271
1161
  }
1272
1162
  async readPage(sessionId, input = {}) {
@@ -1411,6 +1301,8 @@ export class JsonlSessionStore {
1411
1301
  await lease.release();
1412
1302
  }
1413
1303
  finally {
1304
+ delete lease.codeState;
1305
+ delete lease.extensionCheckpoints;
1414
1306
  this.#shared.writerLeases.delete(sessionId);
1415
1307
  }
1416
1308
  }
@@ -1429,7 +1321,7 @@ export class JsonlSessionStore {
1429
1321
  const current = disk.sessions.find((item) => item.id === sessionId);
1430
1322
  if (!current)
1431
1323
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1432
- await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), current.storageTransactionId);
1324
+ await recoverSessionStorageTransaction(this.#sessionDirectory(sessionId), current.storageTransactionId);
1433
1325
  await recoverContextualWorldStateTransactions(this.#shared.root, current);
1434
1326
  await this.#refreshIndex();
1435
1327
  await this.#reconcileTail(sessionId);
@@ -1550,6 +1442,8 @@ export class JsonlSessionStore {
1550
1442
  const resume = meta.resume;
1551
1443
  if (resume?.version !== 1 || !Number.isSafeInteger(resume.todoRevision))
1552
1444
  return;
1445
+ if (resume.userTurns !== undefined && isNativeUserTurn(record))
1446
+ resume.userTurns++;
1553
1447
  if (resume.firstUserMessageIndexed === true && resume.firstUserMessagePreview === undefined) {
1554
1448
  const preview = userMessagePreviewFromRecord(record);
1555
1449
  if (preview !== undefined)
@@ -1640,6 +1534,73 @@ export class JsonlSessionStore {
1640
1534
  contextRecords: state.contextRecords
1641
1535
  };
1642
1536
  }
1537
+ /** Counts native input records, not the post-Compact model context. */
1538
+ async userTurnCount(sessionId) {
1539
+ return this.#serializeRecordOperation(sessionId, async () => {
1540
+ this.#assertOpen();
1541
+ await this.#refreshIndex();
1542
+ const before = this.#shared.index.sessions.find((session) => session.id === sessionId);
1543
+ if (!before)
1544
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1545
+ const indexed = before.resume?.userTurns;
1546
+ if (indexed !== undefined && this.#shared.writerLeases.has(sessionId))
1547
+ return indexed;
1548
+ const nextSequence = before.nextSequence;
1549
+ const transactionId = before.storageTransactionId;
1550
+ const paths = await this.#segmentPaths(sessionId);
1551
+ let count = indexed ?? 0;
1552
+ let lastSequence = 0;
1553
+ let reachedPrefix = false;
1554
+ if (indexed !== undefined) {
1555
+ // No local writer: a previous writer may have committed bytes but not its index.
1556
+ for (const path of [...paths].reverse()) {
1557
+ for await (const record of readRecordsFromTail(path)) {
1558
+ lastSequence = Math.max(lastSequence, record.sequence);
1559
+ if (record.sequence < nextSequence) {
1560
+ reachedPrefix = true;
1561
+ break;
1562
+ }
1563
+ if (isNativeUserTurn(record))
1564
+ count++;
1565
+ }
1566
+ if (reachedPrefix)
1567
+ break;
1568
+ }
1569
+ }
1570
+ if (indexed === undefined || lastSequence < nextSequence - 1) {
1571
+ count = 0;
1572
+ lastSequence = 0;
1573
+ for (const path of paths) {
1574
+ for await (const record of readRecordsFromHead(path)) {
1575
+ lastSequence = record.sequence;
1576
+ if (isNativeUserTurn(record))
1577
+ count++;
1578
+ }
1579
+ }
1580
+ }
1581
+ await this.#refreshIndex();
1582
+ const current = this.#shared.index.sessions.find((session) => session.id === sessionId);
1583
+ if (!current)
1584
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1585
+ if (current.storageTransactionId !== transactionId) {
1586
+ // The indexed prefix no longer belongs to this history. Fall back to the
1587
+ // ordinary physical read boundary without caching or a conflict error.
1588
+ count = 0;
1589
+ for (const path of await this.#segmentPaths(sessionId))
1590
+ for await (const record of readRecordsFromHead(path))
1591
+ if (isNativeUserTurn(record))
1592
+ count++;
1593
+ return count;
1594
+ }
1595
+ if (current.nextSequence === nextSequence &&
1596
+ lastSequence === nextSequence - 1 &&
1597
+ current.resume?.userTurns === undefined) {
1598
+ (current.resume ??= { version: 1, todoRevision: 0 }).userTurns = count;
1599
+ this.#markIndexDirty(sessionId, true);
1600
+ }
1601
+ return count;
1602
+ });
1603
+ }
1643
1604
  async firstUserMessagePreview(sessionId) {
1644
1605
  this.#assertOpen();
1645
1606
  await this.#refreshIndex();
@@ -1788,11 +1749,11 @@ export class JsonlSessionStore {
1788
1749
  if (this.#leaseClosed || this.#shared.closed)
1789
1750
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Session store is closed.');
1790
1751
  }
1791
- #markIndexDirty(sessionId, previewOnly = false) {
1792
- if (!previewOnly)
1793
- this.#shared.previewOnlyDirtySessions.delete(sessionId);
1752
+ #markIndexDirty(sessionId, derivedOnly = false) {
1753
+ if (!derivedOnly)
1754
+ this.#shared.derivedOnlyDirtySessions.delete(sessionId);
1794
1755
  else if (!this.#shared.dirtySessionVersions.has(sessionId))
1795
- this.#shared.previewOnlyDirtySessions.add(sessionId);
1756
+ this.#shared.derivedOnlyDirtySessions.add(sessionId);
1796
1757
  this.#shared.indexDirty = true;
1797
1758
  this.#shared.dirtySessionVersions.set(sessionId, (this.#shared.dirtySessionVersions.get(sessionId) ?? 0) + 1);
1798
1759
  }
@@ -1834,8 +1795,8 @@ export class JsonlSessionStore {
1834
1795
  const latestPath = paths.at(-1);
1835
1796
  await repairSegmentTail(latestPath);
1836
1797
  const records = await readRecords(latestPath);
1837
- await recoverPendingInputImages(this.sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1838
- await cleanupUnreferencedToolResources(this.sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1798
+ await recoverPendingInputImages(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1799
+ await cleanupUnreferencedToolResources(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1839
1800
  const last = records.at(-1);
1840
1801
  const match = /events-(\d{6})\.jsonl$/.exec(latestPath);
1841
1802
  const segment = Number(match?.[1] ?? 1);
@@ -1887,67 +1848,6 @@ export class JsonlSessionStore {
1887
1848
  JsonlSessionStore.#instances.delete(this.#shared.root);
1888
1849
  }
1889
1850
  }
1890
- /** Merge a derived cache only onto the unchanged history it was computed from. */
1891
- function mergeFirstUserMessagePreviewCache(current, cached) {
1892
- if (cached.resume?.firstUserMessageIndexed !== true ||
1893
- current.nextSequence !== cached.nextSequence ||
1894
- current.storageTransactionId !== cached.storageTransactionId ||
1895
- current.resume?.firstUserMessagePreview !== undefined ||
1896
- (current.resume?.firstUserMessageIndexed === true &&
1897
- cached.resume.firstUserMessagePreview === undefined))
1898
- return current;
1899
- return {
1900
- ...current,
1901
- resume: {
1902
- ...(current.resume ?? { version: 1, todoRevision: 0 }),
1903
- firstUserMessageIndexed: true,
1904
- ...(cached.resume?.firstUserMessagePreview === undefined
1905
- ? {}
1906
- : { firstUserMessagePreview: cached.resume.firstUserMessagePreview })
1907
- }
1908
- };
1909
- }
1910
- async function readSessionIndex(path) {
1911
- let raw;
1912
- try {
1913
- raw = await readFile(path, 'utf8');
1914
- }
1915
- catch (cause) {
1916
- if (cause.code === 'ENOENT')
1917
- return undefined;
1918
- throw cause;
1919
- }
1920
- try {
1921
- const value = JSON.parse(raw);
1922
- if (value?.version !== 1 ||
1923
- !Number.isSafeInteger(value.revision) ||
1924
- value.revision < 0 ||
1925
- !Array.isArray(value.sessions) ||
1926
- value.sessions.some((session) => typeof session?.id !== 'string' ||
1927
- !/^[0-9a-f-]{36}$/iu.test(session.id) ||
1928
- typeof session.title !== 'string' ||
1929
- typeof session.workspace !== 'string' ||
1930
- typeof session.createdAt !== 'string' ||
1931
- typeof session.lastActivityAt !== 'string' ||
1932
- !Number.isSafeInteger(session.nextSequence) ||
1933
- session.nextSequence < 1))
1934
- throw new Error('invalid index shape');
1935
- return value;
1936
- }
1937
- catch (cause) {
1938
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is corrupt.', { cause });
1939
- }
1940
- }
1941
- async function persistSessionIndex(path, index) {
1942
- const temporary = `${path}.${randomUUID()}.tmp`;
1943
- try {
1944
- await writeFile(temporary, JSON.stringify(index), { mode: 0o600, flag: 'wx', flush: true });
1945
- await rename(temporary, path);
1946
- }
1947
- finally {
1948
- await rm(temporary, { force: true });
1949
- }
1950
- }
1951
1851
  async function sessionDirectoryExists(path) {
1952
1852
  try {
1953
1853
  await lstat(path);
@@ -1959,17 +1859,6 @@ async function sessionDirectoryExists(path) {
1959
1859
  throw error;
1960
1860
  }
1961
1861
  }
1962
- async function renameIfExists(from, to) {
1963
- try {
1964
- await rename(from, to);
1965
- return true;
1966
- }
1967
- catch (error) {
1968
- if (error.code === 'ENOENT')
1969
- return false;
1970
- throw error;
1971
- }
1972
- }
1973
1862
  async function recoverContextualWorldStateTransactions(root, meta) {
1974
1863
  const directory = join(root, 'sessions', meta.id);
1975
1864
  let names;
@@ -2002,125 +1891,6 @@ async function recoverContextualWorldStateTransactions(root, meta) {
2002
1891
  await rename(backup, canonical);
2003
1892
  }
2004
1893
  }
2005
- function generatedImageFileNames(records) {
2006
- const names = new Set();
2007
- for (const record of records) {
2008
- if (record.type !== 'tool.completed' || !isObject(record.payload))
2009
- continue;
2010
- const artifacts = record.payload.artifacts;
2011
- if (!Array.isArray(artifacts))
2012
- continue;
2013
- for (const artifact of artifacts) {
2014
- if (!isObject(artifact) ||
2015
- artifact.kind !== 'image' ||
2016
- typeof artifact.fileName !== 'string' ||
2017
- basename(artifact.fileName) !== artifact.fileName ||
2018
- !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}\.png$/u.test(artifact.fileName))
2019
- continue;
2020
- names.add(artifact.fileName);
2021
- }
2022
- }
2023
- return [...names];
2024
- }
2025
- async function stageSessionSegments(sessionDirectory, records, operation = 'rollback') {
2026
- let group;
2027
- if (records.length === 0)
2028
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
2029
- const transactionId = randomUUID();
2030
- const files = [];
2031
- const segments = [];
2032
- const flush = async () => {
2033
- if (group === undefined)
2034
- return;
2035
- const number = segments.length + 1;
2036
- const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
2037
- const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
2038
- const handle = await open(temporaryPath, 'wx', 0o600);
2039
- files.push({ path, temporaryPath });
2040
- try {
2041
- await handle.writeFile(group.lines.join(''));
2042
- await handle.sync();
2043
- }
2044
- finally {
2045
- await handle.close();
2046
- }
2047
- segments.push({
2048
- number,
2049
- firstSequence: group.firstSequence,
2050
- lastSequence: group.lastSequence,
2051
- bytes: group.bytes
2052
- });
2053
- group = undefined;
2054
- };
2055
- try {
2056
- for (const record of records) {
2057
- const line = `${JSON.stringify(record)}\n`;
2058
- const bytes = Buffer.byteLength(line);
2059
- if (group !== undefined && group.bytes > 0 && group.bytes + bytes > MAX_SEGMENT_BYTES)
2060
- await flush();
2061
- if (group === undefined) {
2062
- group = {
2063
- lines: [],
2064
- bytes: 0,
2065
- firstSequence: record.sequence,
2066
- lastSequence: record.sequence
2067
- };
2068
- }
2069
- group.lines.push(line);
2070
- group.bytes += bytes;
2071
- group.lastSequence = record.sequence;
2072
- }
2073
- await flush();
2074
- return { files, segments };
2075
- }
2076
- catch (error) {
2077
- await Promise.all(files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
2078
- throw error;
2079
- }
2080
- }
2081
- async function stageUnreferencedImages(sessionDirectory, directoryName, retained, transactionId) {
2082
- const directory = join(sessionDirectory, directoryName);
2083
- let names;
2084
- try {
2085
- names = await readdir(directory);
2086
- }
2087
- catch (error) {
2088
- if (error.code === 'ENOENT')
2089
- return [];
2090
- throw error;
2091
- }
2092
- const moved = [];
2093
- try {
2094
- for (const name of names) {
2095
- if (retained.has(name))
2096
- continue;
2097
- const from = join(directory, name);
2098
- const trash = join(directory, `.${name}.${transactionId}.rollback-trash`);
2099
- await rename(from, trash);
2100
- moved.push({ from: trash, to: from, trash });
2101
- }
2102
- return moved;
2103
- }
2104
- catch (error) {
2105
- await restoreRenamedFiles(moved);
2106
- throw error;
2107
- }
2108
- }
2109
- async function restoreRenamedFiles(files) {
2110
- for (const file of files.toReversed())
2111
- await rename(file.from, file.to).catch(() => undefined);
2112
- }
2113
- function retainedSessionTitle(records, fallback) {
2114
- let title = fallback;
2115
- for (const record of records) {
2116
- if ((record.type === 'session.created' || record.type === 'session.title.updated') &&
2117
- isObject(record.payload) &&
2118
- typeof record.payload.title === 'string' &&
2119
- record.payload.title.trim().length > 0)
2120
- title = record.payload.title;
2121
- }
2122
- return title;
2123
- }
2124
1894
  function restoreSessionMeta(meta, original) {
2125
1895
  meta.id = original.id;
2126
1896
  meta.title = original.title;
@@ -2145,722 +1915,6 @@ function restoreSessionMeta(meta, original) {
2145
1915
  else
2146
1916
  meta.segments = structuredClone(original.segments);
2147
1917
  }
2148
- async function copyGeneratedImages(sourceSessionDirectory, targetSessionDirectory, fileNames) {
2149
- if (fileNames.length === 0)
2150
- return;
2151
- const source = join(sourceSessionDirectory, 'generated_images');
2152
- const target = join(targetSessionDirectory, 'generated_images');
2153
- await mkdir(target, { recursive: true });
2154
- for (const fileName of fileNames)
2155
- await copyFile(join(source, fileName), join(target, fileName));
2156
- }
2157
- function inputImageReferencesFromRecords(records) {
2158
- const references = new Map();
2159
- for (const record of records) {
2160
- if (record.type !== 'turn.user' || !isObject(record.payload))
2161
- continue;
2162
- for (const reference of inputImageReferences(record.payload.images)) {
2163
- const existing = references.get(reference.fileName);
2164
- if (existing !== undefined &&
2165
- (existing.mimeType !== reference.mimeType ||
2166
- existing.byteSize !== reference.byteSize ||
2167
- existing.sha256 !== reference.sha256))
2168
- throw corruptInputImageReference();
2169
- references.set(reference.fileName, reference);
2170
- }
2171
- }
2172
- return [...references.values()];
2173
- }
2174
- function inputImageReferences(value) {
2175
- if (!Array.isArray(value))
2176
- return [];
2177
- const references = [];
2178
- let totalBytes = 0;
2179
- for (const image of value) {
2180
- const reference = inputImageReference(image);
2181
- if (reference === undefined)
2182
- continue;
2183
- totalBytes += reference.byteSize;
2184
- if (references.length >= MAX_INPUT_IMAGES || totalBytes > MAX_INPUT_IMAGES_BYTES)
2185
- throw corruptInputImageReference();
2186
- references.push(reference);
2187
- }
2188
- return references;
2189
- }
2190
- function inputImageReference(value) {
2191
- if (!isObject(value) || !Object.hasOwn(value, 'fileName'))
2192
- return undefined;
2193
- if (typeof value.name !== 'string' ||
2194
- typeof value.mimeType !== 'string' ||
2195
- !INPUT_IMAGE_MIME_TYPES.has(value.mimeType) ||
2196
- typeof value.byteSize !== 'number' ||
2197
- !Number.isSafeInteger(value.byteSize) ||
2198
- value.byteSize <= 0 ||
2199
- value.byteSize > MAX_INPUT_IMAGE_BYTES ||
2200
- typeof value.fileName !== 'string' ||
2201
- !validInputImageFileName(value.fileName) ||
2202
- typeof value.sha256 !== 'string' ||
2203
- !/^[a-f0-9]{64}$/u.test(value.sha256))
2204
- throw corruptInputImageReference();
2205
- return {
2206
- name: value.name,
2207
- fileName: value.fileName,
2208
- mimeType: value.mimeType,
2209
- byteSize: value.byteSize,
2210
- sha256: value.sha256
2211
- };
2212
- }
2213
- function validInputImageFileName(fileName) {
2214
- return (basename(fileName) === fileName && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}\.bin$/u.test(fileName));
2215
- }
2216
- async function readInputImageBytes(directory, reference) {
2217
- const path = join(directory, reference.fileName);
2218
- for (let attempt = 0; attempt < 2; attempt++) {
2219
- let handle;
2220
- try {
2221
- const before = await lstat(path);
2222
- if (!before.isFile() || before.size !== reference.byteSize)
2223
- throw corruptInputImageReference();
2224
- const flags = process.platform === 'win32'
2225
- ? constants.O_RDONLY
2226
- : constants.O_RDONLY | constants.O_NOFOLLOW;
2227
- handle = await open(path, flags);
2228
- const opened = await handle.stat();
2229
- if (!opened.isFile() ||
2230
- opened.size !== reference.byteSize ||
2231
- opened.dev !== before.dev ||
2232
- opened.ino !== before.ino)
2233
- continue;
2234
- const data = await readExactBytes(handle, reference.byteSize);
2235
- const after = await handle.stat();
2236
- if (after.size !== opened.size ||
2237
- after.mtimeMs !== opened.mtimeMs ||
2238
- after.ctimeMs !== opened.ctimeMs)
2239
- continue;
2240
- if (createHash('sha256').update(data).digest('hex') !== reference.sha256)
2241
- throw corruptInputImageReference();
2242
- return data;
2243
- }
2244
- catch (cause) {
2245
- if (cause instanceof MarAgentError)
2246
- throw cause;
2247
- throw corruptInputImageReference(cause);
2248
- }
2249
- finally {
2250
- await handle?.close();
2251
- }
2252
- }
2253
- throw corruptInputImageReference();
2254
- }
2255
- async function readExactBytes(handle, byteSize) {
2256
- const data = Buffer.alloc(byteSize);
2257
- let offset = 0;
2258
- while (offset < byteSize) {
2259
- const { bytesRead } = await handle.read(data, offset, byteSize - offset, offset);
2260
- if (bytesRead === 0)
2261
- throw corruptInputImageReference();
2262
- offset += bytesRead;
2263
- }
2264
- const extra = Buffer.allocUnsafe(1);
2265
- if ((await handle.read(extra, 0, 1, byteSize)).bytesRead !== 0)
2266
- throw corruptInputImageReference();
2267
- return data;
2268
- }
2269
- async function copyInputImages(sourceSessionDirectory, targetSessionDirectory, references) {
2270
- if (references.length === 0)
2271
- return;
2272
- const source = join(sourceSessionDirectory, 'input_images');
2273
- const target = join(targetSessionDirectory, 'input_images');
2274
- await mkdir(target, { recursive: true, mode: 0o700 });
2275
- for (const reference of references) {
2276
- const data = await readInputImageBytes(source, reference);
2277
- await writeFile(join(target, reference.fileName), data, { mode: 0o600, flag: 'wx' });
2278
- }
2279
- }
2280
- function corruptInputImageReference(cause) {
2281
- return new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session input image is corrupt.', {
2282
- ...(cause === undefined ? {} : { cause })
2283
- });
2284
- }
2285
1918
  function isObject(value) {
2286
1919
  return value !== null && typeof value === 'object' && !Array.isArray(value);
2287
1920
  }
2288
- function normalizeSessionSource(value) {
2289
- if (value === undefined || value.type === 'user')
2290
- return { type: 'user' };
2291
- if (value.type !== 'subagent' ||
2292
- typeof value.parentSessionId !== 'string' ||
2293
- !value.parentSessionId.trim() ||
2294
- typeof value.rootSessionId !== 'string' ||
2295
- !value.rootSessionId.trim() ||
2296
- (!value.parentExecutionId && !value.parentOperationId) ||
2297
- [
2298
- value.parentExecutionId,
2299
- value.parentOperationId,
2300
- value.ownerInstanceId,
2301
- value.extensionId
2302
- ].some((identity) => identity !== undefined && (typeof identity !== 'string' || !identity.trim())) ||
2303
- ((value.ownerInstanceId !== undefined || value.extensionId !== undefined) &&
2304
- value.parentOperationId === undefined) ||
2305
- typeof value.description !== 'string' ||
2306
- !value.description.trim() ||
2307
- (value.role !== 'general-purpose' && value.role !== 'explore'))
2308
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session source is invalid.');
2309
- return {
2310
- type: 'subagent',
2311
- parentSessionId: value.parentSessionId,
2312
- rootSessionId: value.rootSessionId,
2313
- ...(value.parentExecutionId === undefined
2314
- ? {}
2315
- : { parentExecutionId: value.parentExecutionId }),
2316
- ...(value.parentOperationId === undefined
2317
- ? {}
2318
- : { parentOperationId: value.parentOperationId }),
2319
- ...(value.ownerInstanceId === undefined ? {} : { ownerInstanceId: value.ownerInstanceId }),
2320
- ...(value.extensionId === undefined ? {} : { extensionId: value.extensionId }),
2321
- role: value.role,
2322
- description: value.description.trim()
2323
- };
2324
- }
2325
- function sessionListFilter(input) {
2326
- const source = input.source ??
2327
- (input.parentSessionId !== undefined || input.rootSessionId !== undefined
2328
- ? 'subagent'
2329
- : 'user');
2330
- if ((source !== 'user' && source !== 'subagent') ||
2331
- (input.parentSessionId !== undefined &&
2332
- (source !== 'subagent' || input.parentSessionId.length === 0)) ||
2333
- (input.rootSessionId !== undefined &&
2334
- (source !== 'subagent' || input.rootSessionId.length === 0)))
2335
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session list filter is invalid.');
2336
- return {
2337
- source,
2338
- ...(input.parentSessionId === undefined ? {} : { parentSessionId: input.parentSessionId }),
2339
- ...(input.rootSessionId === undefined ? {} : { rootSessionId: input.rootSessionId })
2340
- };
2341
- }
2342
- function sessionMatchesFilter(meta, filter) {
2343
- const source = normalizeSessionSource(meta.source);
2344
- return (source.type === filter.source &&
2345
- (filter.parentSessionId === undefined ||
2346
- (source.type === 'subagent' && source.parentSessionId === filter.parentSessionId)) &&
2347
- (filter.rootSessionId === undefined ||
2348
- (source.type === 'subagent' && source.rootSessionId === filter.rootSessionId)));
2349
- }
2350
- function encodeSessionListCursor(revision, offset, filter) {
2351
- return Buffer.from(JSON.stringify({ version: 1, revision, offset, filter })).toString('base64url');
2352
- }
2353
- function decodeSessionListCursor(cursor, revision, filter) {
2354
- if (cursor === undefined)
2355
- return 0;
2356
- try {
2357
- const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
2358
- if (value.version !== 1 ||
2359
- value.revision !== revision ||
2360
- !Number.isSafeInteger(value.offset) ||
2361
- value.offset < 0 ||
2362
- !isDeepStrictEqual(value.filter, filter))
2363
- throw new Error('invalid cursor');
2364
- return value.offset;
2365
- }
2366
- catch {
2367
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Session list cursor is invalid.');
2368
- }
2369
- }
2370
- function sessionTreeIds(sessions, sessionId) {
2371
- if (!sessions.some((session) => session.id === sessionId))
2372
- return [];
2373
- const ids = [sessionId];
2374
- for (let index = 0; index < ids.length; index++) {
2375
- const parentId = ids[index];
2376
- for (const session of sessions) {
2377
- const source = normalizeSessionSource(session.source);
2378
- if (source.type === 'subagent' &&
2379
- source.parentSessionId === parentId &&
2380
- !ids.includes(session.id))
2381
- ids.push(session.id);
2382
- }
2383
- }
2384
- return ids;
2385
- }
2386
- function forkedPayload(type, value, sourceSessionId, targetSessionId, title, identities) {
2387
- if (value === null || typeof value !== 'object' || Array.isArray(value))
2388
- return value;
2389
- const payload = { ...value };
2390
- if (payload.sessionId === sourceSessionId)
2391
- payload.sessionId = targetSessionId;
2392
- const application = (input) => {
2393
- if (typeof input.inputRef !== 'string')
2394
- return input;
2395
- const identity = identities.get(input.inputRef);
2396
- if (!identity)
2397
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork input reference is missing.');
2398
- return {
2399
- ...input,
2400
- inputRef: identity.recordId,
2401
- ...(isObject(input.metadata)
2402
- ? { metadata: { ...input.metadata, recordId: identity.recordId } }
2403
- : {})
2404
- };
2405
- };
2406
- if (type === 'model.context')
2407
- return application(payload);
2408
- if (type === 'compact.completed') {
2409
- if (Array.isArray(payload.retainedModelContext))
2410
- payload.retainedModelContext = payload.retainedModelContext.map((item) => isObject(item) ? application(item) : item);
2411
- if (Array.isArray(payload.pendingInputs))
2412
- payload.pendingInputs = payload.pendingInputs.map((record) => {
2413
- const identity = identities.get(record.recordId);
2414
- if (!identity)
2415
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork pending input reference is missing.');
2416
- return { ...record, ...identity, sessionId: targetSessionId };
2417
- });
2418
- }
2419
- if (type === 'session.title.updated') {
2420
- payload.title = title;
2421
- payload.automatic = false;
2422
- }
2423
- return payload;
2424
- }
2425
- function deriveExecutionState(records) {
2426
- let automaticTitle = false;
2427
- let sawUserTurn = false;
2428
- let compactSequence;
2429
- let todoSequence;
2430
- let todoRevision = 0;
2431
- let todoItems = [];
2432
- let executionModel;
2433
- let firstUserMessagePreview;
2434
- for (const record of records) {
2435
- firstUserMessagePreview ??= userMessagePreviewFromRecord(record);
2436
- if (record.type === 'session.created')
2437
- automaticTitle =
2438
- record.payload?.automaticTitle === true;
2439
- else if (record.type === 'turn.user')
2440
- sawUserTurn = true;
2441
- else if (record.type === 'session.title.updated')
2442
- automaticTitle = false;
2443
- else if (isCompactCheckpoint(record))
2444
- compactSequence = record.sequence;
2445
- else if (record.type === 'todo.updated') {
2446
- const items = todoItemsFromSnapshot(record.payload);
2447
- if (!items)
2448
- continue;
2449
- todoSequence = record.sequence;
2450
- todoRevision++;
2451
- todoItems = items;
2452
- }
2453
- const candidate = executionModelFromRecord(record);
2454
- if (candidate !== undefined)
2455
- executionModel = candidate;
2456
- }
2457
- const boundary = records.findLastIndex(isCompactCheckpoint);
2458
- const checkpoint = latestValidContextGcCheckpoint(records);
2459
- return {
2460
- automaticTitlePending: automaticTitle && !sawUserTurn,
2461
- todo: { revision: todoRevision, items: todoItems },
2462
- contextRecords: boundary < 0 ? [...records] : records.slice(boundary),
2463
- resume: {
2464
- version: 1,
2465
- ...(compactSequence === undefined ? {} : { compactSequence }),
2466
- ...(checkpoint === undefined
2467
- ? {}
2468
- : {
2469
- gcSequence: checkpoint.record.sequence,
2470
- gcGeneration: checkpoint.payload.generation,
2471
- gcBaseSequence: contextGcBaseSequence(checkpoint.payload.base)
2472
- }),
2473
- ...(todoSequence === undefined ? {} : { todoSequence }),
2474
- todoRevision,
2475
- executionModelIndexed: true,
2476
- firstUserMessageIndexed: true,
2477
- ...(firstUserMessagePreview === undefined ? {} : { firstUserMessagePreview }),
2478
- ...(executionModel === undefined ? {} : executionModel)
2479
- }
2480
- };
2481
- }
2482
- function userMessagePreviewFromRecord(record) {
2483
- if (record.type !== 'turn.user' ||
2484
- !isObject(record.payload) ||
2485
- record.payload.inputSource === 'parent_agent' ||
2486
- typeof record.payload.prompt !== 'string')
2487
- return undefined;
2488
- const preview = record.payload.prompt.trim().replace(/\s+/gu, ' ').slice(0, 160);
2489
- return preview || undefined;
2490
- }
2491
- function executionModelFromRecord(record) {
2492
- if (record.type !== 'execution.header' || !isObject(record.payload))
2493
- return undefined;
2494
- const modelId = record.payload.modelId;
2495
- if (typeof modelId !== 'string' || modelId.trim().length === 0)
2496
- return undefined;
2497
- const modelName = record.payload.modelName;
2498
- const reasoningEffort = record.payload.reasoningEffort;
2499
- return {
2500
- modelId,
2501
- ...(typeof modelName === 'string' && modelName.trim().length > 0 ? { modelName } : {}),
2502
- ...(typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0
2503
- ? { reasoningEffort }
2504
- : {})
2505
- };
2506
- }
2507
- export function compactSummaryFromCheckpoint(record) {
2508
- if (record?.type !== 'compact.completed')
2509
- return undefined;
2510
- const payload = record.payload;
2511
- if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
2512
- return undefined;
2513
- const summary = payload.summary;
2514
- return typeof summary === 'string' && summary.trim().length > 0 ? summary : undefined;
2515
- }
2516
- export function replacementHistoryFromCheckpoint(record) {
2517
- if (compactSummaryFromCheckpoint(record) === undefined)
2518
- return undefined;
2519
- const history = record.payload.replacementHistory;
2520
- if (!Array.isArray(history) ||
2521
- !history.every((message) => message !== null &&
2522
- typeof message === 'object' &&
2523
- message.role === 'user' &&
2524
- typeof message.content === 'string' &&
2525
- (message.metadata === undefined ||
2526
- (parseModelInputMetadata(message.metadata) !== undefined &&
2527
- (message.kind === 'extension_input'
2528
- ? message.metadata.source === 'extension'
2529
- : message.kind === 'user_input' && message.metadata.source !== 'extension'))) &&
2530
- (message.kind === 'user_input' ||
2531
- message.kind === 'extension_input' ||
2532
- (message.kind === 'summary' && message.content.trim().length > 0))) ||
2533
- history.filter((message) => message.kind === 'summary').length !== 1)
2534
- return undefined;
2535
- return history.map(({ role, content, kind, metadata }) => ({
2536
- role,
2537
- content,
2538
- kind,
2539
- ...(parseModelInputMetadata(metadata) === undefined
2540
- ? {}
2541
- : { metadata: parseModelInputMetadata(metadata) })
2542
- }));
2543
- }
2544
- export function parseModelInputMetadata(value) {
2545
- if (value === null || typeof value !== 'object' || Array.isArray(value))
2546
- return undefined;
2547
- const metadata = value;
2548
- if (metadata.source !== 'user' &&
2549
- metadata.source !== 'parent_agent' &&
2550
- metadata.source !== 'extension')
2551
- return undefined;
2552
- return {
2553
- source: metadata.source,
2554
- ...(typeof metadata.recordId === 'string' && metadata.recordId.length <= 128
2555
- ? { recordId: metadata.recordId }
2556
- : {}),
2557
- ...(typeof metadata.extensionId === 'string' && metadata.extensionId.length <= 128
2558
- ? { extensionId: metadata.extensionId }
2559
- : {}),
2560
- ...(metadata.truncated === true ? { truncated: true } : {}),
2561
- ...(metadata.smartCompaction === true ? { smartCompaction: true } : {})
2562
- };
2563
- }
2564
- export function inputMetadataFromRecord(record) {
2565
- const payload = record.payload;
2566
- const source = payload.source;
2567
- return {
2568
- source: record.type === 'extension.input' || payload.inputSource === 'extension'
2569
- ? 'extension'
2570
- : payload.inputSource === 'parent_agent'
2571
- ? 'parent_agent'
2572
- : 'user',
2573
- recordId: record.recordId,
2574
- ...(payload.smartCompaction === true ? { smartCompaction: true } : {}),
2575
- ...(typeof source?.extensionId === 'string' && source.extensionId.length <= 128
2576
- ? { extensionId: source.extensionId }
2577
- : {})
2578
- };
2579
- }
2580
- function isCompactCheckpoint(record) {
2581
- return compactSummaryFromCheckpoint(record) !== undefined;
2582
- }
2583
- function sameResumeIndex(left, right) {
2584
- return (left?.version === right.version &&
2585
- left.compactSequence === right.compactSequence &&
2586
- left.gcSequence === right.gcSequence &&
2587
- left.gcGeneration === right.gcGeneration &&
2588
- left.gcBaseSequence === right.gcBaseSequence &&
2589
- left.todoSequence === right.todoSequence &&
2590
- left.todoRevision === right.todoRevision &&
2591
- left.executionModelIndexed === right.executionModelIndexed &&
2592
- left.firstUserMessageIndexed === right.firstUserMessageIndexed &&
2593
- left.firstUserMessagePreview === right.firstUserMessagePreview &&
2594
- left.modelId === right.modelId &&
2595
- left.modelName === right.modelName &&
2596
- left.reasoningEffort === right.reasoningEffort);
2597
- }
2598
- /** Read only the prefix needed by discovery; never load the entire transcript. */
2599
- async function* readRecordsFromHead(path) {
2600
- const handle = await open(path, 'r');
2601
- try {
2602
- const { size } = await handle.stat();
2603
- const chunk = Buffer.allocUnsafe(REVERSE_READ_CHUNK_BYTES);
2604
- let position = 0;
2605
- let pending = Buffer.alloc(0);
2606
- while (position < size) {
2607
- const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, size - position), position);
2608
- if (bytesRead === 0)
2609
- break;
2610
- position += bytesRead;
2611
- const data = Buffer.concat([pending, chunk.subarray(0, bytesRead)]);
2612
- let start = 0;
2613
- for (;;) {
2614
- const end = data.indexOf(0x0a, start);
2615
- if (end < 0)
2616
- break;
2617
- const line = data.subarray(start, end).toString('utf8');
2618
- start = end + 1;
2619
- if (!line)
2620
- continue;
2621
- let record;
2622
- try {
2623
- record = JSON.parse(line);
2624
- }
2625
- catch (cause) {
2626
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
2627
- cause
2628
- });
2629
- }
2630
- yield record;
2631
- }
2632
- pending = Buffer.from(data.subarray(start));
2633
- }
2634
- if (pending.length > 0) {
2635
- let record;
2636
- try {
2637
- record = JSON.parse(pending.toString('utf8'));
2638
- }
2639
- catch {
2640
- return; // Readers ignore only an interrupted final line.
2641
- }
2642
- yield record;
2643
- }
2644
- }
2645
- finally {
2646
- await handle.close();
2647
- }
2648
- }
2649
- async function readRecords(path) {
2650
- const value = await readFile(path, 'utf8');
2651
- const lines = value.split('\n');
2652
- if (lines.at(-1) === '')
2653
- lines.pop();
2654
- const records = [];
2655
- for (const [index, line] of lines.entries()) {
2656
- if (!line)
2657
- continue;
2658
- try {
2659
- records.push(JSON.parse(line));
2660
- }
2661
- catch (cause) {
2662
- if (index === lines.length - 1 && !value.endsWith('\n'))
2663
- break;
2664
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
2665
- cause
2666
- });
2667
- }
2668
- }
2669
- return records;
2670
- }
2671
- /** A pending marker is removable only after the committed JSONL references are checked. */
2672
- async function recoverPendingInputImages(sessionDirectory, readHistory) {
2673
- const directory = join(sessionDirectory, 'input_images');
2674
- let names;
2675
- try {
2676
- names = await readdir(directory);
2677
- }
2678
- catch (error) {
2679
- if (error.code === 'ENOENT')
2680
- return;
2681
- throw error;
2682
- }
2683
- const pending = names.filter((name) => /^\.[0-9a-f-]{36}\.bin\.pending$/iu.test(name));
2684
- const temporary = names.filter((name) => /^\.[0-9a-f-]{36}\.bin\.[0-9a-f-]{36}\.tmp$/iu.test(name));
2685
- if (pending.length === 0 && temporary.length === 0)
2686
- return;
2687
- const referenced = pending.length === 0
2688
- ? new Set()
2689
- : new Set(inputImageReferencesFromRecords(await readHistory()).map((image) => image.fileName));
2690
- for (const name of pending) {
2691
- const fileName = name.slice(1, -'.pending'.length);
2692
- if (!referenced.has(fileName))
2693
- await rm(join(directory, fileName), { force: true });
2694
- await rm(join(directory, name), { force: true });
2695
- }
2696
- for (const name of temporary)
2697
- await rm(join(directory, name), { force: true });
2698
- }
2699
- /** Repair only the final record under the Session writer lock; never skip middle damage. */
2700
- async function repairSegmentTail(path) {
2701
- const data = await readFile(path);
2702
- if (data.length === 0 || data.at(-1) === 10)
2703
- return;
2704
- const boundary = data.lastIndexOf(10);
2705
- let complete = false;
2706
- try {
2707
- const record = JSON.parse(data.subarray(boundary + 1).toString('utf8'));
2708
- complete =
2709
- record?.schemaVersion === 1 &&
2710
- Number.isSafeInteger(record.sequence) &&
2711
- typeof record.sessionId === 'string' &&
2712
- typeof record.type === 'string';
2713
- }
2714
- catch {
2715
- // An interrupted final line is discarded before another record can be appended.
2716
- }
2717
- const handle = await open(path, 'r+');
2718
- try {
2719
- if (complete) {
2720
- const { bytesWritten } = await handle.write(Buffer.from('\n'), 0, 1, data.length);
2721
- if (bytesWritten !== 1)
2722
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session tail repair is incomplete.');
2723
- }
2724
- else
2725
- await handle.truncate(boundary + 1);
2726
- await handle.sync();
2727
- }
2728
- finally {
2729
- await handle.close();
2730
- }
2731
- }
2732
- async function readRecordPageFromTail(path, beforeSequence, limit, positions) {
2733
- const newestFirst = [];
2734
- for await (const record of readRecordsFromTail(path, { beforeSequence, limit, positions })) {
2735
- if (!(record.sequence < beforeSequence))
2736
- continue;
2737
- newestFirst.push(record);
2738
- if (newestFirst.length > limit)
2739
- break;
2740
- }
2741
- return {
2742
- records: newestFirst.slice(0, limit).reverse(),
2743
- hasEarlier: newestFirst.length > limit
2744
- };
2745
- }
2746
- /** Decode complete records newest-first, releasing the file as soon as a reader stops. */
2747
- async function* readRecordsFromTail(path, page) {
2748
- const handle = await open(path, 'r');
2749
- try {
2750
- const metadata = await handle.stat({ bigint: true });
2751
- const size = Number(metadata.size);
2752
- let position = size;
2753
- let cached;
2754
- if (page !== undefined) {
2755
- const signature = `${metadata.dev}:${metadata.ino}:${metadata.size}:${metadata.mtimeNs}:${metadata.ctimeNs}`;
2756
- const previous = page.positions.get(path);
2757
- cached = previous?.signature === signature ? previous : { signature, offsets: new Map() };
2758
- page.positions.delete(path);
2759
- page.positions.set(path, cached);
2760
- if (page.positions.size > MAX_HISTORY_PAGE_FILES)
2761
- page.positions.delete(page.positions.keys().next().value);
2762
- let nearestSequence = Number.POSITIVE_INFINITY;
2763
- for (const [sequence, offset] of cached.offsets) {
2764
- if (sequence >= page.beforeSequence && sequence < nearestSequence) {
2765
- nearestSequence = sequence;
2766
- position = offset;
2767
- }
2768
- }
2769
- }
2770
- let accepted = 0;
2771
- const remember = (record, offset) => {
2772
- if (page === undefined || cached === undefined || record.sequence >= page.beforeSequence)
2773
- return;
2774
- if (++accepted !== page.limit)
2775
- return;
2776
- cached.offsets.set(record.sequence, offset);
2777
- if (cached.offsets.size > MAX_HISTORY_PAGE_OFFSETS)
2778
- cached.offsets.delete(cached.offsets.keys().next().value);
2779
- };
2780
- let pendingLineParts = [];
2781
- let invalidFinalLineMayBeIgnored = false;
2782
- let inspectedFileEnd = position !== size;
2783
- const decode = (line, allowInvalidFinalLine) => {
2784
- if (line.length === 0)
2785
- return undefined;
2786
- try {
2787
- return JSON.parse(line.toString('utf8'));
2788
- }
2789
- catch (cause) {
2790
- if (allowInvalidFinalLine)
2791
- return undefined;
2792
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.', {
2793
- cause
2794
- });
2795
- }
2796
- };
2797
- while (position > 0) {
2798
- const start = Math.max(0, position - REVERSE_READ_CHUNK_BYTES);
2799
- const chunk = Buffer.allocUnsafe(position - start);
2800
- let bytesRead = 0;
2801
- while (bytesRead < chunk.length) {
2802
- const result = await handle.read(chunk, bytesRead, chunk.length - bytesRead, start + bytesRead);
2803
- if (result.bytesRead === 0)
2804
- break;
2805
- bytesRead += result.bytesRead;
2806
- }
2807
- const value = chunk.subarray(0, bytesRead);
2808
- if (!inspectedFileEnd) {
2809
- inspectedFileEnd = true;
2810
- invalidFinalLineMayBeIgnored = value.at(-1) !== 0x0a;
2811
- }
2812
- let lineEnd = value.length;
2813
- for (let index = value.length - 1; index >= 0; index--) {
2814
- if (value[index] !== 0x0a)
2815
- continue;
2816
- const currentPart = value.subarray(index + 1, lineEnd);
2817
- const line = pendingLineParts.length === 0
2818
- ? currentPart
2819
- : Buffer.concat([currentPart, ...pendingLineParts]);
2820
- const record = decode(line, invalidFinalLineMayBeIgnored);
2821
- invalidFinalLineMayBeIgnored = false;
2822
- pendingLineParts = [];
2823
- lineEnd = index;
2824
- if (record !== undefined) {
2825
- remember(record, start + index + 1);
2826
- yield record;
2827
- }
2828
- }
2829
- if (lineEnd > 0)
2830
- pendingLineParts.unshift(Buffer.from(value.subarray(0, lineEnd)));
2831
- position = start;
2832
- }
2833
- if (pendingLineParts.length > 0) {
2834
- const record = decode(Buffer.concat(pendingLineParts), invalidFinalLineMayBeIgnored);
2835
- if (record !== undefined) {
2836
- remember(record, 0);
2837
- yield record;
2838
- }
2839
- }
2840
- }
2841
- finally {
2842
- await handle.close();
2843
- }
2844
- }
2845
- function segmentNumberFromPath(path) {
2846
- const match = /events-(\d{6})\.jsonl$/.exec(path);
2847
- return match === null ? undefined : Number(match[1]);
2848
- }
2849
- function restoreExtensionCheckpoint(records, extensionId, key) {
2850
- for (const record of [...records].reverse()) {
2851
- if (record.type !== 'extension.checkpoint.updated')
2852
- continue;
2853
- const payload = record.payload;
2854
- if (!payload || payload.extensionId !== extensionId || payload.key !== key)
2855
- continue;
2856
- if (typeof payload.revision !== 'number' ||
2857
- !Number.isSafeInteger(payload.revision) ||
2858
- payload.revision < 1)
2859
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Extension checkpoint revision is corrupt.');
2860
- return {
2861
- revision: payload.revision,
2862
- ...(payload.deleted === true ? {} : { value: cloneCoreJson(payload.value) })
2863
- };
2864
- }
2865
- return { revision: 0 };
2866
- }