recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * P1-A: scope 拼写漂移提示(FAILURES.md 5-13 retrieval gap)。
3
+ *
4
+ * matchesScopeFilter 走精确/前缀匹配,拼错或惯例不一致的 scope(`recallnst`、
5
+ * `cc_55bcbfb3`)会静默返回 0 hits,使用者无从知道是"真没有"还是"scope 写错"。
6
+ * 本模块在 0-hit 且显式传了 scope 时给出相近 scope 提示。
7
+ *
8
+ * 只提示、不自动 fallback 重查——自动改写会静默改变查询语义,agent/用户
9
+ * 看到提示后自行决定。
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Pure matching
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** 归一化:小写 + 吃掉 -/_/空格(惯例差异:`cc-foo` vs `cc_foo` vs `ccfoo`)。 */
17
+ export function normalizeScope(s: string): string {
18
+ return s.toLowerCase().replace(/[-_\s]+/g, "");
19
+ }
20
+
21
+ /** Two-row Levenshtein with early exit — 距离超过 max 直接返回 max+1。 */
22
+ export function boundedLevenshtein(a: string, b: string, max: number): number {
23
+ if (Math.abs(a.length - b.length) > max) return max + 1;
24
+ if (a === b) return 0;
25
+
26
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
27
+ let curr = new Array<number>(b.length + 1);
28
+
29
+ for (let i = 1; i <= a.length; i++) {
30
+ curr[0] = i;
31
+ let rowMin = curr[0];
32
+ for (let j = 1; j <= b.length; j++) {
33
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
34
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
35
+ if (curr[j] < rowMin) rowMin = curr[j];
36
+ }
37
+ if (rowMin > max) return max + 1;
38
+ [prev, curr] = [curr, prev];
39
+ }
40
+ return prev[b.length];
41
+ }
42
+
43
+ /**
44
+ * 对输入 scope 给出相近的已知 scope。
45
+ * 两级:① normalize 后精确相等 ② 前缀包含(双向)或编辑距离 ≤ 2。
46
+ * 候选应同时包含完整 scope 与其 prefix 家族(`cc:55bc...` 的 `cc`)——
47
+ * 拼错的通常是短 prefix,对长 session scope 做编辑距离没有意义。
48
+ */
49
+ export function suggestScopes(input: string, knownScopes: string[], maxSuggestions = 3): string[] {
50
+ const norm = normalizeScope(input);
51
+ if (norm.length === 0) return [];
52
+
53
+ const exact: string[] = [];
54
+ const close: Array<{ scope: string; rank: number }> = [];
55
+
56
+ for (const known of knownScopes) {
57
+ const kn = normalizeScope(known);
58
+ if (kn === norm) {
59
+ exact.push(known);
60
+ continue;
61
+ }
62
+ if (kn.startsWith(norm) || norm.startsWith(kn)) {
63
+ close.push({ scope: known, rank: 0.5 });
64
+ continue;
65
+ }
66
+ const d = boundedLevenshtein(norm, kn, 2);
67
+ if (d <= 2) close.push({ scope: known, rank: d });
68
+ }
69
+
70
+ if (exact.length > 0) return exact.slice(0, maxSuggestions);
71
+ return close
72
+ .sort((a, b) => a.rank - b.rank || a.scope.length - b.scope.length)
73
+ .map(c => c.scope)
74
+ .slice(0, maxSuggestions);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Cached provider
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * 带 60s 缓存的建议器工厂——known scopes 来自 store.stats().scopeCounts,
83
+ * 全表聚合不便宜,0-hit 提示是低频路径,短缓存足够新鲜。
84
+ * 候选 = prefix 家族(冒号前段,去重) + 完整 scope 串。
85
+ */
86
+ export function createScopeSuggester(
87
+ getScopeCounts: () => Promise<Record<string, number>>,
88
+ ttlMs = 60_000,
89
+ ): (input: string) => Promise<string[]> {
90
+ let cache: { at: number; candidates: string[] } | null = null;
91
+
92
+ return async function suggest(input: string): Promise<string[]> {
93
+ const now = Date.now();
94
+ if (!cache || now - cache.at > ttlMs) {
95
+ const counts = await getScopeCounts();
96
+ const keys = Object.keys(counts);
97
+ const prefixes = new Set<string>();
98
+ for (const k of keys) {
99
+ const i = k.indexOf(":");
100
+ if (i > 0) prefixes.add(k.slice(0, i));
101
+ }
102
+ // prefix 家族在前:作为查询 scope 它们最常被手敲、也最常被拼错
103
+ cache = { at: now, candidates: [...prefixes, ...keys] };
104
+ }
105
+ return suggestScopes(input, cache.candidates);
106
+ };
107
+ }
108
+
109
+ /** 0-hit 提示行(空建议返回空串,caller 原样拼接即可)。 */
110
+ export function formatScopeSuggestion(inputScope: string, suggestions: string[]): string {
111
+ if (suggestions.length === 0) return "";
112
+ return `⚠️ scope '${inputScope}' 命中 0 条,相近 scope: [${suggestions.join(", ")}]`;
113
+ }
@@ -7,6 +7,8 @@ import type { AuditLogger } from "./audit-log.js";
7
7
  import type { StoredMemoryRecord, DurableMemoryCategory } from "./memory-schema.js";
8
8
  import { persistMemory } from "./capture-engine.js";
9
9
  import { detectLang, getSessionPromptHook } from "./language-hook.js";
10
+ import { redactSecrets } from "./pii-detector.js";
11
+ import { withWriteLock } from "./distill-lock.js";
10
12
 
11
13
  // ============================================================================
12
14
  // Types
@@ -71,6 +73,7 @@ const DEFAULT_DIMENSION_LABELS: Record<string, string> = {
71
73
  errors_and_fixes: "Errors and fix records",
72
74
  problem_solving: "Problem solving process",
73
75
  user_quotes: "User original quotes preserved",
76
+ user_profile: "Stable facts about the user: identity, role, background, long-term context",
74
77
  unfinished_tasks: "Unfinished tasks",
75
78
  current_state: "Current work state",
76
79
  next_steps: "Suggested next steps",
@@ -93,6 +96,15 @@ const DIMENSION_TO_MEMORY: Record<string, DimensionMapping> = {
93
96
  errors_and_fixes: { category: "cases", importance: 0.7 },
94
97
  problem_solving: { category: "patterns", importance: 0.8 },
95
98
  user_quotes: { category: "preferences", importance: 0.7 },
99
+ // user_profile → profile:让 distill 能沉淀"用户是谁"的稳定身份事实(不只是偏好)。
100
+ // distill 走 source=agent,不被 resolveIngestBoundary 降级,直接进 durable profile。
101
+ user_profile: { category: "profile", importance: 0.8 },
102
+ // 2026-05-13 P1.2: 加 unfinished_tasks + next_steps 持久化映射
103
+ // LLM 早在 DEFAULT_DIMENSION_LABELS 行 74/76 已产出这两条,但缺映射被丢弃
104
+ // 都映射到 cases/patterns 走 tag 兜底(extractAndPersist 自动加 tags: ["session_distill", dimKey])
105
+ // 未来如要 unresolved-first 召回,按 tags 含 "unfinished_tasks" 过滤即可
106
+ unfinished_tasks: { category: "cases", importance: 0.75 },
107
+ next_steps: { category: "patterns", importance: 0.65 },
96
108
  };
97
109
 
98
110
  // ============================================================================
@@ -158,8 +170,10 @@ export async function summarizeSession(
158
170
  messages: ConversationMessage[],
159
171
  llm: LLMClient,
160
172
  ): Promise<SummaryResult> {
173
+ // F-3b: Redact per-message BEFORE the 500-char cut — truncating first could
174
+ // slice a secret in half and leak its prefix past the redaction regexes.
161
175
  const conversationText = messages
162
- .map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${m.content.slice(0, 500)}`)
176
+ .map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${redactSecrets(m.content).text.slice(0, 500)}`)
163
177
  .join("\n")
164
178
  .slice(0, 8000);
165
179
 
@@ -295,7 +309,14 @@ export async function distillSession(
295
309
  };
296
310
 
297
311
  if (shouldPersist && Object.keys(summaryResult.dimensions).length > 0) {
298
- persistResult = await extractAndPersist(summaryResult, deps, scope);
312
+ // P0-1: serialize the persist phase per scope across processes. Wait (not skip) —
313
+ // skipping would drop this session's distilled knowledge when two sessions distill
314
+ // the same scope at once. Writes are fast and store-write already makes each write
315
+ // idempotent, so the brief wait is lossless; microcompact/summary above run unlocked.
316
+ persistResult = await withWriteLock(
317
+ `distill-${scope}`,
318
+ () => extractAndPersist(summaryResult, deps, scope),
319
+ );
299
320
  }
300
321
 
301
322
  return {
@@ -6,6 +6,7 @@ import {
6
6
  type SessionCheckpointRecord,
7
7
  SessionCheckpointRecordSchema,
8
8
  } from "./session-schema.js";
9
+ import { buildIdempotentRecordId } from "./idempotency.js";
9
10
 
10
11
  const REPO_STATE_PATTERN = /\bgit status\b|未提交|staged|uncommitted|已修改文件|modified files?|untracked|新增文件|dirty repo|dirty worktree/iu;
11
12
  const SENTENCE_SPLIT_PATTERN = /(?:\r?\n)+|(?<=[。!?!?;;])\s+/u;
@@ -141,7 +142,7 @@ export function buildSessionCheckpointResult(rawInput: unknown): SessionCheckpoi
141
142
 
142
143
  const record = SessionCheckpointRecordSchema.parse({
143
144
  ...sanitizedInput,
144
- checkpointId: randomUUID(),
145
+ checkpointId: buildIdempotentRecordId("chk", sanitizedInput.idempotencyKey) ?? randomUUID(),
145
146
  resolvedScope: resolveCheckpointScope(sanitizedInput),
146
147
  });
147
148
 
@@ -14,6 +14,7 @@ export const SessionCheckpointInputSchema = z.object({
14
14
  nextActions: normalizedStringListSchema("nextActions", 6, 200),
15
15
  entities: normalizedStringListSchema("entities", 8, 120),
16
16
  files: normalizedStringListSchema("files", 12, 220),
17
+ idempotencyKey: optionalBoundedStringSchema(160),
17
18
  updatedAt: z.string().datetime().default(() => new Date().toISOString()),
18
19
  });
19
20
 
@@ -66,9 +67,9 @@ export const ResumeContextResponseSchema = z.object({
66
67
  essentialContext: EssentialContextSchema.optional(),
67
68
  latestCheckpoint: ResumeCheckpointSummarySchema.optional(),
68
69
  /** CC-1: Hint for where to inject recalled context in the prompt. */
69
- injectionHint: z.enum(["system_prompt", "user_attachment"]).default("user_attachment").optional(),
70
+ injectionHint: z.enum(["system_prompt", "user_attachment"]).optional().default("user_attachment"),
70
71
  /** Upstream #345: Mark recalled context as ephemeral — host should discard on compaction, not persist to transcript. */
71
- ephemeral: z.boolean().default(true).optional(),
72
+ ephemeral: z.boolean().optional().default(true),
72
73
  responseMode: ResumeResponseModeSchema.default("default"),
73
74
  responseGuidance: optionalBoundedStringSchema(400),
74
75
  /** Constructive retrieval: LLM-synthesized reconstruction of context. */
@@ -78,6 +78,11 @@ export class SessionCheckpointStore {
78
78
  const timestampToken = parsed.updatedAt.replace(/[:.]/g, "-");
79
79
  const path = join(this.dataDir, `${timestampToken}-${parsed.checkpointId}.json`);
80
80
  try {
81
+ const suffix = `-${parsed.checkpointId}.json`;
82
+ for (const name of readdirSync(this.dataDir).filter((item) => item.endsWith(suffix))) {
83
+ const existingPath = join(this.dataDir, name);
84
+ if (existingPath !== path) unlinkSync(existingPath);
85
+ }
81
86
  writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
82
87
  } catch (err) {
83
88
  console.error("[recallnest] Failed to save session checkpoint:", err instanceof Error ? err.message : String(err));
@@ -177,6 +177,84 @@ export async function persistSkill(
177
177
  };
178
178
  }
179
179
 
180
+ type OutcomeDeps = Pick<MemoryStore, "getById" | "update">;
181
+
182
+ export type SkillOutcomeUpdate =
183
+ | { updated: true; successCount: number; failureCount: number; lastRefinedAt: string }
184
+ | { updated: false; reason: "skill_not_found" | "not_a_skill" | "metadata_parse_error" | "skill_metadata_missing" | "store_update_failed" };
185
+
186
+ /**
187
+ * Record a skill usage outcome by incrementing successCount/failureCount on the skill's metadata.
188
+ *
189
+ * Mapping: `success` → successCount +1; everything else (`failure`/`corrected`/`missed`) → failureCount +1.
190
+ * Returns `{ updated: false, reason }` for missing/corrupt skills without throwing — callers can log and continue.
191
+ */
192
+ export async function recordSkillOutcome(
193
+ store: OutcomeDeps,
194
+ skillId: string,
195
+ outcome: "success" | "failure" | "corrected" | "missed",
196
+ ): Promise<SkillOutcomeUpdate> {
197
+ const entry = await store.getById(skillId);
198
+ if (!entry) {
199
+ return { updated: false, reason: "skill_not_found" };
200
+ }
201
+
202
+ if (entry.category !== SKILL_CATEGORY) {
203
+ return { updated: false, reason: "not_a_skill" };
204
+ }
205
+
206
+ let meta: Record<string, unknown>;
207
+ try {
208
+ meta = JSON.parse(entry.metadata || "{}");
209
+ } catch {
210
+ return { updated: false, reason: "metadata_parse_error" };
211
+ }
212
+
213
+ const skill = meta.skill;
214
+ if (!skill || typeof skill !== "object") {
215
+ return { updated: false, reason: "skill_metadata_missing" };
216
+ }
217
+
218
+ const s = skill as Record<string, unknown>;
219
+ const currentSuccess = typeof s.successCount === "number" ? s.successCount : 0;
220
+ const currentFailure = typeof s.failureCount === "number" ? s.failureCount : 0;
221
+
222
+ const nextSuccessCount = outcome === "success" ? currentSuccess + 1 : currentSuccess;
223
+ const nextFailureCount = outcome === "success" ? currentFailure : currentFailure + 1;
224
+ const nextLastRefinedAt = new Date().toISOString();
225
+
226
+ const nextSkill = {
227
+ ...s,
228
+ successCount: nextSuccessCount,
229
+ failureCount: nextFailureCount,
230
+ lastRefinedAt: nextLastRefinedAt,
231
+ };
232
+
233
+ const updatedMetadata = JSON.stringify({
234
+ ...meta,
235
+ skill: nextSkill,
236
+ });
237
+
238
+ // Use entry.id (full UUID resolved by getById's prefix lookup) for update,
239
+ // not skillId (caller-provided, may be 8+ hex prefix). This avoids ambiguous-prefix
240
+ // mutation risk: if a prefix matched here, update would also need to do prefix
241
+ // resolution, doubling the chance of writing to the wrong entry.
242
+ const updated = await store.update(entry.id, {
243
+ metadata: updatedMetadata,
244
+ });
245
+
246
+ if (!updated) {
247
+ return { updated: false, reason: "store_update_failed" };
248
+ }
249
+
250
+ return {
251
+ updated: true,
252
+ successCount: nextSuccessCount,
253
+ failureCount: nextFailureCount,
254
+ lastRefinedAt: nextLastRefinedAt,
255
+ };
256
+ }
257
+
180
258
  export async function retrieveSkills(
181
259
  store: SearchDeps,
182
260
  embedder: EmbedDeps,