@myagentroam/agent 0.10.59 → 0.10.60

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 (54) 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 +1 -1
  19. package/dist/agent-core/sdk/native-steer.d.ts +1 -1
  20. package/dist/agent-core/sdk/session.d.ts +1 -1
  21. package/dist/agent-core/sdk/session.js +1 -1
  22. package/dist/agent-core/sdk/subagent-backend.d.ts +1 -1
  23. package/dist/agent-core/session/catalog.js +1 -1
  24. package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -8
  25. package/dist/agent-core/session/context-gc-checkpoint.js +82 -137
  26. package/dist/agent-core/session/context-gc-observations.d.ts +1 -1
  27. package/dist/agent-core/session/context-gc-replacements.d.ts +4 -7
  28. package/dist/agent-core/session/context-gc-replacements.js +25 -59
  29. package/dist/agent-core/session/context-gc-subagent.d.ts +0 -3
  30. package/dist/agent-core/session/context-gc-subagent.js +2 -16
  31. package/dist/agent-core/session/deferred-input.d.ts +1 -1
  32. package/dist/agent-core/session/store/current-state.d.ts +17 -0
  33. package/dist/agent-core/session/store/current-state.js +53 -0
  34. package/dist/agent-core/session/store/directory-index.d.ts +18 -0
  35. package/dist/agent-core/session/store/directory-index.js +163 -0
  36. package/dist/agent-core/session/store/execution-state.d.ts +17 -0
  37. package/dist/agent-core/session/store/execution-state.js +178 -0
  38. package/dist/agent-core/session/store/history.d.ts +25 -0
  39. package/dist/agent-core/session/store/history.js +157 -0
  40. package/dist/agent-core/session/store/images.d.ts +21 -0
  41. package/dist/agent-core/session/store/images.js +248 -0
  42. package/dist/agent-core/session/store/index.d.ts +4 -0
  43. package/dist/agent-core/session/store/index.js +2 -0
  44. package/dist/agent-core/session/{jsonl-store.d.ts → store/jsonl-store.d.ts} +12 -92
  45. package/dist/agent-core/session/{jsonl-store.js → store/jsonl-store.js} +108 -1053
  46. package/dist/agent-core/session/store/records.d.ts +23 -0
  47. package/dist/agent-core/session/store/records.js +249 -0
  48. package/dist/agent-core/session/store/types.d.ts +83 -0
  49. package/dist/agent-core/session/store/types.js +1 -0
  50. package/dist/agent-core/session/tool-result-resources.d.ts +1 -1
  51. package/dist/agent-core/subagent/session-controller.d.ts +1 -1
  52. package/dist/agent-core/tools/code-mode/state.d.ts +1 -1
  53. package/dist/agent-core/tools/code-mode/state.js +2 -2
  54. 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, mergeFirstUserMessagePreviewCache, normalizeSessionSource, persistSessionIndex, readSessionIndex, sessionListFilter, sessionMatchesFilter, sessionTreeIds } from './directory-index.js';
15
+ import { deriveExecutionState, executionModelFromRecord, isCompactCheckpoint, 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();
@@ -435,55 +432,7 @@ export class JsonlSessionStore {
435
432
  if (images.length > MAX_INPUT_IMAGES)
436
433
  throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Too many image inputs were provided.');
437
434
  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
+ return writeInputImages(directory, images);
487
436
  }
488
437
  async writeToolResultResource(sessionId, text) {
489
438
  this.#assertOpen();
@@ -1036,20 +985,17 @@ export class JsonlSessionStore {
1036
985
  catch {
1037
986
  // The first append creates the first segment.
1038
987
  }
1039
- const handle = await open(path, 'a', 0o600);
1040
988
  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();
989
+ await appendRecordBytes(path, serialized);
1050
990
  }
1051
- finally {
1052
- await handle.close();
991
+ catch (error) {
992
+ // A failed write/sync/close can leave a complete record on disk.
993
+ const lease = this.#shared.writerLeases.get(sessionId);
994
+ if (lease) {
995
+ delete lease.codeState;
996
+ delete lease.extensionCheckpoints;
997
+ }
998
+ throw error;
1053
999
  }
1054
1000
  // Index refreshes and commits can replace metadata during file I/O. Publish all
1055
1001
  // durable-record metadata onto the current entry without another await boundary.
@@ -1077,6 +1023,7 @@ export class JsonlSessionStore {
1077
1023
  this.#shared.index.revision++;
1078
1024
  this.#markIndexDirty(sessionId);
1079
1025
  }
1026
+ updateSessionCurrentState(this.#shared.writerLeases.get(sessionId), record.type, serialized);
1080
1027
  await completeToolResultResources(this.sessionDirectory(sessionId), record);
1081
1028
  if (record.type === 'turn.user' &&
1082
1029
  isObject(record.payload) &&
@@ -1106,10 +1053,32 @@ export class JsonlSessionStore {
1106
1053
  }
1107
1054
  }
1108
1055
  async readCodeModeState(sessionId) {
1109
- return restoreCodeModeState(await this.read(sessionId));
1056
+ return this.#serializeRecordOperation(sessionId, () => this.#readCodeModeState(sessionId));
1057
+ }
1058
+ async #readCodeModeState(sessionId) {
1059
+ this.#assertOpen();
1060
+ const lease = this.#shared.writerLeases.get(sessionId);
1061
+ if (lease?.owner === undefined)
1062
+ return restoreCodeModeState(await this.#readRecords(sessionId));
1063
+ lease.codeState ??= restoreCodeModeState(await this.#readRecords(sessionId));
1064
+ return applyCodeModeStateMutations(lease.codeState, []);
1110
1065
  }
1111
1066
  async readExtensionCheckpoint(sessionId, extensionId, key) {
1112
- return restoreExtensionCheckpoint(await this.read(sessionId), extensionId, key);
1067
+ return this.#serializeRecordOperation(sessionId, () => this.#readExtensionCheckpoint(sessionId, extensionId, key));
1068
+ }
1069
+ async #readExtensionCheckpoint(sessionId, extensionId, key) {
1070
+ this.#assertOpen();
1071
+ const lease = this.#shared.writerLeases.get(sessionId);
1072
+ if (lease?.owner === undefined)
1073
+ return restoreExtensionCheckpoint(await this.#readRecords(sessionId), extensionId, key);
1074
+ if (!lease.extensionCheckpoints) {
1075
+ const index = new Map();
1076
+ for (const record of await this.#readRecords(sessionId))
1077
+ if (record.type === 'extension.checkpoint.updated')
1078
+ indexExtensionCheckpoint(index, record.payload);
1079
+ lease.extensionCheckpoints = index;
1080
+ }
1081
+ return extensionCheckpointFromPayload(lease.extensionCheckpoints.get(extensionId)?.get(key));
1113
1082
  }
1114
1083
  async writeExtensionCheckpoint(sessionId, extensionId, key, value, expectedRevision, input) {
1115
1084
  if (typeof key !== 'string' ||
@@ -1121,7 +1090,7 @@ export class JsonlSessionStore {
1121
1090
  const release = await this.acquireSessionWriteLease(sessionId, input.owner);
1122
1091
  try {
1123
1092
  return await this.#serializeRecordOperation(sessionId, async () => {
1124
- const current = restoreExtensionCheckpoint(await this.#readRecords(sessionId), extensionId, key);
1093
+ const current = await this.#readExtensionCheckpoint(sessionId, extensionId, key);
1125
1094
  if (expectedRevision !== undefined && expectedRevision !== current.revision)
1126
1095
  throw new MarAgentError('MAR_AGENT_EXTENSION_DATA_CONFLICT', 'Checkpoint revision changed before commit.');
1127
1096
  input.signal.throwIfAborted();
@@ -1203,70 +1172,61 @@ export class JsonlSessionStore {
1203
1172
  async readContext(sessionId) {
1204
1173
  return (await this.readExecutionState(sessionId)).contextRecords;
1205
1174
  }
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) {
1175
+ /** Compare the planned sequence inside the existing record writer queue. */
1176
+ async commitContextGc(sessionId, draft, planningRecords) {
1215
1177
  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
1178
+ const records = planningRecords ?? (await this.readExecutionState(sessionId)).contextRecords;
1179
+ return this.#serializeRecordOperation(sessionId, async () => {
1180
+ const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
1181
+ if (!meta)
1182
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1183
+ const boundarySequence = meta.nextSequence - 1;
1184
+ if (boundarySequence !== draft.boundarySequence)
1185
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC projection changed before checkpoint commit.');
1186
+ if (records.at(-1)?.sequence !== boundarySequence ||
1187
+ records.at(-1)?.sessionId !== sessionId ||
1188
+ !sameContextGcBase(contextGcBaseFromRecords(records, boundarySequence), draft.base))
1189
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC planning boundary is invalid.');
1190
+ const previous = latestValidContextGcCheckpoint(records, boundarySequence, meta.workspace);
1191
+ if (previous) {
1192
+ const minimumUsageGrowth = contextGcMinimumUsageGrowth(draft.trigger.contextWindowTokens);
1193
+ if (draft.trigger.normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal < 2 ||
1194
+ draft.trigger.contextInputTokens - previous.payload.trigger.contextInputTokens <
1195
+ minimumUsageGrowth)
1196
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
1197
+ }
1198
+ const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
1199
+ if (!plannedReplacements || plannedReplacements.length === 0)
1200
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
1201
+ const replacements = normalizeContextGcReplacements([
1202
+ ...(previous?.payload.replacements ?? []),
1203
+ ...plannedReplacements
1204
+ ]);
1205
+ if (!replacements ||
1206
+ (previous && isDeepStrictEqual(replacements, previous.payload.replacements)))
1207
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC replacement map is invalid.');
1208
+ const payload = {
1209
+ ...structuredClone(draft),
1210
+ checkpointId: randomUUID(),
1211
+ generation: (previous?.payload.generation ?? 0) + 1,
1212
+ replacements
1213
+ };
1214
+ const candidate = {
1215
+ schemaVersion: 1,
1216
+ recordId: randomUUID(),
1217
+ sequence: boundarySequence + 1,
1218
+ timestamp: new Date().toISOString(),
1219
+ sessionId,
1220
+ type: 'context.gc.completed',
1221
+ payload
1222
+ };
1223
+ if (Buffer.byteLength(JSON.stringify(payload)) >= MAX_SEGMENT_BYTES ||
1224
+ validateContextGcCheckpoint(records, candidate, meta.workspace, previous?.payload.replacements) === undefined)
1225
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint validation failed.');
1226
+ return this.#appendRecord(sessionId, {
1227
+ type: 'context.gc.completed',
1228
+ payload
1229
+ });
1270
1230
  });
1271
1231
  }
1272
1232
  async readPage(sessionId, input = {}) {
@@ -1411,6 +1371,8 @@ export class JsonlSessionStore {
1411
1371
  await lease.release();
1412
1372
  }
1413
1373
  finally {
1374
+ delete lease.codeState;
1375
+ delete lease.extensionCheckpoints;
1414
1376
  this.#shared.writerLeases.delete(sessionId);
1415
1377
  }
1416
1378
  }
@@ -1887,67 +1849,6 @@ export class JsonlSessionStore {
1887
1849
  JsonlSessionStore.#instances.delete(this.#shared.root);
1888
1850
  }
1889
1851
  }
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
1852
  async function sessionDirectoryExists(path) {
1952
1853
  try {
1953
1854
  await lstat(path);
@@ -1959,17 +1860,6 @@ async function sessionDirectoryExists(path) {
1959
1860
  throw error;
1960
1861
  }
1961
1862
  }
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
1863
  async function recoverContextualWorldStateTransactions(root, meta) {
1974
1864
  const directory = join(root, 'sessions', meta.id);
1975
1865
  let names;
@@ -2002,125 +1892,6 @@ async function recoverContextualWorldStateTransactions(root, meta) {
2002
1892
  await rename(backup, canonical);
2003
1893
  }
2004
1894
  }
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
1895
  function restoreSessionMeta(meta, original) {
2125
1896
  meta.id = original.id;
2126
1897
  meta.title = original.title;
@@ -2145,722 +1916,6 @@ function restoreSessionMeta(meta, original) {
2145
1916
  else
2146
1917
  meta.segments = structuredClone(original.segments);
2147
1918
  }
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
1919
  function isObject(value) {
2286
1920
  return value !== null && typeof value === 'object' && !Array.isArray(value);
2287
1921
  }
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
- }