@remnic/core 9.3.644 → 9.3.646
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 +28 -27
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +16 -16
- package/dist/access-mcp.js +13 -13
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +11 -11
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +2 -2
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-2GCNTHRA.js → chunk-3JSWINVD.js} +98 -35
- package/dist/chunk-3JSWINVD.js.map +1 -0
- package/dist/{chunk-SZL2GGFY.js → chunk-4BISW7RX.js} +2 -2
- package/dist/{chunk-GYITUZFT.js → chunk-A4BTPHIN.js} +171 -134
- package/dist/chunk-A4BTPHIN.js.map +1 -0
- package/dist/{chunk-NV2MGIW7.js → chunk-ALUZN7BE.js} +2 -2
- package/dist/{chunk-RFSCIBCB.js → chunk-AMACWKM4.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-RREI7CNM.js → chunk-D6WVJIS3.js} +9 -42
- package/dist/chunk-D6WVJIS3.js.map +1 -0
- package/dist/{chunk-EGBH5IPJ.js → chunk-DC66QVL2.js} +4 -4
- package/dist/chunk-EVWIEEKZ.js +315 -0
- package/dist/chunk-EVWIEEKZ.js.map +1 -0
- package/dist/{chunk-DM4US77W.js → chunk-FAYDM5WD.js} +6 -4
- package/dist/{chunk-DM4US77W.js.map → chunk-FAYDM5WD.js.map} +1 -1
- package/dist/{chunk-3UR4SBZF.js → chunk-H67ZTMTL.js} +20 -20
- package/dist/{chunk-3SF7QNKF.js → chunk-I3IWTRYB.js} +2 -2
- package/dist/{chunk-Y2XKJJEI.js → chunk-IMWFHBG2.js} +10 -33
- package/dist/chunk-IMWFHBG2.js.map +1 -0
- package/dist/{chunk-IHG27THY.js → chunk-JLOJ5RJ7.js} +68 -59
- package/dist/chunk-JLOJ5RJ7.js.map +1 -0
- package/dist/{chunk-AK4DSORQ.js → chunk-L7W5YW6Y.js} +7 -7
- package/dist/{chunk-4HYWWO3W.js → chunk-MCYT2RNT.js} +3 -3
- package/dist/{chunk-DGNQRNLL.js → chunk-NT5TINK5.js} +2 -2
- package/dist/{chunk-THWT7RJU.js → chunk-OR7R6M5Z.js} +2 -2
- package/dist/{chunk-4MNDVDBS.js → chunk-OTC2KOZ2.js} +2 -2
- package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-TTOYT5RB.js → chunk-PQFUUXWK.js} +2 -2
- package/dist/{chunk-CDHRUNBX.js → chunk-QKE4LHNR.js} +2 -2
- package/dist/{chunk-S2SEPLLA.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-KVUG7FF7.js → chunk-U55D5UD5.js} +7 -7
- package/dist/{chunk-4HYLRROV.js → chunk-UMKPSD35.js} +2 -2
- package/dist/{chunk-NLPH23IQ.js → chunk-UMTG2BN2.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-D2B22JDF.js → chunk-WEPMT6SC.js} +7 -7
- package/dist/{chunk-ECMMM2VE.js → chunk-Z3PZRDLW.js} +2 -2
- package/dist/{chunk-GJSNDJHR.js → chunk-ZPPFKVSD.js} +4 -4
- package/dist/{chunk-TKNTAUA3.js → chunk-ZXWAQFDE.js} +2 -2
- package/dist/cli.js +37 -35
- package/dist/compounding/engine.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/entity-retrieval.js +2 -2
- 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 +91 -78
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +11 -11
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/namespaces/migrate.js +7 -7
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.js +2 -2
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestrator.js +23 -22
- 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/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- 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/storage.js +1 -1
- package/dist/summarizer.js +2 -1
- package/dist/transcript.d.ts +12 -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/dist/verified-recall.js +2 -2
- 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/storage-fallback-category-dirs.test.ts +389 -0
- package/src/storage.ts +36 -4
- package/src/summarizer.ts +15 -35
- package/src/transcript-session-identity.test.ts +424 -0
- package/src/transcript.ts +100 -70
- package/src/utils/category-dir.ts +26 -0
- package/dist/chunk-2GCNTHRA.js.map +0 -1
- package/dist/chunk-GYITUZFT.js.map +0 -1
- package/dist/chunk-IHG27THY.js.map +0 -1
- package/dist/chunk-RREI7CNM.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-SZL2GGFY.js.map → chunk-4BISW7RX.js.map} +0 -0
- /package/dist/{chunk-NV2MGIW7.js.map → chunk-ALUZN7BE.js.map} +0 -0
- /package/dist/{chunk-RFSCIBCB.js.map → chunk-AMACWKM4.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-EGBH5IPJ.js.map → chunk-DC66QVL2.js.map} +0 -0
- /package/dist/{chunk-3UR4SBZF.js.map → chunk-H67ZTMTL.js.map} +0 -0
- /package/dist/{chunk-3SF7QNKF.js.map → chunk-I3IWTRYB.js.map} +0 -0
- /package/dist/{chunk-AK4DSORQ.js.map → chunk-L7W5YW6Y.js.map} +0 -0
- /package/dist/{chunk-4HYWWO3W.js.map → chunk-MCYT2RNT.js.map} +0 -0
- /package/dist/{chunk-DGNQRNLL.js.map → chunk-NT5TINK5.js.map} +0 -0
- /package/dist/{chunk-THWT7RJU.js.map → chunk-OR7R6M5Z.js.map} +0 -0
- /package/dist/{chunk-4MNDVDBS.js.map → chunk-OTC2KOZ2.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-TTOYT5RB.js.map → chunk-PQFUUXWK.js.map} +0 -0
- /package/dist/{chunk-CDHRUNBX.js.map → chunk-QKE4LHNR.js.map} +0 -0
- /package/dist/{chunk-S2SEPLLA.js.map → chunk-RAELB5NX.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-KVUG7FF7.js.map → chunk-U55D5UD5.js.map} +0 -0
- /package/dist/{chunk-4HYLRROV.js.map → chunk-UMKPSD35.js.map} +0 -0
- /package/dist/{chunk-NLPH23IQ.js.map → chunk-UMTG2BN2.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-ECMMM2VE.js.map → chunk-Z3PZRDLW.js.map} +0 -0
- /package/dist/{chunk-GJSNDJHR.js.map → chunk-ZPPFKVSD.js.map} +0 -0
- /package/dist/{chunk-TKNTAUA3.js.map → chunk-ZXWAQFDE.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>();
|
|
@@ -642,6 +652,16 @@ export class TranscriptManager {
|
|
|
642
652
|
const end = new Date(endTime);
|
|
643
653
|
const entries: TranscriptEntry[] = [];
|
|
644
654
|
|
|
655
|
+
// When a sessionKey is given, a partially-applied #1496 migration can leave
|
|
656
|
+
// the SAME raw row in both the primary `session/<hash>` dir and a read-back
|
|
657
|
+
// dir (`other/default` or an old `parts.length >= 3` dir). `readRange` scans
|
|
658
|
+
// EVERY transcript file, so it would append that row once per directory.
|
|
659
|
+
// Dedup by exact raw line to give the same exact-once guarantee `readRecent`
|
|
660
|
+
// / `readToolUse` / the summarizer fetch already provide (cursor review on
|
|
661
|
+
// PR #1504). Only applied for the session-scoped path so unfiltered range
|
|
662
|
+
// scans (each file already enumerated once) keep their existing behavior.
|
|
663
|
+
const keepRawLine = sessionKey ? makeRawLineDeduper() : undefined;
|
|
664
|
+
|
|
645
665
|
try {
|
|
646
666
|
// Get all transcript files from the hierarchical structure
|
|
647
667
|
const transcriptFiles = await this.getAllTranscriptFiles();
|
|
@@ -662,6 +682,7 @@ export class TranscriptManager {
|
|
|
662
682
|
if (entryTime >= start && entryTime < end) {
|
|
663
683
|
// Filter by sessionKey if provided
|
|
664
684
|
if (!sessionKey || entry.sessionKey === sessionKey) {
|
|
685
|
+
if (keepRawLine && !keepRawLine(line)) continue;
|
|
665
686
|
entries.push(entry);
|
|
666
687
|
}
|
|
667
688
|
}
|
|
@@ -711,7 +732,7 @@ export class TranscriptManager {
|
|
|
711
732
|
end: Date,
|
|
712
733
|
sessionKey: string,
|
|
713
734
|
): Promise<TranscriptEntry[]> {
|
|
714
|
-
const { dir, alternateDir, legacyDir } = this.getTranscriptPath(sessionKey);
|
|
735
|
+
const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
|
|
715
736
|
|
|
716
737
|
// Build set of date strings that overlap with [start, end].
|
|
717
738
|
// Always include end's date to handle midnight-crossing lookbacks
|
|
@@ -725,8 +746,17 @@ export class TranscriptManager {
|
|
|
725
746
|
dateStrings.add(end.toISOString().slice(0, 10));
|
|
726
747
|
|
|
727
748
|
const entries: TranscriptEntry[] = [];
|
|
728
|
-
const files = await this.getSessionStorageFiles(
|
|
749
|
+
const files = await this.getSessionStorageFiles(
|
|
750
|
+
this.transcriptsDir,
|
|
751
|
+
dir,
|
|
752
|
+
legacyDir,
|
|
753
|
+
alternateDir,
|
|
754
|
+
readbackDirs,
|
|
755
|
+
);
|
|
729
756
|
|
|
757
|
+
// Dedup identical raw rows that a partially-applied migration may have left
|
|
758
|
+
// in both the primary dir and a read-back dir (issue #1496, cursor review).
|
|
759
|
+
const keepRawLine = makeRawLineDeduper();
|
|
730
760
|
for (const file of files) {
|
|
731
761
|
// Only read files whose date is within the window
|
|
732
762
|
const dateStr = file.name.slice(0, 10);
|
|
@@ -739,7 +769,7 @@ export class TranscriptManager {
|
|
|
739
769
|
try {
|
|
740
770
|
const entry = JSON.parse(line) as TranscriptEntry;
|
|
741
771
|
const ts = new Date(entry.timestamp);
|
|
742
|
-
if (ts >= start && ts < end && entry.sessionKey === sessionKey) {
|
|
772
|
+
if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {
|
|
743
773
|
entries.push(entry);
|
|
744
774
|
}
|
|
745
775
|
} catch {
|
|
@@ -28,6 +28,32 @@ export const ALL_CATEGORY_DIRS: string[] = [
|
|
|
28
28
|
...Object.values(CATEGORY_DIR_MAP),
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Category directories whose files are NOT part of the recall memory corpus.
|
|
33
|
+
*
|
|
34
|
+
* `questions/` holds operational question-QUEUE items written by
|
|
35
|
+
* `StorageManager.writeQuestion()` (frontmatter `{ id, created, priority,
|
|
36
|
+
* resolved }`, body `<question>\n\n**Context:** ...`). They are read only via
|
|
37
|
+
* `readQuestions()` and surfaced through the dedicated, disabled-by-default
|
|
38
|
+
* `injectQuestions` recall-pipeline stage (`{"id":"questions","enabled":false}`)
|
|
39
|
+
* — never as standard frontmatter-backed recall memories. The QMD primary
|
|
40
|
+
* recall path treats them the same way (they are not part of the `memories`
|
|
41
|
+
* corpus), so the filesystem fallback must not pull them into recall either.
|
|
42
|
+
*/
|
|
43
|
+
export const RECALL_NON_MEMORY_DIRS: ReadonlySet<string> = new Set(["questions"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Category directories the filesystem-fallback recall scan should read.
|
|
47
|
+
*
|
|
48
|
+
* This is `ALL_CATEGORY_DIRS` minus the non-memory queue dirs in
|
|
49
|
+
* `RECALL_NON_MEMORY_DIRS`. It is the single source of truth for the corpus the
|
|
50
|
+
* QMD-unavailable fallback (`StorageManager.collectActiveMemoryPaths()`) treats
|
|
51
|
+
* as recall memories — kept in parity with the QMD primary recall corpus.
|
|
52
|
+
*/
|
|
53
|
+
export const RECALL_FALLBACK_DIRS: string[] = ALL_CATEGORY_DIRS.filter(
|
|
54
|
+
(dir) => !RECALL_NON_MEMORY_DIRS.has(dir),
|
|
55
|
+
);
|
|
56
|
+
|
|
31
57
|
/** All category keys (singular form) — used when iterating categories and calling getCategoryDir. */
|
|
32
58
|
export const ALL_CATEGORY_KEYS: string[] = [
|
|
33
59
|
"fact",
|