recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,197 @@
1
+ import { extractBoundaryMetadata, extractCanonicalKey } from "./memory-boundaries.js";
2
+ import { bestSummaryText, cleanText, stripConversationMarkers } from "./context-composer-text.js";
3
+ import type { RetrievalResult } from "./retriever.js";
4
+ import {
5
+ CASE_CUE_TERMS,
6
+ WORKFLOW_CUE_TERMS,
7
+ containsAnyTerm,
8
+ countTermHits,
9
+ looksLikeLowSignalTaskResult,
10
+ looksLikePlanishTaskResult,
11
+ normalizeText,
12
+ } from "./term-registry.js";
13
+
14
+ export type TaskCategory = "patterns" | "cases";
15
+
16
+ function parseResultMetadata(metadata?: string): Record<string, unknown> | null {
17
+ try {
18
+ const parsed = JSON.parse(metadata || "{}");
19
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
20
+ ? parsed as Record<string, unknown>
21
+ : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function formatWorkflowPatternResult(result: RetrievalResult): string {
28
+ const metadata = parseResultMetadata(result.entry.metadata);
29
+ const workflowPattern = metadata?.workflowPattern;
30
+ if (!workflowPattern || typeof workflowPattern !== "object" || Array.isArray(workflowPattern)) {
31
+ return cleanText(stripConversationMarkers(result.entry.text), 220);
32
+ }
33
+
34
+ const pattern = workflowPattern as Record<string, unknown>;
35
+ const title = typeof pattern.title === "string" ? pattern.title.trim() : "";
36
+ const trigger = typeof pattern.trigger === "string" ? pattern.trigger.trim() : "";
37
+ const normalizedTools = Array.isArray(pattern.tools)
38
+ ? pattern.tools.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
39
+ : [];
40
+ const steps = Array.isArray(pattern.steps)
41
+ ? pattern.steps.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
42
+ : [];
43
+
44
+ const parts = [
45
+ title ? `Workflow pattern: ${title}` : "",
46
+ normalizedTools.length > 0 ? `Tools: ${normalizedTools.join(", ")}` : "",
47
+ trigger ? `Use when: ${trigger}` : "",
48
+ steps.length > 0
49
+ ? `Steps: ${steps.slice(0, 1).map((step, index) => `${index + 1}. ${step}`).join(" ")}`
50
+ : "",
51
+ ].filter(Boolean);
52
+
53
+ return cleanText(parts.join(" "), 220);
54
+ }
55
+
56
+ function looksLikeStructuredPatternResult(text: string): boolean {
57
+ const normalized = normalizeText(text);
58
+ return (
59
+ normalized.startsWith("workflow pattern:") ||
60
+ normalized.startsWith("pattern:") ||
61
+ (normalized.includes("use when:") && normalized.includes("steps:")) ||
62
+ (normalized.includes("流程") && normalized.includes("步骤"))
63
+ );
64
+ }
65
+
66
+ function looksLikeStructuredCaseResult(text: string): boolean {
67
+ const normalized = normalizeText(text);
68
+ return (
69
+ normalized.startsWith("case:") ||
70
+ normalized.startsWith("案例:") ||
71
+ (normalized.includes("problem:") && (
72
+ normalized.includes("solution:") ||
73
+ normalized.includes("fix:") ||
74
+ normalized.includes("resolved:") ||
75
+ normalized.includes("workaround:")
76
+ )) ||
77
+ (normalized.includes("问题:") && (
78
+ normalized.includes("解决:") ||
79
+ normalized.includes("修复:") ||
80
+ normalized.includes("方案:") ||
81
+ normalized.includes("原因:")
82
+ ))
83
+ );
84
+ }
85
+
86
+ function looksLikeStructuredTaskResult(category: TaskCategory, text: string): boolean {
87
+ return category === "patterns"
88
+ ? looksLikeStructuredPatternResult(text)
89
+ : looksLikeStructuredCaseResult(text);
90
+ }
91
+
92
+ export function formatTaskResult(result: RetrievalResult): string {
93
+ if (result.entry.category === "patterns") {
94
+ return formatWorkflowPatternResult(result);
95
+ }
96
+ const text = bestSummaryText(result.entry.text, result.entry.metadata);
97
+ return cleanText(text, 220);
98
+ }
99
+
100
+ export function isDurableMemoryScope(scope: string): boolean {
101
+ return scope.startsWith("memory:") || scope.startsWith("asset:");
102
+ }
103
+
104
+ export function scoreWorkflowCandidate(result: RetrievalResult, scope?: string): number {
105
+ const text = normalizeText(result.entry.text);
106
+ const cueHits = WORKFLOW_CUE_TERMS.reduce((count, term) => count + (text.includes(term) ? 1 : 0), 0);
107
+ if (cueHits === 0) return -1;
108
+
109
+ let score = cueHits * 4 + result.score;
110
+ if (result.entry.category === "patterns") score += 3;
111
+ if (result.entry.category === "cases") score += 1;
112
+ if (scope && result.entry.scope === scope) score += 2;
113
+ if (isDurableMemoryScope(result.entry.scope)) score += 2;
114
+ return score;
115
+ }
116
+
117
+ export function isDurableTaskCandidate(result: RetrievalResult): boolean {
118
+ if (isDurableMemoryScope(result.entry.scope)) return true;
119
+ return extractBoundaryMetadata(result.entry.metadata)?.layer === "durable";
120
+ }
121
+
122
+ export function taskResultKey(result: RetrievalResult): string {
123
+ return extractCanonicalKey(result.entry.metadata) || normalizeText(stripConversationMarkers(result.entry.text));
124
+ }
125
+
126
+ export function scoreTaskCandidate(category: TaskCategory, result: RetrievalResult): number {
127
+ const stripped = stripConversationMarkers(result.entry.text);
128
+ const normalized = normalizeText(stripped);
129
+ const boundary = extractBoundaryMetadata(result.entry.metadata);
130
+ const cueTerms = category === "patterns" ? WORKFLOW_CUE_TERMS : CASE_CUE_TERMS;
131
+ const cueHits = countTermHits(normalized, cueTerms);
132
+ const structured = looksLikeStructuredTaskResult(category, stripped);
133
+
134
+ let score = result.score;
135
+ if (isDurableMemoryScope(result.entry.scope)) score += 5;
136
+ if (boundary?.layer === "durable") score += 4;
137
+ if (boundary?.layer === "working") score += 1;
138
+ if (boundary?.layer === "evidence") score -= 4;
139
+ if (extractCanonicalKey(result.entry.metadata)) score += 2;
140
+ if (result.entry.category === category) score += 2;
141
+ if (structured) score += 3;
142
+ score += Math.min(cueHits, 3) * 1.5;
143
+ if (looksLikePlanishTaskResult(normalized)) score -= 5;
144
+ return score;
145
+ }
146
+
147
+ export function isTaskCandidateUseful(category: TaskCategory, result: RetrievalResult): boolean {
148
+ const stripped = stripConversationMarkers(result.entry.text);
149
+ const normalized = normalizeText(stripped);
150
+ if (!normalized || normalized.length < 12) return false;
151
+ if (looksLikeLowSignalTaskResult(normalized)) return false;
152
+ const cueTerms = category === "patterns" ? WORKFLOW_CUE_TERMS : CASE_CUE_TERMS;
153
+ const cueHits = countTermHits(normalized, cueTerms);
154
+ const structured = looksLikeStructuredTaskResult(category, stripped);
155
+ const durable = isDurableTaskCandidate(result);
156
+ const canonicalKey = extractCanonicalKey(result.entry.metadata);
157
+ const durableMemoryScope = isDurableMemoryScope(result.entry.scope);
158
+
159
+ if (durable) {
160
+ if (looksLikePlanishTaskResult(normalized) && !structured) return false;
161
+ if (category === "cases" && !structured && !durableMemoryScope && !canonicalKey) return false;
162
+ return structured || cueHits > 0 || result.entry.category === category;
163
+ }
164
+
165
+ // Non-durable pattern and case hits are often transcript fragments or
166
+ // maintenance notes that happen to mention workflow/case cues. Keep them
167
+ // out unless they are explicitly structured.
168
+ if (category === "patterns" && !structured) return false;
169
+ // For non-durable cases, allow high-quality unstructured ones through:
170
+ // - must have case-related cue terms (problem/solution/fix/error etc.)
171
+ // - must have sufficient text length (not a one-liner)
172
+ // - must have reasonable vector similarity
173
+ if (category === "cases" && !structured) {
174
+ const hasSubstantialContent = normalized.length >= 80;
175
+ const hasHighSimilarity = result.score >= 0.70;
176
+ if (!(hasSubstantialContent && hasHighSimilarity && cueHits > 0)) return false;
177
+ }
178
+
179
+ if (looksLikePlanishTaskResult(normalized)) return false;
180
+ if (structured) return true;
181
+
182
+ if (category === "patterns") {
183
+ return cueHits >= 2;
184
+ }
185
+
186
+ return cueHits >= 2 && containsAnyTerm(normalized, [
187
+ "解决",
188
+ "修复",
189
+ "方案",
190
+ "恢复",
191
+ "workaround",
192
+ "root cause",
193
+ "resolved",
194
+ "solution",
195
+ "fixed",
196
+ ]);
197
+ }
@@ -0,0 +1,137 @@
1
+ import { dedupeText } from "./context-composer-text.js";
2
+ import { taskMentionsScopeIdentity } from "./context-composer-scope.js";
3
+ import {
4
+ buildCaseFallbackQuery,
5
+ buildContinuityFallbackPatterns,
6
+ buildWorkflowFallbackQuery,
7
+ } from "./context-composer-task-fallbacks.js";
8
+ import type { RetrievalResult } from "./retriever.js";
9
+ import {
10
+ looksLikeCaseFallbackTask,
11
+ } from "./term-registry.js";
12
+ import {
13
+ countPatternCueCoverage,
14
+ countTaskSpecificTextMatches,
15
+ looksLikeGenericWindowHandoffTask,
16
+ selectRelevantPatterns,
17
+ selectTaskResults,
18
+ selectWorkflowFallbackCandidates,
19
+ type TaskCategory,
20
+ } from "./context-composer-task-selection.js";
21
+
22
+ export async function buildTaskResultSections(params: {
23
+ retrieveCandidates: (args: {
24
+ category?: TaskCategory;
25
+ query: string;
26
+ limit: number;
27
+ scope?: string;
28
+ }) => Promise<RetrievalResult[]>;
29
+ patternResults: RetrievalResult[];
30
+ caseResults: RetrievalResult[];
31
+ continuityTask: boolean;
32
+ hasLatestCheckpoint: boolean;
33
+ taskLimit: number;
34
+ taskSeed?: string;
35
+ scope?: string;
36
+ strongWorkflowCueTerms?: string[];
37
+ }): Promise<{ relevantPatterns: string[]; recentCases: string[] }> {
38
+ const {
39
+ retrieveCandidates,
40
+ patternResults,
41
+ caseResults,
42
+ continuityTask,
43
+ hasLatestCheckpoint,
44
+ taskLimit,
45
+ taskSeed,
46
+ scope,
47
+ strongWorkflowCueTerms,
48
+ } = params;
49
+
50
+ const retrievedPatterns = selectTaskResults("patterns", patternResults, taskLimit, {
51
+ scope,
52
+ taskSeed,
53
+ });
54
+ const allowSparseCheckpointSupplement = Boolean(
55
+ hasLatestCheckpoint &&
56
+ scope?.startsWith("project:") &&
57
+ taskMentionsScopeIdentity(taskSeed, scope) &&
58
+ retrievedPatterns.length <= 1,
59
+ );
60
+ const shouldProvideContinuityGuidance = continuityTask || allowSparseCheckpointSupplement;
61
+ const workflowFallbackResults = !shouldProvideContinuityGuidance || countPatternCueCoverage(retrievedPatterns) >= 3
62
+ ? []
63
+ : await retrieveCandidates({
64
+ query: buildWorkflowFallbackQuery(taskSeed),
65
+ limit: Math.max(4, taskLimit * 2),
66
+ scope,
67
+ });
68
+ const fallbackPatterns = selectWorkflowFallbackCandidates(workflowFallbackResults, {
69
+ scope,
70
+ taskSeed,
71
+ limit: taskLimit,
72
+ cueTerms: strongWorkflowCueTerms,
73
+ });
74
+ const combinedPatterns = [
75
+ ...retrievedPatterns,
76
+ ...fallbackPatterns,
77
+ ];
78
+ const combinedPatternCueCoverage = countPatternCueCoverage(combinedPatterns);
79
+ const allowSingleContinuityGapSupplement = Boolean(
80
+ continuityTask &&
81
+ retrievedPatterns.length === 1 &&
82
+ combinedPatterns.length === 1 &&
83
+ (
84
+ combinedPatternCueCoverage >= 2 ||
85
+ looksLikeGenericWindowHandoffTask(taskSeed)
86
+ ),
87
+ );
88
+ const noCueCoverageAndNoFallback = combinedPatternCueCoverage === 0 && fallbackPatterns.length === 0;
89
+ const continuityFallbackPatterns = !shouldProvideContinuityGuidance
90
+ ? []
91
+ : combinedPatterns.length === 0 || noCueCoverageAndNoFallback
92
+ ? buildContinuityFallbackPatterns(taskLimit)
93
+ : combinedPatternCueCoverage < 3 && (
94
+ combinedPatterns.length >= 2 ||
95
+ allowSparseCheckpointSupplement ||
96
+ allowSingleContinuityGapSupplement
97
+ )
98
+ ? buildContinuityFallbackPatterns(taskLimit, combinedPatterns)
99
+ : [];
100
+ const relevantPatterns = selectRelevantPatterns([
101
+ ...retrievedPatterns.map((text) => ({ text, sourcePriority: 3 })),
102
+ ...fallbackPatterns.map((text) => ({ text, sourcePriority: 2 })),
103
+ ...continuityFallbackPatterns.map((text) => ({ text, sourcePriority: 1 })),
104
+ ], taskLimit);
105
+
106
+ const retrievedCases = selectTaskResults("cases", caseResults, taskLimit, {
107
+ scope,
108
+ taskSeed,
109
+ });
110
+ const allowSparseCaseFallback = Boolean(
111
+ retrievedCases.length <= 1 &&
112
+ looksLikeCaseFallbackTask(taskSeed) &&
113
+ Math.max(0, ...retrievedCases.map((item) => countTaskSpecificTextMatches(item, taskSeed))) === 0,
114
+ );
115
+ const caseFallbackResults = (retrievedCases.length > 0 && !allowSparseCaseFallback) ||
116
+ (!hasLatestCheckpoint && !looksLikeCaseFallbackTask(taskSeed))
117
+ ? []
118
+ : await retrieveCandidates({
119
+ category: "cases",
120
+ query: buildCaseFallbackQuery(taskSeed),
121
+ // Case fallback is now competing with more durable meta-maintenance
122
+ // cases. Pull a wider pool so generic RecallNest continues can still
123
+ // reach the canonical cleanup cases after specificity filtering.
124
+ limit: Math.max(10, taskLimit * 3),
125
+ scope,
126
+ });
127
+ const fallbackCases = selectTaskResults("cases", caseFallbackResults, taskLimit, {
128
+ scope,
129
+ taskSeed,
130
+ });
131
+ const recentCases = dedupeText([
132
+ ...retrievedCases,
133
+ ...fallbackCases,
134
+ ], taskLimit);
135
+
136
+ return { relevantPatterns, recentCases };
137
+ }
@@ -0,0 +1,342 @@
1
+ import {
2
+ buildProjectScopeCueTerms,
3
+ normalizeScopedValue,
4
+ taskMentionsScopeIdentity,
5
+ } from "./context-composer-scope.js";
6
+ import { dedupeText, stripConversationMarkers } from "./context-composer-text.js";
7
+ import {
8
+ formatTaskResult,
9
+ isDurableMemoryScope,
10
+ isDurableTaskCandidate,
11
+ isTaskCandidateUseful,
12
+ scoreTaskCandidate,
13
+ scoreWorkflowCandidate,
14
+ taskResultKey,
15
+ type TaskCategory,
16
+ } from "./context-composer-task-ranking.js";
17
+ import type { RetrievalResult } from "./retriever.js";
18
+ import {
19
+ CASE_CUE_TERMS,
20
+ TASK_RESULT_SPECIFICITY_GROUPS,
21
+ TASK_CUE_EXTRACTION_LIMIT,
22
+ WORKFLOW_CUE_TERMS,
23
+ buildTaskHintTerms,
24
+ containsAnyTerm,
25
+ extractTerms,
26
+ looksLikeContinuityTask,
27
+ normalizeText,
28
+ taskCueCoverage,
29
+ } from "./term-registry.js";
30
+ export type { TaskCategory } from "./context-composer-task-ranking.js";
31
+
32
+ function countTaskHintMatches(result: RetrievalResult, taskSeed?: string): number {
33
+ const hintTerms = buildTaskHintTerms(taskSeed);
34
+ if (hintTerms.length === 0) return 0;
35
+
36
+ const haystack = normalizeText(stripConversationMarkers(result.entry.text));
37
+ return hintTerms.filter((term) => haystack.includes(term)).length;
38
+ }
39
+
40
+ const GENERIC_TASK_MATCH_TERMS = new Set([
41
+ ...WORKFLOW_CUE_TERMS.map((term) => normalizeText(term)),
42
+ ...CASE_CUE_TERMS.map((term) => normalizeText(term)),
43
+ "continue",
44
+ "继续",
45
+ "接着",
46
+ "项目",
47
+ "project",
48
+ "terminal",
49
+ "window",
50
+ "fresh",
51
+ "new",
52
+ "same",
53
+ "回到",
54
+ "之前",
55
+ "刚才",
56
+ ]);
57
+
58
+ function buildTaskSpecificTerms(taskSeed?: string): string[] {
59
+ if (!taskSeed) return [];
60
+ // Pull a wider term window here so leading generic cues do not crowd out
61
+ // later task-specific nouns in long maintenance prompts.
62
+ return dedupeText(
63
+ extractTerms(taskSeed, TASK_CUE_EXTRACTION_LIMIT)
64
+ .map((term) => normalizeText(term))
65
+ .filter((term) =>
66
+ term.length >= 2 &&
67
+ !GENERIC_TASK_MATCH_TERMS.has(term)
68
+ ),
69
+ 24,
70
+ );
71
+ }
72
+
73
+ function countTaskSpecificMatches(result: RetrievalResult, taskSeed?: string): number {
74
+ const specificTerms = buildTaskSpecificTerms(taskSeed);
75
+ if (specificTerms.length === 0) return 0;
76
+
77
+ const haystack = normalizeText(stripConversationMarkers(result.entry.text));
78
+ return specificTerms.filter((term) => haystack.includes(term)).length;
79
+ }
80
+
81
+ export function countTaskSpecificTextMatches(text: string, taskSeed?: string): number {
82
+ const specificTerms = buildTaskSpecificTerms(taskSeed);
83
+ if (specificTerms.length === 0) return 0;
84
+
85
+ const haystack = normalizeText(stripConversationMarkers(text));
86
+ return specificTerms.filter((term) => haystack.includes(term)).length;
87
+ }
88
+
89
+ function hasUnsupportedTaskResultSpecificity(result: RetrievalResult, taskSeed?: string): boolean {
90
+ if (!taskSeed) return false;
91
+
92
+ const taskHaystack = normalizeText(`${taskSeed} ${buildTaskHintTerms(taskSeed).join(" ")}`);
93
+ if (!taskHaystack) return false;
94
+
95
+ const resultHaystack = normalizeText(stripConversationMarkers(result.entry.text));
96
+ return TASK_RESULT_SPECIFICITY_GROUPS.some((group) =>
97
+ group.resultTerms.some((term) => resultHaystack.includes(term)) &&
98
+ !group.taskTerms.some((term) => taskHaystack.includes(term))
99
+ );
100
+ }
101
+
102
+ export function countPatternCueCoverage(items: string[]): number {
103
+ return new Set(items.flatMap((item) => taskCueCoverage("patterns", item))).size;
104
+ }
105
+
106
+ export function looksLikeGenericWindowHandoffTask(taskSeed?: string): boolean {
107
+ if (!taskSeed) return false;
108
+ const normalized = normalizeText(taskSeed);
109
+ const hasWindowCue = [
110
+ "新窗口",
111
+ "fresh window",
112
+ "new window",
113
+ "cross window",
114
+ "窗口",
115
+ "window",
116
+ "terminal",
117
+ "终端",
118
+ ].some((term) => normalized.includes(term));
119
+ const hasContinuationCue = [
120
+ "继续",
121
+ "continue",
122
+ "同一个",
123
+ "same",
124
+ "项目",
125
+ "project",
126
+ "接力",
127
+ "handoff",
128
+ ].some((term) => normalized.includes(term));
129
+ return hasWindowCue && hasContinuationCue;
130
+ }
131
+
132
+ export function selectRelevantPatterns(
133
+ candidates: Array<{ text: string; sourcePriority: number }>,
134
+ limit: number,
135
+ ): string[] {
136
+ const seen = new Set<string>();
137
+ const remaining = candidates
138
+ .map((item, index) => ({ ...item, index }))
139
+ .filter((item) => {
140
+ const key = normalizeText(item.text);
141
+ if (!key || seen.has(key)) return false;
142
+ seen.add(key);
143
+ return true;
144
+ });
145
+
146
+ const selected: string[] = [];
147
+ const coveredCues = new Set<string>();
148
+
149
+ while (remaining.length > 0 && selected.length < limit) {
150
+ let bestIndex = 0;
151
+ let bestValue = Number.NEGATIVE_INFINITY;
152
+
153
+ for (let index = 0; index < remaining.length; index += 1) {
154
+ const item = remaining[index];
155
+ const uncoveredCueCount = taskCueCoverage("patterns", item.text)
156
+ .filter((term) => !coveredCues.has(term))
157
+ .length;
158
+ const value = item.sourcePriority + uncoveredCueCount * 3 - item.index * 0.01;
159
+ if (value > bestValue) {
160
+ bestValue = value;
161
+ bestIndex = index;
162
+ }
163
+ }
164
+
165
+ const [item] = remaining.splice(bestIndex, 1);
166
+ if (!item) continue;
167
+ selected.push(item.text);
168
+ for (const term of taskCueCoverage("patterns", item.text)) {
169
+ coveredCues.add(term);
170
+ }
171
+ }
172
+
173
+ return selected;
174
+ }
175
+
176
+ function isRelevantToScopedTaskResult(
177
+ result: RetrievalResult,
178
+ params: { scope?: string; taskSeed?: string },
179
+ ): boolean {
180
+ const requestScope = params.scope ? normalizeScopedValue(params.scope) : "";
181
+ const resultScope = normalizeScopedValue(result.entry.scope);
182
+ const haystack = normalizeText(stripConversationMarkers(result.entry.text));
183
+ const resultIsProject = resultScope.startsWith("project:");
184
+ const requestIsProject = requestScope.startsWith("project:");
185
+ const resultHasNamedIdentity = !["memory:", "asset:", "cc:", "codex:", "gemini:", "session:", "agent:", "eval:"]
186
+ .some((prefix) => normalizeText(result.entry.scope).startsWith(prefix));
187
+
188
+ if (params.scope && resultScope === requestScope) return true;
189
+
190
+ if (!params.scope && resultHasNamedIdentity && !taskMentionsScopeIdentity(params.taskSeed, result.entry.scope)) {
191
+ return false;
192
+ }
193
+
194
+ // Unscoped named tasks should not inherit generic durable task results unless
195
+ // the result text itself carries the task identity; otherwise foreign project
196
+ // cases from shared durable scopes can leak into continuity output.
197
+ if (
198
+ !params.scope &&
199
+ isDurableMemoryScope(result.entry.scope) &&
200
+ containsAnyTerm(params.taskSeed || "", ["项目", "project"]) &&
201
+ !looksLikeGenericWindowHandoffTask(params.taskSeed) &&
202
+ buildTaskSpecificTerms(params.taskSeed).length > 0 &&
203
+ !taskMentionsScopeIdentity(params.taskSeed, result.entry.scope) &&
204
+ countTaskSpecificMatches(result, params.taskSeed) === 0
205
+ ) {
206
+ return false;
207
+ }
208
+
209
+ if (resultIsProject && !taskMentionsScopeIdentity(params.taskSeed, result.entry.scope)) {
210
+ return false;
211
+ }
212
+
213
+ if (!params.scope) return true;
214
+
215
+ if (requestIsProject && resultIsProject) {
216
+ const projectCueTerms = buildProjectScopeCueTerms(params.scope);
217
+ if (projectCueTerms.length === 0) return false;
218
+ return projectCueTerms.some((term) => haystack.includes(term));
219
+ }
220
+
221
+ return true;
222
+ }
223
+
224
+ export function selectWorkflowFallbackCandidates(
225
+ results: RetrievalResult[],
226
+ params: {
227
+ scope?: string;
228
+ taskSeed?: string;
229
+ limit: number;
230
+ cueTerms?: string[];
231
+ },
232
+ ): string[] {
233
+ const cueTerms = params.cueTerms || WORKFLOW_CUE_TERMS;
234
+ const ranked = results
235
+ .map((result) => ({
236
+ result,
237
+ score: scoreWorkflowCandidate(result, params.scope),
238
+ }))
239
+ .filter((item) =>
240
+ item.score > 0 &&
241
+ isDurableMemoryScope(item.result.entry.scope) &&
242
+ !hasUnsupportedTaskResultSpecificity(item.result, params.taskSeed) &&
243
+ isRelevantToScopedTaskResult(item.result, {
244
+ scope: params.scope,
245
+ taskSeed: params.taskSeed,
246
+ }) &&
247
+ containsAnyTerm(item.result.entry.text, cueTerms),
248
+ )
249
+ .sort((a, b) => b.score - a.score)
250
+ .map((item) => formatTaskResult(item.result));
251
+
252
+ return dedupeText(ranked, params.limit);
253
+ }
254
+
255
+ export function selectTaskResults(
256
+ category: TaskCategory,
257
+ results: RetrievalResult[],
258
+ limit: number,
259
+ params: { scope?: string; taskSeed?: string } = {},
260
+ ): string[] {
261
+ const taskHintTerms = buildTaskHintTerms(params.taskSeed);
262
+ const ranked = results
263
+ .filter((result) =>
264
+ isTaskCandidateUseful(category, result) &&
265
+ !hasUnsupportedTaskResultSpecificity(result, params.taskSeed) &&
266
+ isRelevantToScopedTaskResult(result, params)
267
+ )
268
+ .map((result) => ({
269
+ result,
270
+ key: taskResultKey(result),
271
+ durable: isDurableTaskCandidate(result),
272
+ score: scoreTaskCandidate(category, result),
273
+ taskHintMatches: countTaskHintMatches(result, params.taskSeed),
274
+ taskSpecificMatches: countTaskSpecificMatches(result, params.taskSeed),
275
+ formatted: formatTaskResult(result),
276
+ }))
277
+ .sort((a, b) => {
278
+ if (b.taskSpecificMatches !== a.taskSpecificMatches) {
279
+ return b.taskSpecificMatches - a.taskSpecificMatches;
280
+ }
281
+ return b.score - a.score;
282
+ });
283
+
284
+ const maxTaskHintMatches = taskHintTerms.length > 0
285
+ ? Math.max(0, ...ranked.map((item) => item.taskHintMatches))
286
+ : 0;
287
+ if (
288
+ taskHintTerms.length > 0 &&
289
+ !looksLikeContinuityTask(params.taskSeed) &&
290
+ maxTaskHintMatches === 0
291
+ ) {
292
+ return [];
293
+ }
294
+
295
+ const hintFiltered = taskHintTerms.length > 0 && maxTaskHintMatches > 0
296
+ ? ranked.filter((item) => item.taskHintMatches === maxTaskHintMatches)
297
+ : ranked;
298
+ const preferred = hintFiltered.some((item) => item.durable)
299
+ ? hintFiltered.filter((item) => item.durable)
300
+ : hintFiltered;
301
+ const seen = new Set<string>();
302
+ const selected: string[] = [];
303
+ if (category === "patterns") {
304
+ const remaining = preferred.filter((item) => item.key && !seen.has(item.key));
305
+ const coveredCues = new Set<string>();
306
+
307
+ while (remaining.length > 0 && selected.length < limit) {
308
+ let bestIndex = 0;
309
+ let bestValue = Number.NEGATIVE_INFINITY;
310
+
311
+ for (let index = 0; index < remaining.length; index += 1) {
312
+ const item = remaining[index];
313
+ const uncoveredCueCount = taskCueCoverage(category, item.formatted)
314
+ .filter((term) => !coveredCues.has(term))
315
+ .length;
316
+ const value = item.score + item.taskSpecificMatches * 4 + uncoveredCueCount * 3;
317
+ if (value > bestValue) {
318
+ bestValue = value;
319
+ bestIndex = index;
320
+ }
321
+ }
322
+
323
+ const [item] = remaining.splice(bestIndex, 1);
324
+ if (!item || !item.key || seen.has(item.key)) continue;
325
+ seen.add(item.key);
326
+ selected.push(item.formatted);
327
+ for (const term of taskCueCoverage(category, item.formatted)) {
328
+ coveredCues.add(term);
329
+ }
330
+ }
331
+ return selected;
332
+ }
333
+
334
+ for (const item of preferred) {
335
+ if (!item.key || seen.has(item.key)) continue;
336
+ seen.add(item.key);
337
+ selected.push(item.formatted);
338
+ if (selected.length >= limit) break;
339
+ }
340
+
341
+ return selected;
342
+ }