@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.
- package/dist/agent-core/runtime/compaction/history.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/retained-context.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/retained-context.js +1 -1
- package/dist/agent-core/runtime/compaction/smart.d.ts +1 -1
- package/dist/agent-core/runtime/compaction/smart.js +1 -1
- package/dist/agent-core/runtime/context/gc-runtime.d.ts +1 -1
- package/dist/agent-core/runtime/context/gc-runtime.js +20 -14
- package/dist/agent-core/runtime/context/gc.d.ts +1 -1
- package/dist/agent-core/runtime/context/gc.js +4 -10
- package/dist/agent-core/runtime/context/projection.d.ts +1 -1
- package/dist/agent-core/runtime/context/projection.js +3 -5
- package/dist/agent-core/runtime/context/restore.d.ts +4 -4
- package/dist/agent-core/runtime/context/restore.js +1 -1
- package/dist/agent-core/runtime/context/usage.d.ts +2 -2
- package/dist/agent-core/runtime/context/usage.js +1 -1
- package/dist/agent-core/runtime/context/world-state.d.ts +2 -2
- package/dist/agent-core/runtime/context/world-state.js +1 -1
- package/dist/agent-core/sdk/agent.js +1 -1
- package/dist/agent-core/sdk/native-steer.d.ts +1 -1
- package/dist/agent-core/sdk/session.d.ts +1 -1
- package/dist/agent-core/sdk/session.js +1 -1
- package/dist/agent-core/sdk/subagent-backend.d.ts +1 -1
- package/dist/agent-core/session/catalog.js +1 -1
- package/dist/agent-core/session/context-gc-checkpoint.d.ts +4 -8
- package/dist/agent-core/session/context-gc-checkpoint.js +82 -137
- package/dist/agent-core/session/context-gc-observations.d.ts +1 -1
- package/dist/agent-core/session/context-gc-replacements.d.ts +4 -7
- package/dist/agent-core/session/context-gc-replacements.js +25 -59
- package/dist/agent-core/session/context-gc-subagent.d.ts +0 -3
- package/dist/agent-core/session/context-gc-subagent.js +2 -16
- package/dist/agent-core/session/deferred-input.d.ts +1 -1
- package/dist/agent-core/session/store/current-state.d.ts +17 -0
- package/dist/agent-core/session/store/current-state.js +53 -0
- package/dist/agent-core/session/store/directory-index.d.ts +18 -0
- package/dist/agent-core/session/store/directory-index.js +163 -0
- package/dist/agent-core/session/store/execution-state.d.ts +17 -0
- package/dist/agent-core/session/store/execution-state.js +178 -0
- package/dist/agent-core/session/store/history.d.ts +25 -0
- package/dist/agent-core/session/store/history.js +157 -0
- package/dist/agent-core/session/store/images.d.ts +21 -0
- package/dist/agent-core/session/store/images.js +248 -0
- package/dist/agent-core/session/store/index.d.ts +4 -0
- package/dist/agent-core/session/store/index.js +2 -0
- package/dist/agent-core/session/{jsonl-store.d.ts → store/jsonl-store.d.ts} +12 -92
- package/dist/agent-core/session/{jsonl-store.js → store/jsonl-store.js} +108 -1053
- package/dist/agent-core/session/store/records.d.ts +23 -0
- package/dist/agent-core/session/store/records.js +249 -0
- package/dist/agent-core/session/store/types.d.ts +83 -0
- package/dist/agent-core/session/store/types.js +1 -0
- package/dist/agent-core/session/tool-result-resources.d.ts +1 -1
- package/dist/agent-core/subagent/session-controller.d.ts +1 -1
- package/dist/agent-core/tools/code-mode/state.d.ts +1 -1
- package/dist/agent-core/tools/code-mode/state.js +2 -2
- package/package.json +1 -1
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 '
|
|
7
|
-
import {
|
|
8
|
-
import { acquireFileLock } from '
|
|
9
|
-
import {
|
|
10
|
-
import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1207
|
-
|
|
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
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
}
|