recallnest 2.6.0 → 3.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 +191 -0
- package/README.md +131 -14
- package/README_CN.md +121 -17
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/__tests__/access-tracker-novelty.test.ts +22 -2
- package/src/__tests__/activity-counter.test.ts +38 -38
- package/src/__tests__/age-label.test.ts +33 -0
- package/src/__tests__/cluster-consolidation.test.ts +313 -22
- package/src/__tests__/consolidation-engine.test.ts +90 -0
- package/src/__tests__/contracts.test.ts +2 -2
- package/src/__tests__/data-checkup.test.ts +31 -3
- package/src/__tests__/dream-pipeline.test.ts +41 -1
- package/src/__tests__/dream-scope-isolation.test.ts +386 -0
- package/src/__tests__/dream-write-batching.test.ts +211 -0
- package/src/__tests__/env-config-invariant.test.ts +80 -0
- package/src/__tests__/env-config.test.ts +60 -0
- package/src/__tests__/ingest-scope-prefix.test.ts +69 -0
- package/src/__tests__/maintenance-locks.test.ts +8 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +17 -0
- package/src/__tests__/memory-output.test.ts +9 -0
- package/src/__tests__/memory-utility.test.ts +256 -0
- package/src/__tests__/openai-contract.test.ts +513 -0
- package/src/__tests__/promotion-distinct-sources.test.ts +162 -0
- package/src/__tests__/retrieve-audit-revision.test.ts +255 -0
- package/src/__tests__/source-heartbeat.test.ts +6 -2
- package/src/__tests__/store-patch-metadata.test.ts +30 -0
- package/src/__tests__/synthesis-contract.test.ts +252 -0
- package/src/__tests__/synthesis-promotion.test.ts +369 -0
- package/src/__tests__/trusted-memory.test.ts +28 -0
- package/src/__tests__/utility-weight.test.ts +57 -0
- package/src/__tests__/verbatim-self-recall.test.ts +279 -0
- package/src/__tests__/version-manager.test.ts +35 -0
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +20 -5
- package/src/env-config.ts +53 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +23 -5
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +12 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/scope-policy.ts +22 -1
- package/src/session-output.ts +4 -3
- package/src/skill-promotion.ts +152 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
package/src/auto-gc.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
parseEvolution,
|
|
15
15
|
computeDecayScore,
|
|
16
16
|
computeUsageAdjustedDecayScore,
|
|
17
|
-
|
|
17
|
+
patchEvolutionOnMeta,
|
|
18
18
|
isActiveMemory,
|
|
19
19
|
} from "./memory-evolution.js";
|
|
20
20
|
import { loadRetentionPolicy, shouldArchiveByPolicy } from "./retention-policy.js";
|
|
@@ -23,6 +23,7 @@ import type { AuditLogger } from "./audit-log.js";
|
|
|
23
23
|
import * as envConfig from "./env-config.js";
|
|
24
24
|
import { isUsageSignalActive } from "./usage-tracker.js";
|
|
25
25
|
import { acquireLock, releaseLock, getLastDistillTime, stampLock, lockPathForKey } from "./distill-lock.js";
|
|
26
|
+
import { logInfo } from "./stderr-log.js";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Config
|
|
@@ -54,6 +55,7 @@ export const DEFAULT_AUTO_GC_CONFIG: AutoGcConfig = {
|
|
|
54
55
|
// ---------------------------------------------------------------------------
|
|
55
56
|
|
|
56
57
|
let lastGcTimestamp = 0;
|
|
58
|
+
let lastVersionGroupRepairTimestamp = 0;
|
|
57
59
|
|
|
58
60
|
// ---------------------------------------------------------------------------
|
|
59
61
|
// Core
|
|
@@ -64,6 +66,7 @@ export interface GcResult {
|
|
|
64
66
|
reason?: string;
|
|
65
67
|
archivedCount: number;
|
|
66
68
|
totalChecked: number;
|
|
69
|
+
dissolvedVersionGroups: number;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -85,38 +88,70 @@ export async function maybeRunGc(
|
|
|
85
88
|
const stats = await store.stats();
|
|
86
89
|
const totalMemories = stats.totalCount ?? 0;
|
|
87
90
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
// Singleton version-group repair has its own persistent throttle. It must not be
|
|
92
|
+
// hidden behind minMemoryCount: a small store can lose one member of a pair too.
|
|
93
|
+
// Reuse the configured maintenance interval, but keep a separate timestamp so a
|
|
94
|
+
// repair-only pass does not postpone a later archive pass (or vice versa).
|
|
95
|
+
const repairStampPath = lockPathForKey("version-group-repair-last");
|
|
96
|
+
const lastRepair = Math.max(
|
|
97
|
+
lastVersionGroupRepairTimestamp,
|
|
98
|
+
getLastDistillTime({ lockPath: repairStampPath }),
|
|
99
|
+
);
|
|
100
|
+
const repairDue = totalMemories > 0
|
|
101
|
+
&& (Date.now() - lastRepair) / 3_600_000 >= config.minHoursSinceLastGc;
|
|
92
102
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// file's mtime. The old module-level var alone let each of the 11 mcp-server
|
|
96
|
-
// processes run gc on its own independent 24h clock.
|
|
103
|
+
// Archive GC retains its existing count and time gates. P0-1: the time throttle
|
|
104
|
+
// is cross-process, using the shared gc-last marker in addition to process state.
|
|
97
105
|
const gcStampPath = lockPathForKey("gc-last");
|
|
98
106
|
const lastGc = Math.max(lastGcTimestamp, getLastDistillTime({ lockPath: gcStampPath }));
|
|
99
107
|
const hoursSinceLastGc = (Date.now() - lastGc) / 3_600_000;
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
const archiveSkipReason = totalMemories < config.minMemoryCount
|
|
109
|
+
? "below_memory_threshold"
|
|
110
|
+
: hoursSinceLastGc < config.minHoursSinceLastGc
|
|
111
|
+
? "too_soon"
|
|
112
|
+
: undefined;
|
|
113
|
+
|
|
114
|
+
// Nothing is due: preserve the old fast no-op path without taking a run lock.
|
|
115
|
+
if (!repairDue && archiveSkipReason) {
|
|
116
|
+
return { triggered: false, reason: archiveSkipReason, archivedCount: 0, totalChecked: 0, dissolvedVersionGroups: 0 };
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
// Concurrency guard: only one process scans the full corpus at a time. A process that
|
|
105
120
|
// passed the throttle in the same instant loses the O_EXCL race here and skips.
|
|
106
121
|
const gcRunLock = { lockPath: lockPathForKey("gc-run"), expireMs: 30 * 60_000 };
|
|
107
122
|
if (!acquireLock(gcRunLock)) {
|
|
108
|
-
return { triggered: false, reason: "locked_by_another_process", archivedCount: 0, totalChecked: 0 };
|
|
123
|
+
return { triggered: false, reason: "locked_by_another_process", archivedCount: 0, totalChecked: 0, dissolvedVersionGroups: 0 };
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
// All conditions met — run GC
|
|
112
|
-
lastGcTimestamp = Date.now(); // in-process immediate throttle
|
|
113
|
-
|
|
114
126
|
try {
|
|
127
|
+
let dissolvedVersionGroups = 0;
|
|
128
|
+
if (repairDue) {
|
|
129
|
+
dissolvedVersionGroups = await store.repairSingletonVersionGroups();
|
|
130
|
+
lastVersionGroupRepairTimestamp = Date.now();
|
|
131
|
+
stampLock({ lockPath: repairStampPath });
|
|
132
|
+
if (dissolvedVersionGroups > 0) {
|
|
133
|
+
logInfo(`[INFO] Auto-GC dissolved ${dissolvedVersionGroups} singleton version group(s)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A repair-only pass is still useful maintenance, but `triggered` continues to
|
|
138
|
+
// mean that the archive scan ran so existing callers keep their old semantics.
|
|
139
|
+
if (archiveSkipReason) {
|
|
140
|
+
return {
|
|
141
|
+
triggered: false,
|
|
142
|
+
reason: archiveSkipReason,
|
|
143
|
+
archivedCount: 0,
|
|
144
|
+
totalChecked: 0,
|
|
145
|
+
dissolvedVersionGroups,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
lastGcTimestamp = Date.now(); // in-process immediate throttle
|
|
115
150
|
const { archivedCount, totalChecked } = await runGcScan(store, config, retentionConfigDir, auditLogger);
|
|
116
151
|
// Stamp completion into the shared throttle marker (create-or-touch → mtime = now)
|
|
117
152
|
// so the other processes observe "last run" via getLastDistillTime.
|
|
118
153
|
stampLock({ lockPath: gcStampPath });
|
|
119
|
-
return { triggered: true, archivedCount, totalChecked };
|
|
154
|
+
return { triggered: true, archivedCount, totalChecked, dissolvedVersionGroups };
|
|
120
155
|
} finally {
|
|
121
156
|
releaseLock(gcRunLock);
|
|
122
157
|
}
|
|
@@ -169,13 +204,47 @@ async function runGcScan(
|
|
|
169
204
|
|
|
170
205
|
// Pass 2: archive sweep.
|
|
171
206
|
const useUsageAdjustedDecay = envConfig.usageDecay() && isUsageSignalActive();
|
|
207
|
+
|
|
208
|
+
// 2026-08-14 批量化(dream 写爆库根治):候选先收集,页末统一一次 patchMetadataBatch
|
|
209
|
+
// —— 原逐条 update 每行一个 LanceDB commit(每天最多 100 个 manifest),现在页数级。
|
|
210
|
+
// activeCounts 仍在"选中"时即刻预递减,保持旧路径"逐行归档、后续行的 retention 判断
|
|
211
|
+
// 看到已减的 activeCount"语义(互审 C6)。selected 与 archivedCount 分开:前者控
|
|
212
|
+
// maxArchivePerRun 上限(选中即占额度),后者按批量写的实际写入数累计。
|
|
213
|
+
let selected = 0;
|
|
214
|
+
let pendingArchive: Array<{ id: string; ageDays: number }> = [];
|
|
215
|
+
const flushArchiveBatch = async (): Promise<void> => {
|
|
216
|
+
if (pendingArchive.length === 0) return;
|
|
217
|
+
const batch = pendingArchive;
|
|
218
|
+
pendingArchive = [];
|
|
219
|
+
const written = await store.patchMetadataBatch(
|
|
220
|
+
batch.map(({ id }) => ({
|
|
221
|
+
id,
|
|
222
|
+
patchFn: (meta: Record<string, unknown>) => patchEvolutionOnMeta(meta, { status: "archived" }),
|
|
223
|
+
})),
|
|
224
|
+
);
|
|
225
|
+
archivedCount += written;
|
|
226
|
+
// F-1: Audit log — record archive operations (silent on failure)。按提交批记录:
|
|
227
|
+
// 批内个别行被并发删除时 audit 会多记一条,best-effort 语义可接受(旧路径里
|
|
228
|
+
// update 失败是直接炸掉整个 GC,连 audit 都到不了)。
|
|
229
|
+
for (const { id, ageDays } of batch) {
|
|
230
|
+
try {
|
|
231
|
+
auditLogger?.log({
|
|
232
|
+
operation: "archive",
|
|
233
|
+
memoryId: id,
|
|
234
|
+
actor: "system",
|
|
235
|
+
details: `auto-gc: age=${Math.floor(ageDays)}d`,
|
|
236
|
+
});
|
|
237
|
+
} catch { /* Audit must never block GC */ }
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
172
241
|
outer:
|
|
173
242
|
for (let offset = 0; ; offset += GC_PAGE_SIZE) {
|
|
174
243
|
const page = await store.listPage({ limit: GC_PAGE_SIZE, offset });
|
|
175
244
|
totalChecked += page.length;
|
|
176
245
|
|
|
177
246
|
for (const entry of page) {
|
|
178
|
-
if (
|
|
247
|
+
if (selected >= config.maxArchivePerRun) break outer;
|
|
179
248
|
|
|
180
249
|
// Only archive active memories
|
|
181
250
|
if (!isActiveMemory(entry.metadata)) continue;
|
|
@@ -215,19 +284,10 @@ async function runGcScan(
|
|
|
215
284
|
}
|
|
216
285
|
|
|
217
286
|
if (shouldArchive) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
try {
|
|
223
|
-
auditLogger?.log({
|
|
224
|
-
operation: "archive",
|
|
225
|
-
memoryId: entry.id,
|
|
226
|
-
actor: "system",
|
|
227
|
-
details: `auto-gc: age=${Math.floor(ageDays)}d`,
|
|
228
|
-
});
|
|
229
|
-
} catch { /* Audit must never block GC */ }
|
|
230
|
-
// Decrement active count for the scope (memory is no longer active)
|
|
287
|
+
pendingArchive.push({ id: entry.id, ageDays });
|
|
288
|
+
selected++;
|
|
289
|
+
// Decrement active count for the scope(预递减:选中即视为不再 active,
|
|
290
|
+
// 同页后续行的 retention 判断与旧逐行路径看到同样的计数)
|
|
231
291
|
const scope = entry.scope ?? "";
|
|
232
292
|
const prev = activeCounts.get(scope) ?? 0;
|
|
233
293
|
if (prev > 0) {
|
|
@@ -236,9 +296,14 @@ async function runGcScan(
|
|
|
236
296
|
}
|
|
237
297
|
}
|
|
238
298
|
|
|
299
|
+
// 页末提交本页候选(含 break outer 前最后一页扫过的部分)
|
|
300
|
+
await flushArchiveBatch();
|
|
239
301
|
if (page.length < GC_PAGE_SIZE) break;
|
|
240
302
|
}
|
|
241
303
|
|
|
304
|
+
// break outer 跳出时 pendingArchive 可能还有未提交的候选 —— 上限命中不弃单(互审 C6)
|
|
305
|
+
await flushArchiveBatch();
|
|
306
|
+
|
|
242
307
|
return { archivedCount, totalChecked };
|
|
243
308
|
}
|
|
244
309
|
|
|
@@ -248,9 +313,11 @@ async function runGcScan(
|
|
|
248
313
|
*/
|
|
249
314
|
export function resetGcTimestamp(): void {
|
|
250
315
|
lastGcTimestamp = 0;
|
|
316
|
+
lastVersionGroupRepairTimestamp = 0;
|
|
251
317
|
try {
|
|
252
318
|
releaseLock({ lockPath: lockPathForKey("gc-last") });
|
|
253
319
|
releaseLock({ lockPath: lockPathForKey("gc-run") });
|
|
320
|
+
releaseLock({ lockPath: lockPathForKey("version-group-repair-last") });
|
|
254
321
|
} catch {
|
|
255
322
|
/* ignore — nothing to clear */
|
|
256
323
|
}
|
package/src/cli.ts
CHANGED
|
@@ -38,9 +38,18 @@ import {
|
|
|
38
38
|
} from "./ingest.js";
|
|
39
39
|
import { runDoctor, formatDoctorResults } from "./doctor.js";
|
|
40
40
|
import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
scanMemoryPromotions,
|
|
43
|
+
scanSynthesisPromotions,
|
|
44
|
+
buildPromoteScanDeps,
|
|
45
|
+
formatPromoteScanResult,
|
|
46
|
+
formatSynthesisPromoteResult,
|
|
47
|
+
} from "./memory-promotion.js";
|
|
42
48
|
import { runDream, formatDreamResult, formatDreamMetrics, DEFAULT_DREAM_CONFIG, classifyDreamFailure, shouldBlockDreamRun, partitionAutoDreamScopes } from "./dream-pipeline.js";
|
|
43
|
-
import {
|
|
49
|
+
import { dreamBudgetMs } from "./env-config.js";
|
|
50
|
+
import { listScopesAboveThreshold, pruneWriteCounts } from "./activity-counter.js";
|
|
51
|
+
import { isTranscriptScope } from "./memory-boundaries.js";
|
|
52
|
+
import { maybeRunGc } from "./auto-gc.js";
|
|
44
53
|
import {
|
|
45
54
|
CaseMemoryInputSchema,
|
|
46
55
|
type CaseMemoryInput,
|
|
@@ -62,6 +71,7 @@ import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.
|
|
|
62
71
|
import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
|
|
63
72
|
import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
|
|
64
73
|
import { WorkflowObservationStore } from "./workflow-observation-store.js";
|
|
74
|
+
import { applyUtilityToMeta, computeMemoryUtility, formatMemoryUtility } from "./memory-utility.js";
|
|
65
75
|
import {
|
|
66
76
|
applyWorkflowObservationScopeSuggestions,
|
|
67
77
|
formatWorkflowObservationScopeReview,
|
|
@@ -702,6 +712,28 @@ program
|
|
|
702
712
|
console.log(formatPromoteScanResult(result));
|
|
703
713
|
});
|
|
704
714
|
|
|
715
|
+
program
|
|
716
|
+
.command("promote-synthesis")
|
|
717
|
+
.description("扫描 dream 合成出的结论(evidence 层),把证据充分的那些晋升出一份 durable 副本")
|
|
718
|
+
.requiredOption("--scope <scope>", "要扫描的 scope,如 memory")
|
|
719
|
+
.option("--apply", "执行晋升;默认 dry-run 只预览")
|
|
720
|
+
.option("--min-evidence <n>", "最少几条仍存活的不同证据(默认 2)", "2")
|
|
721
|
+
.option("--min-importance <n>", "合成条目自身的最低 importance(默认 0.6)", "0.6")
|
|
722
|
+
.action(async (options) => {
|
|
723
|
+
const config = loadConfig();
|
|
724
|
+
const { store, embedder } = createComponents(config);
|
|
725
|
+
const conflictStore = new ConflictCandidateStore();
|
|
726
|
+
const deps = buildPromoteScanDeps({ store, embedder, conflictStore });
|
|
727
|
+
const minDistinctEvidence = Number(options.minEvidence);
|
|
728
|
+
const minImportance = Number(options.minImportance);
|
|
729
|
+
const result = await scanSynthesisPromotions(deps, options.scope, {
|
|
730
|
+
dryRun: !options.apply,
|
|
731
|
+
...(Number.isFinite(minDistinctEvidence) ? { minDistinctEvidence } : {}),
|
|
732
|
+
...(Number.isFinite(minImportance) ? { minImportance } : {}),
|
|
733
|
+
});
|
|
734
|
+
console.log(formatSynthesisPromoteResult(result));
|
|
735
|
+
});
|
|
736
|
+
|
|
705
737
|
program
|
|
706
738
|
.command("export <query>")
|
|
707
739
|
.description("导出检索与蒸馏结果到 markdown/json")
|
|
@@ -1360,7 +1392,7 @@ program
|
|
|
1360
1392
|
program
|
|
1361
1393
|
.command("ingest")
|
|
1362
1394
|
.description("导入对话记录到索引")
|
|
1363
|
-
.option("-s, --source <source>", "数据源: cc / codex / gemini / kimi / memory / desktop / all", "all")
|
|
1395
|
+
.option("-s, --source <source>", "数据源: cc / codex / gemini / kimi / memory / desktop / minis / all", "all")
|
|
1364
1396
|
.option("-l, --limit <n>", "限制处理文件数(调试用)")
|
|
1365
1397
|
.option("-v, --verbose", "详细输出")
|
|
1366
1398
|
.option("--no-dedup", "跳过向量去重")
|
|
@@ -1503,6 +1535,27 @@ program
|
|
|
1503
1535
|
}
|
|
1504
1536
|
}
|
|
1505
1537
|
|
|
1538
|
+
// Minis (iPhone) —— 手机 agent 自己把对话写进这个目录,ingest 顺带扫走。
|
|
1539
|
+
// 为什么是独立源而不是复用 desktop:scope 前缀决定了 memory-boundaries 怎么降权,
|
|
1540
|
+
// 混在 cc: 里的话来源在库里就分辨不出来(那正是 2026-08-12 起挂着的缺口)。
|
|
1541
|
+
if (source === "all" || source === "minis") {
|
|
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
|
+
}
|
|
1558
|
+
|
|
1506
1559
|
// Memory markdown files
|
|
1507
1560
|
if (source === "all" || source === "memory") {
|
|
1508
1561
|
console.log("📚 导入记忆文件...");
|
|
@@ -1587,10 +1640,11 @@ program
|
|
|
1587
1640
|
program
|
|
1588
1641
|
.command("dream")
|
|
1589
1642
|
.description("运行 dream 离线整固管线 (Orient/Gather/Consolidate/Prune)")
|
|
1590
|
-
.option("--scope <scope>", "目标 scope(
|
|
1643
|
+
.option("--scope <scope>", "目标 scope(默认前缀家族匹配;加 --exact-scope 变精确)", "cc")
|
|
1644
|
+
.option("--exact-scope", "把 --scope 当成一个具体 scope 名精确匹配,不做前缀家族展开(调度入口必带)")
|
|
1591
1645
|
.option("--auto", "扫描 activity-counter 中写计数达标的所有 scope,逐个跑 dream(单 scope 失败不阻断其他)")
|
|
1592
1646
|
.option("--force", "跳过 min-writes 门槛强制运行")
|
|
1593
|
-
.action(async (options: { scope: string; auto?: boolean; force?: boolean }) => {
|
|
1647
|
+
.action(async (options: { scope: string; auto?: boolean; force?: boolean; exactScope?: boolean }) => {
|
|
1594
1648
|
const config = loadConfig();
|
|
1595
1649
|
const { store, embedder, llm } = createComponents(config);
|
|
1596
1650
|
// Same activity-stats file the store writes per-scope counts to (beside the LanceDB dir).
|
|
@@ -1602,20 +1656,76 @@ program
|
|
|
1602
1656
|
const eligible = listScopesAboveThreshold(DEFAULT_DREAM_CONFIG.minWritesForDream, { statsPath: activityStatsPath });
|
|
1603
1657
|
// 巨型 scope 交给专用 schedule,不占日常轮次的 wall-clock 预算(理由见
|
|
1604
1658
|
// DreamConfig.autoExcludeScopes:07-30 那轮 6h 只跑了 15/507,而 memory 独占 79% 的条数)。
|
|
1605
|
-
const { run:
|
|
1659
|
+
const { run: nonMemoryScopes, deferred } = partitionAutoDreamScopes(eligible, DEFAULT_DREAM_CONFIG.autoExcludeScopes);
|
|
1606
1660
|
if (deferred.length > 0) {
|
|
1607
1661
|
// 不静默:被挪走的 scope 必须在日志里看得见,否则"今天怎么没跑 memory"只能靠翻代码回答。
|
|
1608
1662
|
// 前缀刻意用 `[dream]` 而不是 `dream --auto:`——后者是 dream-checkup.sh 抓「达标 scope 数」
|
|
1609
1663
|
// 的锚点,多一行同前缀的会混进它的取数。
|
|
1610
1664
|
console.log(`[dream] ${deferred.length} 个 scope 交给专用 schedule,本轮不跑 → ${deferred.join(", ")}`);
|
|
1611
1665
|
}
|
|
1666
|
+
|
|
1667
|
+
// 2026-08-16 策略排除(互审 C2/K5):与上面的 deferred **语义不同,别合并**——
|
|
1668
|
+
// deferred 是"别处会跑",这里是"没有任何 schedule 会跑"。日志措辞必须分开,
|
|
1669
|
+
// 否则将来查"pivot 怎么没巩固"会被误导去找那个并不存在的专用 job。
|
|
1670
|
+
// 同时清写计数:runDream 对它返回 skipped 不会 resetWriteCount,不清则计数无限
|
|
1671
|
+
// 累积、每天都进达标队列白跑一次(这正是 07-02 那个"处理完不出队"bug 的形状)。
|
|
1672
|
+
const neverDream = DEFAULT_DREAM_CONFIG.neverDreamScopes;
|
|
1673
|
+
const policyExcluded = nonMemoryScopes.filter(s => neverDream.includes(s));
|
|
1674
|
+
const afterPolicy = nonMemoryScopes.filter(s => !neverDream.includes(s));
|
|
1675
|
+
if (policyExcluded.length > 0) {
|
|
1676
|
+
const prunedPolicy = await pruneWriteCounts(s => neverDream.includes(s), { statsPath: activityStatsPath });
|
|
1677
|
+
console.log(
|
|
1678
|
+
`[dream] ${policyExcluded.length} 个 scope 按策略不参与巩固(neverDreamScopes,无专用 schedule)` +
|
|
1679
|
+
` → ${policyExcluded.join(", ")};已清写计数 ${prunedPolicy.length} 个`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// 2026-08-14 transcript scope 整体出队(dream 写爆库根治的准入侧):
|
|
1684
|
+
// transcript 是原文层(shared-behaviors §5.3——原文归 Deja/ingest 管,提炼归
|
|
1685
|
+
// pivot 池),历史会话被 ingest 批量灌入就把写计数打过门槛,2026-08 实测达标
|
|
1686
|
+
// 队列 95% 是 codex/cc 会话 scope、dream 派生产物 96.6% 落在不参与日常检索的
|
|
1687
|
+
// transcript scope 上——巩固算力几乎全部空转,还把 _versions 写到 7.7GB。
|
|
1688
|
+
// 出队同时清扫计数(含未达标条目——历史会话只增不减,不清则 stats 文件无限膨胀;
|
|
1689
|
+
// ingest 白天会再写回、次日 04:00 再清,稳态保持小)。判定用 memory-boundaries
|
|
1690
|
+
// 的 isTranscriptScope(接新端时那边本来就要求同步前缀清单)。
|
|
1691
|
+
const scopes = afterPolicy.filter(s => !isTranscriptScope(s));
|
|
1692
|
+
const transcriptEligible = afterPolicy.length - scopes.length;
|
|
1693
|
+
const prunedTranscripts = await pruneWriteCounts(isTranscriptScope, { statsPath: activityStatsPath });
|
|
1694
|
+
if (prunedTranscripts.length > 0 || transcriptEligible > 0) {
|
|
1695
|
+
console.log(
|
|
1696
|
+
`[dream] transcript 原文层不参与巩固(§5.3):清扫 ${prunedTranscripts.length} 个 scope 的写计数` +
|
|
1697
|
+
`(其中达标 ${transcriptEligible} 个)→ 样例: ${prunedTranscripts.slice(0, 5).join(", ")}`,
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// 2026-08-14 全库 GC 兜底(互审 C5-P0):dream 是 auto-gc 的唯一生产触发点,而
|
|
1702
|
+
// maybeRunGc 藏在 runDreamInner 里 —— transcript 出队后 durable 队列可为空,若在
|
|
1703
|
+
// 进循环前就 exit,全库 GC 检查会静默消失、归档管线停转。这里无条件补调一次:
|
|
1704
|
+
// GC 自带 24h stamp + 跨进程锁,循环内已触发过则本次 too_soon 跳过,幂等零成本。
|
|
1705
|
+
// 失败只记日志不并入 blocked:GC 是 best-effort 维护,明天还有机会。
|
|
1706
|
+
const runGcBackstop = async (): Promise<void> => {
|
|
1707
|
+
try {
|
|
1708
|
+
const gc = await maybeRunGc(store, DEFAULT_DREAM_CONFIG.gc);
|
|
1709
|
+
console.log(
|
|
1710
|
+
`[dream] 全库 GC 兜底: ${gc.triggered
|
|
1711
|
+
? `archived ${gc.archivedCount}, dissolved version groups ${gc.dissolvedVersionGroups}`
|
|
1712
|
+
: gc.dissolvedVersionGroups > 0
|
|
1713
|
+
? `dissolved version groups ${gc.dissolvedVersionGroups}; archive skipped — ${gc.reason}`
|
|
1714
|
+
: `skipped — ${gc.reason}`}`,
|
|
1715
|
+
);
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
console.error(`[dream] 全库 GC 兜底失败(不阻断本轮判定): ${err instanceof Error ? err.message : String(err)}`);
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1612
1721
|
if (scopes.length === 0) {
|
|
1613
|
-
//
|
|
1722
|
+
// 三种空分开说:真没人达标 / 达标的全被挪走或全是 transcript——混成一句会让排障跑偏。
|
|
1614
1723
|
console.log(
|
|
1615
1724
|
eligible.length === 0
|
|
1616
1725
|
? "dream --auto: 无达标 scope(写计数均低于门槛)"
|
|
1617
|
-
: "dream --auto: 达标 scope
|
|
1726
|
+
: "dream --auto: 达标 scope 均为 transcript(已清扫)、已交专用 schedule 或按策略排除,本轮无事可做",
|
|
1618
1727
|
);
|
|
1728
|
+
await runGcBackstop();
|
|
1619
1729
|
console.log("[[DREAM_STATUS]] skip");
|
|
1620
1730
|
process.exit(0);
|
|
1621
1731
|
}
|
|
@@ -1625,7 +1735,7 @@ program
|
|
|
1625
1735
|
// 把 07-25~27 三天的调度全堵死(上一次没结束,launchd 起不了新的)。
|
|
1626
1736
|
// 天花板见 DreamConfig.autoRunBudgetMs 的注释——截断会让靠后 scope 系统性饿死,
|
|
1627
1737
|
// 真解是 session scope TTL,不是把预算调大。
|
|
1628
|
-
const budgetMs =
|
|
1738
|
+
const budgetMs = dreamBudgetMs(DEFAULT_DREAM_CONFIG.autoRunBudgetMs);
|
|
1629
1739
|
const deadline = Date.now() + budgetMs;
|
|
1630
1740
|
|
|
1631
1741
|
let fatalFailures = 0;
|
|
@@ -1647,7 +1757,11 @@ program
|
|
|
1647
1757
|
}
|
|
1648
1758
|
processed++;
|
|
1649
1759
|
try {
|
|
1650
|
-
|
|
1760
|
+
// scopeMatch: "exact" —— 这些 scope 名直接来自 activity-stats 的键,是具体
|
|
1761
|
+
// scope 不是 family selector。无冒号的键(实测 11 个,含 `cc` / `global` /
|
|
1762
|
+
// `memory`)走前缀会把整个家族卷进来:`cc` 一旦达标就以 `LIKE 'cc%'` 把 08-14
|
|
1763
|
+
// 刚出队的 transcript 原文层整批捞回(2026-08-16 互审 K4)。
|
|
1764
|
+
const result = await runDream({ store, llm, embedder, scope, force: Boolean(options.force), activityStatsPath, kgStore, scopeMatch: "exact" });
|
|
1651
1765
|
tally[result.output.kind]++;
|
|
1652
1766
|
effectsTotal += result.output.effectsWritten;
|
|
1653
1767
|
if (result.output.degraded) degradedScopes.push(`${scope}=${result.output.degraded}`);
|
|
@@ -1665,6 +1779,10 @@ program
|
|
|
1665
1779
|
}
|
|
1666
1780
|
}
|
|
1667
1781
|
|
|
1782
|
+
// 全库 GC 兜底:循环内的 per-scope GC 若已触发过,这里 too_soon 幂等跳过;
|
|
1783
|
+
// 若全部 scope 都 skip/失败没走到 prune 阶段,这里补上当天唯一一次 GC 检查。
|
|
1784
|
+
await runGcBackstop();
|
|
1785
|
+
|
|
1668
1786
|
// 旧语义是「任一 scope 抛异常即 blocked」,在 475 个 scope 的规模下等于「有一个撞上
|
|
1669
1787
|
// store-write 锁就整轮红」,而整轮红会让 shell 脚本重试 → 重跑全量。改为:代码/数据
|
|
1670
1788
|
// 缺陷一票否决,环境性失败要成规模才算整轮失败。比例按已处理数算,不按总数。
|
|
@@ -1702,6 +1820,9 @@ program
|
|
|
1702
1820
|
force: Boolean(options.force),
|
|
1703
1821
|
activityStatsPath,
|
|
1704
1822
|
kgStore,
|
|
1823
|
+
// 手动调用默认保留 family(`--scope cc` 扫 `cc:*` 是既有用法);
|
|
1824
|
+
// 调度入口(weekly job)必须显式带 --exact-scope。
|
|
1825
|
+
scopeMatch: options.exactScope ? "exact" : "family",
|
|
1705
1826
|
});
|
|
1706
1827
|
|
|
1707
1828
|
console.log(formatDreamResult(result));
|
|
@@ -1936,6 +2057,92 @@ program
|
|
|
1936
2057
|
|
|
1937
2058
|
// ─── doctor ──────────────────────────────────────────────────────────────────
|
|
1938
2059
|
|
|
2060
|
+
program
|
|
2061
|
+
.command("memory-utility")
|
|
2062
|
+
.description("价值回溯:查一条 memory 参与过的任务成功率与 utility(不传 id 时按 scope 批量)")
|
|
2063
|
+
.argument("[memoryId]", "memory id(完整 UUID 或前缀)")
|
|
2064
|
+
.option("--scope <scope>", "批量模式:扫这个 scope 下的条目")
|
|
2065
|
+
.option("--category <category>", "批量模式:只看这个 category")
|
|
2066
|
+
.option("--limit <n>", "批量模式扫描上限", "500")
|
|
2067
|
+
.option("--min-samples <n>", "批量模式只列出有效样本量 ≥ 此值的条目", "0")
|
|
2068
|
+
.option("--links <n>", "展示最近 N 条参与记录", "5")
|
|
2069
|
+
.option("--apply", "把算出的 utility 写回 metadata(默认只读,不写)")
|
|
2070
|
+
.option("--observations-dir <dir>", "从指定目录读 workflow observations(默认生产目录)")
|
|
2071
|
+
.option("--json", "输出 JSON")
|
|
2072
|
+
.action(async (memoryId, options) => {
|
|
2073
|
+
const { store } = createComponents(loadConfig());
|
|
2074
|
+
const observationStore = options.observationsDir
|
|
2075
|
+
? new WorkflowObservationStore(expandHome(options.observationsDir))
|
|
2076
|
+
: new WorkflowObservationStore();
|
|
2077
|
+
// 全量拉 observation:join 要看的是历史全集,不是最近 200 条。
|
|
2078
|
+
const observations = await observationStore.listRecent({ limit: 100_000 });
|
|
2079
|
+
const showLinks = Number(options.links) || 0;
|
|
2080
|
+
|
|
2081
|
+
if (memoryId) {
|
|
2082
|
+
const entry = await store.getById(memoryId);
|
|
2083
|
+
if (!entry) {
|
|
2084
|
+
console.error(`未找到 memory: ${memoryId}`);
|
|
2085
|
+
process.exitCode = 1;
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const summary = computeMemoryUtility(entry, observations);
|
|
2089
|
+
if (options.apply) {
|
|
2090
|
+
await store.patchMetadata(entry.id, (meta) => applyUtilityToMeta(meta, summary));
|
|
2091
|
+
}
|
|
2092
|
+
if (options.json) {
|
|
2093
|
+
console.log(JSON.stringify({ ...summary, applied: Boolean(options.apply) }, null, 2));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
console.log(formatMemoryUtility(summary, { showLinks }));
|
|
2097
|
+
console.log(` observations 池: ${observations.length} 条`);
|
|
2098
|
+
if (options.apply) console.log(" 已写回 metadata.utility");
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (!options.scope) {
|
|
2103
|
+
console.error("需要 <memoryId> 或 --scope");
|
|
2104
|
+
process.exitCode = 1;
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const limit = Number(options.limit) || 500;
|
|
2109
|
+
const minSamples = Number(options.minSamples) || 0;
|
|
2110
|
+
const entries = await store.listPage({
|
|
2111
|
+
scopeFilter: [options.scope],
|
|
2112
|
+
category: options.category,
|
|
2113
|
+
limit,
|
|
2114
|
+
offset: 0,
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
const summaries = entries
|
|
2118
|
+
.map((entry) => computeMemoryUtility(entry, observations))
|
|
2119
|
+
.filter((s) => s.effectiveSamples >= minSamples && s.total > 0)
|
|
2120
|
+
.sort((a, b) => (b.utility ?? 0) - (a.utility ?? 0));
|
|
2121
|
+
|
|
2122
|
+
if (options.apply) {
|
|
2123
|
+
for (const summary of summaries) {
|
|
2124
|
+
await store.patchMetadata(summary.memoryId, (meta) => applyUtilityToMeta(meta, summary));
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
if (options.json) {
|
|
2129
|
+
console.log(JSON.stringify({ scanned: entries.length, matched: summaries.length, summaries }, null, 2));
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
console.log(`scope=${options.scope} 扫描 ${entries.length} 条,其中 ${summaries.length} 条有参与记录`);
|
|
2134
|
+
console.log(`observations 池: ${observations.length} 条`);
|
|
2135
|
+
for (const summary of summaries.slice(0, 20)) {
|
|
2136
|
+
const utility = summary.utility === null ? " null" : summary.utility.toFixed(4).padStart(7);
|
|
2137
|
+
console.log(
|
|
2138
|
+
`${utility} samples=${summary.effectiveSamples.toFixed(2).padStart(6)}`
|
|
2139
|
+
+ ` exact=${String(summary.exactCount).padStart(3)} session=${String(summary.sessionCount).padStart(3)}`
|
|
2140
|
+
+ ` ${summary.memoryId.slice(0, 8)}`,
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (options.apply) console.log(`已写回 ${summaries.length} 条的 metadata.utility`);
|
|
2144
|
+
});
|
|
2145
|
+
|
|
1939
2146
|
program
|
|
1940
2147
|
.command("workflow-observe")
|
|
1941
2148
|
.description("记录一条 workflow primitive observation,作为自进化观测层的 append-only 输入")
|
|
@@ -1949,9 +2156,14 @@ program
|
|
|
1949
2156
|
.option("--tags <tags>", "逗号分隔 tags")
|
|
1950
2157
|
.option("--tools <tools>", "逗号分隔 tools")
|
|
1951
2158
|
.option("--idempotency-key <key>", "同一请求重试时用于替换旧 observation 的稳定键")
|
|
2159
|
+
.option("--reader-id <id>", "P0 join key:读者身份(默认本进程)。CLI 每次是新进程,跨会话关联要显式传")
|
|
2160
|
+
.option("--recalled-ids <ids>", "P0 join key:逗号分隔的 memory id,本次任务实际用到的记忆")
|
|
1952
2161
|
.option("--json", "输出 JSON")
|
|
1953
2162
|
.action(async (workflowId, summary, options) => {
|
|
1954
2163
|
const store = new WorkflowObservationStore();
|
|
2164
|
+
// CLI 是一次性进程,检索账本必然是空的(这次进程没检索过),所以不做自动补,
|
|
2165
|
+
// 只认显式传入——自动补在这里只会写出一个恒空的字段,看着像记了其实没记。
|
|
2166
|
+
const recalledIds = splitCsvOption(options.recalledIds);
|
|
1955
2167
|
const record = buildWorkflowObservationRecord({
|
|
1956
2168
|
workflowId,
|
|
1957
2169
|
summary,
|
|
@@ -1963,6 +2175,8 @@ program
|
|
|
1963
2175
|
tags: splitCsvOption(options.tags),
|
|
1964
2176
|
tools: splitCsvOption(options.tools),
|
|
1965
2177
|
idempotencyKey: options.idempotencyKey,
|
|
2178
|
+
readerId: options.readerId,
|
|
2179
|
+
...(recalledIds.length > 0 ? { recalledIds } : {}),
|
|
1966
2180
|
});
|
|
1967
2181
|
const stored = await store.save(record);
|
|
1968
2182
|
if (options.json) {
|