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