@myagentroam/agent 0.10.60 → 0.10.62
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/sdk/agent.js +1 -1
- package/dist/agent-core/sdk/extension-agents.js +1 -1
- package/dist/agent-core/sdk/session.d.ts +18 -12
- package/dist/agent-core/sdk/session.js +42 -39
- package/dist/agent-core/sdk/subagent-backend.js +20 -17
- package/dist/agent-core/session/store/directory-index.d.ts +2 -2
- package/dist/agent-core/session/store/directory-index.js +17 -11
- package/dist/agent-core/session/store/execution-state.d.ts +1 -0
- package/dist/agent-core/session/store/execution-state.js +9 -0
- package/dist/agent-core/session/store/jsonl-store.d.ts +2 -2
- package/dist/agent-core/session/store/jsonl-store.js +217 -218
- package/dist/agent-core/session/store/types.d.ts +1 -0
- package/dist/agent-core/subagent/scheduler.js +1 -1
- package/dist/agent-core/subagent/session-controller.d.ts +7 -1
- package/dist/agent-core/subagent/session-controller.js +560 -352
- package/package.json +1 -1
|
@@ -11,8 +11,8 @@ import { contextGcBaseFromRecords, contextGcBaseSequence, contextGcMinimumUsageG
|
|
|
11
11
|
import { bindToolResultResources, cleanupUnreferencedToolResources, completeToolResultResources, copyToolResultResources, readToolResultResource, toolResultResourceFiles, writeToolResultResource } from '../tool-result-resources.js';
|
|
12
12
|
import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from '../transaction-recovery.js';
|
|
13
13
|
import { extensionCheckpointFromPayload, indexExtensionCheckpoint, restoreExtensionCheckpoint, updateSessionCurrentState } from './current-state.js';
|
|
14
|
-
import { decodeSessionListCursor, encodeSessionListCursor,
|
|
15
|
-
import { deriveExecutionState, executionModelFromRecord, isCompactCheckpoint, sameResumeIndex, userMessagePreviewFromRecord } from './execution-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
16
|
import { forkedPayload, renameIfExists, restoreRenamedFiles, retainedSessionTitle, stageSessionSegments, stageUnreferencedImages } from './history.js';
|
|
17
17
|
import { MAX_INPUT_IMAGES, copyGeneratedImages, copyInputImages, generatedImageFileNames, inputImageReference, inputImageReferences, inputImageReferencesFromRecords, readInputImageBytes, recoverPendingInputImages, validInputImageFileName, writeInputImages } from './images.js';
|
|
18
18
|
import { MAX_SEGMENT_BYTES, appendRecordBytes, readRecordPageFromTail, readRecords, readRecordsFromHead, readRecordsFromTail, repairSegmentTail, segmentNumberFromPath } from './records.js';
|
|
@@ -158,7 +158,7 @@ export class JsonlSessionStore {
|
|
|
158
158
|
closed: false,
|
|
159
159
|
indexDirty: false,
|
|
160
160
|
dirtySessionVersions: new Map(),
|
|
161
|
-
|
|
161
|
+
derivedOnlyDirtySessions: new Set(),
|
|
162
162
|
pendingPublishIds: new Set(),
|
|
163
163
|
codeStateWrites: new Map(),
|
|
164
164
|
recordWrites: new Map(),
|
|
@@ -185,15 +185,15 @@ export class JsonlSessionStore {
|
|
|
185
185
|
if (!disk)
|
|
186
186
|
throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session index is missing.');
|
|
187
187
|
const dirty = new Map(this.#shared.dirtySessionVersions);
|
|
188
|
-
const
|
|
188
|
+
const derivedOnly = new Set(this.#shared.derivedOnlyDirtySessions);
|
|
189
189
|
const sessions = new Map(disk.sessions.map((session) => [session.id, session]));
|
|
190
190
|
const created = new Set(createdIds);
|
|
191
191
|
for (const session of this.#shared.index.sessions) {
|
|
192
192
|
const current = sessions.get(session.id);
|
|
193
193
|
// Disk membership is authoritative: a stale in-memory snapshot cannot revive a deletion.
|
|
194
194
|
if (created.has(session.id) || (current && dirty.has(session.id)))
|
|
195
|
-
sessions.set(session.id, current &&
|
|
196
|
-
?
|
|
195
|
+
sessions.set(session.id, current && derivedOnly.has(session.id)
|
|
196
|
+
? mergeSessionDerivedCache(current, session)
|
|
197
197
|
: structuredClone(session));
|
|
198
198
|
}
|
|
199
199
|
for (const deletedId of typeof deletedIds === 'string' ? [deletedIds] : (deletedIds ?? []))
|
|
@@ -202,7 +202,7 @@ export class JsonlSessionStore {
|
|
|
202
202
|
version: 1,
|
|
203
203
|
revision: createdIds.length > 0 ||
|
|
204
204
|
deletedIds !== undefined ||
|
|
205
|
-
[...dirty.keys()].some((id) => !
|
|
205
|
+
[...dirty.keys()].some((id) => !derivedOnly.has(id))
|
|
206
206
|
? Math.max(disk.revision, this.#shared.index.revision) + 1
|
|
207
207
|
: disk.revision,
|
|
208
208
|
sessions: [...sessions.values()]
|
|
@@ -214,7 +214,7 @@ export class JsonlSessionStore {
|
|
|
214
214
|
continue;
|
|
215
215
|
if (this.#shared.dirtySessionVersions.get(id) === version) {
|
|
216
216
|
this.#shared.dirtySessionVersions.delete(id);
|
|
217
|
-
this.#shared.
|
|
217
|
+
this.#shared.derivedOnlyDirtySessions.delete(id);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
for (const id of created)
|
|
@@ -257,8 +257,8 @@ export class JsonlSessionStore {
|
|
|
257
257
|
const current = local.get(session.id);
|
|
258
258
|
if (!current || !this.#shared.dirtySessionVersions.has(session.id))
|
|
259
259
|
return session;
|
|
260
|
-
return this.#shared.
|
|
261
|
-
?
|
|
260
|
+
return this.#shared.derivedOnlyDirtySessions.has(session.id)
|
|
261
|
+
? mergeSessionDerivedCache(session, current)
|
|
262
262
|
: current;
|
|
263
263
|
})
|
|
264
264
|
.concat(this.#shared.index.sessions.filter((session) => this.#shared.pendingPublishIds.has(session.id) && !published.has(session.id)))
|
|
@@ -299,6 +299,7 @@ export class JsonlSessionStore {
|
|
|
299
299
|
automaticTitlePending: input.title === undefined,
|
|
300
300
|
resume: {
|
|
301
301
|
version: 1,
|
|
302
|
+
userTurns: 0,
|
|
302
303
|
todoRevision: 0,
|
|
303
304
|
executionModelIndexed: true,
|
|
304
305
|
firstUserMessageIndexed: true
|
|
@@ -366,7 +367,7 @@ export class JsonlSessionStore {
|
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
}
|
|
369
|
-
sessionDirectory(sessionId) {
|
|
370
|
+
#sessionDirectory(sessionId) {
|
|
370
371
|
this.#assertOpen();
|
|
371
372
|
if (!this.#shared.index.sessions.some((session) => session.id === sessionId) &&
|
|
372
373
|
!this.#shared.pendingSessions.has(sessionId))
|
|
@@ -374,7 +375,7 @@ export class JsonlSessionStore {
|
|
|
374
375
|
return join(this.#shared.root, 'sessions', sessionId);
|
|
375
376
|
}
|
|
376
377
|
async readContextualWorldState(sessionId) {
|
|
377
|
-
const path = join(this
|
|
378
|
+
const path = join(this.#sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
378
379
|
try {
|
|
379
380
|
const pathInfo = await lstat(path);
|
|
380
381
|
if (!pathInfo.isFile())
|
|
@@ -403,7 +404,7 @@ export class JsonlSessionStore {
|
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
406
|
async writeContextualWorldState(sessionId, state) {
|
|
406
|
-
const directory = this
|
|
407
|
+
const directory = this.#sessionDirectory(sessionId);
|
|
407
408
|
const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
|
|
408
409
|
if (!meta)
|
|
409
410
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
@@ -431,21 +432,21 @@ export class JsonlSessionStore {
|
|
|
431
432
|
return [];
|
|
432
433
|
if (images.length > MAX_INPUT_IMAGES)
|
|
433
434
|
throw new MarAgentError('MAR_AGENT_HOST_LIMIT_EXCEEDED', 'Too many image inputs were provided.');
|
|
434
|
-
const directory = join(this
|
|
435
|
+
const directory = join(this.#sessionDirectory(sessionId), 'input_images');
|
|
435
436
|
return writeInputImages(directory, images);
|
|
436
437
|
}
|
|
437
438
|
async writeToolResultResource(sessionId, text) {
|
|
438
439
|
this.#assertOpen();
|
|
439
440
|
if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
|
|
440
441
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
441
|
-
return writeToolResultResource(this
|
|
442
|
+
return writeToolResultResource(this.#sessionDirectory(sessionId), text);
|
|
442
443
|
}
|
|
443
444
|
async readToolResultResource(sessionId, input) {
|
|
444
445
|
this.#assertOpen();
|
|
445
446
|
await this.#refreshIndex();
|
|
446
447
|
if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
|
|
447
448
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
448
|
-
return readToolResultResource(this
|
|
449
|
+
return readToolResultResource(this.#sessionDirectory(sessionId), sessionId, input, async (sequence) => {
|
|
449
450
|
await this.#refreshIndex();
|
|
450
451
|
return (await this.readPage(sessionId, { beforeSequence: sequence + 1, limit: 1 })).records.find((record) => record.sequence === sequence);
|
|
451
452
|
});
|
|
@@ -455,7 +456,7 @@ export class JsonlSessionStore {
|
|
|
455
456
|
const references = inputImageReferences(value);
|
|
456
457
|
if (references.length === 0)
|
|
457
458
|
return [];
|
|
458
|
-
const directory = join(this
|
|
459
|
+
const directory = join(this.#sessionDirectory(sessionId), 'input_images');
|
|
459
460
|
const images = [];
|
|
460
461
|
for (const reference of references) {
|
|
461
462
|
const data = await readInputImageBytes(directory, reference);
|
|
@@ -485,7 +486,7 @@ export class JsonlSessionStore {
|
|
|
485
486
|
const reference = inputImageReference(record.payload.images[input.imageIndex]);
|
|
486
487
|
if (reference === undefined)
|
|
487
488
|
return undefined;
|
|
488
|
-
const data = await readInputImageBytes(join(this
|
|
489
|
+
const data = await readInputImageBytes(join(this.#sessionDirectory(sessionId), 'input_images'), reference);
|
|
489
490
|
return {
|
|
490
491
|
name: reference.name,
|
|
491
492
|
mimeType: reference.mimeType,
|
|
@@ -520,6 +521,7 @@ export class JsonlSessionStore {
|
|
|
520
521
|
automaticTitlePending: false,
|
|
521
522
|
resume: {
|
|
522
523
|
version: 1,
|
|
524
|
+
userTurns: 0,
|
|
523
525
|
todoRevision: 0,
|
|
524
526
|
executionModelIndexed: true,
|
|
525
527
|
firstUserMessageIndexed: true
|
|
@@ -566,7 +568,7 @@ export class JsonlSessionStore {
|
|
|
566
568
|
});
|
|
567
569
|
}
|
|
568
570
|
// A fork is unpublished until every segment and referenced image is durable.
|
|
569
|
-
const staged = await stageSessionSegments(this
|
|
571
|
+
const staged = await stageSessionSegments(this.#sessionDirectory(target.id), copied, 'fork');
|
|
570
572
|
for (const file of staged.files)
|
|
571
573
|
await rename(file.temporaryPath, file.path);
|
|
572
574
|
target.segments = staged.segments;
|
|
@@ -574,9 +576,9 @@ export class JsonlSessionStore {
|
|
|
574
576
|
this.#shared.segments.set(target.id, staged.segments.length);
|
|
575
577
|
for (const record of copied)
|
|
576
578
|
this.#applyRecordToResumeIndex(target, record);
|
|
577
|
-
await copyToolResultResources(this
|
|
578
|
-
await copyGeneratedImages(this
|
|
579
|
-
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));
|
|
580
582
|
target.lastActivityAt = target.createdAt;
|
|
581
583
|
this.#shared.pendingSessions.delete(target.id);
|
|
582
584
|
this.#shared.index = {
|
|
@@ -654,102 +656,7 @@ export class JsonlSessionStore {
|
|
|
654
656
|
const retained = records
|
|
655
657
|
.slice(0, boundaryIndex + 1)
|
|
656
658
|
.filter((record) => record.type !== 'responses.continuation');
|
|
657
|
-
|
|
658
|
-
const transactionId = randomUUID();
|
|
659
|
-
const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
|
|
660
|
-
const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
|
|
661
|
-
const contextualWorldStateBackup = `${contextualWorldStatePath}.${targetNextSequence}.${transactionId}.rollback-backup`;
|
|
662
|
-
let contextualWorldStateStaged = false;
|
|
663
|
-
let generatedTrash = [];
|
|
664
|
-
let inputTrash = [];
|
|
665
|
-
const originalMeta = structuredClone(meta);
|
|
666
|
-
const originalIndexRevision = this.#shared.index.revision;
|
|
667
|
-
const originalSegment = this.#shared.segments.get(sessionId);
|
|
668
|
-
const originalPaths = await this.#segmentPaths(sessionId);
|
|
669
|
-
const backups = originalPaths.map((path) => ({
|
|
670
|
-
path,
|
|
671
|
-
backup: `${path}.${transactionId}.rollback-backup`
|
|
672
|
-
}));
|
|
673
|
-
const transaction = {
|
|
674
|
-
version: 1,
|
|
675
|
-
id: transactionId,
|
|
676
|
-
kind: 'rollback',
|
|
677
|
-
originalSegments: originalPaths.map((path) => basename(path)),
|
|
678
|
-
stagedSegments: staged.files.map((item) => ({
|
|
679
|
-
path: basename(item.path),
|
|
680
|
-
temporary: basename(item.temporaryPath)
|
|
681
|
-
})),
|
|
682
|
-
sidecarBackup: basename(contextualWorldStateBackup)
|
|
683
|
-
};
|
|
684
|
-
const installedPaths = [];
|
|
685
|
-
let committed = false;
|
|
686
|
-
try {
|
|
687
|
-
await beginSessionStorageTransaction(this.sessionDirectory(sessionId), transaction);
|
|
688
|
-
contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
|
|
689
|
-
generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
|
|
690
|
-
inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
|
|
691
|
-
inputTrash.push(...(await stageUnreferencedImages(this.sessionDirectory(sessionId), 'tool_results', toolResultResourceFiles(retained), transactionId)));
|
|
692
|
-
for (const item of backups)
|
|
693
|
-
await rename(item.path, item.backup);
|
|
694
|
-
for (const item of staged.files) {
|
|
695
|
-
await rename(item.temporaryPath, item.path);
|
|
696
|
-
installedPaths.push(item.path);
|
|
697
|
-
}
|
|
698
|
-
const state = deriveExecutionState(retained);
|
|
699
|
-
const completedAt = new Date().toISOString();
|
|
700
|
-
meta.title = retainedSessionTitle(retained, meta.title);
|
|
701
|
-
meta.lastActivityAt = completedAt;
|
|
702
|
-
meta.nextSequence = targetNextSequence;
|
|
703
|
-
meta.storageTransactionId = transactionId;
|
|
704
|
-
meta.automaticTitlePending = state.automaticTitlePending;
|
|
705
|
-
meta.resume = state.resume;
|
|
706
|
-
meta.segments = staged.segments;
|
|
707
|
-
this.#shared.segments.set(sessionId, staged.segments.at(-1)?.number ?? 1);
|
|
708
|
-
this.#shared.index.revision++;
|
|
709
|
-
this.#markIndexDirty(sessionId);
|
|
710
|
-
await this.#saveIndex();
|
|
711
|
-
committed = true;
|
|
712
|
-
await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
|
|
713
|
-
return {
|
|
714
|
-
id: meta.id,
|
|
715
|
-
title: meta.title,
|
|
716
|
-
workspace: meta.workspace,
|
|
717
|
-
createdAt: meta.createdAt,
|
|
718
|
-
lastActivityAt: meta.lastActivityAt,
|
|
719
|
-
source: normalizeSessionSource(meta.source)
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
catch (error) {
|
|
723
|
-
if (!committed) {
|
|
724
|
-
committed = await this.#storageTransactionCommitted(sessionId, transactionId);
|
|
725
|
-
if (committed) {
|
|
726
|
-
this.#shared.dirtySessionVersions.delete(sessionId);
|
|
727
|
-
this.#shared.indexDirty = this.#shared.dirtySessionVersions.size > 0;
|
|
728
|
-
await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
if (!committed) {
|
|
732
|
-
await Promise.all(installedPaths.map((path) => rm(path, { force: true }))).catch(() => undefined);
|
|
733
|
-
await restoreRenamedFiles(backups.map((item) => ({ from: item.backup, to: item.path })));
|
|
734
|
-
if (contextualWorldStateStaged)
|
|
735
|
-
await restoreRenamedFiles([
|
|
736
|
-
{ from: contextualWorldStateBackup, to: contextualWorldStatePath }
|
|
737
|
-
]);
|
|
738
|
-
await restoreRenamedFiles([...generatedTrash, ...inputTrash]);
|
|
739
|
-
restoreSessionMeta(meta, originalMeta);
|
|
740
|
-
this.#shared.index.revision = originalIndexRevision;
|
|
741
|
-
if (originalSegment === undefined)
|
|
742
|
-
this.#shared.segments.delete(sessionId);
|
|
743
|
-
else
|
|
744
|
-
this.#shared.segments.set(sessionId, originalSegment);
|
|
745
|
-
this.#markIndexDirty(sessionId);
|
|
746
|
-
await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), originalMeta.storageTransactionId);
|
|
747
|
-
}
|
|
748
|
-
throw error;
|
|
749
|
-
}
|
|
750
|
-
finally {
|
|
751
|
-
await Promise.all(staged.files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
|
|
752
|
-
}
|
|
659
|
+
return await this.#replaceHistory(meta, retained, { kind: 'rollback' });
|
|
753
660
|
}
|
|
754
661
|
finally {
|
|
755
662
|
await release();
|
|
@@ -817,100 +724,120 @@ export class JsonlSessionStore {
|
|
|
817
724
|
}
|
|
818
725
|
}
|
|
819
726
|
];
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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;
|
|
862
785
|
meta.storageTransactionId = transactionId;
|
|
863
|
-
meta.
|
|
864
|
-
meta.
|
|
865
|
-
this.#shared.segments.set(sessionId, 1);
|
|
866
|
-
this.#shared.index.revision++;
|
|
867
|
-
this.#markIndexDirty(sessionId);
|
|
868
|
-
await this.#saveIndex();
|
|
869
|
-
committed = true;
|
|
870
|
-
await recoverSessionStorageTransaction(this.sessionDirectory(sessionId), transactionId);
|
|
871
|
-
return {
|
|
872
|
-
id: meta.id,
|
|
873
|
-
title: meta.title,
|
|
874
|
-
workspace: meta.workspace,
|
|
875
|
-
createdAt: meta.createdAt,
|
|
876
|
-
lastActivityAt: meta.lastActivityAt,
|
|
877
|
-
source: normalizeSessionSource(meta.source)
|
|
878
|
-
};
|
|
786
|
+
meta.automaticTitlePending = state.automaticTitlePending;
|
|
787
|
+
meta.resume = state.resume;
|
|
879
788
|
}
|
|
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
|
-
|
|
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);
|
|
905
818
|
}
|
|
906
|
-
throw error;
|
|
907
819
|
}
|
|
908
|
-
|
|
909
|
-
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);
|
|
910
836
|
}
|
|
837
|
+
throw error;
|
|
911
838
|
}
|
|
912
839
|
finally {
|
|
913
|
-
await
|
|
840
|
+
await Promise.all(staged.files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
|
|
914
841
|
}
|
|
915
842
|
}
|
|
916
843
|
async getSession(sessionId) {
|
|
@@ -970,7 +897,7 @@ export class JsonlSessionStore {
|
|
|
970
897
|
payload: input.payload
|
|
971
898
|
};
|
|
972
899
|
JSON.stringify(record); // Reject malformed payloads before binding resources.
|
|
973
|
-
await bindToolResultResources(this
|
|
900
|
+
await bindToolResultResources(this.#sessionDirectory(sessionId), record, (sequence) => this.#readRecordAtSequence(before, sequence), true);
|
|
974
901
|
const serialized = `${JSON.stringify(record)}\n`;
|
|
975
902
|
let segment = await this.#currentSegment(sessionId);
|
|
976
903
|
let path = this.#segmentPath(sessionId, segment);
|
|
@@ -990,6 +917,12 @@ export class JsonlSessionStore {
|
|
|
990
917
|
}
|
|
991
918
|
catch (error) {
|
|
992
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
|
+
}
|
|
993
926
|
const lease = this.#shared.writerLeases.get(sessionId);
|
|
994
927
|
if (lease) {
|
|
995
928
|
delete lease.codeState;
|
|
@@ -1024,7 +957,7 @@ export class JsonlSessionStore {
|
|
|
1024
957
|
this.#markIndexDirty(sessionId);
|
|
1025
958
|
}
|
|
1026
959
|
updateSessionCurrentState(this.#shared.writerLeases.get(sessionId), record.type, serialized);
|
|
1027
|
-
await completeToolResultResources(this
|
|
960
|
+
await completeToolResultResources(this.#sessionDirectory(sessionId), record);
|
|
1028
961
|
if (record.type === 'turn.user' &&
|
|
1029
962
|
isObject(record.payload) &&
|
|
1030
963
|
Array.isArray(record.payload.images))
|
|
@@ -1032,7 +965,7 @@ export class JsonlSessionStore {
|
|
|
1032
965
|
if (isObject(image) &&
|
|
1033
966
|
typeof image.fileName === 'string' &&
|
|
1034
967
|
validInputImageFileName(image.fileName))
|
|
1035
|
-
await rm(join(this
|
|
968
|
+
await rm(join(this.#sessionDirectory(sessionId), 'input_images', `.${image.fileName}.pending`), { force: true }).catch(() => undefined);
|
|
1036
969
|
return record;
|
|
1037
970
|
}
|
|
1038
971
|
async read(sessionId) {
|
|
@@ -1169,9 +1102,6 @@ export class JsonlSessionStore {
|
|
|
1169
1102
|
return this.#rebuildExecutionState(meta);
|
|
1170
1103
|
}
|
|
1171
1104
|
}
|
|
1172
|
-
async readContext(sessionId) {
|
|
1173
|
-
return (await this.readExecutionState(sessionId)).contextRecords;
|
|
1174
|
-
}
|
|
1175
1105
|
/** Compare the planned sequence inside the existing record writer queue. */
|
|
1176
1106
|
async commitContextGc(sessionId, draft, planningRecords) {
|
|
1177
1107
|
this.#assertOpen();
|
|
@@ -1391,7 +1321,7 @@ export class JsonlSessionStore {
|
|
|
1391
1321
|
const current = disk.sessions.find((item) => item.id === sessionId);
|
|
1392
1322
|
if (!current)
|
|
1393
1323
|
throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
|
|
1394
|
-
await recoverSessionStorageTransaction(this
|
|
1324
|
+
await recoverSessionStorageTransaction(this.#sessionDirectory(sessionId), current.storageTransactionId);
|
|
1395
1325
|
await recoverContextualWorldStateTransactions(this.#shared.root, current);
|
|
1396
1326
|
await this.#refreshIndex();
|
|
1397
1327
|
await this.#reconcileTail(sessionId);
|
|
@@ -1512,6 +1442,8 @@ export class JsonlSessionStore {
|
|
|
1512
1442
|
const resume = meta.resume;
|
|
1513
1443
|
if (resume?.version !== 1 || !Number.isSafeInteger(resume.todoRevision))
|
|
1514
1444
|
return;
|
|
1445
|
+
if (resume.userTurns !== undefined && isNativeUserTurn(record))
|
|
1446
|
+
resume.userTurns++;
|
|
1515
1447
|
if (resume.firstUserMessageIndexed === true && resume.firstUserMessagePreview === undefined) {
|
|
1516
1448
|
const preview = userMessagePreviewFromRecord(record);
|
|
1517
1449
|
if (preview !== undefined)
|
|
@@ -1602,6 +1534,73 @@ export class JsonlSessionStore {
|
|
|
1602
1534
|
contextRecords: state.contextRecords
|
|
1603
1535
|
};
|
|
1604
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
|
+
}
|
|
1605
1604
|
async firstUserMessagePreview(sessionId) {
|
|
1606
1605
|
this.#assertOpen();
|
|
1607
1606
|
await this.#refreshIndex();
|
|
@@ -1750,11 +1749,11 @@ export class JsonlSessionStore {
|
|
|
1750
1749
|
if (this.#leaseClosed || this.#shared.closed)
|
|
1751
1750
|
throw new MarAgentError('MAR_AGENT_DISPOSED', 'Session store is closed.');
|
|
1752
1751
|
}
|
|
1753
|
-
#markIndexDirty(sessionId,
|
|
1754
|
-
if (!
|
|
1755
|
-
this.#shared.
|
|
1752
|
+
#markIndexDirty(sessionId, derivedOnly = false) {
|
|
1753
|
+
if (!derivedOnly)
|
|
1754
|
+
this.#shared.derivedOnlyDirtySessions.delete(sessionId);
|
|
1756
1755
|
else if (!this.#shared.dirtySessionVersions.has(sessionId))
|
|
1757
|
-
this.#shared.
|
|
1756
|
+
this.#shared.derivedOnlyDirtySessions.add(sessionId);
|
|
1758
1757
|
this.#shared.indexDirty = true;
|
|
1759
1758
|
this.#shared.dirtySessionVersions.set(sessionId, (this.#shared.dirtySessionVersions.get(sessionId) ?? 0) + 1);
|
|
1760
1759
|
}
|
|
@@ -1796,8 +1795,8 @@ export class JsonlSessionStore {
|
|
|
1796
1795
|
const latestPath = paths.at(-1);
|
|
1797
1796
|
await repairSegmentTail(latestPath);
|
|
1798
1797
|
const records = await readRecords(latestPath);
|
|
1799
|
-
await recoverPendingInputImages(this
|
|
1800
|
-
await cleanupUnreferencedToolResources(this
|
|
1798
|
+
await recoverPendingInputImages(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
|
|
1799
|
+
await cleanupUnreferencedToolResources(this.#sessionDirectory(sessionId), () => this.#readRecords(sessionId));
|
|
1801
1800
|
const last = records.at(-1);
|
|
1802
1801
|
const match = /events-(\d{6})\.jsonl$/.exec(latestPath);
|
|
1803
1802
|
const segment = Number(match?.[1] ?? 1);
|