recallnest 3.0.0 → 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 +304 -0
- package/README.md +251 -258
- package/README_CN.md +226 -247
- 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 +12 -3
- 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 +459 -71
- 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-stable.ts +5 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +136 -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 +294 -121
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-extractor.ts +17 -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 +311 -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 +55 -5
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- 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 +15 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +20 -1
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +59 -12
- package/src/skill-promotion.ts +67 -1
- 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
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
package/src/cli.ts
CHANGED
|
@@ -10,19 +10,20 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Command } from "commander";
|
|
13
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
|
|
17
17
|
import { metaDir } from "./compat.js";
|
|
18
18
|
import { PACKAGE_VERSION } from "./version.js";
|
|
19
|
-
import type { RetrievalResult } from "./retriever.js";
|
|
20
19
|
import { applyRetrievalProfile, listRetrievalProfiles } from "./retrieval-profiles.js";
|
|
21
20
|
import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
22
|
-
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset,
|
|
23
|
-
import { indexAsset,
|
|
24
|
-
import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, resolveDbPath, type LocalMemoryConfig } from "./runtime-config.js";
|
|
21
|
+
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, formatDirtyBriefEmptyMessage, formatDirtyBriefReadErrors, getExportsDir, listMemoryAssets, scanDirtyBriefAssets, listPinAssets, pinSummaryLine, saveBriefAsset, writeExportArtifact } from "./memory-assets.js";
|
|
22
|
+
import { indexAsset, pinMemory } from "./asset-sync.js";
|
|
23
|
+
import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, resolveDataDir, resolveDbPath, type LocalMemoryConfig } from "./runtime-config.js";
|
|
25
24
|
import { KGStore } from "./kg-store.js";
|
|
25
|
+
import { extractTriggerCandidates, normalizeTriggerTexts } from "./trigger-store.js";
|
|
26
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
26
27
|
import { isKGModeEnabled } from "./kg-extractor.js";
|
|
27
28
|
import {
|
|
28
29
|
formatDedupReasonSummary,
|
|
@@ -35,8 +36,11 @@ import {
|
|
|
35
36
|
ingestMarkdownFiles,
|
|
36
37
|
ingestObsidianVault,
|
|
37
38
|
ingestConnectorFile,
|
|
39
|
+
parseCCTranscript,
|
|
38
40
|
} from "./ingest.js";
|
|
39
41
|
import { runDoctor, formatDoctorResults } from "./doctor.js";
|
|
42
|
+
import { archiveIngestedMinisFiles, isCompleteJsonl, settledCheck } from "./minis-archive.js";
|
|
43
|
+
import { isProcessed } from "./tracker.js";
|
|
40
44
|
import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
|
|
41
45
|
import {
|
|
42
46
|
scanMemoryPromotions,
|
|
@@ -46,10 +50,17 @@ import {
|
|
|
46
50
|
formatSynthesisPromoteResult,
|
|
47
51
|
} from "./memory-promotion.js";
|
|
48
52
|
import { runDream, formatDreamResult, formatDreamMetrics, DEFAULT_DREAM_CONFIG, classifyDreamFailure, shouldBlockDreamRun, partitionAutoDreamScopes } from "./dream-pipeline.js";
|
|
49
|
-
import { dreamBudgetMs } from "./env-config.js";
|
|
53
|
+
import { dataDir as envDataDir, dreamBudgetMs, transcriptIngest } from "./env-config.js";
|
|
54
|
+
import { planIngest } from "./ingest-plan.js";
|
|
50
55
|
import { listScopesAboveThreshold, pruneWriteCounts } from "./activity-counter.js";
|
|
51
56
|
import { isTranscriptScope } from "./memory-boundaries.js";
|
|
52
57
|
import { maybeRunGc } from "./auto-gc.js";
|
|
58
|
+
import {
|
|
59
|
+
formatReconcileSummary,
|
|
60
|
+
reconcileMemoryDocuments,
|
|
61
|
+
undoMemoryReconcile,
|
|
62
|
+
type ReconcileOutcome,
|
|
63
|
+
} from "./memory-reconcile.js";
|
|
53
64
|
import {
|
|
54
65
|
CaseMemoryInputSchema,
|
|
55
66
|
type CaseMemoryInput,
|
|
@@ -58,7 +69,9 @@ import {
|
|
|
58
69
|
WorkflowPatternInputSchema,
|
|
59
70
|
type WorkflowPatternInput,
|
|
60
71
|
} from "./memory-schema.js";
|
|
61
|
-
import { classifyLegacyScope, type MemoryEntry } from "./store.js";
|
|
72
|
+
import { classifyLegacyScope, loadLanceDB, MemoryStore, type MemoryEntry } from "./store.js";
|
|
73
|
+
import { backfillCheckpointMirror, openReadOnlyMirrorReader, purgeCheckpointMirror, type CheckpointMirrorDeps } from "./checkpoint-mirror.js";
|
|
74
|
+
import { autoRegisterBabelMemory } from "./language-hook.js";
|
|
62
75
|
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
63
76
|
import { resolveConflictCandidate } from "./conflict-engine.js";
|
|
64
77
|
import { escalateConflicts } from "./conflict-escalation.js";
|
|
@@ -146,7 +159,8 @@ function resolveSourcePath(source: string, key: string): string {
|
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
function formatIngestSummary(result: IngestResult): string {
|
|
149
|
-
|
|
162
|
+
const subagentNote = result.subagentFilesSkipped ? `, ${result.subagentFilesSkipped} subagent files skipped` : "";
|
|
163
|
+
return `${result.filesProcessed} files, ${result.chunksIngested} ingested, ${result.chunksDeduped} deduped (${formatDedupReasonSummary(result)}), ${result.errors.length} errors${subagentNote}`;
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
function maybeWarnHighCCDedupRate(result: IngestResult): void {
|
|
@@ -268,6 +282,42 @@ function parseRequiredLimitOption(value: string | undefined, field: string, min
|
|
|
268
282
|
return Math.min(max, Math.max(min, parsed));
|
|
269
283
|
}
|
|
270
284
|
|
|
285
|
+
/**
|
|
286
|
+
* 记忆对账动手前的路径核对。锁与审计日志走 envConfig.dataDir()(RECALLNEST_DATA_DIR,否则当前目录下的 data),
|
|
287
|
+
* 库走 config.dbPath(相对路径按代码所在仓库解析)——两者必须是同一个 data 目录,否则对账拿的锁不是 dream / GC
|
|
288
|
+
* 用的那把、forget 记录也读错地方;从别的工作区跑时尤其容易错位(第三轮互审 N1)。库里必须已有 memories 表,
|
|
289
|
+
* 免得路径写错时对着一个新建的空库「对账」。
|
|
290
|
+
*/
|
|
291
|
+
function checkReconcilePaths(
|
|
292
|
+
config: LocalMemoryConfig,
|
|
293
|
+
options: { requireTable?: boolean } = {},
|
|
294
|
+
): { ok: boolean; reason: string; dataDir: string; dbPath: string } {
|
|
295
|
+
const dbPath = resolveDbPath(config);
|
|
296
|
+
const dataDir = resolve(envDataDir());
|
|
297
|
+
const real = (p: string): string => {
|
|
298
|
+
try { return realpathSync(p); } catch { return p; }
|
|
299
|
+
};
|
|
300
|
+
if (real(dataDir) !== real(resolveDataDir(config))) {
|
|
301
|
+
return { ok: false, reason: `锁与审计目录 ${dataDir} 和库所在目录 ${resolveDataDir(config)} 不一致(从仓库根目录运行,或设 RECALLNEST_DATA_DIR)`, dataDir, dbPath };
|
|
302
|
+
}
|
|
303
|
+
if (options.requireTable !== false && !existsSync(join(dbPath, "memories.lance"))) {
|
|
304
|
+
return { ok: false, reason: `库目录里没有 memories 表:${dbPath}`, dataDir, dbPath };
|
|
305
|
+
}
|
|
306
|
+
return { ok: true, reason: "", dataDir, dbPath };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function reconcileToIngestResult(outcome: ReconcileOutcome): IngestResult {
|
|
310
|
+
return {
|
|
311
|
+
source: "memory",
|
|
312
|
+
filesProcessed: outcome.plan?.currentFiles ?? 0,
|
|
313
|
+
chunksIngested: outcome.applied?.inserted ?? 0,
|
|
314
|
+
chunksSkipped: 0,
|
|
315
|
+
chunksDeduped: 0,
|
|
316
|
+
dedupReasonCounts: { hard: 0, exact: 0, "llm-skip": 0, "llm-merge": 0, unique: 0 },
|
|
317
|
+
errors: outcome.applied?.insertErrors ?? [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
271
321
|
function toScopeFilter(scope?: string): string[] | undefined {
|
|
272
322
|
const trimmed = typeof scope === "string" ? scope.trim() : "";
|
|
273
323
|
return trimmed ? [trimmed] : undefined;
|
|
@@ -459,18 +509,6 @@ function collectRepeatedOption(value: string, previous: string[] = []): string[]
|
|
|
459
509
|
return [...previous, value];
|
|
460
510
|
}
|
|
461
511
|
|
|
462
|
-
function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
|
|
463
|
-
if (!entry) {
|
|
464
|
-
throw new Error("Memory entry not found.");
|
|
465
|
-
}
|
|
466
|
-
return {
|
|
467
|
-
entry,
|
|
468
|
-
score: entry.importance || 0.7,
|
|
469
|
-
sources: {
|
|
470
|
-
fused: { score: entry.importance || 0.7 },
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
512
|
|
|
475
513
|
// ─── search ──────────────────────────────────────────────────────────────────
|
|
476
514
|
|
|
@@ -554,17 +592,17 @@ program
|
|
|
554
592
|
return;
|
|
555
593
|
}
|
|
556
594
|
|
|
557
|
-
const
|
|
558
|
-
await store.update(entry.id, { importance: pinnedImportance }, scopeSelection.scopeFilter);
|
|
559
|
-
|
|
560
|
-
const asset = buildPinAsset(entryToRetrievalResult(entry), {
|
|
595
|
+
const pinned = await pinMemory(store, embedder, entry, scopeSelection.scopeFilter, {
|
|
561
596
|
title: options.title,
|
|
562
597
|
summary: options.summary,
|
|
563
598
|
query: options.query,
|
|
564
599
|
profile: options.profile,
|
|
565
600
|
});
|
|
566
|
-
|
|
567
|
-
|
|
601
|
+
if (!pinned) {
|
|
602
|
+
console.log(`Memory not found: ${memoryId}`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const { asset, path } = pinned;
|
|
568
606
|
|
|
569
607
|
console.log([
|
|
570
608
|
`Pinned : ${asset.id.slice(0, 8)}`,
|
|
@@ -657,9 +695,11 @@ program
|
|
|
657
695
|
.description("归档旧规则生成的脏 brief,并从索引删除对应 asset scope")
|
|
658
696
|
.option("--apply", "执行清理;默认只预览")
|
|
659
697
|
.action(async (options) => {
|
|
660
|
-
const
|
|
698
|
+
const scan = scanDirtyBriefAssets();
|
|
699
|
+
const dirty = scan.items;
|
|
700
|
+
const readErrors = formatDirtyBriefReadErrors(scan.readErrors);
|
|
661
701
|
if (dirty.length === 0) {
|
|
662
|
-
console.log(
|
|
702
|
+
console.log(formatDirtyBriefEmptyMessage(scan));
|
|
663
703
|
return;
|
|
664
704
|
}
|
|
665
705
|
|
|
@@ -669,6 +709,7 @@ program
|
|
|
669
709
|
console.log(`${item.id.slice(0, 8)} ${item.title} [${item.scope}] ${item.reasons.join("; ")}`);
|
|
670
710
|
}
|
|
671
711
|
console.log("\nRun with --apply to archive files and delete their indexed asset scopes.");
|
|
712
|
+
if (readErrors) console.log(`\n${readErrors}`);
|
|
672
713
|
return;
|
|
673
714
|
}
|
|
674
715
|
|
|
@@ -688,6 +729,7 @@ program
|
|
|
688
729
|
`Archived : ${archived}`,
|
|
689
730
|
`Index rows : ${deleted}`,
|
|
690
731
|
].join("\n"));
|
|
732
|
+
if (readErrors) console.log(`\n${readErrors}`);
|
|
691
733
|
});
|
|
692
734
|
|
|
693
735
|
program
|
|
@@ -1103,6 +1145,109 @@ conflictsCommand
|
|
|
1103
1145
|
console.log(formatConflictResolution(result));
|
|
1104
1146
|
});
|
|
1105
1147
|
|
|
1148
|
+
const kgFilterHelp = (cmd: string) => `
|
|
1149
|
+
过滤选项(默认全关;一个都不传时输出与不带这组选项时完全一样):
|
|
1150
|
+
${cmd === "kg-compare" ? "只过滤分组候选一侧,再拿过滤后的候选做双向对照;正则一侧不变。\n " : ""}依次过 谓词白名单 → 函数型判定 → 证据门槛,输出 funnel:每一步还剩多少组,没启用的标 skipped。
|
|
1151
|
+
JSON 另带 predicateStats:本 scope 每个谓词的 subjects / pairs / functionality / multiValuedSubjects,
|
|
1152
|
+
按 functionality 降序。
|
|
1153
|
+
functionality = 用到该谓词的不同 subject 数 ÷ 不同 (subject, 归一化 object) 对数,取值 (0, 1],
|
|
1154
|
+
越接近 1 越像「一个主体同一时间只有一个值」(located_in 这类)。有候选组的谓词恒 < 1。
|
|
1155
|
+
|
|
1156
|
+
示例:
|
|
1157
|
+
conflicts ${cmd} --scope memory:pivot --functional-min 0.8 --json
|
|
1158
|
+
conflicts ${cmd} --scope memory:pivot --functional-min 0.8 --min-second-mentions 2
|
|
1159
|
+
conflicts ${cmd} --scope memory:pivot --predicates located_in,owned_by
|
|
1160
|
+
`;
|
|
1161
|
+
|
|
1162
|
+
/** 四个过滤选项挂到命令上(kg-candidates / kg-compare 共用)。 */
|
|
1163
|
+
function withKGFilterOptions(command: Command): Command {
|
|
1164
|
+
const kgFilterHelpText = kgFilterHelp(command.name());
|
|
1165
|
+
return command
|
|
1166
|
+
.option("--predicates <list>", "谓词白名单,逗号分隔,精确匹配(谓词本身不归一化)")
|
|
1167
|
+
.option("--functional-min <ratio>", "只留 functionality ≥ ratio 的谓词上的候选组,ratio ∈ (0, 1],例如 0.8")
|
|
1168
|
+
.option("--functional-min-subjects <n>", "配合 --functional-min:谓词的 subject 少于 n 个按不满足处理(默认 3)")
|
|
1169
|
+
.option("--min-second-mentions <n>", "只留第二多取值的 mention_count ≥ n 的组(默认 1,即不过滤)")
|
|
1170
|
+
.addHelpText("after", kgFilterHelpText);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
withKGFilterOptions(
|
|
1174
|
+
conflictsCommand
|
|
1175
|
+
.command("kg-candidates")
|
|
1176
|
+
.description("只读:KG 三元组按 (subject, predicate) 分组的多值候选(影子期,候选非判决)")
|
|
1177
|
+
.requiredOption("--scope <scope>", "scope(精确匹配)")
|
|
1178
|
+
.option("-n, --limit <n>", "按排序取前 n 组", "20")
|
|
1179
|
+
.option("--sample <n>", "改为随机抽 n 组(确定性,配 --seed)")
|
|
1180
|
+
.option("--seed <seed>", "抽样种子", "1")
|
|
1181
|
+
.option("--json", "JSON 格式输出"),
|
|
1182
|
+
).action(async (options) => {
|
|
1183
|
+
const {
|
|
1184
|
+
filterKGCandidates,
|
|
1185
|
+
findKGContradictionCandidates,
|
|
1186
|
+
formatCandidateReport,
|
|
1187
|
+
hasCandidateFilter,
|
|
1188
|
+
loadScopeTriples,
|
|
1189
|
+
parseCandidateFilterArgs,
|
|
1190
|
+
sampleCandidates,
|
|
1191
|
+
} = await import("./kg-contradiction.js");
|
|
1192
|
+
const filterOpts = parseCandidateFilterArgs(options);
|
|
1193
|
+
const dbPath = resolveDbPath(loadConfig());
|
|
1194
|
+
const triples = await loadScopeTriples(dbPath, options.scope);
|
|
1195
|
+
const all = findKGContradictionCandidates(triples);
|
|
1196
|
+
const filtered = hasCandidateFilter(filterOpts) ? filterKGCandidates(all, triples, filterOpts) : undefined;
|
|
1197
|
+
const candidates = filtered ? filtered.candidates : all;
|
|
1198
|
+
const sampling = options.sample !== undefined;
|
|
1199
|
+
const n = sampling ? parseRequiredLimitOption(options.sample, "--sample", 1, 1000) : parseLimitOption(options.limit, 20, 1, 1000);
|
|
1200
|
+
const seed = Number.parseInt(String(options.seed), 10) || 1;
|
|
1201
|
+
const shown = sampling ? sampleCandidates(candidates, n, seed) : candidates.slice(0, n);
|
|
1202
|
+
const mode = sampling ? `random sample n=${n} seed=${seed}` : `top ${n}`;
|
|
1203
|
+
|
|
1204
|
+
if (options.json) {
|
|
1205
|
+
const filterFields = filtered ? { funnel: filtered.funnel, predicateStats: filtered.predicateStats } : {};
|
|
1206
|
+
console.log(JSON.stringify({ scope: options.scope, tripleCount: triples.length, candidateCount: candidates.length, mode, ...filterFields, candidates: shown }, null, 2));
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
console.log(formatCandidateReport(options.scope, triples.length, candidates.length, shown, mode, filtered?.funnel));
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
withKGFilterOptions(
|
|
1213
|
+
conflictsCommand
|
|
1214
|
+
.command("kg-compare")
|
|
1215
|
+
.description("只读:同一 scope 上现有正则矛盾检测 (memory lint) 与 KG 分组候选的双向对照")
|
|
1216
|
+
.requiredOption("--scope <scope>", "scope(精确匹配)")
|
|
1217
|
+
.option("-n, --limit <n>", "文本输出每节最多列几条;0 = 全部", "0")
|
|
1218
|
+
.option("--json", "JSON 格式输出(始终完整)"),
|
|
1219
|
+
).action(async (options) => {
|
|
1220
|
+
const {
|
|
1221
|
+
compareRegexWithCandidates,
|
|
1222
|
+
filterKGCandidates,
|
|
1223
|
+
findKGContradictionCandidates,
|
|
1224
|
+
formatComparisonReport,
|
|
1225
|
+
hasCandidateFilter,
|
|
1226
|
+
loadRegexInput,
|
|
1227
|
+
loadScopeTriples,
|
|
1228
|
+
parseCandidateFilterArgs,
|
|
1229
|
+
regexHitPairs,
|
|
1230
|
+
} = await import("./kg-contradiction.js");
|
|
1231
|
+
const filterOpts = parseCandidateFilterArgs(options);
|
|
1232
|
+
const dbPath = resolveDbPath(loadConfig());
|
|
1233
|
+
const [triples, regexInput] = await Promise.all([loadScopeTriples(dbPath, options.scope), loadRegexInput(dbPath, options.scope)]);
|
|
1234
|
+
const all = findKGContradictionCandidates(triples);
|
|
1235
|
+
// 过滤只作用在分组候选一侧;正则一侧与判断「有没有三元组」用的全部三元组都不变
|
|
1236
|
+
const filtered = hasCandidateFilter(filterOpts) ? filterKGCandidates(all, triples, filterOpts) : undefined;
|
|
1237
|
+
const candidates = filtered ? filtered.candidates : all;
|
|
1238
|
+
const pairs = regexHitPairs(regexInput.entries);
|
|
1239
|
+
const cmp = compareRegexWithCandidates(pairs, candidates, triples, new Set(regexInput.entries.map((e) => e.id)));
|
|
1240
|
+
const meta = { tripleCount: triples.length, regexInputCount: regexInput.entries.length, scanLimited: regexInput.scanLimited };
|
|
1241
|
+
|
|
1242
|
+
if (options.json) {
|
|
1243
|
+
const filterFields = filtered ? { funnel: filtered.funnel, predicateStats: filtered.predicateStats } : {};
|
|
1244
|
+
console.log(JSON.stringify({ scope: options.scope, ...meta, ...filterFields, ...cmp }, null, 2));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const limit = Number.parseInt(String(options.limit), 10);
|
|
1248
|
+
console.log(formatComparisonReport(options.scope, meta, cmp, Number.isFinite(limit) && limit > 0 ? limit : 0, filtered?.funnel));
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1106
1251
|
program
|
|
1107
1252
|
.command("seed-patterns [file]")
|
|
1108
1253
|
.description("批量写入 continuity / workflow pattern seeds")
|
|
@@ -1420,6 +1565,51 @@ program
|
|
|
1420
1565
|
return;
|
|
1421
1566
|
}
|
|
1422
1567
|
const results: any[] = [];
|
|
1568
|
+
// 记忆对账没执行 / 出错 / 护栏拦下(24 小时去抖后)→ 整条 ingest 以退出码 3 结束,调用脚本据此发专门的报警
|
|
1569
|
+
let memoryReconcileAlert = false;
|
|
1570
|
+
// 对话原文 2026-10-06 起默认不入库(RECALLNEST_TRANSCRIPT_INGEST=on 才入);这一轮跑哪些来源见 ingest-plan.ts
|
|
1571
|
+
const plan = planIngest(source, transcriptIngest());
|
|
1572
|
+
if (plan.skippedTranscriptSources.length > 0) {
|
|
1573
|
+
console.log(`\n⏸ 对话入库已关(RECALLNEST_TRANSCRIPT_INGEST 没有设为 on):${plan.skippedTranscriptSources.join(" / ")} 不切片、不嵌入、不写库`);
|
|
1574
|
+
console.log(" 原文在 Deja 里查;Minis 投递目录的文件照样挪进存档;记忆文件照常。要重开见 env-config.ts transcriptIngest");
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Minis 投递目录 → data/minis-archive(Deja 从那里读,记忆库不读它),原因见 minis-archive.ts 文件头。
|
|
1578
|
+
// 入库开着:先入库,再只挪台账里记着已入库的。入库关着:没有「已入库」可等,放稳了(至少 60 秒没被改过)、
|
|
1579
|
+
// 整个文件写完了、能解析出对话的就挪——这里要是照旧拿台账当闸,文件永远不会被标成已处理,也就永远进不了 Deja,而且不报错。
|
|
1580
|
+
const runMinis = async (ingestOpts?: { limit?: number; verbose: boolean; noDedup: boolean; llm: typeof llm | null; recentHours?: number }) => {
|
|
1581
|
+
const minisSource = config.sources.minis;
|
|
1582
|
+
if (!minisSource) return;
|
|
1583
|
+
const minisPath = resolveSourcePath(minisSource.path, "minis");
|
|
1584
|
+
if (!existsSync(minisPath)) {
|
|
1585
|
+
if (source === "minis") console.log(`⚠️ Minis 目录不存在: ${minisPath}`);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (plan.minis === "ingest-then-archive" && ingestOpts) {
|
|
1589
|
+
console.log("📱 导入 Minis 对话...");
|
|
1590
|
+
const r = await ingestCCTranscripts(store, embedder, minisPath, { ...ingestOpts, scopePrefix: "minis" });
|
|
1591
|
+
results.push(r);
|
|
1592
|
+
console.log(` ✅ Minis: ${formatIngestSummary(r)}`);
|
|
1593
|
+
} else {
|
|
1594
|
+
console.log("📱 Minis 对话:不入库,只挪存档...");
|
|
1595
|
+
}
|
|
1596
|
+
const archiveDir = resolve(metaDir(import.meta), "..", "data", "minis-archive");
|
|
1597
|
+
const moved = archiveIngestedMinisFiles(
|
|
1598
|
+
minisPath,
|
|
1599
|
+
archiveDir,
|
|
1600
|
+
plan.minis === "ingest-then-archive" ? isProcessed : settledCheck(),
|
|
1601
|
+
// 关着时多看一条「文件写完了」:解析会跳过残缺行,半截文件也解析得出对话
|
|
1602
|
+
(p) => (plan.minis === "ingest-then-archive" || isCompleteJsonl(p)) && parseCCTranscript(p).length > 0,
|
|
1603
|
+
);
|
|
1604
|
+
if (moved.archived.length > 0 || moved.errors.length > 0) {
|
|
1605
|
+
const failed = moved.errors.length > 0
|
|
1606
|
+
? `,${moved.errors.length} 个出错、原件留在原处:${moved.errors.join(";")}`
|
|
1607
|
+
: "";
|
|
1608
|
+
console.log(` 📦 Minis 存档: ${moved.archived.length} 个移入 data/minis-archive${failed}`);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
// 只挪存档用不着嵌入接口,所以排在接口预检之前:接口挂了(预检失败会直接退出)也不耽误对话进 Deja
|
|
1612
|
+
if (plan.minis === "archive-only") await runMinis();
|
|
1423
1613
|
|
|
1424
1614
|
// Pre-flight: validate embedding API before processing any files
|
|
1425
1615
|
console.log("\n🔑 验证 Embedding API...");
|
|
@@ -1449,8 +1639,8 @@ program
|
|
|
1449
1639
|
console.log(" ⚠️ LLM: 未配置,原始对话将跳过不存入(配置 config.json → llm 以启用)");
|
|
1450
1640
|
}
|
|
1451
1641
|
|
|
1452
|
-
// Drain pending queue if LLM is available
|
|
1453
|
-
if (effectiveLlm) {
|
|
1642
|
+
// Drain pending queue if LLM is available(队列里是对话切片,回填等于入库,跟着对话入库的开关走)
|
|
1643
|
+
if (effectiveLlm && plan.drainPendingQueue) {
|
|
1454
1644
|
const { drainPendingQueue } = await import("./ingest.js");
|
|
1455
1645
|
const drained = await drainPendingQueue(store, embedder, effectiveLlm);
|
|
1456
1646
|
if (drained.processed > 0) {
|
|
@@ -1469,7 +1659,7 @@ program
|
|
|
1469
1659
|
|
|
1470
1660
|
// CC Transcripts
|
|
1471
1661
|
// CC 按 cwd 编码 project 子目录,所有有 jsonl 的子目录都要 ingest(不只是 jsonl 最多的那个)
|
|
1472
|
-
if (
|
|
1662
|
+
if (plan.transcriptSources.includes("cc")) {
|
|
1473
1663
|
console.log("📝 导入 Claude Code 对话...");
|
|
1474
1664
|
const ccSource = config.sources.cc;
|
|
1475
1665
|
if (ccSource) {
|
|
@@ -1489,7 +1679,7 @@ program
|
|
|
1489
1679
|
}
|
|
1490
1680
|
|
|
1491
1681
|
// Codex Sessions
|
|
1492
|
-
if (
|
|
1682
|
+
if (plan.transcriptSources.includes("codex")) {
|
|
1493
1683
|
console.log("🤖 导入 Codex 对话...");
|
|
1494
1684
|
const r = await ingestCodexSessions(store, embedder, ingestOpts);
|
|
1495
1685
|
results.push(r);
|
|
@@ -1497,7 +1687,7 @@ program
|
|
|
1497
1687
|
}
|
|
1498
1688
|
|
|
1499
1689
|
// Kimi Code Sessions (wire.jsonl 事件流,~/.kimi-code/sessions/)
|
|
1500
|
-
if (
|
|
1690
|
+
if (plan.transcriptSources.includes("kimi")) {
|
|
1501
1691
|
console.log("🌙 导入 Kimi Code 对话...");
|
|
1502
1692
|
const r = await ingestKimiSessions(store, embedder, ingestOpts);
|
|
1503
1693
|
results.push(r);
|
|
@@ -1505,7 +1695,7 @@ program
|
|
|
1505
1695
|
}
|
|
1506
1696
|
|
|
1507
1697
|
// Gemini Sessions (JSON format under ~/.gemini/tmp/*/chats/)
|
|
1508
|
-
if (
|
|
1698
|
+
if (plan.transcriptSources.includes("gemini")) {
|
|
1509
1699
|
console.log("💎 导入 Gemini 对话...");
|
|
1510
1700
|
const r = await ingestGeminiSessions(store, embedder, {
|
|
1511
1701
|
limit,
|
|
@@ -1519,7 +1709,7 @@ program
|
|
|
1519
1709
|
}
|
|
1520
1710
|
|
|
1521
1711
|
// Claude Desktop / claude.ai web conversations (CC transcript format)
|
|
1522
|
-
if (
|
|
1712
|
+
if (plan.transcriptSources.includes("desktop")) {
|
|
1523
1713
|
const desktopSource = config.sources.desktop;
|
|
1524
1714
|
if (desktopSource) {
|
|
1525
1715
|
const desktopPath = resolve(metaDir(import.meta), "..", desktopSource.path);
|
|
@@ -1538,29 +1728,14 @@ program
|
|
|
1538
1728
|
// Minis (iPhone) —— 手机 agent 自己把对话写进这个目录,ingest 顺带扫走。
|
|
1539
1729
|
// 为什么是独立源而不是复用 desktop:scope 前缀决定了 memory-boundaries 怎么降权,
|
|
1540
1730
|
// 混在 cc: 里的话来源在库里就分辨不出来(那正是 2026-08-12 起挂着的缺口)。
|
|
1541
|
-
if (
|
|
1542
|
-
const minisSource = config.sources.minis;
|
|
1543
|
-
if (minisSource) {
|
|
1544
|
-
const minisPath = resolveSourcePath(minisSource.path, "minis");
|
|
1545
|
-
if (existsSync(minisPath)) {
|
|
1546
|
-
console.log("📱 导入 Minis 对话...");
|
|
1547
|
-
const r = await ingestCCTranscripts(store, embedder, minisPath, {
|
|
1548
|
-
...ingestOpts,
|
|
1549
|
-
scopePrefix: "minis",
|
|
1550
|
-
});
|
|
1551
|
-
results.push(r);
|
|
1552
|
-
console.log(` ✅ Minis: ${formatIngestSummary(r)}`);
|
|
1553
|
-
} else if (source === "minis") {
|
|
1554
|
-
console.log(`⚠️ Minis 目录不存在: ${minisPath}`);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1731
|
+
if (plan.minis === "ingest-then-archive") await runMinis(ingestOpts);
|
|
1558
1732
|
|
|
1559
1733
|
// Memory markdown files
|
|
1560
|
-
if (
|
|
1734
|
+
if (plan.memory) {
|
|
1561
1735
|
console.log("📚 导入记忆文件...");
|
|
1562
1736
|
const memSource = config.sources.memory;
|
|
1563
|
-
if (memSource) {
|
|
1737
|
+
if (memSource && memSource.path === "auto") {
|
|
1738
|
+
// auto 路径保持旧行为(追加式导入 + 全库判重);对账只在显式配置路径时启用(互审 K4)
|
|
1564
1739
|
const memPath = resolveSourcePath(memSource.path, "memory");
|
|
1565
1740
|
const r = await ingestMarkdownFiles(store, embedder, memPath, "memory", {
|
|
1566
1741
|
verbose,
|
|
@@ -1570,6 +1745,36 @@ program
|
|
|
1570
1745
|
});
|
|
1571
1746
|
results.push(r);
|
|
1572
1747
|
console.log(` ✅ Memory: ${formatIngestSummary(r)}`);
|
|
1748
|
+
} else if (memSource) {
|
|
1749
|
+
// 显式路径:按文件对账(src/memory-reconcile.ts)。它每轮全量比对现行文件,--recent / --limit 不适用(互审 K2)
|
|
1750
|
+
if (recentHours !== undefined || limit !== undefined) {
|
|
1751
|
+
console.log(" ℹ️ 记忆文件走对账(每轮全量比对现行文件),--recent / --limit 对它不生效");
|
|
1752
|
+
}
|
|
1753
|
+
const paths = checkReconcilePaths(config);
|
|
1754
|
+
if (!paths.ok) {
|
|
1755
|
+
console.log(` ⚠️ Memory: 对账没有执行——${paths.reason}`);
|
|
1756
|
+
memoryReconcileAlert = true;
|
|
1757
|
+
} else {
|
|
1758
|
+
try {
|
|
1759
|
+
const outcome = await reconcileMemoryDocuments({
|
|
1760
|
+
store,
|
|
1761
|
+
embedder,
|
|
1762
|
+
llm: effectiveLlm,
|
|
1763
|
+
auditLogger: createAuditLogger(),
|
|
1764
|
+
memDir: resolveSourcePath(memSource.path, "memory"),
|
|
1765
|
+
dataDir: paths.dataDir,
|
|
1766
|
+
explicitPath: true,
|
|
1767
|
+
apply: true,
|
|
1768
|
+
});
|
|
1769
|
+
results.push(reconcileToIngestResult(outcome));
|
|
1770
|
+
console.log(` ✅ Memory: ${formatReconcileSummary(outcome)}`);
|
|
1771
|
+
if (outcome.journalPath) console.log(` 对账日志: ${outcome.journalPath}`);
|
|
1772
|
+
if (outcome.alert) memoryReconcileAlert = true;
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
console.log(` ❌ Memory: 对账出错——${err instanceof Error ? err.message : String(err)}`);
|
|
1775
|
+
memoryReconcileAlert = true;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1573
1778
|
}
|
|
1574
1779
|
}
|
|
1575
1780
|
|
|
@@ -1629,6 +1834,233 @@ program
|
|
|
1629
1834
|
` 总计: ${totalChunks} chunks 已索引, ${totalDeduped} chunks 去重 (hard:${totalDedupReasons.hard}, exact:${totalDedupReasons.exact}, llm-skip:${totalDedupReasons["llm-skip"]}, llm-merge:${totalDedupReasons["llm-merge"]}), ${totalErrors} errors`,
|
|
1630
1835
|
);
|
|
1631
1836
|
console.log();
|
|
1837
|
+
if (memoryReconcileAlert) {
|
|
1838
|
+
console.log("⚠️ 记忆对账需要人看(见上面 Memory: 行),以退出码 3 结束");
|
|
1839
|
+
process.exitCode = 3;
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
// ─── checkpoint-mirror-backfill ───────────────────────────────────────────────
|
|
1844
|
+
// checkpoint 镜像(src/checkpoint-mirror.ts)的一次性回填:每场会话取最新的 rich checkpoint 写进 scope
|
|
1845
|
+
// `checkpoint`,已是这一版的跳过,可重复跑。输出只有计数,不打印 checkpoint 正文。
|
|
1846
|
+
// --db-path / --checkpoints-dir 只给演练用:指向生产库以外的副本;指向配置里的生产库会被拒绝(生产库不传这个参数)。
|
|
1847
|
+
|
|
1848
|
+
program
|
|
1849
|
+
.command("checkpoint-mirror-backfill")
|
|
1850
|
+
.description("把现有 checkpoint 按会话取最新一份镜像进记忆库(scope checkpoint;默认写库,--dry-run 只数)")
|
|
1851
|
+
.option("--dry-run", "只数会做什么,不嵌入、不写库")
|
|
1852
|
+
.option("--db-path <dir>", "演练用:写进这个 LanceDB 副本,而不是配置里的生产库")
|
|
1853
|
+
.option("--checkpoints-dir <dir>", "演练用:从这个目录读 checkpoint,而不是 data/session-checkpoints")
|
|
1854
|
+
.action(async (options: { dryRun?: boolean; dbPath?: string; checkpointsDir?: string }) => {
|
|
1855
|
+
const config = loadConfig();
|
|
1856
|
+
// The mirror's write lock lives under the data dir resolved from the working directory, the
|
|
1857
|
+
// database under the repo; both must be the ones the running MCP / API processes use, or the
|
|
1858
|
+
// backfill and a live save would hold different locks (互审 R3 N1). Run from the repo root.
|
|
1859
|
+
// A drill copy given on the command line — not config.dbPath, which only resolveDbPath reads.
|
|
1860
|
+
const drillCopy = options.dbPath;
|
|
1861
|
+
const paths = checkReconcilePaths(config, { requireTable: !drillCopy });
|
|
1862
|
+
if (!paths.ok) {
|
|
1863
|
+
console.error(`❌ ${paths.reason}`);
|
|
1864
|
+
process.exitCode = 2;
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const { store: prodStore, embedder } = createComponents(config);
|
|
1868
|
+
let dbPath = paths.dbPath;
|
|
1869
|
+
if (drillCopy) {
|
|
1870
|
+
const target = realpathSync(resolve(expandHome(drillCopy)));
|
|
1871
|
+
if (existsSync(paths.dbPath) && target === realpathSync(paths.dbPath)) {
|
|
1872
|
+
console.error("❌ --db-path 指向的就是生产库;写生产库不传这个参数");
|
|
1873
|
+
process.exitCode = 2;
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (!existsSync(join(target, "memories.lance"))) {
|
|
1877
|
+
console.error(`❌ 库目录里没有 memories 表:${target}`);
|
|
1878
|
+
process.exitCode = 2;
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
dbPath = target;
|
|
1882
|
+
}
|
|
1883
|
+
const { SessionCheckpointStore } = await import("./session-store.js");
|
|
1884
|
+
const checkpointStore = options.checkpointsDir
|
|
1885
|
+
? new SessionCheckpointStore(resolve(expandHome(options.checkpointsDir)))
|
|
1886
|
+
: new SessionCheckpointStore();
|
|
1887
|
+
const records = await checkpointStore.listRecent({ limit: Number.MAX_SAFE_INTEGER });
|
|
1888
|
+
console.log(`库: ${dbPath}`);
|
|
1889
|
+
console.log(`checkpoint 目录: ${checkpointStore.dataDir}`);
|
|
1890
|
+
let deps: CheckpointMirrorDeps;
|
|
1891
|
+
if (options.dryRun) {
|
|
1892
|
+
// Plain reads only — MemoryStore's initialization could add columns or build an index.
|
|
1893
|
+
const reader = await openReadOnlyMirrorReader(dbPath);
|
|
1894
|
+
deps = {
|
|
1895
|
+
store: { getById: (id: string) => reader.getById(id), upsertUnlessNewer: async () => { throw new Error("dry run does not write"); } },
|
|
1896
|
+
embedder,
|
|
1897
|
+
};
|
|
1898
|
+
} else {
|
|
1899
|
+
// Same tokenizer the MCP server registers at startup, so backfilled rows get the same
|
|
1900
|
+
// language / fts_text as rows the MCP tool writes (互审 R3 N3).
|
|
1901
|
+
await autoRegisterBabelMemory();
|
|
1902
|
+
deps = { store: drillCopy ? new MemoryStore({ dbPath, vectorDim: embedder.dimensions }) : prodStore, embedder };
|
|
1903
|
+
}
|
|
1904
|
+
const result = await backfillCheckpointMirror(deps, records, {
|
|
1905
|
+
dryRun: Boolean(options.dryRun),
|
|
1906
|
+
onProgress: (done, total) => {
|
|
1907
|
+
if (done % 50 === 0 || done === total) console.log(` ${done}/${total}`);
|
|
1908
|
+
},
|
|
1909
|
+
});
|
|
1910
|
+
console.log(JSON.stringify({ dryRun: Boolean(options.dryRun), ...result }, null, 2));
|
|
1911
|
+
if ((result.statuses.failed ?? 0) > 0) process.exitCode = 1;
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// ─── checkpoint-mirror-purge ─────────────────────────────────────────────────
|
|
1915
|
+
// 撤回 checkpoint 镜像:建 data/checkpoint-mirror.off 停掉所有进程的镜像写入(每次写都读它),
|
|
1916
|
+
// 再持镜像写锁按 scope 精确等于 checkpoint 删除。不带 --yes 只数不删。恢复镜像:删掉开关文件。
|
|
1917
|
+
|
|
1918
|
+
program
|
|
1919
|
+
.command("checkpoint-mirror-purge")
|
|
1920
|
+
.description("撤回 checkpoint 镜像:停写并删除 scope 恰为 checkpoint 的全部行(不带 --yes 只数)")
|
|
1921
|
+
.option("--yes", "确认执行")
|
|
1922
|
+
.action(async (options: { yes?: boolean }) => {
|
|
1923
|
+
const config = loadConfig();
|
|
1924
|
+
const paths = checkReconcilePaths(config);
|
|
1925
|
+
if (!paths.ok) {
|
|
1926
|
+
console.error(`❌ ${paths.reason}`);
|
|
1927
|
+
process.exitCode = 2;
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
console.log(`库: ${paths.dbPath}`);
|
|
1931
|
+
if (!options.yes) {
|
|
1932
|
+
const table = await (await (await loadLanceDB()).connect(paths.dbPath)).openTable("memories");
|
|
1933
|
+
console.log(`scope = 'checkpoint' 的行数: ${await table.countRows("scope = 'checkpoint'")}(加 --yes 才会停写并删除)`);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const result = await purgeCheckpointMirror(paths.dbPath);
|
|
1937
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1938
|
+
console.log("开关文件 data/checkpoint-mirror.off 已建,镜像写入已停;要恢复,删掉这个文件。几分钟后再跑一次本命令(不带 --yes)确认仍是 0。");
|
|
1939
|
+
if (result.rowsAfter !== 0) process.exitCode = 1;
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
// ─── reconcile-memory ────────────────────────────────────────────────────────
|
|
1943
|
+
// 记忆文件对账的手动入口:默认只打印计划不写库;--apply 执行;--undo 按对账日志逐条撤销。
|
|
1944
|
+
// 定时导入(ingest)在显式配置 sources.memory.path 时自动做同一件事,这里是给首轮存量清理、
|
|
1945
|
+
// 护栏拦下后人工放行、以及出问题时撤销用的。输出只有数量、文件名和 id 前 8 位,不打印切片正文。
|
|
1946
|
+
|
|
1947
|
+
program
|
|
1948
|
+
.command("reconcile-memory")
|
|
1949
|
+
.description("记忆文件对账:让 memory 源的文档切片与现行记忆文件一一对应(默认只看计划,不写库)")
|
|
1950
|
+
.option("--apply", "执行:插入 → 恢复 → 下架")
|
|
1951
|
+
.option("--max-retire <n>", "本次下架上限(默认 max(300, 活跃文档切片数的 25%),也可用 RECALLNEST_MEMORY_RECONCILE_MAX_RETIRE)")
|
|
1952
|
+
.option("--undo <journal>", "按对账日志(data/reconcile-journals/*.jsonl)逐条撤销")
|
|
1953
|
+
.option("--json", "以 JSON 输出计划与结果")
|
|
1954
|
+
.action(async (options) => {
|
|
1955
|
+
const config = loadConfig();
|
|
1956
|
+
const paths = checkReconcilePaths(config);
|
|
1957
|
+
console.log(`库: ${paths.dbPath}`);
|
|
1958
|
+
console.log(`数据目录(锁 / 审计 / 对账日志): ${paths.dataDir}`);
|
|
1959
|
+
if (!paths.ok) {
|
|
1960
|
+
console.error(`❌ ${paths.reason}`);
|
|
1961
|
+
process.exitCode = 2;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const { store, embedder, llm } = createComponents(config);
|
|
1965
|
+
|
|
1966
|
+
if (options.undo) {
|
|
1967
|
+
const journalPath = resolve(String(options.undo));
|
|
1968
|
+
let r: Awaited<ReturnType<typeof undoMemoryReconcile>>;
|
|
1969
|
+
try {
|
|
1970
|
+
r = await undoMemoryReconcile({ store, auditLogger: createAuditLogger() }, journalPath, { auditPath: join(paths.dataDir, "audit.jsonl") });
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
|
|
1973
|
+
process.exitCode = 2;
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const ids8 = (ids: string[]) => `${ids.slice(0, 20).map((id) => id.slice(0, 8)).join(" ")}${ids.length > 20 ? ` …(共 ${ids.length})` : ""}`;
|
|
1977
|
+
console.log(`撤销完成:写回 ${r.restored} 条,本轮插入的 ${r.insertsRetired} 条改为下架,期间被改过而跳过 ${r.skippedConflict.length} 条,合并链依赖未满足而保留 ${r.skippedDependency.length} 条,文字已被 forget 而保留非活跃 ${r.skippedForgotten.length} 条`);
|
|
1978
|
+
if (r.skippedConflict.length > 0) console.log(` 被改过而跳过的 id(前 8 位): ${ids8(r.skippedConflict)}`);
|
|
1979
|
+
if (r.skippedDependency.length > 0) console.log(` 保留的合并链成员 id(它原来指向的那一行已不活跃、或本次撤销会把它撤掉,撤了会断链): ${ids8(r.skippedDependency)}`);
|
|
1980
|
+
if (r.skippedForgotten.length > 0) console.log(` 文字已被 forget、没撤回活跃的 id: ${ids8(r.skippedForgotten)}`);
|
|
1981
|
+
if (r.unparsedLines > 0) console.log(` ⚠️ 日志里有 ${r.unparsedLines} 行解析不了(多半是写到一半的截断尾行),已跳过`);
|
|
1982
|
+
console.log(`撤销日志: ${r.undoJournalPath}`);
|
|
1983
|
+
console.log("⚠️ 撤销后把 sources.memory.path 改回 auto 或暂停导入,否则下一轮对账会按现行文件重新对一遍");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const memSource = config.sources.memory;
|
|
1988
|
+
if (!memSource || memSource.path === "auto") {
|
|
1989
|
+
console.error("❌ 需要在 config.json 里显式配置 sources.memory.path(当前为 auto 或未配置)");
|
|
1990
|
+
process.exitCode = 2;
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const memDir = resolveSourcePath(memSource.path, "memory");
|
|
1994
|
+
console.log(`记忆目录: ${memDir}`);
|
|
1995
|
+
const maxRetire = options.maxRetire !== undefined
|
|
1996
|
+
? parseRequiredLimitOption(options.maxRetire, "--max-retire", 0, Number.MAX_SAFE_INTEGER)
|
|
1997
|
+
: undefined;
|
|
1998
|
+
|
|
1999
|
+
const outcome = await reconcileMemoryDocuments({
|
|
2000
|
+
store,
|
|
2001
|
+
embedder,
|
|
2002
|
+
llm,
|
|
2003
|
+
auditLogger: createAuditLogger(),
|
|
2004
|
+
memDir,
|
|
2005
|
+
dataDir: paths.dataDir,
|
|
2006
|
+
explicitPath: true,
|
|
2007
|
+
apply: Boolean(options.apply),
|
|
2008
|
+
maxRetire,
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
const plan = outcome.plan;
|
|
2012
|
+
const countBy = <T,>(items: T[], key: (item: T) => string): Record<string, number> => {
|
|
2013
|
+
const out: Record<string, number> = {};
|
|
2014
|
+
for (const item of items) out[key(item)] = (out[key(item)] ?? 0) + 1;
|
|
2015
|
+
return out;
|
|
2016
|
+
};
|
|
2017
|
+
const topFiles = (items: Array<{ file: string | null }>): Array<[string, number]> =>
|
|
2018
|
+
Object.entries(countBy(items, (i) => i.file ?? "(无文件名)")).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
2019
|
+
|
|
2020
|
+
if (options.json) {
|
|
2021
|
+
console.log(JSON.stringify({
|
|
2022
|
+
ran: outcome.ran,
|
|
2023
|
+
skippedReason: outcome.skippedReason ?? null,
|
|
2024
|
+
mode: options.apply ? "apply" : "dry-run",
|
|
2025
|
+
maxRetire: outcome.maxRetire,
|
|
2026
|
+
guard: outcome.guard,
|
|
2027
|
+
alert: outcome.alert,
|
|
2028
|
+
journalPath: outcome.journalPath,
|
|
2029
|
+
plan: plan && {
|
|
2030
|
+
currentFiles: plan.currentFiles,
|
|
2031
|
+
currentChunks: plan.currentChunks,
|
|
2032
|
+
currentDistinct: plan.currentDistinct,
|
|
2033
|
+
existingRows: plan.existingRows,
|
|
2034
|
+
activeDocRows: plan.activeDocRows,
|
|
2035
|
+
filesWithActiveDocRows: plan.filesWithActiveDocRows,
|
|
2036
|
+
keep: plan.keep,
|
|
2037
|
+
insert: plan.insert.length,
|
|
2038
|
+
insertAltId: plan.insertAltId,
|
|
2039
|
+
insertByFile: topFiles(plan.insert.map((i) => ({ file: i.chunk.file }))),
|
|
2040
|
+
retire: plan.retire.length,
|
|
2041
|
+
retireByReason: countBy(plan.retire, (r) => r.reason),
|
|
2042
|
+
retireByFile: topFiles(plan.retire),
|
|
2043
|
+
reactivate: countBy(plan.reactivate, (r) => `${r.kind}${r.kind === "broken-consolidation" && r.targetRetiredThisRun ? "(目标本轮下架)" : ""}`),
|
|
2044
|
+
representedByConsolidation: plan.representedByConsolidation,
|
|
2045
|
+
exceptionForgotten: plan.exceptionForgotten,
|
|
2046
|
+
exceptionOtherInactive: plan.exceptionOtherInactive,
|
|
2047
|
+
exceptionOtherInactiveIds: plan.exceptionOtherInactiveIds.map((id) => id.slice(0, 8)),
|
|
2048
|
+
},
|
|
2049
|
+
applied: outcome.applied,
|
|
2050
|
+
}, null, 2));
|
|
2051
|
+
} else {
|
|
2052
|
+
console.log(`\n${options.apply ? "执行结果" : "计划(未写库,加 --apply 执行)"}: ${formatReconcileSummary(outcome)}`);
|
|
2053
|
+
if (plan) {
|
|
2054
|
+
console.log(` 库里 scope "memory" 共 ${plan.existingRows} 行,其中活跃文档切片 ${plan.activeDocRows} 条、涉及 ${plan.filesWithActiveDocRows} 个文件;单轮下架上限 ${outcome.maxRetire}`);
|
|
2055
|
+
const byReason = countBy(plan.retire, (r) => r.reason);
|
|
2056
|
+
if (plan.retire.length > 0) console.log(` 下架按原因: ${Object.entries(byReason).map(([k, v]) => `${k} ${v}`).join(",")}`);
|
|
2057
|
+
if (plan.retire.length > 0) console.log(` 下架最多的文件: ${topFiles(plan.retire).map(([f, n]) => `${f} ${n}`).join(",")}`);
|
|
2058
|
+
if (plan.insert.length > 0) console.log(` 插入最多的文件: ${topFiles(plan.insert.map((i) => ({ file: i.chunk.file }))).map(([f, n]) => `${f} ${n}`).join(",")}`);
|
|
2059
|
+
if (plan.exceptionOtherInactive > 0) console.log(` 例外②(同文只剩被别的机制下架的行,未推翻)id 前 8 位: ${plan.exceptionOtherInactiveIds.slice(0, 20).map((id) => id.slice(0, 8)).join(" ")}`);
|
|
2060
|
+
}
|
|
2061
|
+
if (outcome.journalPath) console.log(` 对账日志(撤销用): ${outcome.journalPath}`);
|
|
2062
|
+
}
|
|
2063
|
+
if (outcome.alert) process.exitCode = 3;
|
|
1632
2064
|
});
|
|
1633
2065
|
|
|
1634
2066
|
// ─── dream ───────────────────────────────────────────────────────────────────
|
|
@@ -2057,6 +2489,94 @@ program
|
|
|
2057
2489
|
|
|
2058
2490
|
// ─── doctor ──────────────────────────────────────────────────────────────────
|
|
2059
2491
|
|
|
2492
|
+
program
|
|
2493
|
+
.command("triggers-backfill")
|
|
2494
|
+
.description("T-Mem triggers 回填:从正文抠「她以后会怎么问」句写入 memory_triggers 侧表(默认 dry-run,零 LLM)")
|
|
2495
|
+
.option("-s, --scope <scope>", "限定 scope", "memory:pivot")
|
|
2496
|
+
.option("-n, --limit <n>", "最多扫描条数", "5000")
|
|
2497
|
+
.option("--apply", "真写入(默认只预览)")
|
|
2498
|
+
.option("--include-batch", "连 tags 含 pivot-apply 的批量提炼条目一起回填(默认跳过:09-21 校准其走样率约七成,先别给它们加联想入口)")
|
|
2499
|
+
.option("--rebuild", "不看正文,只按 metadata.triggers 重建侧表(迁移 / 回滚后恢复用)")
|
|
2500
|
+
.option("--samples <n>", "预览时打印几条样例", "12")
|
|
2501
|
+
.action(async (options) => {
|
|
2502
|
+
const config = loadConfig();
|
|
2503
|
+
const { store, embedder, triggerStore } = createComponents(config);
|
|
2504
|
+
const scopeFilter = toScopeFilter(options.scope);
|
|
2505
|
+
const limit = parseLimitOption(options.limit, 5000, 1, 100000);
|
|
2506
|
+
const samples = parseLimitOption(options.samples, 12, 0, 200);
|
|
2507
|
+
const entries = await store.list(scopeFilter, undefined, limit, 0);
|
|
2508
|
+
const already = options.rebuild ? new Set<string>() : await triggerStore.listMemoryIds(scopeFilter);
|
|
2509
|
+
const counts = { scanned: 0, skippedBatch: 0, skippedInactive: 0, skippedHas: 0, noLine: 0, planned: 0, written: 0, rows: 0 };
|
|
2510
|
+
const preview: Array<{ id: string; triggers: string[] }> = [];
|
|
2511
|
+
for (const entry of entries) {
|
|
2512
|
+
counts.scanned += 1;
|
|
2513
|
+
if (!isActiveMemory(entry.metadata)) { counts.skippedInactive += 1; continue; }
|
|
2514
|
+
let meta: Record<string, unknown> = {};
|
|
2515
|
+
try { meta = JSON.parse(entry.metadata || "{}"); } catch { meta = {}; }
|
|
2516
|
+
const tags = Array.isArray(meta.tags) ? (meta.tags as unknown[]).map(String) : [];
|
|
2517
|
+
let texts: string[];
|
|
2518
|
+
if (options.rebuild) {
|
|
2519
|
+
texts = normalizeTriggerTexts(Array.isArray(meta.triggers) ? (meta.triggers as unknown[]).map(String) : []);
|
|
2520
|
+
if (texts.length === 0) { counts.noLine += 1; continue; }
|
|
2521
|
+
} else {
|
|
2522
|
+
if (!options.includeBatch && tags.includes("pivot-apply")) { counts.skippedBatch += 1; continue; }
|
|
2523
|
+
const hasMeta = Array.isArray(meta.triggers) && (meta.triggers as unknown[]).length > 0;
|
|
2524
|
+
if (already.has(entry.id) || hasMeta) { counts.skippedHas += 1; continue; }
|
|
2525
|
+
texts = extractTriggerCandidates(entry.text);
|
|
2526
|
+
if (texts.length === 0) { counts.noLine += 1; continue; }
|
|
2527
|
+
}
|
|
2528
|
+
counts.planned += 1;
|
|
2529
|
+
if (preview.length < samples) preview.push({ id: entry.id.slice(0, 8), triggers: texts });
|
|
2530
|
+
if (!options.apply) continue;
|
|
2531
|
+
const n = await triggerStore.upsertForMemory(entry.id, entry.scope, texts, (t) => embedder.embedBatchQuery(t));
|
|
2532
|
+
counts.rows += n;
|
|
2533
|
+
counts.written += 1;
|
|
2534
|
+
if (!options.rebuild) {
|
|
2535
|
+
meta.triggers = texts;
|
|
2536
|
+
await store.update(entry.id, { metadata: JSON.stringify(meta) });
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
const mode = options.apply ? "APPLY" : "DRY-RUN";
|
|
2540
|
+
console.log(`[triggers-backfill ${mode}] scope=${options.scope} scanned=${counts.scanned} planned=${counts.planned}` +
|
|
2541
|
+
` skipped(batch=${counts.skippedBatch} inactive=${counts.skippedInactive} already=${counts.skippedHas}) noLine=${counts.noLine}` +
|
|
2542
|
+
(options.apply ? ` written=${counts.written} rows=${counts.rows}` : ""));
|
|
2543
|
+
for (const p of preview) console.log(` ${p.id} ${p.triggers.map((t) => `「${t}」`).join(" ")}`);
|
|
2544
|
+
if (options.apply) {
|
|
2545
|
+
const stat = await triggerStore.stats(scopeFilter);
|
|
2546
|
+
console.log(`侧表现状:${stat.rows} 条 trigger / ${stat.memories} 个宿主(scope=${options.scope})`);
|
|
2547
|
+
} else {
|
|
2548
|
+
console.log("(预览。加 --apply 真写入)");
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
program
|
|
2553
|
+
.command("triggers-calibrate")
|
|
2554
|
+
.description("T-Mem trigger 硬闸校准:对 canary 用例逐条打印 query 对侧表 trigger 的最高余弦(不看 gate),用来定 RECALLNEST_TRIGGER_GATE")
|
|
2555
|
+
.option("--cases <path>", "canary 文件", "eval/cases-canary.json")
|
|
2556
|
+
.option("-s, --scope <scope>", "限定 scope(缺省用各 case 自己的 scope)")
|
|
2557
|
+
.option("-k, --top <n>", "每条打印几个命中", "4")
|
|
2558
|
+
.action(async (options) => {
|
|
2559
|
+
const config = loadConfig();
|
|
2560
|
+
const { embedder, triggerStore } = createComponents(config);
|
|
2561
|
+
const { readFileSync } = require("node:fs");
|
|
2562
|
+
const { resolve: resolvePath } = require("node:path");
|
|
2563
|
+
const raw = JSON.parse(readFileSync(resolvePath(options.cases), "utf8"));
|
|
2564
|
+
const cases: Array<{ name: string; query: string; scope?: string; targets?: string[] }> = Array.isArray(raw) ? raw : raw.cases;
|
|
2565
|
+
const top = parseLimitOption(options.top, 4, 1, 20);
|
|
2566
|
+
const stat = await triggerStore.stats();
|
|
2567
|
+
console.log(`侧表:${stat.rows} 条 trigger / ${stat.memories} 个宿主`);
|
|
2568
|
+
for (const c of cases) {
|
|
2569
|
+
const scope = options.scope || c.scope;
|
|
2570
|
+
const qv = await embedder.embedQuery(c.query);
|
|
2571
|
+
const hits = await triggerStore.search(qv, top, scope ? [scope] : undefined, { minCosine: 0, softCosine: 0, minOverlap: 0, minQueryTokens: 0, queryText: c.query });
|
|
2572
|
+
const targets = c.targets || [];
|
|
2573
|
+
const line = hits
|
|
2574
|
+
.map((h) => `${h.memoryId.slice(0, 8)}${targets.some((t) => h.memoryId.startsWith(t)) ? "✓" : " "} cos=${h.cosine.toFixed(3)} ov=${(h.overlap ?? 0).toFixed(2)}「${h.text.slice(0, 28)}」`)
|
|
2575
|
+
.join(" | ");
|
|
2576
|
+
console.log(`${c.name}\n ${line || "(no trigger rows in scope)"}`);
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2060
2580
|
program
|
|
2061
2581
|
.command("memory-utility")
|
|
2062
2582
|
.description("价值回溯:查一条 memory 参与过的任务成功率与 utility(不传 id 时按 scope 批量)")
|