recallnest 3.0.1 → 4.0.0
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/CHANGELOG.md +203 -0
- package/README.md +42 -17
- package/README_CN.md +31 -15
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +10 -2
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +434 -68
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/env-config.ts +118 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +69 -104
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +276 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +3 -4
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +6 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-output.ts +11 -0
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +57 -9
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ingest` 命令这一轮跑哪些来源。
|
|
3
|
+
*
|
|
4
|
+
* 2026-10-06 起对话原文默认不再入库(开关 `RECALLNEST_TRANSCRIPT_INGEST`,见 env-config.ts `transcriptIngest`)。
|
|
5
|
+
* 入库有两个触发入口——launchd 定时那一轮、pull-from-macbook.sh 拉完之后那一轮——两个都跑
|
|
6
|
+
* `scripts/incremental-ingest.sh` → `cli.ts ingest --source all`,所以闸放在命令内部,而不是去改脚本或卸定时任务。
|
|
7
|
+
*
|
|
8
|
+
* 关着的时候三件事不能跟着停,这也是把决定单独抽成一个函数、逐格测到的原因:
|
|
9
|
+
* - 记忆文件对账(memory 来源)照常;
|
|
10
|
+
* - Minis 投递目录里的文件照样挪进 data/minis-archive:Deja 靠那个存档读 Minis 的对话,而挪存档原先拿
|
|
11
|
+
* 「台账里记着已入库」当闸——只跳过入库的话,文件永远不会被标成已处理,也就永远不会被挪,而且不报错;
|
|
12
|
+
* - 显式点名一个对话来源(`--source cc`)时要说清没有执行,不能安静地什么都不做。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const CONVERSATION_SOURCES = ["cc", "codex", "kimi", "gemini", "desktop", "minis"] as const;
|
|
16
|
+
export type ConversationSource = (typeof CONVERSATION_SOURCES)[number];
|
|
17
|
+
|
|
18
|
+
export type MinisMode =
|
|
19
|
+
/** 入库(切片、嵌入),再把台账里记着已入库的文件挪进存档——开关打开时的行为 */
|
|
20
|
+
| "ingest-then-archive"
|
|
21
|
+
/** 不入库;能解析出对话的文件直接挪进存档——开关关着时的行为 */
|
|
22
|
+
| "archive-only"
|
|
23
|
+
/** 这一轮不碰 Minis 投递目录 */
|
|
24
|
+
| "skip";
|
|
25
|
+
|
|
26
|
+
export interface IngestPlan {
|
|
27
|
+
/** 这一轮要切片、嵌入、写库的对话来源(不含 minis,它单独看 `minis`) */
|
|
28
|
+
transcriptSources: Exclude<ConversationSource, "minis">[];
|
|
29
|
+
minis: MinisMode;
|
|
30
|
+
/** 记忆文件(memory 来源) */
|
|
31
|
+
memory: boolean;
|
|
32
|
+
/** 待处理队列里攒的是没有 LLM 时跳过的对话切片,回填等于入库,跟着开关走 */
|
|
33
|
+
drainPendingQueue: boolean;
|
|
34
|
+
/** 这一轮点到了、但因为开关关着而没有入库的对话来源(含 minis),日志里要说出来 */
|
|
35
|
+
skippedTranscriptSources: ConversationSource[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isConversationSource(source: string): source is ConversationSource {
|
|
39
|
+
return (CONVERSATION_SOURCES as readonly string[]).includes(source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function planIngest(source: string, transcriptIngestOn: boolean): IngestPlan {
|
|
43
|
+
const wants = (name: string) => source === "all" || source === name;
|
|
44
|
+
const requested = CONVERSATION_SOURCES.filter((name) => wants(name));
|
|
45
|
+
const requestedTranscripts = requested.filter(
|
|
46
|
+
(name): name is Exclude<ConversationSource, "minis"> => name !== "minis",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
transcriptSources: transcriptIngestOn ? requestedTranscripts : [],
|
|
51
|
+
minis: !wants("minis") ? "skip" : transcriptIngestOn ? "ingest-then-archive" : "archive-only",
|
|
52
|
+
memory: wants("memory"),
|
|
53
|
+
drainPendingQueue: transcriptIngestOn,
|
|
54
|
+
skippedTranscriptSources: transcriptIngestOn ? [] : requested,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/ingest.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { join, basename, resolve, dirname } from "node:path";
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
15
15
|
import { redactSecrets } from "./pii-detector.js";
|
|
16
|
+
import { normalizeDedupText } from "./text-fingerprint.js";
|
|
16
17
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
17
18
|
import type { Embedder } from "./embedder.js";
|
|
18
19
|
import { chunkDocument, type ChunkerConfig } from "./chunker.js";
|
|
@@ -35,6 +36,8 @@ import {
|
|
|
35
36
|
import { compressToolOutput } from "./tool-output-compressor.js";
|
|
36
37
|
import { tagNarrativeIfEnabled } from "./narrative-tagger.js";
|
|
37
38
|
|
|
39
|
+
export { normalizeDedupText };
|
|
40
|
+
|
|
38
41
|
// ============================================================================
|
|
39
42
|
// Types
|
|
40
43
|
// ============================================================================
|
|
@@ -53,6 +56,8 @@ export interface IngestResult {
|
|
|
53
56
|
chunksDeduped: number;
|
|
54
57
|
dedupReasonCounts: DedupReasonCounts;
|
|
55
58
|
errors: string[];
|
|
59
|
+
/** 子 agent 独立会话文件被整文件跳过的数量(目前只有 Codex 分支会产生;见 isCodexSubagentSessionFile)。 */
|
|
60
|
+
subagentFilesSkipped?: number;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
interface ConversationTurn {
|
|
@@ -111,19 +116,21 @@ export type DedupReason = "hard" | "exact" | "llm-skip" | "llm-merge" | "unique"
|
|
|
111
116
|
|
|
112
117
|
export type DedupReasonCounts = Record<DedupReason, number>;
|
|
113
118
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* 去重结论只回答「这条进来的切片存不存」,不带任何指向已有记忆的动作。
|
|
121
|
+
*
|
|
122
|
+
* 2026-10-05 之前这里还有一个 `secondaryDeletes`:模型可以顺手标记「过时」的已有记忆,
|
|
123
|
+
* 导入调用点原样 `store.delete`——候选不分 scope,不写审计、不记日志、错误吞掉。
|
|
124
|
+
* 2026-09-22T16:38:03Z 记忆文件导入就这样删掉了当天手写进人工维护那一层的一条偏好,
|
|
125
|
+
* 11 秒后同一轮导入写入了内容相近的文档切片;到 10-05 核对时,显式存过、不在主表、又没有
|
|
126
|
+
* forget 记录的手写记忆有 17 条。导入只新增和跳过,删除一律走 forget_memory(有隐私级别
|
|
127
|
+
* 检查与审计)。经过与数字见 CHANGELOG「Ingest dedup can no longer delete existing memories」;
|
|
128
|
+
* 回归用例 `ingest-dedup-no-deletes.test.ts`。
|
|
129
|
+
*/
|
|
121
130
|
export interface DedupCheckResult {
|
|
122
131
|
action: "store" | "skip";
|
|
123
132
|
reason: DedupReason;
|
|
124
133
|
existingText?: string;
|
|
125
|
-
/** Secondary actions on other existing memories (only populated with LLM dedup). */
|
|
126
|
-
secondaryDeletes?: DedupAction[];
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
function createDedupReasonCounts(): DedupReasonCounts {
|
|
@@ -143,26 +150,6 @@ function recordDedupDecision(result: IngestResult, decision: DedupCheckResult):
|
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
/**
|
|
147
|
-
* Execute secondary delete actions from a dedup decision.
|
|
148
|
-
* Errors are isolated per-action to avoid cascading failures.
|
|
149
|
-
*/
|
|
150
|
-
async function executeSecondaryDeletes(
|
|
151
|
-
store: MemoryStore,
|
|
152
|
-
deletes: DedupAction[],
|
|
153
|
-
): Promise<number> {
|
|
154
|
-
let executed = 0;
|
|
155
|
-
for (const del of deletes) {
|
|
156
|
-
try {
|
|
157
|
-
await store.delete(del.id);
|
|
158
|
-
executed++;
|
|
159
|
-
} catch {
|
|
160
|
-
// Per-action error isolation: log and continue
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return executed;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
153
|
export function getDedupSkippedCount(result: IngestResult): number {
|
|
167
154
|
return result.dedupReasonCounts.hard
|
|
168
155
|
+ result.dedupReasonCounts.exact
|
|
@@ -181,22 +168,6 @@ export function formatDedupReasonSummary(result: IngestResult): string {
|
|
|
181
168
|
return `hard:${counts.hard}, exact:${counts.exact}, llm-skip:${counts["llm-skip"]}, llm-merge:${counts["llm-merge"]}`;
|
|
182
169
|
}
|
|
183
170
|
|
|
184
|
-
/**
|
|
185
|
-
* Two-stage dedup: vector pre-filter + optional LLM semantic decision.
|
|
186
|
-
*
|
|
187
|
-
* Returns: "store" | "skip"
|
|
188
|
-
*
|
|
189
|
-
* Note: when the LLM says MERGE, we currently keep the new chunk instead of
|
|
190
|
-
* dropping it. We do not have a structured ingest-time merge path yet, and
|
|
191
|
-
* swallowing "same topic + new information" transcript chunks is worse for
|
|
192
|
-
* recall fidelity than storing an incremental near-duplicate.
|
|
193
|
-
*/
|
|
194
|
-
function normalizeDedupText(value: string): string {
|
|
195
|
-
return value
|
|
196
|
-
.replace(/^\[(用户|助手)\]\s*/gm, "")
|
|
197
|
-
.replace(/\s+/g, " ")
|
|
198
|
-
.trim();
|
|
199
|
-
}
|
|
200
171
|
|
|
201
172
|
/**
|
|
202
173
|
* Detect content that carries temporal events, file operations, or explicit
|
|
@@ -306,6 +277,16 @@ function shouldForceCreateToolChoicePreference(
|
|
|
306
277
|
: { shouldForceCreate: false };
|
|
307
278
|
}
|
|
308
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Two-stage dedup: vector pre-filter + optional LLM semantic decision.
|
|
282
|
+
*
|
|
283
|
+
* Returns: "store" | "skip"
|
|
284
|
+
*
|
|
285
|
+
* Note: when the LLM says MERGE, we currently keep the new chunk instead of
|
|
286
|
+
* dropping it. We do not have a structured ingest-time merge path yet, and
|
|
287
|
+
* swallowing "same topic + new information" transcript chunks is worse for
|
|
288
|
+
* recall fidelity than storing an incremental near-duplicate.
|
|
289
|
+
*/
|
|
309
290
|
export async function dedupCheck(
|
|
310
291
|
store: MemoryStore,
|
|
311
292
|
vector: number[],
|
|
@@ -415,8 +396,6 @@ export async function dedupCheck(
|
|
|
415
396
|
const hasMulti = typeof (llm as any).dedupDecisionMulti === "function";
|
|
416
397
|
|
|
417
398
|
let primaryAction: "CREATE" | "MERGE" | "SKIP" = "CREATE";
|
|
418
|
-
let primaryReason = "";
|
|
419
|
-
const secondaryDeletes: DedupAction[] = [];
|
|
420
399
|
|
|
421
400
|
if (hasMulti) {
|
|
422
401
|
const candidates = results
|
|
@@ -424,48 +403,20 @@ export async function dedupCheck(
|
|
|
424
403
|
.slice(0, DEDUP_CANDIDATE_LIMIT)
|
|
425
404
|
.map(r => ({ id: r.entry.id, text: r.entry.text }));
|
|
426
405
|
|
|
406
|
+
// 只取「存 / 并 / 跳」这一个结论。候选来自全库(不分 scope),模型对它们没有处置权,
|
|
407
|
+
// 见 DedupCheckResult 上方的说明。
|
|
427
408
|
const decision = await llm.dedupDecisionMulti(text, candidates);
|
|
428
409
|
primaryAction = decision.action;
|
|
429
|
-
primaryReason = decision.reason;
|
|
430
|
-
|
|
431
|
-
// Build secondary deletes from validated actions (resolve IDs at parse time)
|
|
432
|
-
if (decision.actions) {
|
|
433
|
-
for (const act of decision.actions) {
|
|
434
|
-
const target = candidates[act.match_index - 1];
|
|
435
|
-
if (target) {
|
|
436
|
-
secondaryDeletes.push({
|
|
437
|
-
id: target.id,
|
|
438
|
-
action: "delete",
|
|
439
|
-
reason: typeof act.reason === "string" ? act.reason : "",
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
410
|
} else {
|
|
445
411
|
const decision = await llm.dedupDecision(text, existingText);
|
|
446
412
|
primaryAction = decision.action;
|
|
447
|
-
primaryReason = decision.reason;
|
|
448
413
|
}
|
|
449
414
|
|
|
450
415
|
if (primaryAction === "SKIP") {
|
|
451
|
-
return {
|
|
452
|
-
action: "skip",
|
|
453
|
-
reason: "llm-skip",
|
|
454
|
-
existingText,
|
|
455
|
-
...(secondaryDeletes.length > 0 ? { secondaryDeletes } : {}),
|
|
456
|
-
};
|
|
416
|
+
return { action: "skip", reason: "llm-skip", existingText };
|
|
457
417
|
}
|
|
458
418
|
if (primaryAction === "MERGE") {
|
|
459
|
-
return {
|
|
460
|
-
action: "store",
|
|
461
|
-
reason: "llm-merge",
|
|
462
|
-
existingText,
|
|
463
|
-
...(secondaryDeletes.length > 0 ? { secondaryDeletes } : {}),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
// CREATE — still execute secondary deletes if any
|
|
467
|
-
if (secondaryDeletes.length > 0) {
|
|
468
|
-
return { action: "store", reason: "unique", secondaryDeletes };
|
|
419
|
+
return { action: "store", reason: "llm-merge", existingText };
|
|
469
420
|
}
|
|
470
421
|
} catch {
|
|
471
422
|
// LLM failed, fall through to store
|
|
@@ -516,7 +467,7 @@ function fallbackExtraction(text: string): SmartExtraction {
|
|
|
516
467
|
* Uses LLM 6-category extraction when available, falls back to heuristic.
|
|
517
468
|
* Returns: category + L0 + L1 + importance for each text.
|
|
518
469
|
*/
|
|
519
|
-
async function smartExtractBatch(
|
|
470
|
+
export async function smartExtractBatch(
|
|
520
471
|
texts: string[],
|
|
521
472
|
llm?: LLMClient | null,
|
|
522
473
|
): Promise<SmartExtraction[]> {
|
|
@@ -545,7 +496,7 @@ function isCoreSummaryEnabled(): boolean {
|
|
|
545
496
|
* Only runs when RECALLNEST_CORE_SUMMARY=true and LLM is available.
|
|
546
497
|
* Returns null array when disabled (no overhead).
|
|
547
498
|
*/
|
|
548
|
-
async function generateCoreSummaries(
|
|
499
|
+
export async function generateCoreSummaries(
|
|
549
500
|
texts: string[],
|
|
550
501
|
llm?: LLMClient | null,
|
|
551
502
|
): Promise<(string | null)[]> {
|
|
@@ -715,7 +666,7 @@ function initialTier(extraction: Pick<SmartExtraction, "category" | "importance"
|
|
|
715
666
|
return "peripheral";
|
|
716
667
|
}
|
|
717
668
|
|
|
718
|
-
function buildIngestedEntry(params: {
|
|
669
|
+
export function buildIngestedEntry(params: {
|
|
719
670
|
source: string;
|
|
720
671
|
scope: string;
|
|
721
672
|
text: string;
|
|
@@ -884,6 +835,11 @@ export function parseCCTranscript(filePath: string): ConversationTurn[] {
|
|
|
884
835
|
for (const line of lines) {
|
|
885
836
|
try {
|
|
886
837
|
const obj = JSON.parse(line);
|
|
838
|
+
// 子 agent 侧链(isSidechain:true)的 user 行是主 agent 写给子 agent 的任务信封,不是人说的话。
|
|
839
|
+
// 今天 CC 把侧链分文件存在 <session>/subagents/agent-*.jsonl,而本函数的调用方只读项目目录顶层
|
|
840
|
+
// 的 .jsonl,所以这一行当前不会命中;留着是防 CC 哪天把侧链内联回主文件(2026-09-08 借鉴 ECC
|
|
841
|
+
// observe.sh 的「按结构字段认谁在说、不认 role」)。
|
|
842
|
+
if (obj.isSidechain === true) continue;
|
|
887
843
|
// 这一行里所有的图片信号。用户亲手贴的那部分在下面单独精确认,
|
|
888
844
|
// 其余的一律按补集归入 AI 产图——不枚举它是截图、读图、生图还是配图。
|
|
889
845
|
const lineImageSignals = countImageSignals(obj);
|
|
@@ -1101,6 +1057,28 @@ export function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
|
|
|
1101
1057
|
// Codex Session Parser
|
|
1102
1058
|
// ============================================================================
|
|
1103
1059
|
|
|
1060
|
+
/**
|
|
1061
|
+
* Codex multi_agent 派出的子 agent 会话是独立的 rollout 文件,首行 session_meta 带 parent_thread_id
|
|
1062
|
+
* (另有 agent_nickname / agent_role / multi_agent_version)。文件里 role=user 的 input_text 是主 agent
|
|
1063
|
+
* 派的任务,不是人说的话——2026-09-08 实测本机 5,597 个 Codex 会话文件里 2,555 个(45.6%)是子 agent 文件,
|
|
1064
|
+
* 其 user turn 占全部 user turn 的 46.7%(近 7 天 63.7%)。ingest 前整文件跳过,与 Kimi 分支只读
|
|
1065
|
+
* agents/main/wire.jsonl 同一个判据:按结构字段认「谁在说」,不认 role。只读首行,不整读文件。
|
|
1066
|
+
*/
|
|
1067
|
+
export function isCodexSubagentSessionFile(filePath: string): boolean {
|
|
1068
|
+
try {
|
|
1069
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1070
|
+
const nl = content.indexOf("\n");
|
|
1071
|
+
const first = (nl === -1 ? content : content.slice(0, nl)).trim();
|
|
1072
|
+
if (!first) return false;
|
|
1073
|
+
const obj = JSON.parse(first);
|
|
1074
|
+
if (obj?.type !== "session_meta") return false;
|
|
1075
|
+
const parent = obj.payload?.parent_thread_id;
|
|
1076
|
+
return typeof parent === "string" && parent.length > 0;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1104
1082
|
export function parseCodexSession(filePath: string): ConversationTurn[] {
|
|
1105
1083
|
const turns: ConversationTurn[] = [];
|
|
1106
1084
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -1266,6 +1244,14 @@ export async function ingestCodexSessions(
|
|
|
1266
1244
|
continue;
|
|
1267
1245
|
}
|
|
1268
1246
|
|
|
1247
|
+
// 子 agent 独立会话整文件跳过(判据与理由见 isCodexSubagentSessionFile)。标记 processed 是为了
|
|
1248
|
+
// 下一轮非 --recent 的增量不再重读它;子 agent 的结论本就该由主 agent 落盘,原文由 Deja 全量索引兜底。
|
|
1249
|
+
if (isCodexSubagentSessionFile(filePath)) {
|
|
1250
|
+
result.subagentFilesSkipped = (result.subagentFilesSkipped ?? 0) + 1;
|
|
1251
|
+
markProcessed(filePath, stat.size, 0, stat.mtimeMs);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1269
1255
|
const turns = parseCodexSession(filePath);
|
|
1270
1256
|
if (turns.length === 0) {
|
|
1271
1257
|
result.chunksSkipped++;
|
|
@@ -1300,9 +1286,6 @@ export async function ingestCodexSessions(
|
|
|
1300
1286
|
if (!options.noDedup) {
|
|
1301
1287
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
1302
1288
|
recordDedupDecision(result, decision);
|
|
1303
|
-
if (decision.secondaryDeletes?.length) {
|
|
1304
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
1305
|
-
}
|
|
1306
1289
|
if (decision.action === "skip") continue;
|
|
1307
1290
|
}
|
|
1308
1291
|
dedupedTexts.push(batch[j]);
|
|
@@ -1601,9 +1584,6 @@ export async function ingestKimiSessions(
|
|
|
1601
1584
|
if (!options.noDedup) {
|
|
1602
1585
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
1603
1586
|
recordDedupDecision(result, decision);
|
|
1604
|
-
if (decision.secondaryDeletes?.length) {
|
|
1605
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
1606
|
-
}
|
|
1607
1587
|
if (decision.action === "skip") continue;
|
|
1608
1588
|
}
|
|
1609
1589
|
dedupedTexts.push(batch[j]);
|
|
@@ -1806,9 +1786,6 @@ export async function ingestGeminiSessions(
|
|
|
1806
1786
|
if (!options.noDedup) {
|
|
1807
1787
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
1808
1788
|
recordDedupDecision(result, decision);
|
|
1809
|
-
if (decision.secondaryDeletes?.length) {
|
|
1810
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
1811
|
-
}
|
|
1812
1789
|
if (decision.action === "skip") continue;
|
|
1813
1790
|
}
|
|
1814
1791
|
dedupedTexts.push(batch[j]);
|
|
@@ -1876,7 +1853,7 @@ export async function ingestGeminiSessions(
|
|
|
1876
1853
|
* Split a markdown file by headings (## or #) into chunks.
|
|
1877
1854
|
* Each chunk includes the heading + content under it.
|
|
1878
1855
|
*/
|
|
1879
|
-
function parseMarkdown(filePath: string): Array<{ text: string; heading: string }> {
|
|
1856
|
+
export function parseMarkdown(filePath: string): Array<{ text: string; heading: string }> {
|
|
1880
1857
|
const content = readFileSync(filePath, "utf-8");
|
|
1881
1858
|
const chunks: Array<{ text: string; heading: string }> = [];
|
|
1882
1859
|
|
|
@@ -2042,9 +2019,6 @@ export async function ingestCCTranscripts(
|
|
|
2042
2019
|
if (!options.noDedup) {
|
|
2043
2020
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
2044
2021
|
recordDedupDecision(result, decision);
|
|
2045
|
-
if (decision.secondaryDeletes?.length) {
|
|
2046
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
2047
|
-
}
|
|
2048
2022
|
if (decision.action === "skip") continue;
|
|
2049
2023
|
}
|
|
2050
2024
|
|
|
@@ -2196,9 +2170,6 @@ export async function ingestMarkdownFiles(
|
|
|
2196
2170
|
if (!options.noDedup) {
|
|
2197
2171
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
2198
2172
|
recordDedupDecision(result, decision);
|
|
2199
|
-
if (decision.secondaryDeletes?.length) {
|
|
2200
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
2201
|
-
}
|
|
2202
2173
|
if (decision.action === "skip") continue;
|
|
2203
2174
|
}
|
|
2204
2175
|
dedupedTexts.push(batch[j]);
|
|
@@ -2330,9 +2301,6 @@ export async function ingestGenericText(
|
|
|
2330
2301
|
if (!options.noDedup) {
|
|
2331
2302
|
const decision = await dedupCheck(store, vector, batch[j], options.llm);
|
|
2332
2303
|
recordDedupDecision(result, decision);
|
|
2333
|
-
if (decision.secondaryDeletes?.length) {
|
|
2334
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
2335
|
-
}
|
|
2336
2304
|
if (decision.action === "skip") continue;
|
|
2337
2305
|
}
|
|
2338
2306
|
dedupedTexts.push(batch[j]);
|
|
@@ -2449,9 +2417,6 @@ export async function ingestConnectorFile(
|
|
|
2449
2417
|
if (!options.noDedup) {
|
|
2450
2418
|
const decision = await dedupCheck(store, vector, texts[j], options.llm);
|
|
2451
2419
|
recordDedupDecision(result, decision);
|
|
2452
|
-
if (decision.secondaryDeletes?.length) {
|
|
2453
|
-
await executeSecondaryDeletes(store, decision.secondaryDeletes);
|
|
2454
|
-
}
|
|
2455
2420
|
if (decision.action === "skip") continue;
|
|
2456
2421
|
}
|
|
2457
2422
|
dedupedTexts.push(texts[j]);
|