@remnic/core 9.3.645 → 9.3.647
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/access-cli.js +18 -17
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +13 -13
- package/dist/access-mcp.js +10 -10
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +8 -8
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/{chunk-6YLJZ2EU.js → chunk-3D6L7CEP.js} +92 -29
- package/dist/chunk-3D6L7CEP.js.map +1 -0
- package/dist/{chunk-3SF7QNKF.js → chunk-6BNFVP7Y.js} +2 -2
- package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
- package/dist/{chunk-HRXCPKN4.js → chunk-APWJRJFW.js} +2 -2
- package/dist/{chunk-VDX2J7OX.js → chunk-BUKK5SWA.js} +2 -2
- package/dist/{chunk-T22TZJWJ.js → chunk-DC66QVL2.js} +2 -2
- package/dist/chunk-EVWIEEKZ.js +315 -0
- package/dist/chunk-EVWIEEKZ.js.map +1 -0
- package/dist/{chunk-IZXOO6NP.js → chunk-FAYDM5WD.js} +2 -2
- package/dist/{chunk-Y2XKJJEI.js → chunk-IMWFHBG2.js} +10 -33
- package/dist/chunk-IMWFHBG2.js.map +1 -0
- package/dist/{chunk-W5ISYSIL.js → chunk-L7W5YW6Y.js} +4 -4
- package/dist/{chunk-DGNQRNLL.js → chunk-NT5TINK5.js} +2 -2
- package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-CDHRUNBX.js → chunk-QKE4LHNR.js} +2 -2
- package/dist/{chunk-BTRL6GG7.js → chunk-RAELB5NX.js} +5 -5
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/chunk-S4DDLTPX.js +140 -0
- package/dist/chunk-S4DDLTPX.js.map +1 -0
- package/dist/{chunk-MBT3ASYV.js → chunk-TA4LQ5SR.js} +12 -12
- package/dist/{chunk-KVUG7FF7.js → chunk-U55D5UD5.js} +7 -7
- package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-D2B22JDF.js → chunk-WEPMT6SC.js} +7 -7
- package/dist/{chunk-IHG27THY.js → chunk-XUGQQPGO.js} +77 -61
- package/dist/chunk-XUGQQPGO.js.map +1 -0
- package/dist/{chunk-YOXCJP3A.js → chunk-ZPPFKVSD.js} +3 -3
- package/dist/cli.js +30 -28
- package/dist/{first-start-migration-FF7YFGRP.js → first-start-migration-PG5HBC3K.js} +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -63
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +11 -11
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/search.js +4 -4
- package/dist/operator-toolkit.js +6 -6
- package/dist/orchestrator.js +14 -13
- package/dist/resume-bundles.js +3 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +7 -7
- package/dist/session-identity.d.ts +118 -0
- package/dist/session-identity.js +21 -0
- package/dist/session-identity.js.map +1 -0
- package/dist/session-transcript-migration.d.ts +65 -0
- package/dist/session-transcript-migration.js +16 -0
- package/dist/session-transcript-migration.js.map +1 -0
- package/dist/summarizer.js +2 -1
- package/dist/transcript.d.ts +18 -6
- package/dist/transcript.js +2 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/cli.ts +79 -0
- package/src/index.ts +18 -0
- package/src/session-identity.test.ts +130 -0
- package/src/session-identity.ts +281 -0
- package/src/session-transcript-migration.test.ts +350 -0
- package/src/session-transcript-migration.ts +527 -0
- package/src/summarizer.ts +15 -35
- package/src/transcript-session-identity.test.ts +424 -0
- package/src/transcript.test.ts +110 -0
- package/src/transcript.ts +121 -73
- package/dist/chunk-6YLJZ2EU.js.map +0 -1
- package/dist/chunk-IHG27THY.js.map +0 -1
- package/dist/chunk-Y2XKJJEI.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-3SF7QNKF.js.map → chunk-6BNFVP7Y.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
- /package/dist/{chunk-HRXCPKN4.js.map → chunk-APWJRJFW.js.map} +0 -0
- /package/dist/{chunk-VDX2J7OX.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-T22TZJWJ.js.map → chunk-DC66QVL2.js.map} +0 -0
- /package/dist/{chunk-IZXOO6NP.js.map → chunk-FAYDM5WD.js.map} +0 -0
- /package/dist/{chunk-W5ISYSIL.js.map → chunk-L7W5YW6Y.js.map} +0 -0
- /package/dist/{chunk-DGNQRNLL.js.map → chunk-NT5TINK5.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-CDHRUNBX.js.map → chunk-QKE4LHNR.js.map} +0 -0
- /package/dist/{chunk-BTRL6GG7.js.map → chunk-RAELB5NX.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-MBT3ASYV.js.map → chunk-TA4LQ5SR.js.map} +0 -0
- /package/dist/{chunk-KVUG7FF7.js.map → chunk-U55D5UD5.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-D2B22JDF.js.map → chunk-WEPMT6SC.js.map} +0 -0
- /package/dist/{chunk-YOXCJP3A.js.map → chunk-ZPPFKVSD.js.map} +0 -0
- /package/dist/{first-start-migration-FF7YFGRP.js.map → first-start-migration-PG5HBC3K.js.map} +0 -0
package/src/transcript.ts
CHANGED
|
@@ -3,13 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import type { TranscriptEntry, Checkpoint, PluginConfig } from "./types.js";
|
|
5
5
|
import { analyzeSessionIntegrity, type SessionIntegrityReport } from "./session-integrity.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
encodeStoragePathSegmentWithHash,
|
|
9
|
-
isSafeLegacyPathSegment,
|
|
10
|
-
resolveSafeStoragePath,
|
|
11
|
-
storagePathHash,
|
|
12
|
-
} from "./storage-paths.js";
|
|
6
|
+
import { resolveSafeStoragePath } from "./storage-paths.js";
|
|
7
|
+
import { sessionStoragePaths } from "./session-identity.js";
|
|
13
8
|
|
|
14
9
|
type DirectorySessionStatus = "missing" | "empty" | "matches" | "occupied";
|
|
15
10
|
type DirectoryOwnershipCacheEntry = {
|
|
@@ -17,16 +12,25 @@ type DirectoryOwnershipCacheEntry = {
|
|
|
17
12
|
fileSizes: Map<string, number>;
|
|
18
13
|
};
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
/**
|
|
16
|
+
* De-duplicate JSONL lines collected across multiple read-back candidate
|
|
17
|
+
* directories. A partially-applied migration (issue #1496) can leave the SAME
|
|
18
|
+
* raw row in both the primary `session/<hash>` tree and a legacy read-back dir
|
|
19
|
+
* (`other/default` or an old `parts.length >= 3` directory) when the source was
|
|
20
|
+
* copied but not yet trimmed. Without this guard, `readRecent`, `readToolUse`,
|
|
21
|
+
* footprint estimation, and the summarizer fetch would return that row twice
|
|
22
|
+
* (cursor review on PR #1504). The exact raw JSONL line is the stable identity:
|
|
23
|
+
* the migration preserves byte content per session, so a copied-but-not-trimmed
|
|
24
|
+
* row is byte-identical in both locations. Returns `true` the first time a line
|
|
25
|
+
* is seen, `false` for every subsequent duplicate.
|
|
26
|
+
*/
|
|
27
|
+
function makeRawLineDeduper(): (rawLine: string) => boolean {
|
|
28
|
+
const seen = new Set<string>();
|
|
29
|
+
return (rawLine: string): boolean => {
|
|
30
|
+
if (seen.has(rawLine)) return false;
|
|
31
|
+
seen.add(rawLine);
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -64,13 +68,17 @@ export class TranscriptManager {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
|
-
*
|
|
71
|
+
* Resolve the storage path pieces for a sessionKey via the shared
|
|
72
|
+
* {@link sessionStoragePaths} helper (issue #1496, rule #22).
|
|
68
73
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
+
* Legacy `agent:<id>:...` shapes keep their existing readable channel paths.
|
|
75
|
+
* Arbitrary / non-legacy keys (e.g. `pi-geek:abc123`) route to
|
|
76
|
+
* `transcripts/session/<hash>/YYYY-MM-DD.jsonl` from the FIRST write — they
|
|
77
|
+
* never start life in `other/default`. For those keys a list of
|
|
78
|
+
* read-back-only `readbackDirs` is returned so data written by older builds
|
|
79
|
+
* (under `other/default`, or under an old `parts.length >= 3` directory such
|
|
80
|
+
* as `baz/default`) stays discoverable until migrated. New writes never
|
|
81
|
+
* target read-back dirs. See `session-identity.ts`.
|
|
74
82
|
*
|
|
75
83
|
* @returns Object with raw channel identifiers and encoded storage path pieces.
|
|
76
84
|
*/
|
|
@@ -81,49 +89,21 @@ export class TranscriptManager {
|
|
|
81
89
|
channelId: string;
|
|
82
90
|
alternateDir: string;
|
|
83
91
|
legacyDir?: string;
|
|
92
|
+
readbackDirs: string[];
|
|
84
93
|
} {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
// Default fallback
|
|
88
|
-
let channelType = "other";
|
|
89
|
-
let channelId = "default";
|
|
90
|
-
|
|
91
|
-
if (parts.length >= 3) {
|
|
92
|
-
// parts[0] = "agent", parts[1] = agent name, parts[2] = channel type
|
|
93
|
-
channelType = parts[2];
|
|
94
|
-
|
|
95
|
-
// Extract channel ID based on pattern
|
|
96
|
-
if (channelType === "main") {
|
|
97
|
-
channelId = "default";
|
|
98
|
-
} else if (channelType === "discord" && parts.length >= 5 && parts[3] === "channel") {
|
|
99
|
-
channelId = parts[4];
|
|
100
|
-
} else if (channelType === "slack" && parts.length >= 5 && parts[3] === "channel") {
|
|
101
|
-
channelId = parts[4];
|
|
102
|
-
} else if (channelType === "cron" && parts.length >= 4) {
|
|
103
|
-
channelId = parts[3];
|
|
104
|
-
} else if (parts.length >= 4) {
|
|
105
|
-
// For other types, use the 4th part as ID if available
|
|
106
|
-
channelId = parts[3];
|
|
107
|
-
}
|
|
108
|
-
}
|
|
94
|
+
const paths = sessionStoragePaths(sessionKey);
|
|
109
95
|
|
|
110
96
|
// Daily rotation: transcripts/{channelType}/{channelId}/YYYY-MM-DD.jsonl
|
|
111
97
|
const today = new Date().toISOString().slice(0, 10);
|
|
112
|
-
|
|
113
|
-
encodeStoragePathSegment(channelType),
|
|
114
|
-
encodeStoragePathSegment(channelId),
|
|
115
|
-
);
|
|
116
|
-
const alternateDir = path.join(
|
|
117
|
-
encodeStoragePathSegmentWithHash(channelType),
|
|
118
|
-
`${encodeStoragePathSegmentWithHash(channelId)}--session-${storagePathHash(sessionKey)}`,
|
|
119
|
-
);
|
|
98
|
+
|
|
120
99
|
return {
|
|
121
|
-
dir,
|
|
100
|
+
dir: paths.dir,
|
|
122
101
|
file: `${today}.jsonl`,
|
|
123
|
-
channelType,
|
|
124
|
-
channelId,
|
|
125
|
-
alternateDir,
|
|
126
|
-
legacyDir:
|
|
102
|
+
channelType: paths.channelType,
|
|
103
|
+
channelId: paths.channelId,
|
|
104
|
+
alternateDir: paths.alternateDir,
|
|
105
|
+
legacyDir: paths.legacyDir,
|
|
106
|
+
readbackDirs: paths.readbackDirs,
|
|
127
107
|
};
|
|
128
108
|
}
|
|
129
109
|
|
|
@@ -183,9 +163,16 @@ export class TranscriptManager {
|
|
|
183
163
|
file: string;
|
|
184
164
|
alternateDir: string;
|
|
185
165
|
legacyDir?: string;
|
|
166
|
+
readbackDirs: string[];
|
|
186
167
|
} {
|
|
187
168
|
const p = this.getTranscriptPath(sessionKey);
|
|
188
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
dir: p.dir,
|
|
171
|
+
file: p.file,
|
|
172
|
+
alternateDir: p.alternateDir,
|
|
173
|
+
legacyDir: p.legacyDir,
|
|
174
|
+
readbackDirs: p.readbackDirs,
|
|
175
|
+
};
|
|
189
176
|
}
|
|
190
177
|
|
|
191
178
|
private async selectStorageDirForWrite(
|
|
@@ -344,11 +331,12 @@ export class TranscriptManager {
|
|
|
344
331
|
dir: string,
|
|
345
332
|
legacyDir?: string,
|
|
346
333
|
alternateDir?: string,
|
|
334
|
+
readbackDirs: string[] = [],
|
|
347
335
|
): Promise<Array<{ cacheKey: string; name: string; path: string }>> {
|
|
348
336
|
const files: Array<{ cacheKey: string; name: string; path: string }> = [];
|
|
349
337
|
const seenDirs = new Set<string>();
|
|
350
338
|
|
|
351
|
-
for (const candidateDir of [dir, alternateDir, legacyDir]) {
|
|
339
|
+
for (const candidateDir of [dir, alternateDir, legacyDir, ...readbackDirs]) {
|
|
352
340
|
if (!candidateDir || seenDirs.has(candidateDir)) continue;
|
|
353
341
|
seenDirs.add(candidateDir);
|
|
354
342
|
|
|
@@ -400,10 +388,19 @@ export class TranscriptManager {
|
|
|
400
388
|
startTime: Date,
|
|
401
389
|
endTime: Date,
|
|
402
390
|
): Promise<Array<{ timestamp: string; sessionKey: string; tool: string }>> {
|
|
403
|
-
const { dir, alternateDir, legacyDir } = this.getToolUsagePath(sessionKey);
|
|
391
|
+
const { dir, alternateDir, legacyDir, readbackDirs } = this.getToolUsagePath(sessionKey);
|
|
404
392
|
try {
|
|
405
|
-
const files = await this.getSessionStorageFiles(
|
|
393
|
+
const files = await this.getSessionStorageFiles(
|
|
394
|
+
this.toolUsageDir,
|
|
395
|
+
dir,
|
|
396
|
+
legacyDir,
|
|
397
|
+
alternateDir,
|
|
398
|
+
readbackDirs,
|
|
399
|
+
);
|
|
406
400
|
const out: Array<{ timestamp: string; sessionKey: string; tool: string }> = [];
|
|
401
|
+
// Dedup identical raw rows that a partially-applied migration may have left
|
|
402
|
+
// in both the primary dir and a read-back dir (issue #1496, cursor review).
|
|
403
|
+
const keepRawLine = makeRawLineDeduper();
|
|
407
404
|
for (const file of files) {
|
|
408
405
|
const raw = await readFile(file.path, "utf-8");
|
|
409
406
|
for (const line of raw.split("\n")) {
|
|
@@ -414,7 +411,7 @@ export class TranscriptManager {
|
|
|
414
411
|
if (!Number.isFinite(ts)) continue;
|
|
415
412
|
if (ts >= startTime.getTime() && ts < endTime.getTime()) {
|
|
416
413
|
if (typeof obj.tool === "string" && typeof obj.sessionKey === "string") {
|
|
417
|
-
if (obj.sessionKey === sessionKey) {
|
|
414
|
+
if (obj.sessionKey === sessionKey && keepRawLine(line)) {
|
|
418
415
|
out.push({ timestamp: obj.timestamp, sessionKey: obj.sessionKey, tool: obj.tool });
|
|
419
416
|
}
|
|
420
417
|
}
|
|
@@ -431,11 +428,24 @@ export class TranscriptManager {
|
|
|
431
428
|
}
|
|
432
429
|
|
|
433
430
|
async estimateSessionFootprint(sessionKey: string): Promise<{ bytes: number; tokens: number }> {
|
|
434
|
-
const { dir, alternateDir, legacyDir } = this.getTranscriptPath(sessionKey);
|
|
431
|
+
const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
|
|
435
432
|
let bytes = 0;
|
|
436
433
|
|
|
434
|
+
// NOTE: this is a best-effort byte ESTIMATE for compaction sizing, not an
|
|
435
|
+
// exact row count, and is maintained via an incremental per-file cache. A
|
|
436
|
+
// copied-but-not-trimmed migration window (issue #1496) can transiently
|
|
437
|
+
// over-estimate by counting a duplicated row in both the primary and a
|
|
438
|
+
// read-back dir; that self-heals once the migration trims the source. The
|
|
439
|
+
// exact-once guarantee that matters for callers lives in readRecent /
|
|
440
|
+
// readToolUse / the summarizer fetch, which dedup by raw line.
|
|
437
441
|
try {
|
|
438
|
-
const files = await this.getSessionStorageFiles(
|
|
442
|
+
const files = await this.getSessionStorageFiles(
|
|
443
|
+
this.transcriptsDir,
|
|
444
|
+
dir,
|
|
445
|
+
legacyDir,
|
|
446
|
+
alternateDir,
|
|
447
|
+
readbackDirs,
|
|
448
|
+
);
|
|
439
449
|
const cached = this.sessionFootprintCache.get(sessionKey);
|
|
440
450
|
if (!cached) {
|
|
441
451
|
const fileBytes = new Map<string, number>();
|
|
@@ -636,12 +646,28 @@ export class TranscriptManager {
|
|
|
636
646
|
* Read transcript entries for a date range.
|
|
637
647
|
* Returns entries within the time range, optionally filtered by sessionKey.
|
|
638
648
|
* Reads from all channel subdirectories in the hierarchical structure.
|
|
649
|
+
*
|
|
650
|
+
* The window is half-open `[start, end)`: the upper bound is exclusive so
|
|
651
|
+
* caller-specified ranges (explicit dates, analytics buckets) don't
|
|
652
|
+
* double-count at shared boundaries (CLAUDE.md rule #35 / AGENTS.md rule 23).
|
|
653
|
+
* readRecent's inclusive-of-now behavior is handled by readRecent itself
|
|
654
|
+
* extending its end, not by relaxing this bound.
|
|
639
655
|
*/
|
|
640
656
|
async readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]> {
|
|
641
657
|
const start = new Date(startTime);
|
|
642
658
|
const end = new Date(endTime);
|
|
643
659
|
const entries: TranscriptEntry[] = [];
|
|
644
660
|
|
|
661
|
+
// When a sessionKey is given, a partially-applied #1496 migration can leave
|
|
662
|
+
// the SAME raw row in both the primary `session/<hash>` dir and a read-back
|
|
663
|
+
// dir (`other/default` or an old `parts.length >= 3` dir). `readRange` scans
|
|
664
|
+
// EVERY transcript file, so it would append that row once per directory.
|
|
665
|
+
// Dedup by exact raw line to give the same exact-once guarantee `readRecent`
|
|
666
|
+
// / `readToolUse` / the summarizer fetch already provide (cursor review on
|
|
667
|
+
// PR #1504). Only applied for the session-scoped path so unfiltered range
|
|
668
|
+
// scans (each file already enumerated once) keep their existing behavior.
|
|
669
|
+
const keepRawLine = sessionKey ? makeRawLineDeduper() : undefined;
|
|
670
|
+
|
|
645
671
|
try {
|
|
646
672
|
// Get all transcript files from the hierarchical structure
|
|
647
673
|
const transcriptFiles = await this.getAllTranscriptFiles();
|
|
@@ -658,10 +684,11 @@ export class TranscriptManager {
|
|
|
658
684
|
const entry = JSON.parse(line) as TranscriptEntry;
|
|
659
685
|
const entryTime = new Date(entry.timestamp);
|
|
660
686
|
|
|
661
|
-
// Check if entry is within time range
|
|
687
|
+
// Check if entry is within time range (half-open: exclusive end)
|
|
662
688
|
if (entryTime >= start && entryTime < end) {
|
|
663
689
|
// Filter by sessionKey if provided
|
|
664
690
|
if (!sessionKey || entry.sessionKey === sessionKey) {
|
|
691
|
+
if (keepRawLine && !keepRawLine(line)) continue;
|
|
665
692
|
entries.push(entry);
|
|
666
693
|
}
|
|
667
694
|
}
|
|
@@ -693,8 +720,17 @@ export class TranscriptManager {
|
|
|
693
720
|
* specific channel instead of scanning all 95+ transcript files across all channels.
|
|
694
721
|
*/
|
|
695
722
|
async readRecent(hours: number, sessionKey?: string): Promise<TranscriptEntry[]> {
|
|
696
|
-
const
|
|
697
|
-
|
|
723
|
+
const now = new Date();
|
|
724
|
+
// The recent-read window is inclusive of "now": a turn that is buffered and
|
|
725
|
+
// then immediately read back can carry the same millisecond timestamp as
|
|
726
|
+
// this read, so a half-open [start, now) filter would drop it. Extend the
|
|
727
|
+
// upper bound by 1ms so the exclusive [start, end) filters in
|
|
728
|
+
// readRecentForSession / readRange (which stay half-open for
|
|
729
|
+
// caller-specified ranges, CLAUDE.md rule #35) still capture an entry
|
|
730
|
+
// stamped at exactly "now". This is the only place the "up to now" boundary
|
|
731
|
+
// is widened; explicit ranges are unaffected.
|
|
732
|
+
const end = new Date(now.getTime() + 1);
|
|
733
|
+
const start = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
|
698
734
|
|
|
699
735
|
if (sessionKey) {
|
|
700
736
|
return this.readRecentForSession(start, end, sessionKey);
|
|
@@ -711,7 +747,7 @@ export class TranscriptManager {
|
|
|
711
747
|
end: Date,
|
|
712
748
|
sessionKey: string,
|
|
713
749
|
): Promise<TranscriptEntry[]> {
|
|
714
|
-
const { dir, alternateDir, legacyDir } = this.getTranscriptPath(sessionKey);
|
|
750
|
+
const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
|
|
715
751
|
|
|
716
752
|
// Build set of date strings that overlap with [start, end].
|
|
717
753
|
// Always include end's date to handle midnight-crossing lookbacks
|
|
@@ -725,8 +761,17 @@ export class TranscriptManager {
|
|
|
725
761
|
dateStrings.add(end.toISOString().slice(0, 10));
|
|
726
762
|
|
|
727
763
|
const entries: TranscriptEntry[] = [];
|
|
728
|
-
const files = await this.getSessionStorageFiles(
|
|
764
|
+
const files = await this.getSessionStorageFiles(
|
|
765
|
+
this.transcriptsDir,
|
|
766
|
+
dir,
|
|
767
|
+
legacyDir,
|
|
768
|
+
alternateDir,
|
|
769
|
+
readbackDirs,
|
|
770
|
+
);
|
|
729
771
|
|
|
772
|
+
// Dedup identical raw rows that a partially-applied migration may have left
|
|
773
|
+
// in both the primary dir and a read-back dir (issue #1496, cursor review).
|
|
774
|
+
const keepRawLine = makeRawLineDeduper();
|
|
730
775
|
for (const file of files) {
|
|
731
776
|
// Only read files whose date is within the window
|
|
732
777
|
const dateStr = file.name.slice(0, 10);
|
|
@@ -739,7 +784,10 @@ export class TranscriptManager {
|
|
|
739
784
|
try {
|
|
740
785
|
const entry = JSON.parse(line) as TranscriptEntry;
|
|
741
786
|
const ts = new Date(entry.timestamp);
|
|
742
|
-
|
|
787
|
+
// Half-open window: exclusive end. readRecent widens its own end by
|
|
788
|
+
// 1ms so a just-written "now" entry is still captured here without
|
|
789
|
+
// relaxing this bound for direct callers (tests/transcript-boundary).
|
|
790
|
+
if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {
|
|
743
791
|
entries.push(entry);
|
|
744
792
|
}
|
|
745
793
|
} catch {
|