recallnest 3.0.0 → 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.
Files changed (184) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +222 -254
  3. package/README_CN.md +206 -243
  4. package/package.json +3 -2
  5. package/src/capture-engine.ts +26 -4
  6. package/src/context-composer-stable.ts +5 -1
  7. package/src/embedder.ts +24 -0
  8. package/src/env-config.ts +18 -0
  9. package/src/ingest.ts +225 -17
  10. package/src/kg-extractor.ts +17 -0
  11. package/src/memory-output.ts +35 -0
  12. package/src/pivot-apply.ts +52 -1
  13. package/src/pivot-evidence.ts +128 -0
  14. package/src/ppr-traversal.ts +25 -1
  15. package/src/runtime-config.ts +9 -0
  16. package/src/session-engine.ts +11 -4
  17. package/src/session-output.ts +9 -1
  18. package/src/session-store.ts +2 -3
  19. package/src/skill-promotion.ts +67 -1
  20. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  21. package/src/__tests__/activity-counter.test.ts +0 -125
  22. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  23. package/src/__tests__/admission-control.test.ts +0 -206
  24. package/src/__tests__/age-label.test.ts +0 -33
  25. package/src/__tests__/anchor-generator.test.ts +0 -67
  26. package/src/__tests__/api-server-security.test.ts +0 -102
  27. package/src/__tests__/auto-consolidation.test.ts +0 -168
  28. package/src/__tests__/auto-recall.test.ts +0 -131
  29. package/src/__tests__/babel-integration.test.ts +0 -144
  30. package/src/__tests__/batch-dedup.test.ts +0 -65
  31. package/src/__tests__/belief-history.test.ts +0 -220
  32. package/src/__tests__/canary-eval.test.ts +0 -114
  33. package/src/__tests__/capture-engine.test.ts +0 -1328
  34. package/src/__tests__/capture-heuristic.test.ts +0 -171
  35. package/src/__tests__/cascade-forget.test.ts +0 -210
  36. package/src/__tests__/category-min-scores.test.ts +0 -116
  37. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  38. package/src/__tests__/circuit-breaker.test.ts +0 -99
  39. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  40. package/src/__tests__/confidence-metadata.test.ts +0 -125
  41. package/src/__tests__/confidence-tracker.test.ts +0 -261
  42. package/src/__tests__/conflict-advisor.test.ts +0 -206
  43. package/src/__tests__/conflict-escalation.test.ts +0 -149
  44. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  45. package/src/__tests__/conflict-output.test.ts +0 -85
  46. package/src/__tests__/conflict-store.test.ts +0 -418
  47. package/src/__tests__/connector-v1.test.ts +0 -181
  48. package/src/__tests__/consolidation-engine.test.ts +0 -520
  49. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  50. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  51. package/src/__tests__/context-composer-pins.test.ts +0 -64
  52. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  53. package/src/__tests__/context-reconstructor.test.ts +0 -65
  54. package/src/__tests__/context-renderer.test.ts +0 -91
  55. package/src/__tests__/continuity-eval.test.ts +0 -275
  56. package/src/__tests__/continuity-schema.test.ts +0 -97
  57. package/src/__tests__/contracts.test.ts +0 -262
  58. package/src/__tests__/conversation-importer.test.ts +0 -692
  59. package/src/__tests__/core-recall-eval.test.ts +0 -235
  60. package/src/__tests__/core-summary.test.ts +0 -109
  61. package/src/__tests__/data-checkup.test.ts +0 -304
  62. package/src/__tests__/decay-exemption.test.ts +0 -129
  63. package/src/__tests__/dedup-actions.test.ts +0 -58
  64. package/src/__tests__/dedup-guard.test.ts +0 -39
  65. package/src/__tests__/deterministic-id.test.ts +0 -39
  66. package/src/__tests__/distill-lock.test.ts +0 -305
  67. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  68. package/src/__tests__/doctor.test.ts +0 -319
  69. package/src/__tests__/dream-pipeline.test.ts +0 -820
  70. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  71. package/src/__tests__/dream-write-batching.test.ts +0 -211
  72. package/src/__tests__/drill-down.test.ts +0 -108
  73. package/src/__tests__/embedder.test.ts +0 -122
  74. package/src/__tests__/emotion-decay.test.ts +0 -46
  75. package/src/__tests__/emotion-detector.test.ts +0 -56
  76. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  77. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  78. package/src/__tests__/entity-resolver.test.ts +0 -58
  79. package/src/__tests__/env-config-invariant.test.ts +0 -80
  80. package/src/__tests__/env-config.test.ts +0 -211
  81. package/src/__tests__/error-signature-boost.test.ts +0 -87
  82. package/src/__tests__/error-signature.test.ts +0 -95
  83. package/src/__tests__/error-taxonomy.test.ts +0 -129
  84. package/src/__tests__/ethics-layer.test.ts +0 -476
  85. package/src/__tests__/event-segmenter.test.ts +0 -153
  86. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  87. package/src/__tests__/frequency-tracker.test.ts +0 -113
  88. package/src/__tests__/freshness.test.ts +0 -186
  89. package/src/__tests__/graph-export.test.ts +0 -489
  90. package/src/__tests__/hotness-blend.test.ts +0 -283
  91. package/src/__tests__/ingest-chunking.test.ts +0 -44
  92. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  93. package/src/__tests__/ingest.test.ts +0 -360
  94. package/src/__tests__/interference-detector.test.ts +0 -203
  95. package/src/__tests__/kg-extractor.test.ts +0 -192
  96. package/src/__tests__/kg-store.test.ts +0 -300
  97. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  98. package/src/__tests__/llm-consolidation.test.ts +0 -226
  99. package/src/__tests__/maintenance-locks.test.ts +0 -197
  100. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  101. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  102. package/src/__tests__/memory-boundaries.test.ts +0 -282
  103. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  104. package/src/__tests__/memory-evolution.test.ts +0 -271
  105. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  106. package/src/__tests__/memory-lint.test.ts +0 -421
  107. package/src/__tests__/memory-output.test.ts +0 -224
  108. package/src/__tests__/memory-promotion.test.ts +0 -315
  109. package/src/__tests__/memory-utility.test.ts +0 -256
  110. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  111. package/src/__tests__/multi-vector.test.ts +0 -242
  112. package/src/__tests__/narrative-architecture.test.ts +0 -456
  113. package/src/__tests__/noise-filter.test.ts +0 -205
  114. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  115. package/src/__tests__/obsidian-connector.test.ts +0 -363
  116. package/src/__tests__/omitted-reporting.test.ts +0 -98
  117. package/src/__tests__/openai-contract.test.ts +0 -513
  118. package/src/__tests__/pending-review.test.ts +0 -77
  119. package/src/__tests__/pii-redact.test.ts +0 -333
  120. package/src/__tests__/pivot-apply.test.ts +0 -373
  121. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  122. package/src/__tests__/pivot-distill.test.ts +0 -1178
  123. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  124. package/src/__tests__/ppr-traversal.test.ts +0 -256
  125. package/src/__tests__/prediction-engine.test.ts +0 -349
  126. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  127. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  128. package/src/__tests__/preference-matcher.test.ts +0 -282
  129. package/src/__tests__/preference-slots.test.ts +0 -205
  130. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  131. package/src/__tests__/prospective-memory.test.ts +0 -283
  132. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  133. package/src/__tests__/query-entity-detector.test.ts +0 -171
  134. package/src/__tests__/query-expander-alias.test.ts +0 -146
  135. package/src/__tests__/query-expander.test.ts +0 -34
  136. package/src/__tests__/recall-gate.test.ts +0 -266
  137. package/src/__tests__/recall-governor.test.ts +0 -227
  138. package/src/__tests__/recall-mode.test.ts +0 -64
  139. package/src/__tests__/resolve-db-path.test.ts +0 -130
  140. package/src/__tests__/resolve-tier.test.ts +0 -64
  141. package/src/__tests__/result-synthesizer.test.ts +0 -198
  142. package/src/__tests__/resume-context.test.ts +0 -4898
  143. package/src/__tests__/retrieval-eval.test.ts +0 -163
  144. package/src/__tests__/retrieval-trace.test.ts +0 -213
  145. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  146. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  147. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  148. package/src/__tests__/retroactive-boost.test.ts +0 -223
  149. package/src/__tests__/salience-scoring.test.ts +0 -106
  150. package/src/__tests__/scope-inventory.test.ts +0 -200
  151. package/src/__tests__/scope-policy.test.ts +0 -69
  152. package/src/__tests__/scope-suggester.test.ts +0 -80
  153. package/src/__tests__/session-checkpoint.test.ts +0 -291
  154. package/src/__tests__/session-distiller.test.ts +0 -640
  155. package/src/__tests__/session-gc.test.ts +0 -215
  156. package/src/__tests__/skill-engine.test.ts +0 -552
  157. package/src/__tests__/skill-promotion.test.ts +0 -654
  158. package/src/__tests__/skill-verifier.test.ts +0 -122
  159. package/src/__tests__/source-heartbeat.test.ts +0 -254
  160. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  161. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  162. package/src/__tests__/store-read-consistency.test.ts +0 -122
  163. package/src/__tests__/store-schema-migration.test.ts +0 -87
  164. package/src/__tests__/store-scope-audit.test.ts +0 -81
  165. package/src/__tests__/store-upsert.test.ts +0 -201
  166. package/src/__tests__/supersede-chain.test.ts +0 -154
  167. package/src/__tests__/synthesis-contract.test.ts +0 -252
  168. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  169. package/src/__tests__/temporal-parser.test.ts +0 -241
  170. package/src/__tests__/temporal-validity.test.ts +0 -160
  171. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  172. package/src/__tests__/term-registry.test.ts +0 -121
  173. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  174. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  175. package/src/__tests__/topic-tag.test.ts +0 -113
  176. package/src/__tests__/trusted-memory.test.ts +0 -867
  177. package/src/__tests__/ui-server-security.test.ts +0 -93
  178. package/src/__tests__/usage-tracker.test.ts +0 -287
  179. package/src/__tests__/utility-weight.test.ts +0 -57
  180. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  181. package/src/__tests__/version-manager.test.ts +0 -248
  182. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  183. package/src/__tests__/workflow-observation.test.ts +0 -433
  184. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -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
+ }
@@ -10,7 +10,11 @@ import type { KGTriple, NeighborhoodResult } from "./kg-store.js";
10
10
  // ============================================================================
11
11
 
12
12
  export interface PPRConfig {
13
- /** Teleport probability back to seed nodes (default: 0.85) */
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,
@@ -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);
@@ -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(" ") : "Checkpoint captured current task state without repo-state details.";
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 FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
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 = isFallbackSummary && !hasStructuredContent ? "minimal" : "rich";
162
+ const quality: CheckpointQuality = summaryIsFallback && !hasStructuredContent ? "minimal" : "rich";
156
163
 
157
164
  return {
158
165
  record,
@@ -1,6 +1,7 @@
1
1
  import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
2
2
  import { cleanText } from "./context-composer-text.js";
3
3
  import { formatIsoAgeLabel } from "./age-label.js";
4
+ import { isFallbackSummary } from "./session-engine.js";
4
5
 
5
6
  function listBlock(label: string, items: string[]): string[] {
6
7
  if (items.length === 0) return [];
@@ -12,7 +13,14 @@ function listBlock(label: string, items: string[]): string[] {
12
13
 
13
14
  export function formatCheckpointRecallSummary(record: SessionCheckpointRecord): string {
14
15
  const parts: string[] = [];
15
- const baseSummary = record.summary.trim();
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;
16
24
  if (baseSummary) {
17
25
  parts.push(baseSummary);
18
26
  }
@@ -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 === FALLBACK_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
  }
@@ -34,6 +34,37 @@ export interface PromotionCandidate {
34
34
  distinctSources?: number;
35
35
  }
36
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">;
66
+ }
67
+
37
68
  export interface PromotionScanResult {
38
69
  candidates: PromotionCandidate[];
39
70
  scannedCases: number;
@@ -46,6 +77,12 @@ export interface PromotionScanResult {
46
77
  rejectedSingleSource: number;
47
78
  /** No-silent-caps 披露:拿不到来源指针、跨来源判据弃权放行的簇数。 */
48
79
  abstainedUnknownSource: number;
80
+ /**
81
+ * P4 反模式出口:被判 rejected 的簇的**内容**,不只是计数。
82
+ * 注意与 abstainedUnknownSource 的区别 —— 弃权的簇不进这里:
83
+ * 不知道来源就不能指控它是「同一个坑踩三次」。
84
+ */
85
+ antiPatterns?: AntiPatternCandidate[];
49
86
  }
50
87
 
51
88
  export interface PromotionConfig {
@@ -462,6 +499,7 @@ export async function scanForPromotions(
462
499
  vectorlessSkipped: 0,
463
500
  rejectedSingleSource: 0,
464
501
  abstainedUnknownSource: 0,
502
+ antiPatterns: [],
465
503
  };
466
504
 
467
505
  // 2. Cluster cases bucket-by-bucket (topicTag, untagged fallback)。
@@ -497,6 +535,19 @@ export async function scanForPromotions(
497
535
  const sourceVerdict = judgeDistinctSources(cluster.members, cfg.minDistinctSources);
498
536
  if (sourceVerdict.status === "rejected") {
499
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
+ });
500
551
  continue;
501
552
  }
502
553
  if (sourceVerdict.status === "abstained") result.abstainedUnknownSource++;
@@ -640,10 +691,25 @@ export function formatPromotionResult(result: PromotionScanResult): string {
640
691
  if (result.rejectedSingleSource > 0) {
641
692
  lines.push(
642
693
  `⚠️ ${result.rejectedSingleSource} cluster(s) had enough entries but too few distinct sources`
643
- + ` — 同一次经历的重复不算规律(走 failure-burst 反模式那条路,不是这条)。`,
694
+ + ` — 同一次经历的重复不算规律(不晋升,改走下面的反模式清单)。`,
644
695
  );
645
696
  }
646
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
+ }
712
+
647
713
  if (result.candidates.length === 0) {
648
714
  lines.push("No promotion candidates found.");
649
715
  return lines.join("\n");