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
package/src/retriever.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { expandQueryWithAliases } from "./aliases.js";
|
|
|
13
13
|
import { expandQuery } from "./query-expander.js";
|
|
14
14
|
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
15
15
|
import { type AccessTracker, computeHotnessScore, parseAccessMetadata } from "./access-tracker.js";
|
|
16
|
+
import { readUtility } from "./memory-utility.js";
|
|
16
17
|
import { weibullDecay, resolveTier, isDecayExempt, adjustHalfLifeForEmotion, computeArousalBoost } from "./decay-engine.js";
|
|
17
18
|
import { logInfo, logWarn } from "./stderr-log.js";
|
|
18
19
|
import { extractBoundaryMetadata, isDurableMemoryScope, isTranscriptScope } from "./memory-boundaries.js";
|
|
@@ -39,7 +40,7 @@ import { isKGModeEnabled } from "./kg-extractor.js";
|
|
|
39
40
|
import { detectEntities } from "./query-entity-detector.js";
|
|
40
41
|
import { buildGraph, pprTraverse } from "./ppr-traversal.js";
|
|
41
42
|
import { logInfo } from "./stderr-log.js";
|
|
42
|
-
import type
|
|
43
|
+
import { AUDIT_RETRIEVED_CAP, type AuditLogger } from "./audit-log.js";
|
|
43
44
|
import { parseEmotion, isEmotionScoringEnabled } from "./memory-schema.js";
|
|
44
45
|
import { parseNarrative, isNarrativeModeEnabled } from "./narrative-schema.js";
|
|
45
46
|
import type { EmotionMetadata } from "./memory-schema.js";
|
|
@@ -111,6 +112,19 @@ export interface RetrievalConfig {
|
|
|
111
112
|
* 0 = disabled (default), 0.15 = recommended.
|
|
112
113
|
*/
|
|
113
114
|
hotnessWeight: number;
|
|
115
|
+
/**
|
|
116
|
+
* Utility weight — 价值回溯(memory-utility.ts)算出的效用值对最终分的影响强度。
|
|
117
|
+
* Formula: score *= 1 + utilityWeight * utility,utility ∈ [-0.8, 0.8]。
|
|
118
|
+
* 与 hotness/frequency 的区别在信号来源:那两个数「被读过多少次」,
|
|
119
|
+
* 这个数「被用到的那些任务成没成」——前者是热度,后者是因果贡献。
|
|
120
|
+
* 没有 utility 的条目分数原样不动(bonus-only 冷启动,不惩罚没数据的)。
|
|
121
|
+
*
|
|
122
|
+
* 0 = disabled(默认)。**默认关是有意的影子期**:目前生产库里带 join key 的
|
|
123
|
+
* observation 从 0 开始积累,样本不足时开启只会放大噪声。
|
|
124
|
+
* 升级条件写在 open-loops「MemOS 价值回溯」条目里,不在这行注释里——
|
|
125
|
+
* 注释不会有人定期回来读,那个条目每窗口都会被推到眼前。
|
|
126
|
+
*/
|
|
127
|
+
utilityWeight: number;
|
|
114
128
|
/**
|
|
115
129
|
* Per-category score thresholds. When a result's category matches a key,
|
|
116
130
|
* that threshold is used instead of hardMinScore. Categories not listed
|
|
@@ -234,6 +248,15 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
|
234
248
|
bm25Weight: 0.3,
|
|
235
249
|
minScore: 0.3,
|
|
236
250
|
rerank: "cross-encoder",
|
|
251
|
+
// ⚠️ 2026-08-23 查实:**这里超过 20 的部分从来没生效过**。两条候选腿最终都落到
|
|
252
|
+
// store.ts 的 `clampInt(limit, 1, 20)`(vectorSearch:588 / bm25Search:643),
|
|
253
|
+
// 路径是 retriever.ts:1109 算出 ceil(candidatePoolSize × multiplier) →
|
|
254
|
+
// runVectorSearch/runBM25Search(:1341/:1373) → store.*Search(limit) → 被压成 20。
|
|
255
|
+
// 那个 clamp 从初始提交 c10607a(2026-03-03)就在,而 07-16 调参(9374a4f)只改了本文件、
|
|
256
|
+
// 没碰 store.ts —— 所以这个 30 从落地当天就是空转。retrieval-profiles.ts 的 24/26/28 同理。
|
|
257
|
+
// **不要顺手去改那个 clamp**:动它等于扩候选池,而扩池方案已于 2026-08-22 跑 shadow 后被
|
|
258
|
+
// 实测否决(RecallNest memory:pivot `rejected-recall-widen-candidate-pool`,主犯认定订正为
|
|
259
|
+
// applyLayerAdmission 79.4% 而非 hard_min 13.5%)。要推翻它得先拿出那条否决记录要求的新证据。
|
|
237
260
|
candidatePoolSize: 30,
|
|
238
261
|
sourceDiversity: 1,
|
|
239
262
|
recencyHalfLifeDays: 14,
|
|
@@ -254,6 +277,7 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
|
254
277
|
hardMinScore: 0.35,
|
|
255
278
|
timeDecayHalfLifeDays: 60,
|
|
256
279
|
hotnessWeight: 0,
|
|
280
|
+
utilityWeight: 0,
|
|
257
281
|
};
|
|
258
282
|
|
|
259
283
|
/**
|
|
@@ -894,12 +918,28 @@ export class MemoryRetriever {
|
|
|
894
918
|
|
|
895
919
|
// F-1: Audit log — record retrieve operation (non-blocking, silent on failure)
|
|
896
920
|
try {
|
|
897
|
-
this.auditLogger
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
921
|
+
if (this.auditLogger) {
|
|
922
|
+
const retrieved = results.slice(0, AUDIT_RETRIEVED_CAP).map((r) => {
|
|
923
|
+
const evo = parseEvolution(r.entry.metadata, r.entry.timestamp);
|
|
924
|
+
const boundary = extractBoundaryMetadata(r.entry.metadata);
|
|
925
|
+
return {
|
|
926
|
+
id: r.entry.id.slice(0, 8),
|
|
927
|
+
rev: evo.version,
|
|
928
|
+
st: evo.status,
|
|
929
|
+
...(boundary?.layer ? { ly: boundary.layer } : {}),
|
|
930
|
+
...(boundary?.authority ? { au: boundary.authority } : {}),
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
this.auditLogger.log({
|
|
934
|
+
operation: "retrieve",
|
|
935
|
+
scope: context.scopeFilter?.[0],
|
|
936
|
+
actor: context.source || "manual",
|
|
937
|
+
details: `query="${context.query.slice(0, 80)}" hits=${results.length}`,
|
|
938
|
+
...(retrieved.length > 0 ? { retrieved } : {}),
|
|
939
|
+
// Only when the list is short of the truth — a truncated list must look truncated.
|
|
940
|
+
...(results.length > retrieved.length ? { retrievedTotal: results.length } : {}),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
903
943
|
} catch {
|
|
904
944
|
// Audit must never block retrieval
|
|
905
945
|
}
|
|
@@ -1212,8 +1252,14 @@ export class MemoryRetriever {
|
|
|
1212
1252
|
const weighted = this.applyImportanceWeight(boosted);
|
|
1213
1253
|
trace?.endStage(weighted.length, weighted.map(r => r.score));
|
|
1214
1254
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1255
|
+
// 价值回溯:紧跟 importance 之后,因为两者回答的是同一类问题(这条本身有多值得),
|
|
1256
|
+
// 只是 importance 说的是「存的时候认为多重要」,utility 说的是「用起来结果怎么样」。
|
|
1257
|
+
trace?.startStage("utility_weight", weighted.length);
|
|
1258
|
+
const utilityWeighted = this.applyUtilityWeight(weighted);
|
|
1259
|
+
trace?.endStage(utilityWeighted.length, utilityWeighted.map(r => r.score));
|
|
1260
|
+
|
|
1261
|
+
trace?.startStage("confidence_weight", utilityWeighted.length);
|
|
1262
|
+
const confidenceWeighted = this.applyConfidence(utilityWeighted);
|
|
1217
1263
|
trace?.endStage(confidenceWeighted.length, confidenceWeighted.map(r => r.score));
|
|
1218
1264
|
|
|
1219
1265
|
let afterEmotionVector = confidenceWeighted;
|
|
@@ -2163,6 +2209,33 @@ export class MemoryRetriever {
|
|
|
2163
2209
|
*
|
|
2164
2210
|
* No-op when hotnessWeight is 0 or AccessTracker is absent.
|
|
2165
2211
|
*/
|
|
2212
|
+
/**
|
|
2213
|
+
* 价值回溯加权:被用在成功任务里的记忆上浮,被用在失败任务里的下沉。
|
|
2214
|
+
*
|
|
2215
|
+
* 这是 utility 唯一的消费点——它**只喂排序**,不进 tier、不进 decay 豁免、
|
|
2216
|
+
* 更不写回 importance(那一列是阈值语义,≥0.95 会触发永久免衰减,
|
|
2217
|
+
* 详见 memory-utility.ts 抬头的三条理由)。
|
|
2218
|
+
*
|
|
2219
|
+
* No-op when utilityWeight is 0(默认)或条目没有 utility。
|
|
2220
|
+
*/
|
|
2221
|
+
private applyUtilityWeight(results: RetrievalResult[]): RetrievalResult[] {
|
|
2222
|
+
const raw = this.config.utilityWeight;
|
|
2223
|
+
const weight = Math.min(1, Math.max(0, Number.isFinite(raw) ? raw : 0));
|
|
2224
|
+
if (weight <= 0) return results;
|
|
2225
|
+
|
|
2226
|
+
const weighted = results.map((r) => {
|
|
2227
|
+
const utility = readUtility(r.entry.metadata);
|
|
2228
|
+
if (utility === null) return r; // 没数据 ≠ 中性差评,原样放行
|
|
2229
|
+
const factor = 1 + weight * utility;
|
|
2230
|
+
return {
|
|
2231
|
+
...r,
|
|
2232
|
+
score: clamp01(r.score * factor, 0),
|
|
2233
|
+
};
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
return weighted.sort((a, b) => b.score - a.score);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2166
2239
|
private applyHotnessBlend(results: RetrievalResult[]): RetrievalResult[] {
|
|
2167
2240
|
const raw = this.config.hotnessWeight;
|
|
2168
2241
|
const alpha = Math.min(1, Math.max(0, Number.isFinite(raw) ? raw : 0));
|
package/src/runtime-config.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface LocalMemoryConfig {
|
|
|
27
27
|
dimensions?: number;
|
|
28
28
|
taskQuery?: string;
|
|
29
29
|
taskPassage?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Per-request timeout (ms) for the embedding client. Omit to keep the SDK default
|
|
32
|
+
* (600s in openai v7); `RECALLNEST_EMBEDDING_TIMEOUT_MS` overrides this.
|
|
33
|
+
*/
|
|
34
|
+
timeoutMs?: number;
|
|
30
35
|
};
|
|
31
36
|
llm?: {
|
|
32
37
|
apiKey: string;
|
|
@@ -156,6 +161,10 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
156
161
|
dimensions: config.embedding.dimensions,
|
|
157
162
|
taskQuery: config.embedding.taskQuery,
|
|
158
163
|
taskPassage: config.embedding.taskPassage,
|
|
164
|
+
// Env wins over config.json so a single deployment can bound its embed calls
|
|
165
|
+
// without editing the shared file. Both unset → no `timeout` reaches the SDK,
|
|
166
|
+
// which is the behavior every release through v3.0.0 had.
|
|
167
|
+
timeoutMs: envConfig.embeddingTimeoutMs() ?? config.embedding.timeoutMs,
|
|
159
168
|
};
|
|
160
169
|
|
|
161
170
|
const embedder = createEmbedder(embeddingConfig);
|
package/src/scope-policy.ts
CHANGED
|
@@ -38,8 +38,29 @@ export function resolveSessionScope(sessionId?: string): string | undefined {
|
|
|
38
38
|
return normalized ? `session:${normalized}` : undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* scope 过滤的匹配模式(2026-08-16 加,互审 C1/K3 双方独立命中)。
|
|
43
|
+
*
|
|
44
|
+
* - `family`(默认,历史行为):**由 scope 字符串里有没有冒号隐式决定** —— 含冒号走精确,
|
|
45
|
+
* 不含冒号走前缀。`search_memory` 等靠它做 `project` → `project:*` 的家族匹配。
|
|
46
|
+
* - `exact`:调用方明说"我给的就是一个具体 scope 名",一律精确匹配。
|
|
47
|
+
*
|
|
48
|
+
* 为什么要显式模式:隐式规则在调用方**拿真实 scope 名当参数**时会失效——真实 scope 名
|
|
49
|
+
* 里恰好没有冒号(`memory` / `cc` / `global`),于是被当成 family selector。
|
|
50
|
+
* 实测后果:weekly `dream --scope memory` 借前缀把 `memory:pivot`(手写提炼层)
|
|
51
|
+
* 一起卷进同一轮聚类,14 条 insight 的来源横跨两个 scope(2026-08-16 查库坐实)。
|
|
52
|
+
* 修法不是改默认语义(会伤 search_memory),是让 dream 这类"喂具体 scope 名"的
|
|
53
|
+
* 入口显式声明 exact。
|
|
54
|
+
*/
|
|
55
|
+
export type ScopeMatchMode = "exact" | "family";
|
|
56
|
+
|
|
57
|
+
export function matchesScopeFilter(
|
|
58
|
+
rowScope: string,
|
|
59
|
+
scopeFilter?: string[],
|
|
60
|
+
mode: ScopeMatchMode = "family",
|
|
61
|
+
): boolean {
|
|
42
62
|
if (!scopeFilter || scopeFilter.length === 0) return true;
|
|
63
|
+
if (mode === "exact") return scopeFilter.some((scope) => rowScope === scope);
|
|
43
64
|
return scopeFilter.some((scope) => scope.includes(":") ? rowScope === scope : rowScope.startsWith(scope));
|
|
44
65
|
}
|
|
45
66
|
|
package/src/session-engine.ts
CHANGED
|
@@ -8,6 +8,14 @@ import {
|
|
|
8
8
|
} from "./session-schema.js";
|
|
9
9
|
import { buildIdempotentRecordId } from "./idempotency.js";
|
|
10
10
|
|
|
11
|
+
/** 当 summary 里每一句都被 repo-state 规则滤掉时的兜底文案(单一来源,勿再字面重复)。 */
|
|
12
|
+
export const FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
|
|
13
|
+
|
|
14
|
+
/** summary 是否已退化成兜底文案——即「本来写了东西,但整段被清洗空了」。 */
|
|
15
|
+
export function isFallbackSummary(summary: string | null | undefined): boolean {
|
|
16
|
+
return (summary ?? "").trim() === FALLBACK_SUMMARY;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
const REPO_STATE_PATTERN = /\bgit status\b|未提交|staged|uncommitted|已修改文件|modified files?|untracked|新增文件|dirty repo|dirty worktree/iu;
|
|
12
20
|
const SENTENCE_SPLIT_PATTERN = /(?:\r?\n)+|(?<=[。!?!?;;])\s+/u;
|
|
13
21
|
const CHECKPOINT_SANITIZABLE_FIELDS = ["summary", "task", "decisions", "openLoops", "nextActions"] as const;
|
|
@@ -51,7 +59,7 @@ function sanitizeCheckpointSummary(summary: string): SanitizedStringResult {
|
|
|
51
59
|
.map(normalizeCheckpointText)
|
|
52
60
|
.filter(Boolean);
|
|
53
61
|
const kept = parts.filter((part) => !hasRepoStateText(part));
|
|
54
|
-
const value = kept[0] ? kept.join(" ") :
|
|
62
|
+
const value = kept[0] ? kept.join(" ") : FALLBACK_SUMMARY;
|
|
55
63
|
return {
|
|
56
64
|
value,
|
|
57
65
|
changed: kept.length !== parts.length,
|
|
@@ -146,13 +154,12 @@ export function buildSessionCheckpointResult(rawInput: unknown): SessionCheckpoi
|
|
|
146
154
|
resolvedScope: resolveCheckpointScope(sanitizedInput),
|
|
147
155
|
});
|
|
148
156
|
|
|
149
|
-
const
|
|
150
|
-
const isFallbackSummary = record.summary === FALLBACK_SUMMARY;
|
|
157
|
+
const summaryIsFallback = isFallbackSummary(record.summary);
|
|
151
158
|
const hasStructuredContent =
|
|
152
159
|
record.decisions.length > 0 ||
|
|
153
160
|
record.openLoops.length > 0 ||
|
|
154
161
|
record.nextActions.length > 0;
|
|
155
|
-
const quality: CheckpointQuality =
|
|
162
|
+
const quality: CheckpointQuality = summaryIsFallback && !hasStructuredContent ? "minimal" : "rich";
|
|
156
163
|
|
|
157
164
|
return {
|
|
158
165
|
record,
|
package/src/session-output.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
|
|
2
2
|
import { cleanText } from "./context-composer-text.js";
|
|
3
|
+
import { formatIsoAgeLabel } from "./age-label.js";
|
|
4
|
+
import { isFallbackSummary } from "./session-engine.js";
|
|
3
5
|
|
|
4
6
|
function listBlock(label: string, items: string[]): string[] {
|
|
5
7
|
if (items.length === 0) return [];
|
|
@@ -11,7 +13,14 @@ function listBlock(label: string, items: string[]): string[] {
|
|
|
11
13
|
|
|
12
14
|
export function formatCheckpointRecallSummary(record: SessionCheckpointRecord): string {
|
|
13
15
|
const parts: string[] = [];
|
|
14
|
-
const
|
|
16
|
+
const rawSummary = record.summary.trim();
|
|
17
|
+
// summary 整段被 repo-state 清洗规则滤空时会退化成兜底文案,headline 就只剩一句无信息的
|
|
18
|
+
// 套话。此时改用第一条实质内容顶上,并保留标记,让读者能分辨「本来就没写」和「被洗掉了」。
|
|
19
|
+
const substitute = isFallbackSummary(rawSummary)
|
|
20
|
+
? [...record.decisions, ...record.nextActions, ...record.openLoops]
|
|
21
|
+
.find((item) => item.trim().length > 0) ?? ""
|
|
22
|
+
: "";
|
|
23
|
+
const baseSummary = substitute ? `[summary sanitized] ${substitute}` : rawSummary;
|
|
15
24
|
if (baseSummary) {
|
|
16
25
|
parts.push(baseSummary);
|
|
17
26
|
}
|
|
@@ -32,7 +41,7 @@ export function formatCheckpointSaved(record: SessionCheckpointRecord): string {
|
|
|
32
41
|
`Checkpoint ${record.checkpointId.slice(0, 8)}`,
|
|
33
42
|
`Session: ${record.sessionId}`,
|
|
34
43
|
`Scope: ${record.resolvedScope}`,
|
|
35
|
-
`Updated: ${record.updatedAt}`,
|
|
44
|
+
`Updated: ${record.updatedAt} (${formatIsoAgeLabel(record.updatedAt)})`,
|
|
36
45
|
`Summary: ${record.summary}`,
|
|
37
46
|
...listBlock("Decisions", record.decisions),
|
|
38
47
|
...listBlock("Open loops", record.openLoops),
|
|
@@ -48,7 +57,7 @@ export function formatCheckpointSummary(record: SessionCheckpointRecord | null):
|
|
|
48
57
|
`Latest checkpoint`,
|
|
49
58
|
`Session: ${record.sessionId}`,
|
|
50
59
|
`Scope: ${record.resolvedScope}`,
|
|
51
|
-
`Updated: ${record.updatedAt}`,
|
|
60
|
+
`Updated: ${record.updatedAt} (${formatIsoAgeLabel(record.updatedAt)})`,
|
|
52
61
|
`Summary: ${record.summary}`,
|
|
53
62
|
];
|
|
54
63
|
if (record.nextActions.length > 0) {
|
|
@@ -123,7 +132,7 @@ export function formatResumeContext(response: ResumeContextResponse): string {
|
|
|
123
132
|
if (response.latestCheckpoint.resolvedScope) {
|
|
124
133
|
lines.push(`Scope: ${response.latestCheckpoint.resolvedScope}`);
|
|
125
134
|
}
|
|
126
|
-
lines.push(`Updated: ${response.latestCheckpoint.updatedAt}`);
|
|
135
|
+
lines.push(`Updated: ${response.latestCheckpoint.updatedAt} (${formatIsoAgeLabel(response.latestCheckpoint.updatedAt)})`);
|
|
127
136
|
lines.push(`Summary: ${response.latestCheckpoint.summary}`);
|
|
128
137
|
}
|
|
129
138
|
|
package/src/session-store.ts
CHANGED
|
@@ -4,12 +4,11 @@ import { basename, join, resolve } from "node:path";
|
|
|
4
4
|
import { metaDir } from "./compat.js";
|
|
5
5
|
import type { SessionCheckpointRecord } from "./session-schema.js";
|
|
6
6
|
import { SessionCheckpointRecordSchema } from "./session-schema.js";
|
|
7
|
-
import { normalizeCheckpointScope, type CheckpointQuality } from "./session-engine.js";
|
|
7
|
+
import { isFallbackSummary, normalizeCheckpointScope, type CheckpointQuality } from "./session-engine.js";
|
|
8
8
|
|
|
9
|
-
const FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
|
|
10
9
|
|
|
11
10
|
export function classifyCheckpointQuality(record: SessionCheckpointRecord): CheckpointQuality {
|
|
12
|
-
const isFallback = record.summary
|
|
11
|
+
const isFallback = isFallbackSummary(record.summary);
|
|
13
12
|
const hasContent = record.decisions.length > 0 || record.openLoops.length > 0 || record.nextActions.length > 0;
|
|
14
13
|
return isFallback && !hasContent ? "minimal" : "rich";
|
|
15
14
|
}
|
package/src/skill-promotion.ts
CHANGED
|
@@ -30,6 +30,39 @@ export interface PromotionCandidate {
|
|
|
30
30
|
verification?: VerifyResult;
|
|
31
31
|
/** Read-signal evidence aggregated from source entries(Artel 读驱动升华:被真实读过的候选提权)。 */
|
|
32
32
|
readEvidence?: { totalAccess: number; distinctReaders: number };
|
|
33
|
+
/** 这个候选覆盖了几个不同来源(episode)。1 = 同一次经历的重复,不是跨任务规律。 */
|
|
34
|
+
distinctSources?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 被跨来源判据拒掉的簇 —— 「同一个坑踩了 N 次」。
|
|
39
|
+
*
|
|
40
|
+
* 这不是待晋升的规律,是反模式证据:`judgeDistinctSources` 判 `rejected` 时原本只
|
|
41
|
+
* `rejectedSingleSource++` 然后 continue,信号变成一个数字后消失(同文件下方注释
|
|
42
|
+
* 早就写着"走 failure-burst 反模式那条路",而那条路一直不存在)。
|
|
43
|
+
*
|
|
44
|
+
* 字段借自 MemTensor/memmy-agent 的 `Memory/src/service/evolution/negative-experience-pipeline.ts`
|
|
45
|
+
* (其 `NegativeExperienceSource` 枚举里字面就有 `tool_failure_burst`)。
|
|
46
|
+
* **有意只借它列的证据字段**:memmy 那份 schema 还有 `preference`(该怎么做)与
|
|
47
|
+
* `verification`(怎么确认没再犯),但那两格要的是判断不是统计 —— 扫描器编出来的
|
|
48
|
+
* 「该怎么做」比没有更糟。它们列在 `needsJudgement` 里等人或 LLM 填。
|
|
49
|
+
*/
|
|
50
|
+
export interface AntiPatternCandidate {
|
|
51
|
+
/** 目前只有一条来路;留成联合类型是为了将来接别的信号(如工具连续失败)时不改调用方。 */
|
|
52
|
+
source: "single_source_burst";
|
|
53
|
+
/** 撞上它的场景 —— 取簇种子的首行,和 candidates 的 suggestedName 同款提取。 */
|
|
54
|
+
trigger: string;
|
|
55
|
+
/** 反模式描述 —— 簇内多条的共性摘要。 */
|
|
56
|
+
antiPattern: string;
|
|
57
|
+
/** 同一个坑重复了几次。 */
|
|
58
|
+
occurrences: number;
|
|
59
|
+
/** 实际覆盖几个不同来源(判 rejected 说明它 < minDistinctSources)。 */
|
|
60
|
+
distinctSources: number;
|
|
61
|
+
/** 0-1。同一来源里重复越多越强,但不因此就变成"规律"。 */
|
|
62
|
+
evidenceStrength: number;
|
|
63
|
+
sourceEntries: Array<{ id: string; text: string }>;
|
|
64
|
+
/** 扫描器有意不填的字段,等判断补上。写死成数组是为了让调用方能机械检查。 */
|
|
65
|
+
needsJudgement: ReadonlyArray<"preference" | "verification">;
|
|
33
66
|
}
|
|
34
67
|
|
|
35
68
|
export interface PromotionScanResult {
|
|
@@ -40,6 +73,16 @@ export interface PromotionScanResult {
|
|
|
40
73
|
truncatedCases: number;
|
|
41
74
|
/** 向量回填失败(库中无向量)而被跳过的条目数。 */
|
|
42
75
|
vectorlessSkipped: number;
|
|
76
|
+
/** No-silent-caps 披露:条数够、但来源不够(同一个坑重复)而被拒的簇数。 */
|
|
77
|
+
rejectedSingleSource: number;
|
|
78
|
+
/** No-silent-caps 披露:拿不到来源指针、跨来源判据弃权放行的簇数。 */
|
|
79
|
+
abstainedUnknownSource: number;
|
|
80
|
+
/**
|
|
81
|
+
* P4 反模式出口:被判 rejected 的簇的**内容**,不只是计数。
|
|
82
|
+
* 注意与 abstainedUnknownSource 的区别 —— 弃权的簇不进这里:
|
|
83
|
+
* 不知道来源就不能指控它是「同一个坑踩三次」。
|
|
84
|
+
*/
|
|
85
|
+
antiPatterns?: AntiPatternCandidate[];
|
|
43
86
|
}
|
|
44
87
|
|
|
45
88
|
export interface PromotionConfig {
|
|
@@ -58,6 +101,19 @@ export interface PromotionConfig {
|
|
|
58
101
|
/** pattern_to_skill 通道最多探测的 structured patterns 数(每个一次 vectorSearch,
|
|
59
102
|
* default: 1000,按 recency 优先)。 */
|
|
60
103
|
maxPatternProbes: number;
|
|
104
|
+
/**
|
|
105
|
+
* 一个簇要来自几个**不同来源**才算规律(default: 2)。
|
|
106
|
+
*
|
|
107
|
+
* 借鉴 MemOS 的 L1→L2 判据:它数的是「几个不同 episode」,而这里原本数的是
|
|
108
|
+
* 「几条相似 case」——差的是维度不是数字。**同一个坑踩三次**在旧口径下是 3 条
|
|
109
|
+
* 相似 case、会被识别成一条规律;在 MemOS 那儿是 1 个 episode 重复,够不上 L2。
|
|
110
|
+
* 而反复撞同一个坑本该走 failure-burst 生成「Avoid」反模式,跟「这类子任务该怎么做」
|
|
111
|
+
* 是相反的两种东西,现在它们混在同一条通道里。
|
|
112
|
+
*
|
|
113
|
+
* 审计原文的要害:「两个完全不同的任务遇到同类子问题,才诱导出一条可迁移 L2」——
|
|
114
|
+
* 最小值是 2,但要害在「跨任务」不在「2」。设 1 = 关闭该判据(旧行为,对照用)。
|
|
115
|
+
*/
|
|
116
|
+
minDistinctSources: number;
|
|
61
117
|
}
|
|
62
118
|
|
|
63
119
|
const DEFAULT_CONFIG: PromotionConfig = {
|
|
@@ -68,6 +124,7 @@ const DEFAULT_CONFIG: PromotionConfig = {
|
|
|
68
124
|
maxBucketSize: 2_000,
|
|
69
125
|
pageSize: 1_000,
|
|
70
126
|
maxPatternProbes: 1_000,
|
|
127
|
+
minDistinctSources: 2,
|
|
71
128
|
};
|
|
72
129
|
|
|
73
130
|
// ---------------------------------------------------------------------------
|
|
@@ -268,6 +325,98 @@ export function greedyCluster(
|
|
|
268
325
|
return clusters;
|
|
269
326
|
}
|
|
270
327
|
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Source identity (MemOS-style episode counting)
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 一条记录来自「哪一次」——跨来源去重的 key。
|
|
334
|
+
*
|
|
335
|
+
* 三级取值,越靠前越可靠:
|
|
336
|
+
* 1. `metadata.evidenceMemories` —— dream 合成产物指向的簇内真实源(synthesis-contract
|
|
337
|
+
* 2026-08-23 起写入,已验证互不重复且无悬空)。同一批源出来的两条派生物
|
|
338
|
+
* 签名相同 → 算一个来源,不是两个。
|
|
339
|
+
* 2. `tags` 里的 `src:<sessionId 前 8 位>` —— shared-behaviors §5.3 要求 pivot 写入必带。
|
|
340
|
+
* 2026-08-23 实测:`memory:pivot` 池 1699 条覆盖率 99.8%,其中 cases 260 条 100% 覆盖。
|
|
341
|
+
* ⚠️ 但全库只有 2.3%、`category=cases` 全库仅 1.2% —— **这个判据只在 pivot 池成立**,
|
|
342
|
+
* 别拿去当全库口径。
|
|
343
|
+
* 3. 兜底 `day:<scope>:<YYYY-MM-DD>` —— 没有来源指针时退到「同一天同一 scope 算一次」。
|
|
344
|
+
* 粗,但比把同一次经历数成 N 次强。
|
|
345
|
+
*/
|
|
346
|
+
export function extractSourceKey(entry: Pick<MemoryEntry, "metadata" | "scope" | "timestamp">): string {
|
|
347
|
+
let meta: Record<string, unknown> = {};
|
|
348
|
+
try {
|
|
349
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
350
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
351
|
+
meta = parsed as Record<string, unknown>;
|
|
352
|
+
}
|
|
353
|
+
} catch { /* fall through to day bucket */ }
|
|
354
|
+
|
|
355
|
+
const evidence = meta.evidenceMemories;
|
|
356
|
+
if (Array.isArray(evidence) && evidence.length > 0) {
|
|
357
|
+
const ids = evidence.filter((v): v is string => typeof v === "string").sort();
|
|
358
|
+
if (ids.length > 0) return `ev:${ids.join("|")}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Array.isArray(meta.tags)) {
|
|
362
|
+
const src = meta.tags.find((t): t is string => typeof t === "string" && t.startsWith("src:"));
|
|
363
|
+
if (src) return src;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const day = Number.isFinite(entry.timestamp) && entry.timestamp > 0
|
|
367
|
+
? new Date(entry.timestamp).toISOString().slice(0, 10)
|
|
368
|
+
: "unknown";
|
|
369
|
+
return `${UNKNOWN_SOURCE_PREFIX}${entry.scope}:${day}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 兜底 key 的前缀。带这个前缀 = 我们其实不知道它来自哪一次。 */
|
|
373
|
+
export const UNKNOWN_SOURCE_PREFIX = "day:";
|
|
374
|
+
|
|
375
|
+
/** 一组记录覆盖了几个不同来源。1 = 全部来自同一次经历。 */
|
|
376
|
+
export function countDistinctSources(
|
|
377
|
+
entries: ReadonlyArray<Pick<MemoryEntry, "metadata" | "scope" | "timestamp">>,
|
|
378
|
+
): number {
|
|
379
|
+
return new Set(entries.map(extractSourceKey)).size;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export type SourceVerdict =
|
|
383
|
+
| { status: "ok"; distinctSources: number }
|
|
384
|
+
| { status: "rejected"; distinctSources: number }
|
|
385
|
+
/** 没有可信的来源指针 —— 不知道就不判,放行。 */
|
|
386
|
+
| { status: "abstained" };
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 判一个簇是否来自足够多的不同经历。**带弃权路径**。
|
|
390
|
+
*
|
|
391
|
+
* 为什么要弃权而不是一律判:兜底 key 是「同 scope 同一天算一次」,而一天内解决三个
|
|
392
|
+
* 不同问题写三条 case 是完全正常的——那是 3 个 episode,兜底会把它们并成 1 个,
|
|
393
|
+
* 于是一条真规律被当成「同一个坑踩三次」拒掉。**误伤的代价比漏判高**:漏判只是
|
|
394
|
+
* 维持现状(旧行为),误伤是把本来能升华的东西堵死,而且堵得无声无息。
|
|
395
|
+
*
|
|
396
|
+
* 所以判据只在**拿得到真来源指针**时生效。实际效果就是它只在 `memory:pivot` 池成立
|
|
397
|
+
* (2026-08-23 实测该池 cases 100% 带 `src:`,而全库 cases 只有 1.2%)——
|
|
398
|
+
* 这恰好是本方案的适用范围,不是巧合:§5.3 要求 pivot 写入必带来源指针,
|
|
399
|
+
* 那条规矩本来就是为了让「回原文核对」成为可能,跨来源计数是它的另一个消费者。
|
|
400
|
+
*/
|
|
401
|
+
export function judgeDistinctSources(
|
|
402
|
+
entries: ReadonlyArray<Pick<MemoryEntry, "metadata" | "scope" | "timestamp">>,
|
|
403
|
+
minDistinctSources: number,
|
|
404
|
+
): SourceVerdict {
|
|
405
|
+
if (minDistinctSources <= 1) {
|
|
406
|
+
return { status: "ok", distinctSources: countDistinctSources(entries) };
|
|
407
|
+
}
|
|
408
|
+
const keys = entries.map(extractSourceKey);
|
|
409
|
+
const known = keys.filter((k) => !k.startsWith(UNKNOWN_SOURCE_PREFIX));
|
|
410
|
+
if (known.length === 0) return { status: "abstained" };
|
|
411
|
+
|
|
412
|
+
// 有真指针的按真指针数;没指针的每条各算一次未知来源——不能让「不知道」
|
|
413
|
+
// 白白顶上一个来源,也不能因为混了几条没指针的就整簇弃权。
|
|
414
|
+
const distinctSources = new Set(keys).size;
|
|
415
|
+
return distinctSources >= minDistinctSources
|
|
416
|
+
? { status: "ok", distinctSources }
|
|
417
|
+
: { status: "rejected", distinctSources };
|
|
418
|
+
}
|
|
419
|
+
|
|
271
420
|
// ---------------------------------------------------------------------------
|
|
272
421
|
// Read evidence (Artel-style read-driven promotion)
|
|
273
422
|
// ---------------------------------------------------------------------------
|
|
@@ -348,6 +497,9 @@ export async function scanForPromotions(
|
|
|
348
497
|
scannedPatterns: patterns.length,
|
|
349
498
|
truncatedCases: 0,
|
|
350
499
|
vectorlessSkipped: 0,
|
|
500
|
+
rejectedSingleSource: 0,
|
|
501
|
+
abstainedUnknownSource: 0,
|
|
502
|
+
antiPatterns: [],
|
|
351
503
|
};
|
|
352
504
|
|
|
353
505
|
// 2. Cluster cases bucket-by-bucket (topicTag, untagged fallback)。
|
|
@@ -378,6 +530,29 @@ export async function scanForPromotions(
|
|
|
378
530
|
if (cluster.members.length < cfg.minCaseOccurrences) continue;
|
|
379
531
|
if (caseCandidates >= cfg.maxCandidates) break;
|
|
380
532
|
|
|
533
|
+
// 跨来源判据:条数够不代表是规律。同一次经历重复 N 条走的是 failure-burst
|
|
534
|
+
// 反模式那条路,不是「这类子任务该怎么做」这条。拿不到来源指针时弃权放行。
|
|
535
|
+
const sourceVerdict = judgeDistinctSources(cluster.members, cfg.minDistinctSources);
|
|
536
|
+
if (sourceVerdict.status === "rejected") {
|
|
537
|
+
result.rejectedSingleSource++;
|
|
538
|
+
// P4:不晋升,但也不让它无声消失 —— 同一个坑重复本身就是要记的东西。
|
|
539
|
+
// 仍然 continue,晋升判据分毫未动。
|
|
540
|
+
result.antiPatterns.push({
|
|
541
|
+
source: "single_source_burst",
|
|
542
|
+
trigger: extractName(cluster.seed.text),
|
|
543
|
+
antiPattern: summarizeCluster(cluster.members),
|
|
544
|
+
occurrences: cluster.members.length,
|
|
545
|
+
distinctSources: sourceVerdict.distinctSources,
|
|
546
|
+
// Bayesian smoothing,与 candidates 的 confidence 同款,饱和于 1。
|
|
547
|
+
evidenceStrength: cluster.members.length / (cluster.members.length + 2),
|
|
548
|
+
sourceEntries: cluster.members.map(m => ({ id: m.id, text: m.text })),
|
|
549
|
+
needsJudgement: ["preference", "verification"],
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (sourceVerdict.status === "abstained") result.abstainedUnknownSource++;
|
|
554
|
+
const distinctSources = sourceVerdict.status === "ok" ? sourceVerdict.distinctSources : undefined;
|
|
555
|
+
|
|
381
556
|
// Compute average intra-cluster similarity for scoring
|
|
382
557
|
const avgSim = computeAverageIntraClusterSimilarity(cluster.members);
|
|
383
558
|
|
|
@@ -397,6 +572,7 @@ export async function scanForPromotions(
|
|
|
397
572
|
caseReadEvidence.distinctReaders,
|
|
398
573
|
),
|
|
399
574
|
readEvidence: caseReadEvidence,
|
|
575
|
+
distinctSources,
|
|
400
576
|
});
|
|
401
577
|
}
|
|
402
578
|
}
|
|
@@ -433,6 +609,16 @@ export async function scanForPromotions(
|
|
|
433
609
|
|
|
434
610
|
if (similarCases.length < 2) continue;
|
|
435
611
|
|
|
612
|
+
// 同一判据也管 pattern→skill:MemOS 的 L2/L3→Skill 门就是「N 个独立 episode」。
|
|
613
|
+
// 支撑证据全来自同一次经历时,这条 skill 学的是那一次,不是那一类。
|
|
614
|
+
const patternVerdict = judgeDistinctSources(similarCases.map(h => h.entry), cfg.minDistinctSources);
|
|
615
|
+
if (patternVerdict.status === "rejected") {
|
|
616
|
+
result.rejectedSingleSource++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (patternVerdict.status === "abstained") result.abstainedUnknownSource++;
|
|
620
|
+
const patternDistinctSources = patternVerdict.status === "ok" ? patternVerdict.distinctSources : undefined;
|
|
621
|
+
|
|
436
622
|
// A2: zero-LLM verifier — pattern 当 draft,supporting cases 当 evidence。
|
|
437
623
|
// steps 传 [] 不是漏传:pattern.text 已含完整 Steps 块,resonance 用整段文本即可;
|
|
438
624
|
// extractSteps(pattern.text) 仅用于下面的 suggestedImplementation 字段。
|
|
@@ -462,6 +648,7 @@ export async function scanForPromotions(
|
|
|
462
648
|
),
|
|
463
649
|
verification,
|
|
464
650
|
readEvidence: patternReadEvidence,
|
|
651
|
+
distinctSources: patternDistinctSources,
|
|
465
652
|
});
|
|
466
653
|
}
|
|
467
654
|
|
|
@@ -495,6 +682,33 @@ export function formatPromotionResult(result: PromotionScanResult): string {
|
|
|
495
682
|
if (result.vectorlessSkipped > 0) {
|
|
496
683
|
lines.push(`⚠️ ${result.vectorlessSkipped} entries skipped (no vector in store).`);
|
|
497
684
|
}
|
|
685
|
+
if (result.abstainedUnknownSource > 0) {
|
|
686
|
+
lines.push(
|
|
687
|
+
`ℹ️ ${result.abstainedUnknownSource} cluster(s) 没有可信来源指针,跨来源判据已弃权放行`
|
|
688
|
+
+ `(这些 scope 里的条目没带 src: tag,判了就是瞎判)。`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (result.rejectedSingleSource > 0) {
|
|
692
|
+
lines.push(
|
|
693
|
+
`⚠️ ${result.rejectedSingleSource} cluster(s) had enough entries but too few distinct sources`
|
|
694
|
+
+ ` — 同一次经历的重复不算规律(不晋升,改走下面的反模式清单)。`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// P4:被拒的簇在这里落地。只显示计数等于让信号消失在一个数字里 ——
|
|
699
|
+
// 消费者看到的是这段文本,不是 result 对象。
|
|
700
|
+
// `?.` 不是防御性编程的装饰:PromotionScanResult 是导出接口,旧调用方构造的对象
|
|
701
|
+
// 没有这个字段,运行时会是 undefined(TS 编译期拦不住外部构造)。
|
|
702
|
+
if (result.antiPatterns?.length) {
|
|
703
|
+
lines.push(`\n🔁 ${result.antiPatterns.length} anti-pattern(s) — 同一个坑重复出现,不是规律但要记:\n`);
|
|
704
|
+
for (const [i, ap] of result.antiPatterns.entries()) {
|
|
705
|
+
lines.push(`### A${i + 1}. ${ap.trigger}`);
|
|
706
|
+
lines.push(`重复 ${ap.occurrences} 次,来自 ${ap.distinctSources} 个来源(证据强度 ${(ap.evidenceStrength * 100).toFixed(0)}%)`);
|
|
707
|
+
lines.push(`共性: ${ap.antiPattern}`);
|
|
708
|
+
lines.push(`待补判断: ${ap.needsJudgement.join(" / ")} — 扫描器只出证据,「该怎么做」和「怎么确认没再犯」要人或 LLM 填。`);
|
|
709
|
+
lines.push("");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
498
712
|
|
|
499
713
|
if (result.candidates.length === 0) {
|
|
500
714
|
lines.push("No promotion candidates found.");
|
|
@@ -507,7 +721,10 @@ export function formatPromotionResult(result: PromotionScanResult): string {
|
|
|
507
721
|
lines.push(`### ${i + 1}. [${c.type}] ${c.suggestedName}`);
|
|
508
722
|
lines.push(`Confidence: ${(c.confidence * 100).toFixed(1)}%`);
|
|
509
723
|
lines.push(`Description: ${c.suggestedDescription}`);
|
|
510
|
-
lines.push(
|
|
724
|
+
lines.push(
|
|
725
|
+
`Sources: ${c.sourceEntries.length} entries`
|
|
726
|
+
+ (c.distinctSources !== undefined ? ` from ${c.distinctSources} distinct source(s)` : ""),
|
|
727
|
+
);
|
|
511
728
|
if (c.readEvidence) {
|
|
512
729
|
lines.push(`Read evidence: ${c.readEvidence.totalAccess} total reads, ${c.readEvidence.distinctReaders} distinct reader(s)`);
|
|
513
730
|
}
|