@caupulican/pi-agent-core 0.81.27 → 0.81.30
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/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/compaction/branch-summarization.js +5 -3
- package/dist/compaction/branch-summarization.js.map +1 -1
- package/dist/compaction/extraction.d.ts.map +1 -1
- package/dist/compaction/extraction.js +12 -2
- package/dist/compaction/extraction.js.map +1 -1
- package/dist/compaction/verification.d.ts.map +1 -1
- package/dist/compaction/verification.js +27 -14
- package/dist/compaction/verification.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +30 -5
- package/dist/proxy.js.map +1 -1
- package/dist/session/session-manager.d.ts +12 -0
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +270 -46
- package/dist/session/session-manager.js.map +1 -1
- package/dist/utils/streaming-lines.d.ts +20 -0
- package/dist/utils/streaming-lines.d.ts.map +1 -0
- package/dist/utils/streaming-lines.js +79 -0
- package/dist/utils/streaming-lines.js.map +1 -0
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline";
|
|
|
6
6
|
import { StringDecoder } from "node:string_decoder";
|
|
7
7
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
8
8
|
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
9
|
+
import { StreamingLineDecoder } from "../utils/streaming-lines.js";
|
|
9
10
|
import { uuidv7 } from "../uuid.js";
|
|
10
11
|
import { compactToolResultDetailsForRetention } from "./message-retention.js";
|
|
11
12
|
export const CURRENT_SESSION_VERSION = 3;
|
|
@@ -145,13 +146,14 @@ export function buildSessionContext(entries, leafId, byId) {
|
|
|
145
146
|
if (!leaf) {
|
|
146
147
|
return { messages: [], thinkingLevel: "off", model: null };
|
|
147
148
|
}
|
|
148
|
-
// Walk from leaf to root,
|
|
149
|
+
// Walk from leaf to root, then reverse once. Repeated front insertion makes long branches quadratic.
|
|
149
150
|
const path = [];
|
|
150
151
|
let current = leaf;
|
|
151
152
|
while (current) {
|
|
152
|
-
path.
|
|
153
|
+
path.push(current);
|
|
153
154
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
154
155
|
}
|
|
156
|
+
path.reverse();
|
|
155
157
|
// Extract settings and find compaction
|
|
156
158
|
let thinkingLevel = "off";
|
|
157
159
|
let model = null;
|
|
@@ -273,45 +275,28 @@ export function loadEntriesFromFile(filePath, options) {
|
|
|
273
275
|
const fd = openSync(resolvedFilePath, "r");
|
|
274
276
|
try {
|
|
275
277
|
const decoder = new StringDecoder("utf8");
|
|
278
|
+
const lines = new StreamingLineDecoder(maxLineChars, { overflow: "skip", lineEndings: "lf" });
|
|
276
279
|
const buffer = Buffer.allocUnsafe(SESSION_READ_BUFFER_SIZE);
|
|
277
|
-
let pending = "";
|
|
278
|
-
let skippingOversizedLine = false;
|
|
279
280
|
while (true) {
|
|
280
281
|
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
|
281
282
|
if (bytesRead === 0)
|
|
282
283
|
break;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
pending = "";
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
pending = pending.slice(resumeIndex + 1);
|
|
291
|
-
skippingOversizedLine = false;
|
|
292
|
-
}
|
|
293
|
-
let lineStart = 0;
|
|
294
|
-
let newlineIndex = pending.indexOf("\n", lineStart);
|
|
295
|
-
while (newlineIndex !== -1) {
|
|
296
|
-
if (newlineIndex - lineStart <= maxLineChars) {
|
|
297
|
-
const entry = parseSessionEntryLine(pending.slice(lineStart, newlineIndex));
|
|
298
|
-
if (entry)
|
|
299
|
-
entries.push(entry);
|
|
300
|
-
}
|
|
301
|
-
lineStart = newlineIndex + 1;
|
|
302
|
-
newlineIndex = pending.indexOf("\n", lineStart);
|
|
303
|
-
}
|
|
304
|
-
pending = pending.slice(lineStart);
|
|
305
|
-
if (pending.length > maxLineChars) {
|
|
306
|
-
skippingOversizedLine = true;
|
|
307
|
-
pending = "";
|
|
284
|
+
for (const line of lines.push(decoder.write(buffer.subarray(0, bytesRead)))) {
|
|
285
|
+
const entry = parseSessionEntryLine(line);
|
|
286
|
+
if (entry)
|
|
287
|
+
entries.push(entry);
|
|
308
288
|
}
|
|
309
289
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
290
|
+
for (const line of lines.push(decoder.end())) {
|
|
291
|
+
const entry = parseSessionEntryLine(line);
|
|
292
|
+
if (entry)
|
|
293
|
+
entries.push(entry);
|
|
294
|
+
}
|
|
295
|
+
const finalLine = lines.finish();
|
|
296
|
+
if (finalLine !== undefined) {
|
|
297
|
+
const entry = parseSessionEntryLine(finalLine);
|
|
298
|
+
if (entry)
|
|
299
|
+
entries.push(entry);
|
|
315
300
|
}
|
|
316
301
|
}
|
|
317
302
|
finally {
|
|
@@ -326,6 +311,87 @@ export function loadEntriesFromFile(filePath, options) {
|
|
|
326
311
|
}
|
|
327
312
|
return entries;
|
|
328
313
|
}
|
|
314
|
+
const ENTRY_ID_PREFIX_BYTES = 64 * 1024;
|
|
315
|
+
const COMPACTED_PAYLOAD_RELEASE_MIN_CHARS = 16 * 1024;
|
|
316
|
+
function indexSessionEntryFileLocations(filePath, startOffset, locations, retainedIds) {
|
|
317
|
+
const fd = openSync(filePath, "r");
|
|
318
|
+
const buffer = Buffer.allocUnsafe(SESSION_READ_BUFFER_SIZE);
|
|
319
|
+
let position = startOffset;
|
|
320
|
+
let lineOffset = startOffset;
|
|
321
|
+
let lineLength = 0;
|
|
322
|
+
let prefixLength = 0;
|
|
323
|
+
let prefixParts = [];
|
|
324
|
+
const finishLine = () => {
|
|
325
|
+
if (prefixLength > 0) {
|
|
326
|
+
const prefix = prefixParts.length === 1
|
|
327
|
+
? prefixParts[0].toString("utf8")
|
|
328
|
+
: Buffer.concat(prefixParts, prefixLength).toString("utf8");
|
|
329
|
+
const id = /"id"\s*:\s*"([A-Za-z0-9_-]+)"/.exec(prefix)?.[1];
|
|
330
|
+
if (id && retainedIds.has(id))
|
|
331
|
+
locations.set(id, { offset: lineOffset, length: lineLength });
|
|
332
|
+
}
|
|
333
|
+
lineLength = 0;
|
|
334
|
+
prefixLength = 0;
|
|
335
|
+
prefixParts = [];
|
|
336
|
+
};
|
|
337
|
+
try {
|
|
338
|
+
while (true) {
|
|
339
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, position);
|
|
340
|
+
if (bytesRead === 0)
|
|
341
|
+
break;
|
|
342
|
+
const chunkOffset = position;
|
|
343
|
+
position += bytesRead;
|
|
344
|
+
let segmentStart = 0;
|
|
345
|
+
while (segmentStart < bytesRead) {
|
|
346
|
+
const newlineIndex = buffer.indexOf(0x0a, segmentStart);
|
|
347
|
+
const segmentEnd = newlineIndex === -1 || newlineIndex >= bytesRead ? bytesRead : newlineIndex;
|
|
348
|
+
const segmentLength = segmentEnd - segmentStart;
|
|
349
|
+
lineLength += segmentLength;
|
|
350
|
+
const prefixRemaining = ENTRY_ID_PREFIX_BYTES - prefixLength;
|
|
351
|
+
if (prefixRemaining > 0 && segmentLength > 0) {
|
|
352
|
+
const retainedLength = Math.min(prefixRemaining, segmentLength);
|
|
353
|
+
prefixParts.push(Buffer.from(buffer.subarray(segmentStart, segmentStart + retainedLength)));
|
|
354
|
+
prefixLength += retainedLength;
|
|
355
|
+
}
|
|
356
|
+
if (newlineIndex === -1 || newlineIndex >= bytesRead)
|
|
357
|
+
break;
|
|
358
|
+
finishLine();
|
|
359
|
+
lineOffset = chunkOffset + newlineIndex + 1;
|
|
360
|
+
segmentStart = newlineIndex + 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (lineLength > 0)
|
|
364
|
+
finishLine();
|
|
365
|
+
return position;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
closeSync(fd);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function retainedStringChars(value, stopAfter) {
|
|
372
|
+
const pending = [value];
|
|
373
|
+
const seen = new WeakSet();
|
|
374
|
+
let chars = 0;
|
|
375
|
+
while (pending.length > 0) {
|
|
376
|
+
const item = pending.pop();
|
|
377
|
+
if (typeof item === "string") {
|
|
378
|
+
chars += item.length;
|
|
379
|
+
if (chars >= stopAfter)
|
|
380
|
+
return chars;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!item || typeof item !== "object" || seen.has(item))
|
|
384
|
+
continue;
|
|
385
|
+
seen.add(item);
|
|
386
|
+
if (Array.isArray(item)) {
|
|
387
|
+
pending.push(...item);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
pending.push(...Object.values(item));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return chars;
|
|
394
|
+
}
|
|
329
395
|
function readSessionHeader(filePath) {
|
|
330
396
|
try {
|
|
331
397
|
const fd = openSync(filePath, "r");
|
|
@@ -603,10 +669,14 @@ export class SessionManager {
|
|
|
603
669
|
persist;
|
|
604
670
|
flushed = false;
|
|
605
671
|
fileEntries = [];
|
|
672
|
+
entries = [];
|
|
606
673
|
byId = new Map();
|
|
607
674
|
labelsById = new Map();
|
|
608
675
|
labelTimestampsById = new Map();
|
|
609
676
|
leafId = null;
|
|
677
|
+
entryFileLocations = new Map();
|
|
678
|
+
coldPayloadEntryIds = new Set();
|
|
679
|
+
indexedSessionFileBytes = 0;
|
|
610
680
|
constructor(cwd, sessionDir, sessionFile, persist, agentDir, newSessionOptions) {
|
|
611
681
|
this.cwd = resolvePath(cwd);
|
|
612
682
|
this.sessionDir = normalizePath(sessionDir);
|
|
@@ -624,6 +694,7 @@ export class SessionManager {
|
|
|
624
694
|
}
|
|
625
695
|
/** Switch to a different session file (used for resume and branching) */
|
|
626
696
|
setSessionFile(sessionFile) {
|
|
697
|
+
this._resetEntryFileIndex(true);
|
|
627
698
|
this.sessionFile = resolvePath(sessionFile);
|
|
628
699
|
if (existsSync(this.sessionFile)) {
|
|
629
700
|
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
|
@@ -650,6 +721,7 @@ export class SessionManager {
|
|
|
650
721
|
}
|
|
651
722
|
this._buildIndex();
|
|
652
723
|
this.flushed = true;
|
|
724
|
+
this._releaseExistingCompactedMessagePayloads();
|
|
653
725
|
}
|
|
654
726
|
else {
|
|
655
727
|
const explicitPath = this.sessionFile;
|
|
@@ -672,17 +744,27 @@ export class SessionManager {
|
|
|
672
744
|
parentSession: options?.parentSession,
|
|
673
745
|
};
|
|
674
746
|
this.fileEntries = [header];
|
|
747
|
+
this.entries = [];
|
|
675
748
|
this.byId.clear();
|
|
676
749
|
this.labelsById.clear();
|
|
750
|
+
this.labelTimestampsById.clear();
|
|
677
751
|
this.leafId = null;
|
|
678
752
|
this.flushed = false;
|
|
753
|
+
this._resetEntryFileIndex(true);
|
|
679
754
|
if (this.persist) {
|
|
680
755
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
681
756
|
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
682
757
|
}
|
|
683
758
|
return this.sessionFile;
|
|
684
759
|
}
|
|
760
|
+
_resetEntryFileIndex(clearColdPayloads = false) {
|
|
761
|
+
this.entryFileLocations.clear();
|
|
762
|
+
this.indexedSessionFileBytes = 0;
|
|
763
|
+
if (clearColdPayloads)
|
|
764
|
+
this.coldPayloadEntryIds.clear();
|
|
765
|
+
}
|
|
685
766
|
_buildIndex() {
|
|
767
|
+
this.entries = [];
|
|
686
768
|
this.byId.clear();
|
|
687
769
|
this.labelsById.clear();
|
|
688
770
|
this.labelTimestampsById.clear();
|
|
@@ -690,6 +772,7 @@ export class SessionManager {
|
|
|
690
772
|
for (const entry of this.fileEntries) {
|
|
691
773
|
if (entry.type === "session")
|
|
692
774
|
continue;
|
|
775
|
+
this.entries.push(entry);
|
|
693
776
|
this.byId.set(entry.id, entry);
|
|
694
777
|
this.leafId = entry.id;
|
|
695
778
|
if (entry.type === "label") {
|
|
@@ -703,14 +786,18 @@ export class SessionManager {
|
|
|
703
786
|
}
|
|
704
787
|
}
|
|
705
788
|
}
|
|
789
|
+
for (const id of this.coldPayloadEntryIds) {
|
|
790
|
+
if (!this.byId.has(id))
|
|
791
|
+
this.coldPayloadEntryIds.delete(id);
|
|
792
|
+
}
|
|
706
793
|
}
|
|
707
|
-
_rewriteFile() {
|
|
708
|
-
if (!this.persist || !
|
|
794
|
+
_rewriteFile(targetFile = this.sessionFile) {
|
|
795
|
+
if (!this.persist || !targetFile)
|
|
709
796
|
return;
|
|
710
797
|
// Write-then-rename keeps the session file valid at every instant: truncating
|
|
711
798
|
// in place leaves a torn-file window for crashes or a concurrent process
|
|
712
799
|
// appending to the same session.
|
|
713
|
-
const tempFile = `${
|
|
800
|
+
const tempFile = `${targetFile}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
714
801
|
const fd = openSync(tempFile, "wx");
|
|
715
802
|
try {
|
|
716
803
|
for (const entry of this.fileEntries) {
|
|
@@ -720,7 +807,9 @@ export class SessionManager {
|
|
|
720
807
|
finally {
|
|
721
808
|
closeSync(fd);
|
|
722
809
|
}
|
|
723
|
-
renameSync(tempFile,
|
|
810
|
+
renameSync(tempFile, targetFile);
|
|
811
|
+
if (targetFile === this.sessionFile)
|
|
812
|
+
this._resetEntryFileIndex();
|
|
724
813
|
}
|
|
725
814
|
isPersisted() {
|
|
726
815
|
return this.persist;
|
|
@@ -740,6 +829,132 @@ export class SessionManager {
|
|
|
740
829
|
getSessionFile() {
|
|
741
830
|
return this.sessionFile;
|
|
742
831
|
}
|
|
832
|
+
_ensureEntryFileLocations(retainedIds) {
|
|
833
|
+
if (!this.sessionFile || !existsSync(this.sessionFile))
|
|
834
|
+
return;
|
|
835
|
+
const fileBytes = statSync(this.sessionFile).size;
|
|
836
|
+
if (fileBytes < this.indexedSessionFileBytes)
|
|
837
|
+
this._resetEntryFileIndex();
|
|
838
|
+
if (fileBytes === this.indexedSessionFileBytes)
|
|
839
|
+
return;
|
|
840
|
+
const idsToIndex = new Set(this.coldPayloadEntryIds);
|
|
841
|
+
for (const id of retainedIds)
|
|
842
|
+
idsToIndex.add(id);
|
|
843
|
+
this.indexedSessionFileBytes = indexSessionEntryFileLocations(this.sessionFile, this.indexedSessionFileBytes, this.entryFileLocations, idsToIndex);
|
|
844
|
+
}
|
|
845
|
+
_readCompactedMessageProperty(entryId, property) {
|
|
846
|
+
if (!this.sessionFile)
|
|
847
|
+
throw new Error(`Compacted session payload ${entryId} is unavailable`);
|
|
848
|
+
let location = this.entryFileLocations.get(entryId);
|
|
849
|
+
if (!location) {
|
|
850
|
+
this._resetEntryFileIndex();
|
|
851
|
+
this._ensureEntryFileLocations(new Set([entryId]));
|
|
852
|
+
location = this.entryFileLocations.get(entryId);
|
|
853
|
+
}
|
|
854
|
+
if (!location)
|
|
855
|
+
throw new Error(`Compacted session payload ${entryId} is unavailable`);
|
|
856
|
+
const fd = openSync(this.sessionFile, "r");
|
|
857
|
+
const buffer = Buffer.allocUnsafe(location.length);
|
|
858
|
+
let bytesRead = 0;
|
|
859
|
+
try {
|
|
860
|
+
while (bytesRead < buffer.length) {
|
|
861
|
+
const count = readSync(fd, buffer, bytesRead, buffer.length - bytesRead, location.offset + bytesRead);
|
|
862
|
+
if (count === 0)
|
|
863
|
+
break;
|
|
864
|
+
bytesRead += count;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
closeSync(fd);
|
|
869
|
+
}
|
|
870
|
+
const entry = parseSessionEntryLine(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
871
|
+
if (!entry || entry.type !== "message" || entry.id !== entryId) {
|
|
872
|
+
throw new Error(`Compacted session payload ${entryId} could not be restored`);
|
|
873
|
+
}
|
|
874
|
+
return entry.message[property];
|
|
875
|
+
}
|
|
876
|
+
_releaseMessageProperty(entry, property) {
|
|
877
|
+
const message = entry.message;
|
|
878
|
+
const descriptor = Object.getOwnPropertyDescriptor(message, property);
|
|
879
|
+
if (!descriptor || descriptor.get || !("value" in descriptor))
|
|
880
|
+
return;
|
|
881
|
+
if (retainedStringChars(descriptor.value, COMPACTED_PAYLOAD_RELEASE_MIN_CHARS) <
|
|
882
|
+
COMPACTED_PAYLOAD_RELEASE_MIN_CHARS) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const entryId = entry.id;
|
|
886
|
+
const enumerable = descriptor.enumerable;
|
|
887
|
+
this.coldPayloadEntryIds.add(entryId);
|
|
888
|
+
Object.defineProperty(message, property, {
|
|
889
|
+
configurable: true,
|
|
890
|
+
enumerable,
|
|
891
|
+
get: () => this._readCompactedMessageProperty(entryId, property),
|
|
892
|
+
set: (value) => {
|
|
893
|
+
Object.defineProperty(message, property, {
|
|
894
|
+
configurable: true,
|
|
895
|
+
enumerable,
|
|
896
|
+
writable: true,
|
|
897
|
+
value,
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
_releaseExistingCompactedMessagePayloads() {
|
|
903
|
+
let current = this.leafId ? this.byId.get(this.leafId) : undefined;
|
|
904
|
+
while (current) {
|
|
905
|
+
if (current.type === "compaction") {
|
|
906
|
+
this._releaseCompactedMessagePayloads(current.firstKeptEntryId, current.parentId);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
current = current.parentId ? this.byId.get(current.parentId) : undefined;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
_releaseCompactedMessagePayloads(firstKeptEntryId, compactionParentId) {
|
|
913
|
+
if (!this.persist || !this.flushed || !this.sessionFile)
|
|
914
|
+
return;
|
|
915
|
+
const keptEntryIds = new Set();
|
|
916
|
+
let currentId = compactionParentId;
|
|
917
|
+
let foundFirstKept = false;
|
|
918
|
+
while (currentId) {
|
|
919
|
+
keptEntryIds.add(currentId);
|
|
920
|
+
if (currentId === firstKeptEntryId) {
|
|
921
|
+
foundFirstKept = true;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
currentId = this.byId.get(currentId)?.parentId ?? null;
|
|
925
|
+
}
|
|
926
|
+
if (!foundFirstKept)
|
|
927
|
+
return;
|
|
928
|
+
const retainedIds = new Set();
|
|
929
|
+
const releases = [];
|
|
930
|
+
for (const entry of this.entries) {
|
|
931
|
+
if (entry.type !== "message")
|
|
932
|
+
continue;
|
|
933
|
+
const message = entry.message;
|
|
934
|
+
const properties = [];
|
|
935
|
+
for (const property of ["content", "output"]) {
|
|
936
|
+
const descriptor = Object.getOwnPropertyDescriptor(message, property);
|
|
937
|
+
if (descriptor &&
|
|
938
|
+
!descriptor.get &&
|
|
939
|
+
"value" in descriptor &&
|
|
940
|
+
retainedStringChars(descriptor.value, COMPACTED_PAYLOAD_RELEASE_MIN_CHARS) >=
|
|
941
|
+
COMPACTED_PAYLOAD_RELEASE_MIN_CHARS) {
|
|
942
|
+
retainedIds.add(entry.id);
|
|
943
|
+
if (!keptEntryIds.has(entry.id))
|
|
944
|
+
properties.push(property);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (properties.length > 0)
|
|
948
|
+
releases.push({ entry, properties });
|
|
949
|
+
}
|
|
950
|
+
this._ensureEntryFileLocations(retainedIds);
|
|
951
|
+
for (const release of releases) {
|
|
952
|
+
if (!this.entryFileLocations.has(release.entry.id))
|
|
953
|
+
continue;
|
|
954
|
+
for (const property of release.properties)
|
|
955
|
+
this._releaseMessageProperty(release.entry, property);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
743
958
|
_persist(entry) {
|
|
744
959
|
if (!this.persist || !this.sessionFile)
|
|
745
960
|
return;
|
|
@@ -772,6 +987,7 @@ export class SessionManager {
|
|
|
772
987
|
}
|
|
773
988
|
_appendEntry(entry) {
|
|
774
989
|
this.fileEntries.push(entry);
|
|
990
|
+
this.entries.push(entry);
|
|
775
991
|
this.byId.set(entry.id, entry);
|
|
776
992
|
this.leafId = entry.id;
|
|
777
993
|
this._persist(entry);
|
|
@@ -820,10 +1036,11 @@ export class SessionManager {
|
|
|
820
1036
|
}
|
|
821
1037
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
822
1038
|
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook) {
|
|
1039
|
+
const compactionParentId = this.leafId;
|
|
823
1040
|
const entry = {
|
|
824
1041
|
type: "compaction",
|
|
825
1042
|
id: generateId(this.byId),
|
|
826
|
-
parentId:
|
|
1043
|
+
parentId: compactionParentId,
|
|
827
1044
|
timestamp: new Date().toISOString(),
|
|
828
1045
|
summary,
|
|
829
1046
|
firstKeptEntryId,
|
|
@@ -832,6 +1049,7 @@ export class SessionManager {
|
|
|
832
1049
|
fromHook,
|
|
833
1050
|
};
|
|
834
1051
|
this._appendEntry(entry);
|
|
1052
|
+
this._releaseCompactedMessagePayloads(firstKeptEntryId, compactionParentId);
|
|
835
1053
|
return entry.id;
|
|
836
1054
|
}
|
|
837
1055
|
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
|
|
@@ -988,11 +1206,16 @@ export class SessionManager {
|
|
|
988
1206
|
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
989
1207
|
*/
|
|
990
1208
|
getEntries() {
|
|
991
|
-
return this.
|
|
1209
|
+
return this.entries.slice();
|
|
1210
|
+
}
|
|
1211
|
+
/** Entries appended at or after `startIndex`, returned as a defensive shallow slice. */
|
|
1212
|
+
getEntriesSince(startIndex) {
|
|
1213
|
+
const normalizedStart = Number.isFinite(startIndex) ? Math.max(0, Math.floor(startIndex)) : 0;
|
|
1214
|
+
return this.entries.slice(normalizedStart);
|
|
992
1215
|
}
|
|
993
1216
|
/** Return current session entry count without allocating a defensive entries array. */
|
|
994
1217
|
getEntryCount() {
|
|
995
|
-
return this.
|
|
1218
|
+
return this.entries.length;
|
|
996
1219
|
}
|
|
997
1220
|
/**
|
|
998
1221
|
* Return recent user-entered prompt text for input recall, oldest entry first.
|
|
@@ -1170,7 +1393,6 @@ export class SessionManager {
|
|
|
1170
1393
|
}
|
|
1171
1394
|
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
1172
1395
|
this.sessionId = newSessionId;
|
|
1173
|
-
this.sessionFile = newSessionFile;
|
|
1174
1396
|
this._buildIndex();
|
|
1175
1397
|
// Only write the file now if it contains an assistant message.
|
|
1176
1398
|
// Otherwise defer to _persist(), which creates the file on the
|
|
@@ -1179,12 +1401,15 @@ export class SessionManager {
|
|
|
1179
1401
|
// no-assistant guard later resets flushed to false.
|
|
1180
1402
|
const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
|
|
1181
1403
|
if (hasAssistant) {
|
|
1182
|
-
|
|
1404
|
+
// Keep the source file active while cold compacted getters are serialized into the branch.
|
|
1405
|
+
this._rewriteFile(newSessionFile);
|
|
1183
1406
|
this.flushed = true;
|
|
1184
1407
|
}
|
|
1185
1408
|
else {
|
|
1186
1409
|
this.flushed = false;
|
|
1187
1410
|
}
|
|
1411
|
+
this.sessionFile = newSessionFile;
|
|
1412
|
+
this._resetEntryFileIndex();
|
|
1188
1413
|
return newSessionFile;
|
|
1189
1414
|
}
|
|
1190
1415
|
// In-memory mode: replace current session with the path + labels
|
|
@@ -1226,9 +1451,8 @@ export class SessionManager {
|
|
|
1226
1451
|
*/
|
|
1227
1452
|
static open(path, agentDir, sessionDir, cwdOverride) {
|
|
1228
1453
|
const resolvedPath = resolvePath(path);
|
|
1229
|
-
//
|
|
1230
|
-
const
|
|
1231
|
-
const header = entries.find((e) => e.type === "session");
|
|
1454
|
+
// Read only the header here; the constructor performs the one full session load.
|
|
1455
|
+
const header = readSessionHeader(resolvedPath);
|
|
1232
1456
|
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
|
|
1233
1457
|
// If no sessionDir provided, derive from file's parent directory
|
|
1234
1458
|
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|