recallnest 2.6.1 → 3.0.1
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 +292 -0
- package/README.md +280 -195
- package/README_CN.md +266 -199
- 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/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/capture-engine.ts +26 -4
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/context-composer-stable.ts +5 -1
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +44 -5
- package/src/env-config.ts +71 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +248 -22
- package/src/kg-extractor.ts +17 -0
- 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 +47 -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/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/runtime-config.ts +9 -0
- package/src/scope-policy.ts +22 -1
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +13 -4
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +218 -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/src/__tests__/access-tracker-novelty.test.ts +0 -304
- 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__/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 -945
- 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 -430
- 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 -276
- 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 -780
- 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.test.ts +0 -151
- 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.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 -189
- 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 -345
- 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 -215
- package/src/__tests__/memory-promotion.test.ts +0 -315
- 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__/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__/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__/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 -250
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -205
- 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__/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 -839
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/version-manager.test.ts +0 -213
- 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/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Utility — 价值回溯(MemOS Reflect2Evolve 的零 LLM 变体)。
|
|
3
|
+
*
|
|
4
|
+
* 借鉴来源:MemOS 自进化记忆系统借鉴评估(2026-06-13 审计,C1「价值回溯算术内核」)。
|
|
5
|
+
* MemOS 原式两处烧 LLM(任务级 R_human 三轴 rubric + 步级反思权重 α_t),
|
|
6
|
+
* 这里两处都换成库里现成的确定性信号:
|
|
7
|
+
* R_human → workflow observation 的四档 outcome 查表(OUTCOME_REWARD)
|
|
8
|
+
* α_t → join 强度(任务级精确命中 vs 会话级弱关联),见 JOIN_WEIGHT
|
|
9
|
+
* 回溯本体在 MemOS 里就是纯算术,这里同样零 LLM、零网络。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ **这一列不写进 `importance`**,理由是硬的:
|
|
12
|
+
* 1. importance 是**阈值语义**——`resolveTier` 拿 ≥0.95 判 core、`isDecayExempt`
|
|
13
|
+
* 拿 core+≥0.95 判**永不衰减**(decay-engine.ts:180-186)。一条效用高的记忆
|
|
14
|
+
* 被顶过 0.95 就顺手拿到永久免衰减,这是串扰不是特性。
|
|
15
|
+
* 2. 那一列已经有写手:STC 的 retroactive-boost 在改它,传播的是**语义相关性**;
|
|
16
|
+
* 价值回溯传播的是**因果贡献**。两件事不该共用一列,否则互相盖。
|
|
17
|
+
* 3. importance 不能为负,而负值那半最值钱——「上次这么干失败了」在 importance 里没有容器装。
|
|
18
|
+
* utility 落在 metadata(与 tier/accessCount/readerIds 同族),只喂检索排序:
|
|
19
|
+
* `resolveTier` 与 `isDecayExempt` 都不读这个字段,tier 与豁免行为天然零变化。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { MemoryEntry } from "./store.js";
|
|
23
|
+
import type {
|
|
24
|
+
WorkflowObservationOutcome,
|
|
25
|
+
WorkflowObservationRecord,
|
|
26
|
+
} from "./workflow-observation-schema.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// 常量
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* outcome → 回报值。取代 MemOS 的 LLM 三轴打分(目标 0.45/过程 0.30/满意 0.25)。
|
|
34
|
+
* 数值可调,关键是它**确定性、零成本**——LLM 打的分没法在 7 万条库上重放。
|
|
35
|
+
*/
|
|
36
|
+
export const OUTCOME_REWARD: Record<WorkflowObservationOutcome, number> = {
|
|
37
|
+
success: 0.8,
|
|
38
|
+
corrected: 0.2,
|
|
39
|
+
missed: -0.3,
|
|
40
|
+
failure: -0.8,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* join 强度权重,充当 MemOS α_t(反思权重)的位置。
|
|
45
|
+
* exact = observation 自己列出了这条 memory;
|
|
46
|
+
* session = 只知道「同一个会话读过这条、且那个会话报了这个结果」,弱得多——
|
|
47
|
+
* 一个会话会跑很多任务,readerIds 又是累积的,不加权就会把无关任务的成败摊到这条头上。
|
|
48
|
+
*/
|
|
49
|
+
export const JOIN_WEIGHT = { exact: 1.0, session: 0.25 } as const;
|
|
50
|
+
|
|
51
|
+
/** 半衰期(天)。旧结果该让位给新结果,与 MemOS priority 式的 0.5^(Δdays/30) 同形。 */
|
|
52
|
+
export const UTILITY_HALF_LIFE_DAYS = 30;
|
|
53
|
+
|
|
54
|
+
/** 低于这个有效样本量时不下结论(utility=null)。 */
|
|
55
|
+
export const MIN_EFFECTIVE_SAMPLES = 0.5;
|
|
56
|
+
|
|
57
|
+
export type JoinKind = keyof typeof JOIN_WEIGHT;
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// 类型
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface MemoryOutcomeLink {
|
|
64
|
+
observationId: string;
|
|
65
|
+
workflowId: string;
|
|
66
|
+
outcome: WorkflowObservationOutcome;
|
|
67
|
+
recordedAt: string;
|
|
68
|
+
join: JoinKind;
|
|
69
|
+
summary: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MemoryOutcomeSummary {
|
|
73
|
+
memoryId: string;
|
|
74
|
+
/** 参与过的任务总数(exact + session)。 */
|
|
75
|
+
total: number;
|
|
76
|
+
exactCount: number;
|
|
77
|
+
sessionCount: number;
|
|
78
|
+
counts: Record<WorkflowObservationOutcome, number>;
|
|
79
|
+
/** success / total。没有样本时为 null——0% 和「没数据」不是一回事。 */
|
|
80
|
+
successRate: number | null;
|
|
81
|
+
/** 只看精确 join 的成功率,null 表示没有精确样本。 */
|
|
82
|
+
exactSuccessRate: number | null;
|
|
83
|
+
/**
|
|
84
|
+
* 效用值 ∈ [-0.8, 0.8],可为负。样本不足时为 null。
|
|
85
|
+
* null 与 0 语义不同:null = 没数据,0 = 有数据但正负相抵。
|
|
86
|
+
*/
|
|
87
|
+
utility: number | null;
|
|
88
|
+
/** 时间折扣后的有效样本量,用于判断这个 utility 有多可信。 */
|
|
89
|
+
effectiveSamples: number;
|
|
90
|
+
/**
|
|
91
|
+
* utility 是拿哪部分样本算的。
|
|
92
|
+
* `exact` = 有精确 join 样本,会话级样本被排除在计算之外(默认策略,见 summarizeMemoryOutcomes);
|
|
93
|
+
* `session` = 只有会话级样本,聊胜于无;`none` = 没样本。
|
|
94
|
+
*/
|
|
95
|
+
utilityBasis: "exact" | "session" | "none";
|
|
96
|
+
links: MemoryOutcomeLink[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// join
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/** 从 memory metadata 里取 readerIds(会话级 join 的另一半 key)。 */
|
|
104
|
+
export function readReaderIds(metadata?: string): string[] {
|
|
105
|
+
try {
|
|
106
|
+
const meta = JSON.parse(metadata || "{}") as Record<string, unknown>;
|
|
107
|
+
return Array.isArray(meta.readerIds)
|
|
108
|
+
? meta.readerIds.filter((r): r is string => typeof r === "string")
|
|
109
|
+
: [];
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 把一条 memory 和 workflow observations 配对起来——本方案要补的那个 join key。
|
|
117
|
+
*
|
|
118
|
+
* 两条路,精确的优先:
|
|
119
|
+
* exact —— observation.recalledIds 直接点名了这条 memory
|
|
120
|
+
* session —— observation.readerId 在这条 memory 的 readerIds 里(同一会话读过它)
|
|
121
|
+
* 同一条 observation 只算一次,exact 覆盖 session。
|
|
122
|
+
*/
|
|
123
|
+
export function linkMemoryToObservations(
|
|
124
|
+
memoryId: string,
|
|
125
|
+
memoryMetadata: string | undefined,
|
|
126
|
+
observations: readonly WorkflowObservationRecord[],
|
|
127
|
+
): MemoryOutcomeLink[] {
|
|
128
|
+
const readerIds = new Set(readReaderIds(memoryMetadata));
|
|
129
|
+
const links: MemoryOutcomeLink[] = [];
|
|
130
|
+
|
|
131
|
+
for (const obs of observations) {
|
|
132
|
+
const exact = Array.isArray(obs.recalledIds) && obs.recalledIds.includes(memoryId);
|
|
133
|
+
const session = !exact && !!obs.readerId && readerIds.has(obs.readerId);
|
|
134
|
+
if (!exact && !session) continue;
|
|
135
|
+
links.push({
|
|
136
|
+
observationId: obs.observationId,
|
|
137
|
+
workflowId: obs.workflowId,
|
|
138
|
+
outcome: obs.outcome,
|
|
139
|
+
recordedAt: obs.recordedAt,
|
|
140
|
+
join: exact ? "exact" : "session",
|
|
141
|
+
summary: obs.summary,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return links.sort((a, b) => Date.parse(b.recordedAt) - Date.parse(a.recordedAt));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// 汇总 + 效用
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function emptyCounts(): Record<WorkflowObservationOutcome, number> {
|
|
153
|
+
return { success: 0, failure: 0, corrected: 0, missed: 0 };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 汇总一条 memory 的参与记录,算出成功率与 utility。
|
|
158
|
+
*
|
|
159
|
+
* utility = Σ(w_join · decay · R) / Σ(w_join · decay)
|
|
160
|
+
* 加权平均而不是求和:一条被读一百次的记忆不该仅凭次数就压过一条被读三次但每次都救场的。
|
|
161
|
+
*/
|
|
162
|
+
export function summarizeMemoryOutcomes(
|
|
163
|
+
memoryId: string,
|
|
164
|
+
links: readonly MemoryOutcomeLink[],
|
|
165
|
+
now = Date.now(),
|
|
166
|
+
options: { preferExact?: boolean } = {},
|
|
167
|
+
): MemoryOutcomeSummary {
|
|
168
|
+
const preferExact = options.preferExact ?? true;
|
|
169
|
+
const counts = emptyCounts();
|
|
170
|
+
let exactCount = 0;
|
|
171
|
+
let sessionCount = 0;
|
|
172
|
+
let exactSuccess = 0;
|
|
173
|
+
const acc = {
|
|
174
|
+
exact: { reward: 0, weight: 0 },
|
|
175
|
+
session: { reward: 0, weight: 0 },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const link of links) {
|
|
179
|
+
counts[link.outcome]++;
|
|
180
|
+
if (link.join === "exact") {
|
|
181
|
+
exactCount++;
|
|
182
|
+
if (link.outcome === "success") exactSuccess++;
|
|
183
|
+
} else {
|
|
184
|
+
sessionCount++;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ageDays = Math.max(0, (now - Date.parse(link.recordedAt)) / 86_400_000);
|
|
188
|
+
const decay = Math.pow(0.5, ageDays / UTILITY_HALF_LIFE_DAYS);
|
|
189
|
+
const weight = JOIN_WEIGHT[link.join] * decay;
|
|
190
|
+
acc[link.join].reward += weight * OUTCOME_REWARD[link.outcome];
|
|
191
|
+
acc[link.join].weight += weight;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 有精确样本时不掺会话级样本 —— 这不是洁癖,是 2026-08-23 落地当天的实测结论:
|
|
195
|
+
// 同一个会话跑多个任务时,readerId join 会把无关任务的成败摊到每条被读过的记忆头上。
|
|
196
|
+
// 实测那次 5 条记忆的会话级成功率全是 44%(同一批 observation),
|
|
197
|
+
// 而精确成功率是 100% / 33% / 0% —— 后者才带信息,前者是会话噪声的常数。
|
|
198
|
+
// 一条从没被精确点名过的记忆(历史 observation / CLI 上报)仍退回会话级,聊胜于无。
|
|
199
|
+
const useExact = preferExact && acc.exact.weight >= MIN_EFFECTIVE_SAMPLES;
|
|
200
|
+
const basisAcc = useExact
|
|
201
|
+
? acc.exact
|
|
202
|
+
: { reward: acc.exact.reward + acc.session.reward, weight: acc.exact.weight + acc.session.weight };
|
|
203
|
+
|
|
204
|
+
const total = links.length;
|
|
205
|
+
const hasUtility = basisAcc.weight >= MIN_EFFECTIVE_SAMPLES;
|
|
206
|
+
return {
|
|
207
|
+
memoryId,
|
|
208
|
+
total,
|
|
209
|
+
exactCount,
|
|
210
|
+
sessionCount,
|
|
211
|
+
counts,
|
|
212
|
+
successRate: total > 0 ? counts.success / total : null,
|
|
213
|
+
exactSuccessRate: exactCount > 0 ? exactSuccess / exactCount : null,
|
|
214
|
+
utility: hasUtility ? basisAcc.reward / basisAcc.weight : null,
|
|
215
|
+
effectiveSamples: basisAcc.weight,
|
|
216
|
+
utilityBasis: !hasUtility ? "none" : useExact ? "exact" : "session",
|
|
217
|
+
links: [...links],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 一步到位:memory + observations → 汇总。 */
|
|
222
|
+
export function computeMemoryUtility(
|
|
223
|
+
entry: Pick<MemoryEntry, "id" | "metadata">,
|
|
224
|
+
observations: readonly WorkflowObservationRecord[],
|
|
225
|
+
now = Date.now(),
|
|
226
|
+
options: { preferExact?: boolean } = {},
|
|
227
|
+
): MemoryOutcomeSummary {
|
|
228
|
+
return summarizeMemoryOutcomes(
|
|
229
|
+
entry.id,
|
|
230
|
+
linkMemoryToObservations(entry.id, entry.metadata, observations),
|
|
231
|
+
now,
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// 写回 metadata
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** metadata 里 utility 三件套的字段名。改名要同步 retriever 的 applyUtilityWeight。 */
|
|
241
|
+
export const UTILITY_FIELD = "utility";
|
|
242
|
+
export const UTILITY_SAMPLES_FIELD = "utilitySamples";
|
|
243
|
+
export const UTILITY_UPDATED_FIELD = "utilityUpdatedAt";
|
|
244
|
+
export const UTILITY_BASIS_FIELD = "utilityBasis";
|
|
245
|
+
|
|
246
|
+
/** 从 metadata 读 utility;没有/坏了都返回 null(不是 0——见上文语义说明)。 */
|
|
247
|
+
export function readUtility(metadata?: string): number | null {
|
|
248
|
+
try {
|
|
249
|
+
const meta = JSON.parse(metadata || "{}") as Record<string, unknown>;
|
|
250
|
+
const value = meta[UTILITY_FIELD];
|
|
251
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 把 utility 写进 metadata 对象(纯函数,调用方负责落库)。
|
|
259
|
+
* utility=null 时**清除**旧值,而不是写 0:证据没了就该退回「没数据」,不是退回「中性」。
|
|
260
|
+
*/
|
|
261
|
+
export function applyUtilityToMeta(
|
|
262
|
+
meta: Record<string, unknown>,
|
|
263
|
+
summary: MemoryOutcomeSummary,
|
|
264
|
+
now = Date.now(),
|
|
265
|
+
): Record<string, unknown> {
|
|
266
|
+
const next = { ...meta };
|
|
267
|
+
if (summary.utility === null) {
|
|
268
|
+
delete next[UTILITY_FIELD];
|
|
269
|
+
delete next[UTILITY_SAMPLES_FIELD];
|
|
270
|
+
delete next[UTILITY_BASIS_FIELD];
|
|
271
|
+
delete next[UTILITY_UPDATED_FIELD];
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
274
|
+
next[UTILITY_FIELD] = Number(summary.utility.toFixed(4));
|
|
275
|
+
next[UTILITY_SAMPLES_FIELD] = Number(summary.effectiveSamples.toFixed(4));
|
|
276
|
+
next[UTILITY_BASIS_FIELD] = summary.utilityBasis;
|
|
277
|
+
next[UTILITY_UPDATED_FIELD] = now;
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// 输出
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function pct(value: number | null): string {
|
|
286
|
+
return value === null ? "n/a" : `${Math.round(value * 100)}%`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function formatMemoryUtility(summary: MemoryOutcomeSummary, options: { showLinks?: number } = {}): string {
|
|
290
|
+
const lines: string[] = [];
|
|
291
|
+
lines.push(`Memory ${summary.memoryId}`);
|
|
292
|
+
lines.push(` 参与任务: ${summary.total} 条 (精确 ${summary.exactCount} / 会话级 ${summary.sessionCount})`);
|
|
293
|
+
lines.push(
|
|
294
|
+
` outcome: success=${summary.counts.success} corrected=${summary.counts.corrected}`
|
|
295
|
+
+ ` missed=${summary.counts.missed} failure=${summary.counts.failure}`,
|
|
296
|
+
);
|
|
297
|
+
lines.push(` 成功率: ${pct(summary.successRate)} (仅精确 join: ${pct(summary.exactSuccessRate)})`);
|
|
298
|
+
lines.push(
|
|
299
|
+
` utility: ${summary.utility === null ? "null(样本不足,不下结论)" : summary.utility.toFixed(4)}`
|
|
300
|
+
+ ` [基于 ${summary.utilityBasis} join,有效样本 ${summary.effectiveSamples.toFixed(2)}]`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const limit = options.showLinks ?? 0;
|
|
304
|
+
if (limit > 0 && summary.links.length > 0) {
|
|
305
|
+
lines.push(" 最近参与:");
|
|
306
|
+
for (const link of summary.links.slice(0, limit)) {
|
|
307
|
+
lines.push(
|
|
308
|
+
` ${link.recordedAt.slice(0, 10)} [${link.join}] ${link.workflowId} → ${link.outcome}`
|
|
309
|
+
+ ` ${link.summary.slice(0, 60)}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return lines.join("\n");
|
|
314
|
+
}
|
package/src/pivot-apply.ts
CHANGED
|
@@ -36,9 +36,17 @@ import {
|
|
|
36
36
|
import { assignDefaultConfidence } from "./confidence-tracker.js";
|
|
37
37
|
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
38
38
|
import { scanForPII, redactSecrets } from "./pii-detector.js";
|
|
39
|
+
import {
|
|
40
|
+
EvidenceCoordinateSchema,
|
|
41
|
+
EvidenceWindowSchema,
|
|
42
|
+
PIVOT_EVIDENCE_CONTRACT_VERSION,
|
|
43
|
+
PivotEvidenceProvenanceSchema,
|
|
44
|
+
evidenceWindowIssues,
|
|
45
|
+
} from "./pivot-evidence.js";
|
|
39
46
|
|
|
40
47
|
export const PIVOT_APPLY_SOURCE = "session_distill";
|
|
41
48
|
export const PIVOT_APPLY_CAPTURE = "pivot_apply_v1";
|
|
49
|
+
export const PIVOT_APPLY_EVIDENCE_CAPTURE = "pivot_apply_v2";
|
|
42
50
|
export const PIVOT_SCOPE = "memory:pivot";
|
|
43
51
|
|
|
44
52
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
@@ -77,12 +85,46 @@ export const PivotCandidateInputSchema = z
|
|
|
77
85
|
candidateHash: z.string().regex(SHA256_HEX, "candidateHash must be 64-char sha256 hex"),
|
|
78
86
|
batchId: z.string().min(1).max(64),
|
|
79
87
|
importance: z.number().min(0).max(1),
|
|
88
|
+
evidenceContractVersion: z.literal(PIVOT_EVIDENCE_CONTRACT_VERSION).optional(),
|
|
89
|
+
sourceFingerprint: z.string().regex(SHA256_HEX).optional(),
|
|
90
|
+
anchorCoordinate: EvidenceCoordinateSchema.optional(),
|
|
91
|
+
evidenceWindows: z.array(EvidenceWindowSchema).max(16).optional(),
|
|
80
92
|
})
|
|
81
93
|
.refine((c) => KIND_CATEGORY[c.kind] === c.proposedCategory, {
|
|
82
94
|
message: "proposedCategory does not match kind (corpus-verified mapping violated)",
|
|
95
|
+
})
|
|
96
|
+
.superRefine((candidate, context) => {
|
|
97
|
+
const fields = [
|
|
98
|
+
candidate.evidenceContractVersion,
|
|
99
|
+
candidate.sourceFingerprint,
|
|
100
|
+
candidate.anchorCoordinate,
|
|
101
|
+
candidate.evidenceWindows,
|
|
102
|
+
];
|
|
103
|
+
if (fields.every((field) => field === undefined)) return;
|
|
104
|
+
const provenance = PivotEvidenceProvenanceSchema.safeParse({
|
|
105
|
+
evidenceContractVersion: candidate.evidenceContractVersion,
|
|
106
|
+
sourceFingerprint: candidate.sourceFingerprint,
|
|
107
|
+
anchorCoordinate: candidate.anchorCoordinate,
|
|
108
|
+
evidenceWindows: candidate.evidenceWindows,
|
|
109
|
+
});
|
|
110
|
+
if (!provenance.success) {
|
|
111
|
+
context.addIssue({
|
|
112
|
+
code: z.ZodIssueCode.custom,
|
|
113
|
+
message: "evidence provenance must be complete and match contract version 1",
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const issue of evidenceWindowIssues(provenance.data, candidate.evidence.length, candidate.sessionId)) {
|
|
118
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: issue });
|
|
119
|
+
}
|
|
83
120
|
});
|
|
84
121
|
export type PivotCandidateInput = z.infer<typeof PivotCandidateInputSchema>;
|
|
85
122
|
|
|
123
|
+
export const PivotEvidenceCandidateInputSchema = PivotCandidateInputSchema.refine(
|
|
124
|
+
(candidate) => candidate.evidenceContractVersion === PIVOT_EVIDENCE_CONTRACT_VERSION,
|
|
125
|
+
{ message: "evidence-coordinate apply requires contract version 1 provenance" },
|
|
126
|
+
);
|
|
127
|
+
|
|
86
128
|
export type PivotApplyDisposition =
|
|
87
129
|
| "stored"
|
|
88
130
|
| "updated"
|
|
@@ -195,10 +237,18 @@ export async function persistPivotCandidate(
|
|
|
195
237
|
const language = detectLang(text);
|
|
196
238
|
const fts_text = tokenizeFts(text, language);
|
|
197
239
|
|
|
240
|
+
const evidenceProvenance = input.evidenceContractVersion === PIVOT_EVIDENCE_CONTRACT_VERSION
|
|
241
|
+
? PivotEvidenceProvenanceSchema.parse({
|
|
242
|
+
evidenceContractVersion: input.evidenceContractVersion,
|
|
243
|
+
sourceFingerprint: input.sourceFingerprint,
|
|
244
|
+
anchorCoordinate: input.anchorCoordinate,
|
|
245
|
+
evidenceWindows: input.evidenceWindows,
|
|
246
|
+
})
|
|
247
|
+
: null;
|
|
198
248
|
let metadata = buildStructuredMetadata({
|
|
199
249
|
source: PIVOT_APPLY_SOURCE,
|
|
200
250
|
tags: [...input.tags, "pivot-apply", `batch:${input.batchId}`],
|
|
201
|
-
capture: PIVOT_APPLY_CAPTURE,
|
|
251
|
+
capture: evidenceProvenance ? PIVOT_APPLY_EVIDENCE_CAPTURE : PIVOT_APPLY_CAPTURE,
|
|
202
252
|
category: input.proposedCategory,
|
|
203
253
|
canonicalKey: input.canonicalKey,
|
|
204
254
|
extra: {
|
|
@@ -210,6 +260,7 @@ export async function persistPivotCandidate(
|
|
|
210
260
|
reportId: input.reportId,
|
|
211
261
|
candidateHash: input.candidateHash,
|
|
212
262
|
batchId: input.batchId,
|
|
263
|
+
...(evidenceProvenance ?? {}),
|
|
213
264
|
},
|
|
214
265
|
},
|
|
215
266
|
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const PIVOT_EVIDENCE_CONTRACT_VERSION = 1 as const;
|
|
6
|
+
|
|
7
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
8
|
+
|
|
9
|
+
export const EvidenceCoordinateSchema = z.object({
|
|
10
|
+
sessionId: z.string().min(1).max(200),
|
|
11
|
+
ordinal: z.number().int().nonnegative(),
|
|
12
|
+
role: z.enum(["user", "assistant"]),
|
|
13
|
+
contentDigest: z.string().regex(SHA256_HEX),
|
|
14
|
+
}).strict();
|
|
15
|
+
export type EvidenceCoordinate = z.infer<typeof EvidenceCoordinateSchema>;
|
|
16
|
+
|
|
17
|
+
export const EvidenceWindowTurnSchema = z.object({
|
|
18
|
+
ordinal: z.number().int().nonnegative(),
|
|
19
|
+
role: z.enum(["user", "assistant"]),
|
|
20
|
+
contentDigest: z.string().regex(SHA256_HEX),
|
|
21
|
+
evidenceIndexes: z.array(z.number().int().nonnegative()).min(1).max(16),
|
|
22
|
+
}).strict();
|
|
23
|
+
export type EvidenceWindowTurn = z.infer<typeof EvidenceWindowTurnSchema>;
|
|
24
|
+
|
|
25
|
+
export const EvidenceWindowSchema = z.object({
|
|
26
|
+
sessionId: z.string().min(1).max(200),
|
|
27
|
+
startOrdinal: z.number().int().nonnegative(),
|
|
28
|
+
endOrdinal: z.number().int().nonnegative(),
|
|
29
|
+
turns: z.array(EvidenceWindowTurnSchema).min(1).max(16),
|
|
30
|
+
}).strict();
|
|
31
|
+
export type EvidenceWindow = z.infer<typeof EvidenceWindowSchema>;
|
|
32
|
+
|
|
33
|
+
export const PivotEvidenceProvenanceSchema = z.object({
|
|
34
|
+
evidenceContractVersion: z.literal(PIVOT_EVIDENCE_CONTRACT_VERSION),
|
|
35
|
+
sourceFingerprint: z.string().regex(SHA256_HEX),
|
|
36
|
+
anchorCoordinate: EvidenceCoordinateSchema,
|
|
37
|
+
evidenceWindows: z.array(EvidenceWindowSchema).max(16),
|
|
38
|
+
}).strict();
|
|
39
|
+
export type PivotEvidenceProvenance = z.infer<typeof PivotEvidenceProvenanceSchema>;
|
|
40
|
+
|
|
41
|
+
export function turnContentDigest(input: {
|
|
42
|
+
sessionId: string;
|
|
43
|
+
ordinal: number;
|
|
44
|
+
role: "user" | "assistant";
|
|
45
|
+
text: string;
|
|
46
|
+
}): string {
|
|
47
|
+
const canonical = JSON.stringify({
|
|
48
|
+
schemaVersion: PIVOT_EVIDENCE_CONTRACT_VERSION,
|
|
49
|
+
sessionId: input.sessionId,
|
|
50
|
+
ordinal: input.ordinal,
|
|
51
|
+
role: input.role,
|
|
52
|
+
text: input.text.replace(/\u0000/g, "").trim(),
|
|
53
|
+
});
|
|
54
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function evidenceWindowIssues(
|
|
58
|
+
provenance: PivotEvidenceProvenance,
|
|
59
|
+
evidenceCount: number,
|
|
60
|
+
expectedSessionId: string,
|
|
61
|
+
): string[] {
|
|
62
|
+
const issues: string[] = [];
|
|
63
|
+
if (provenance.anchorCoordinate.sessionId !== expectedSessionId) {
|
|
64
|
+
issues.push("anchor coordinate sessionId does not match candidate sessionId");
|
|
65
|
+
}
|
|
66
|
+
if (provenance.anchorCoordinate.role !== "user") {
|
|
67
|
+
issues.push("anchor coordinate must point to a user turn");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const seenEvidence = new Set<number>();
|
|
71
|
+
let previousEnd = -2;
|
|
72
|
+
for (const [windowIndex, window] of provenance.evidenceWindows.entries()) {
|
|
73
|
+
if (window.sessionId !== expectedSessionId) {
|
|
74
|
+
issues.push(`evidence window ${windowIndex} sessionId does not match candidate sessionId`);
|
|
75
|
+
}
|
|
76
|
+
const first = window.turns[0];
|
|
77
|
+
const last = window.turns[window.turns.length - 1];
|
|
78
|
+
if (window.startOrdinal !== first.ordinal || window.endOrdinal !== last.ordinal) {
|
|
79
|
+
issues.push(`evidence window ${windowIndex} bounds do not match its turns`);
|
|
80
|
+
}
|
|
81
|
+
if (window.startOrdinal <= previousEnd + 1) {
|
|
82
|
+
issues.push(`evidence window ${windowIndex} overlaps or should be merged with the previous window`);
|
|
83
|
+
}
|
|
84
|
+
previousEnd = window.endOrdinal;
|
|
85
|
+
|
|
86
|
+
for (let turnIndex = 0; turnIndex < window.turns.length; turnIndex++) {
|
|
87
|
+
const turn = window.turns[turnIndex];
|
|
88
|
+
if (turn.ordinal !== window.startOrdinal + turnIndex) {
|
|
89
|
+
issues.push(`evidence window ${windowIndex} contains a non-contiguous ordinal range`);
|
|
90
|
+
}
|
|
91
|
+
const localIndexes = new Set<number>();
|
|
92
|
+
for (const evidenceIndex of turn.evidenceIndexes) {
|
|
93
|
+
if (localIndexes.has(evidenceIndex)) {
|
|
94
|
+
issues.push(`evidence index ${evidenceIndex} is repeated inside one turn`);
|
|
95
|
+
}
|
|
96
|
+
localIndexes.add(evidenceIndex);
|
|
97
|
+
if (evidenceIndex < 0 || evidenceIndex >= evidenceCount) {
|
|
98
|
+
issues.push(`evidence index ${evidenceIndex} is outside the candidate evidence array`);
|
|
99
|
+
} else if (seenEvidence.has(evidenceIndex)) {
|
|
100
|
+
issues.push(`evidence index ${evidenceIndex} is mapped more than once`);
|
|
101
|
+
} else {
|
|
102
|
+
seenEvidence.add(evidenceIndex);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (let index = 0; index < evidenceCount; index++) {
|
|
108
|
+
if (!seenEvidence.has(index)) issues.push(`evidence index ${index} has no source coordinate`);
|
|
109
|
+
}
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add provenance to content-addressed payloads only when the v1 evidence
|
|
115
|
+
* contract is present. Historical 2026-08-03 candidates remain byte-for-byte
|
|
116
|
+
* compatible; a partial or malformed new contract fails closed.
|
|
117
|
+
*/
|
|
118
|
+
export function evidenceIdentityPayload(value: unknown): Record<string, unknown> {
|
|
119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
120
|
+
const record = value as Record<string, unknown>;
|
|
121
|
+
if (record.evidenceContractVersion === undefined) return {};
|
|
122
|
+
return PivotEvidenceProvenanceSchema.parse({
|
|
123
|
+
evidenceContractVersion: record.evidenceContractVersion,
|
|
124
|
+
sourceFingerprint: record.sourceFingerprint,
|
|
125
|
+
anchorCoordinate: record.anchorCoordinate,
|
|
126
|
+
evidenceWindows: record.evidenceWindows,
|
|
127
|
+
});
|
|
128
|
+
}
|
package/src/ppr-traversal.ts
CHANGED
|
@@ -10,7 +10,11 @@ import type { KGTriple, NeighborhoodResult } from "./kg-store.js";
|
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
12
|
export interface PPRConfig {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Probability of CONTINUING to diffuse along an edge at each step.
|
|
15
|
+
* The teleport-back-to-seed probability is the complement: (1 - dampingFactor).
|
|
16
|
+
* (The previous version of this comment had the two reversed.)
|
|
17
|
+
*/
|
|
14
18
|
dampingFactor: number;
|
|
15
19
|
/** Max power-iteration rounds (default: 10) */
|
|
16
20
|
maxIterations: number;
|
|
@@ -31,6 +35,26 @@ export interface PPRResult {
|
|
|
31
35
|
path: string[];
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* NOTE on dampingFactor = 0.85 (measured 2026-08-28, deliberately NOT changed):
|
|
40
|
+
*
|
|
41
|
+
* 0.85 is Brin & Page's default, tuned for billion-node web link graphs.
|
|
42
|
+
* HippoRAG 2 (arXiv 2502.14802, Table 13) uses 0.5 for knowledge graphs.
|
|
43
|
+
*
|
|
44
|
+
* Sensitivity measured on our own production KG (60,536 nodes / 66,332 edges,
|
|
45
|
+
* 10 real seeds, Top-10 Jaccard against alpha=0.85):
|
|
46
|
+
* alpha=0.30 -> 0.617 | 0.50 -> 0.648 | 0.70 -> 0.760 | 0.95 -> 0.598
|
|
47
|
+
* alpha=0.30 vs alpha=0.95 -> 0.371 (~63% of results differ)
|
|
48
|
+
* So the parameter IS sensitive. It is nevertheless left at 0.85, because
|
|
49
|
+
* tuning it does not fix the actual failure mode: at every alpha tested,
|
|
50
|
+
* generic hub nodes ("user" / "assistant" / "Alice") take ~30% of the top-20
|
|
51
|
+
* and land at ranks 2-4. Changing alpha only reshuffles the hubs among
|
|
52
|
+
* themselves.
|
|
53
|
+
*
|
|
54
|
+
* Fix the graph first (entity normalization + hub suppression), then tune.
|
|
55
|
+
* Revisit only after the top-20 hub share drops materially; any change must
|
|
56
|
+
* go through a shadow run first.
|
|
57
|
+
*/
|
|
34
58
|
export const DEFAULT_PPR_CONFIG: PPRConfig = {
|
|
35
59
|
dampingFactor: 0.85,
|
|
36
60
|
maxIterations: 10,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall Ledger — 进程内的「这次会话读到过哪些记忆」账本。
|
|
3
|
+
*
|
|
4
|
+
* 存在的理由(P0 join key):价值回溯要的是 `(memory_id, outcome)` 配对,而库里
|
|
5
|
+
* 一边有结果没参与者(workflow observation 不记 memory),一边有参与者没结果
|
|
6
|
+
* (access-tracker 记命中不记任务成败)。这个模块补的就是中间那一段:
|
|
7
|
+
* 检索命中时记一笔,`workflow_observe` 上报时把窗口内命中的 id 一起写进 observation。
|
|
8
|
+
*
|
|
9
|
+
* 为什么是**进程级**而不是挂在 AccessTracker 实例上:
|
|
10
|
+
* `createComponentResolver`(runtime-config.ts:226)按 profileName 缓存 components,
|
|
11
|
+
* 每个 profile 各有一个 AccessTracker → 各自随机一个 readerId。检索走 profile A、
|
|
12
|
+
* 上报走 default 时,两边 readerId 对不上,join 直接失效。而 access-tracker.ts:63
|
|
13
|
+
* 的注释本来就写着「one stdio MCP server process ≈ one CC session」——那句话描述的
|
|
14
|
+
* 语义单位是**进程**,实现却落在实例上。这里把它扶正,顺带修掉多 profile 时
|
|
15
|
+
* readerIds/distinctReaderCount 被同一个会话灌进多个假 reader 的虚高。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 本进程(≈ 本次 CC 会话)的读者身份。所有 AccessTracker 默认共享它。 */
|
|
19
|
+
export const PROCESS_READER_ID = `r-${crypto.randomUUID().slice(0, 8)}`;
|
|
20
|
+
|
|
21
|
+
/** 默认回看窗口:一次任务从检索到上报 outcome 的常见跨度。 */
|
|
22
|
+
export const DEFAULT_RECALL_WINDOW_MS = 30 * 60_000;
|
|
23
|
+
|
|
24
|
+
/** 一条 observation 最多带多少个 memory id。防止长会话把整个 top-k 历史灌进 observation。 */
|
|
25
|
+
export const RECALLED_IDS_CAP = 64;
|
|
26
|
+
|
|
27
|
+
/** 账本自身的常驻上限;超出时按最后命中时间淘汰最老的。 */
|
|
28
|
+
const LEDGER_CAP = 512;
|
|
29
|
+
|
|
30
|
+
/** memoryId → 最后一次被检索返回的时间戳(ms)。 */
|
|
31
|
+
const hits = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 记录这批 id 在本进程被检索返回过。
|
|
35
|
+
*
|
|
36
|
+
* 注意调用点在 novelty/cooldown gate **之前**:那两道闸问的是「值不值得强化衰减半衰期」,
|
|
37
|
+
* 这里问的是「agent 有没有真的看到这条」。同一批 id 两个问题的答案可以不同,
|
|
38
|
+
* 用强化闸去过滤 join 参与者会系统性漏掉最常被读到的那些条目。
|
|
39
|
+
*/
|
|
40
|
+
export function recordRecallHits(ids: readonly string[], now = Date.now()): void {
|
|
41
|
+
for (const id of ids) {
|
|
42
|
+
if (typeof id === "string" && id.length > 0) hits.set(id, now);
|
|
43
|
+
}
|
|
44
|
+
if (hits.size > LEDGER_CAP) {
|
|
45
|
+
const sorted = [...hits.entries()].sort((a, b) => b[1] - a[1]);
|
|
46
|
+
hits.clear();
|
|
47
|
+
for (const [id, ts] of sorted.slice(0, LEDGER_CAP)) hits.set(id, ts);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 取窗口内被命中过的 memory id,最近命中的排前面。
|
|
53
|
+
* windowMs <= 0 表示不限时间(仍受 cap 约束)。
|
|
54
|
+
*/
|
|
55
|
+
export function recentRecallHits(
|
|
56
|
+
windowMs = DEFAULT_RECALL_WINDOW_MS,
|
|
57
|
+
cap = RECALLED_IDS_CAP,
|
|
58
|
+
now = Date.now(),
|
|
59
|
+
): string[] {
|
|
60
|
+
const cutoff = windowMs > 0 ? now - windowMs : Number.NEGATIVE_INFINITY;
|
|
61
|
+
return [...hits.entries()]
|
|
62
|
+
.filter(([, ts]) => ts >= cutoff)
|
|
63
|
+
.sort((a, b) => b[1] - a[1])
|
|
64
|
+
.slice(0, Math.max(0, cap))
|
|
65
|
+
.map(([id]) => id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 当前账本条目数(诊断/测试用)。 */
|
|
69
|
+
export function recallLedgerSize(): number {
|
|
70
|
+
return hits.size;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 清空账本。测试隔离用;生产路径不调用。 */
|
|
74
|
+
export function resetRecallLedger(): void {
|
|
75
|
+
hits.clear();
|
|
76
|
+
}
|