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,181 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { summarizeConflictAdvice } from "./conflict-advisor.js";
4
+ import type { Embedder } from "./embedder.js";
5
+ import {
6
+ extractBoundaryMetadata,
7
+ extractPromotedFrom,
8
+ normalizeCanonicalKey,
9
+ parseMetadataObject,
10
+ } from "./memory-boundaries.js";
11
+ import type { MemoryStore } from "./store.js";
12
+ import {
13
+ type ConflictCandidateInput,
14
+ ConflictCandidateInputSchema,
15
+ type ConflictCandidateRecord,
16
+ ConflictCandidateRecordSchema,
17
+ type ConflictResolutionResult,
18
+ ConflictResolutionResultSchema,
19
+ type ResolveConflictInput,
20
+ ResolveConflictInputSchema,
21
+ } from "./conflict-schema.js";
22
+ import type { ConflictCandidateStore } from "./conflict-store.js";
23
+
24
+ function normalizeFingerprintPart(value: string): string {
25
+ return normalizeCanonicalKey(value) || "na";
26
+ }
27
+
28
+ export function buildConflictFingerprint(params: {
29
+ canonicalKey: string;
30
+ existingMemoryId: string;
31
+ incomingText: string;
32
+ sourceMemoryId?: string;
33
+ }): string {
34
+ return [
35
+ normalizeFingerprintPart(params.canonicalKey),
36
+ normalizeFingerprintPart(params.existingMemoryId),
37
+ normalizeFingerprintPart(params.sourceMemoryId || "no-source"),
38
+ normalizeFingerprintPart(params.incomingText),
39
+ ].join("--").slice(0, 240);
40
+ }
41
+
42
+ export function buildConflictCandidateRecord(rawInput: unknown): ConflictCandidateRecord {
43
+ const input = ConflictCandidateInputSchema.parse(rawInput);
44
+ return ConflictCandidateRecordSchema.parse({
45
+ ...input,
46
+ conflictId: randomUUID(),
47
+ status: "open",
48
+ });
49
+ }
50
+
51
+ export function reopenConflictCandidate(record: ConflictCandidateRecord): ConflictCandidateRecord {
52
+ const now = new Date().toISOString();
53
+ return ConflictCandidateRecordSchema.parse({
54
+ ...record,
55
+ status: "open",
56
+ reopenCount: (record.reopenCount || 0) + 1,
57
+ lastReopenedAt: now,
58
+ updatedAt: now,
59
+ resolvedAt: undefined,
60
+ resolutionNotes: undefined,
61
+ });
62
+ }
63
+
64
+ function buildMergedConflictMetadata(
65
+ record: ConflictCandidateRecord,
66
+ resolvedAt: string,
67
+ mergedText: string,
68
+ ): string {
69
+ const existingMetadata = parseMetadataObject(record.existing.metadata) ?? {};
70
+ const incomingMetadata = parseMetadataObject(record.incoming.metadata) ?? {};
71
+ const boundary = extractBoundaryMetadata(record.existing.metadata)
72
+ ?? extractBoundaryMetadata(record.incoming.metadata);
73
+ const promotedFrom = extractPromotedFrom(record.incoming.metadata)
74
+ ?? extractPromotedFrom(record.existing.metadata);
75
+ const source = typeof existingMetadata.source === "string"
76
+ ? existingMetadata.source
77
+ : typeof incomingMetadata.source === "string"
78
+ ? incomingMetadata.source
79
+ : record.incoming.source;
80
+
81
+ return JSON.stringify({
82
+ ...existingMetadata,
83
+ ...(source ? { source } : {}),
84
+ canonicalKey: record.canonicalKey,
85
+ ...(boundary ? { boundary } : {}),
86
+ ...(promotedFrom ? { promotedFrom } : {}),
87
+ mergedFrom: {
88
+ conflictId: record.conflictId,
89
+ resolvedAt,
90
+ existingMemoryId: record.existing.memoryId,
91
+ ...(record.incoming.sourceMemoryId ? { incomingSourceMemoryId: record.incoming.sourceMemoryId } : {}),
92
+ mergedText,
93
+ },
94
+ });
95
+ }
96
+
97
+ export interface ResolveConflictDeps {
98
+ store: Pick<MemoryStore, "getById" | "update">;
99
+ embedder: Pick<Embedder, "embedPassage">;
100
+ conflictStore: Pick<ConflictCandidateStore, "getById" | "replace">;
101
+ }
102
+
103
+ export async function resolveConflictCandidate(
104
+ deps: ResolveConflictDeps,
105
+ rawInput: unknown,
106
+ ): Promise<ConflictResolutionResult> {
107
+ const input = ResolveConflictInputSchema.parse(rawInput);
108
+ const record = await deps.conflictStore.getById(input.conflictId);
109
+
110
+ if (!record) {
111
+ throw new Error(`Conflict ${input.conflictId} not found`);
112
+ }
113
+ if (record.status !== "open") {
114
+ throw new Error(`Conflict ${input.conflictId} is already ${record.status}`);
115
+ }
116
+
117
+ const now = new Date().toISOString();
118
+ let updatedMemoryId: string | undefined;
119
+
120
+ if (input.resolution === "accept_incoming" || input.resolution === "merge") {
121
+ const existing = await deps.store.getById(record.existing.memoryId);
122
+ if (!existing) {
123
+ throw new Error(`Existing memory ${record.existing.memoryId} not found`);
124
+ }
125
+
126
+ let nextText = record.incoming.text;
127
+ let nextCategory = record.incoming.category;
128
+ let nextImportance = record.incoming.importance;
129
+ let nextMetadata = record.incoming.metadata;
130
+
131
+ if (input.resolution === "merge") {
132
+ if (record.existing.category !== record.incoming.category) {
133
+ throw new Error("Merge resolution only supports same-category durable conflicts");
134
+ }
135
+ const mergeSuggestion = summarizeConflictAdvice(record).mergeSuggestion;
136
+ const mergedText = input.mergedText || mergeSuggestion;
137
+ if (!mergedText) {
138
+ throw new Error("Merge resolution requires mergedText or an available merge suggestion");
139
+ }
140
+ nextText = mergedText;
141
+ nextCategory = record.existing.category;
142
+ nextImportance = Math.max(existing.importance, record.incoming.importance);
143
+ nextMetadata = buildMergedConflictMetadata(record, now, mergedText);
144
+ }
145
+
146
+ const vector = await deps.embedder.embedPassage(nextText);
147
+ const updated = await deps.store.update(existing.id, {
148
+ text: nextText,
149
+ vector,
150
+ importance: nextImportance,
151
+ category: nextCategory,
152
+ metadata: nextMetadata,
153
+ timestamp: Date.now(),
154
+ });
155
+ if (!updated) {
156
+ throw new Error(`Failed to update durable memory ${existing.id}`);
157
+ }
158
+ updatedMemoryId = updated.id;
159
+ }
160
+
161
+ const resolved = ConflictCandidateRecordSchema.parse({
162
+ ...record,
163
+ status: input.resolution === "accept_incoming"
164
+ ? "accepted-incoming"
165
+ : input.resolution === "merge"
166
+ ? "merged"
167
+ : "kept-existing",
168
+ updatedAt: now,
169
+ resolvedAt: now,
170
+ ...(input.notes ? { resolutionNotes: input.notes } : {}),
171
+ });
172
+ await deps.conflictStore.replace(resolved);
173
+
174
+ return ConflictResolutionResultSchema.parse({
175
+ conflictId: resolved.conflictId,
176
+ status: resolved.status,
177
+ updatedAt: resolved.updatedAt,
178
+ resolvedAt: resolved.resolvedAt,
179
+ ...(updatedMemoryId ? { updatedMemoryId } : {}),
180
+ });
181
+ }
@@ -0,0 +1,138 @@
1
+ import type { ConflictSuggestedResolution, ConflictAdviceConfidence } from "./conflict-advisor.js";
2
+ import { summarizeConflictAdvice } from "./conflict-advisor.js";
3
+ import { type ConflictAttention, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
4
+ import {
5
+ type ConflictCandidateRecord,
6
+ ConflictCandidateRecordSchema,
7
+ type ConflictEscalationResultItem,
8
+ ConflictEscalationResultItemSchema,
9
+ type EscalateConflictsInput,
10
+ EscalateConflictsInputSchema,
11
+ type EscalateConflictsResult,
12
+ EscalateConflictsResultSchema,
13
+ } from "./conflict-schema.js";
14
+ import type { ConflictCandidateStore } from "./conflict-store.js";
15
+
16
+ export type EscalationAttention = Extract<ConflictAttention, "stale" | "escalated">;
17
+
18
+ export interface EscalateConflictsDeps {
19
+ conflictStore: Pick<ConflictCandidateStore, "listRecent" | "replace">;
20
+ }
21
+
22
+ function toEscalationAttention(attention: ConflictAttention): EscalationAttention | null {
23
+ return attention === "stale" || attention === "escalated" ? attention : null;
24
+ }
25
+
26
+ export function buildConflictEscalationItem(
27
+ record: ConflictCandidateRecord,
28
+ now = new Date(),
29
+ ): ConflictEscalationResultItem | null {
30
+ const lifecycle = summarizeConflictLifecycle(record, now);
31
+ const attention = toEscalationAttention(lifecycle.attention);
32
+ if (!attention) return null;
33
+
34
+ const advice = summarizeConflictAdvice(record);
35
+ const alreadyEscalated = record.lastEscalationAttention === attention;
36
+ return ConflictEscalationResultItemSchema.parse({
37
+ conflictId: record.conflictId,
38
+ canonicalKey: record.canonicalKey,
39
+ attention,
40
+ openAgeDays: lifecycle.openAgeDays,
41
+ reopenCount: lifecycle.reopenCount,
42
+ escalationCount: Math.max(0, record.escalationCount || 0),
43
+ suggestedResolution: advice.suggestedResolution,
44
+ confidence: advice.confidence,
45
+ action: alreadyEscalated ? "already-escalated" : "pending",
46
+ clusterKey: advice.clusterKey,
47
+ });
48
+ }
49
+
50
+ export function markConflictEscalated(
51
+ record: ConflictCandidateRecord,
52
+ params: {
53
+ attention: EscalationAttention;
54
+ notes?: string;
55
+ now?: Date;
56
+ },
57
+ ): ConflictCandidateRecord {
58
+ const nowIso = (params.now || new Date()).toISOString();
59
+ const note = params.notes?.trim();
60
+
61
+ return ConflictCandidateRecordSchema.parse({
62
+ ...record,
63
+ escalationCount: Math.max(0, record.escalationCount || 0) + 1,
64
+ lastEscalatedAt: nowIso,
65
+ lastEscalationAttention: params.attention,
66
+ updatedAt: nowIso,
67
+ ...(note ? { resolutionNotes: note } : {}),
68
+ });
69
+ }
70
+
71
+ function sortEscalationItems(items: ConflictEscalationResultItem[]): ConflictEscalationResultItem[] {
72
+ return [...items].sort((a, b) => {
73
+ const attentionRankA = a.attention === "escalated" ? 2 : 1;
74
+ const attentionRankB = b.attention === "escalated" ? 2 : 1;
75
+ if (attentionRankB !== attentionRankA) return attentionRankB - attentionRankA;
76
+ if (b.openAgeDays !== a.openAgeDays) return b.openAgeDays - a.openAgeDays;
77
+ if (b.reopenCount !== a.reopenCount) return b.reopenCount - a.reopenCount;
78
+ return a.conflictId.localeCompare(b.conflictId);
79
+ });
80
+ }
81
+
82
+ export async function escalateConflicts(
83
+ deps: EscalateConflictsDeps,
84
+ rawInput: unknown,
85
+ { now: nowOverride }: { now?: Date } = {},
86
+ ): Promise<EscalateConflictsResult> {
87
+ const input = EscalateConflictsInputSchema.parse(rawInput);
88
+ const now = nowOverride ?? new Date();
89
+ const records = await deps.conflictStore.listRecent({
90
+ status: "open",
91
+ canonicalKey: input.canonicalKey,
92
+ limit: input.limit,
93
+ });
94
+
95
+ const eligibleItems = sortEscalationItems(
96
+ records
97
+ .map((record) => buildConflictEscalationItem(record, now))
98
+ .filter((item): item is ConflictEscalationResultItem => Boolean(item))
99
+ .filter((item) => !input.attention || item.attention === input.attention)
100
+ .slice(0, input.top),
101
+ );
102
+
103
+ const items: ConflictEscalationResultItem[] = [];
104
+ let escalated = 0;
105
+
106
+ for (const item of eligibleItems) {
107
+ const record = records.find((candidate) => candidate.conflictId === item.conflictId);
108
+ if (!record) continue;
109
+
110
+ if (!input.apply || item.action === "already-escalated") {
111
+ items.push(item);
112
+ continue;
113
+ }
114
+
115
+ const updated = markConflictEscalated(record, {
116
+ attention: item.attention,
117
+ notes: input.notes,
118
+ now,
119
+ });
120
+ await deps.conflictStore.replace(updated);
121
+ escalated += 1;
122
+ items.push(ConflictEscalationResultItemSchema.parse({
123
+ ...item,
124
+ escalationCount: updated.escalationCount,
125
+ action: "escalated",
126
+ }));
127
+ }
128
+
129
+ return EscalateConflictsResultSchema.parse({
130
+ apply: input.apply,
131
+ scanned: records.length,
132
+ eligible: eligibleItems.length,
133
+ escalated,
134
+ skipped: eligibleItems.filter((item) => item.action === "already-escalated").length,
135
+ items,
136
+ });
137
+ }
138
+
@@ -0,0 +1,67 @@
1
+ import type { ConflictCandidateRecord } from "./conflict-schema.js";
2
+
3
+ export const CONFLICT_ATTENTION_LEVELS = [
4
+ "fresh",
5
+ "aging",
6
+ "stale",
7
+ "escalated",
8
+ "resolved",
9
+ ] as const;
10
+
11
+ export type ConflictAttention = (typeof CONFLICT_ATTENTION_LEVELS)[number];
12
+
13
+ const DAY_MS = 24 * 60 * 60 * 1000;
14
+
15
+ export interface ConflictLifecycleSummary {
16
+ attention: ConflictAttention;
17
+ openAgeDays: number;
18
+ reopenCount: number;
19
+ isOpen: boolean;
20
+ needsAttention: boolean;
21
+ }
22
+
23
+ export function parseConflictAttention(value?: string | null): ConflictAttention | undefined {
24
+ if (!value) return undefined;
25
+ return CONFLICT_ATTENTION_LEVELS.includes(value as ConflictAttention)
26
+ ? value as ConflictAttention
27
+ : undefined;
28
+ }
29
+
30
+ export function summarizeConflictLifecycle(
31
+ record: ConflictCandidateRecord,
32
+ now = new Date(),
33
+ ): ConflictLifecycleSummary {
34
+ const isOpen = record.status === "open";
35
+ const reopenCount = Math.max(0, record.reopenCount || 0);
36
+ if (!isOpen) {
37
+ return {
38
+ attention: "resolved",
39
+ openAgeDays: 0,
40
+ reopenCount,
41
+ isOpen: false,
42
+ needsAttention: false,
43
+ };
44
+ }
45
+
46
+ const openedAtRaw = record.lastReopenedAt || record.createdAt;
47
+ const openedAtMs = Date.parse(openedAtRaw);
48
+ const ageMs = Number.isFinite(openedAtMs) ? Math.max(0, now.getTime() - openedAtMs) : 0;
49
+ const openAgeDays = Math.floor(ageMs / DAY_MS);
50
+
51
+ let attention: ConflictAttention = "fresh";
52
+ if (openAgeDays >= 7 || reopenCount >= 3) {
53
+ attention = "escalated";
54
+ } else if (openAgeDays >= 3 || reopenCount >= 2) {
55
+ attention = "stale";
56
+ } else if (openAgeDays >= 1 || reopenCount >= 1) {
57
+ attention = "aging";
58
+ }
59
+
60
+ return {
61
+ attention,
62
+ openAgeDays,
63
+ reopenCount,
64
+ isOpen: true,
65
+ needsAttention: attention === "stale" || attention === "escalated",
66
+ };
67
+ }
@@ -0,0 +1,218 @@
1
+ import type { ConflictCandidateRecord, ConflictResolutionResult } from "./conflict-schema.js";
2
+ import type { ConflictAuditSummary, ConflictClusterSummary } from "./conflict-advisor.js";
3
+ import { summarizeConflictAdvice } from "./conflict-advisor.js";
4
+ import { summarizeConflictLifecycle } from "./conflict-lifecycle.js";
5
+ import type { EscalateConflictsResult } from "./conflict-schema.js";
6
+
7
+ export interface ConflictAuditRenderOptions {
8
+ generatedAt: string;
9
+ limit: number;
10
+ top: number;
11
+ status?: string;
12
+ canonicalKey?: string;
13
+ }
14
+
15
+ function shortAdviceLabel(record: ConflictCandidateRecord): string {
16
+ const advice = summarizeConflictAdvice(record);
17
+ const action = advice.suggestedResolution === "keep_existing"
18
+ ? "keep"
19
+ : advice.suggestedResolution === "accept_incoming"
20
+ ? "accept"
21
+ : "review";
22
+ const confidence = advice.confidence === "high"
23
+ ? "h"
24
+ : advice.confidence === "medium"
25
+ ? "m"
26
+ : "l";
27
+ return `${action}[${confidence}]`;
28
+ }
29
+
30
+ export function formatConflictList(records: ConflictCandidateRecord[]): string {
31
+ if (records.length === 0) return "No conflicts found.";
32
+
33
+ const lines = [
34
+ "ID Status Attention Advice Category Canonical key Updated Existing -> Incoming",
35
+ "-------- ------------------ ---------- --------- ------------ ------------------------------ ---------- ------------------------------",
36
+ ];
37
+
38
+ for (const record of records) {
39
+ const lifecycle = summarizeConflictLifecycle(record);
40
+ lines.push(
41
+ `${record.conflictId.slice(0, 8).padEnd(8)} ${record.status.padEnd(18)} ${lifecycle.attention.padEnd(10)} ${shortAdviceLabel(record).padEnd(9)} ${record.category.padEnd(12)} ${record.canonicalKey.slice(0, 30).padEnd(30)} ${record.updatedAt.slice(0, 10)} ${record.existing.memoryId.slice(0, 8)} -> ${record.incoming.sourceMemoryId?.slice(0, 8) || "manual"}`,
42
+ );
43
+ }
44
+
45
+ return lines.join("\n");
46
+ }
47
+
48
+ export function formatConflictClusters(clusters: ConflictClusterSummary[]): string {
49
+ if (clusters.length === 0) return "No conflict clusters found.";
50
+
51
+ const lines = [
52
+ "Cluster Attention Open/All Advice Category Canonical key Latest Latest ID",
53
+ "------------------------------ ---------- -------- --------- ------------ ------------------------------ ---------- --------",
54
+ ];
55
+
56
+ for (const cluster of clusters) {
57
+ const action = cluster.suggestedResolution === "keep_existing"
58
+ ? "keep"
59
+ : cluster.suggestedResolution === "accept_incoming"
60
+ ? "accept"
61
+ : "review";
62
+ const confidence = cluster.confidence === "high"
63
+ ? "h"
64
+ : cluster.confidence === "medium"
65
+ ? "m"
66
+ : "l";
67
+ lines.push(
68
+ `${cluster.clusterKey.slice(0, 30).padEnd(30)} ${cluster.attention.padEnd(10)} ${`${cluster.openCount}/${cluster.totalCount}`.padEnd(8)} ${`${action}[${confidence}]`.padEnd(9)} ${cluster.category.padEnd(12)} ${cluster.canonicalKey.slice(0, 30).padEnd(30)} ${cluster.latestUpdatedAt.slice(0, 10)} ${cluster.latestConflictId.slice(0, 8)}`,
69
+ );
70
+ }
71
+
72
+ return lines.join("\n");
73
+ }
74
+
75
+ export function formatConflictAudit(summary: ConflictAuditSummary): string {
76
+ const lines = [
77
+ "Conflict audit",
78
+ `Open conflicts : ${summary.openConflicts}/${summary.totalConflicts}`,
79
+ `Open clusters : ${summary.openClusters}/${summary.totalClusters}`,
80
+ `Attention : fresh=${summary.attentionCounts.fresh}, aging=${summary.attentionCounts.aging}, stale=${summary.attentionCounts.stale}, escalated=${summary.attentionCounts.escalated}, resolved=${summary.attentionCounts.resolved}`,
81
+ "",
82
+ "Suggested actions:",
83
+ ...summary.suggestedActions.map((action) => `- ${action}`),
84
+ ];
85
+
86
+ lines.push("");
87
+ lines.push("Priority clusters:");
88
+ if (summary.priorityClusters.length === 0) {
89
+ lines.push("- None");
90
+ } else {
91
+ lines.push(formatConflictClusters(summary.priorityClusters));
92
+ }
93
+
94
+ return lines.join("\n");
95
+ }
96
+
97
+ export function formatConflictAuditMarkdown(
98
+ summary: ConflictAuditSummary,
99
+ options: ConflictAuditRenderOptions,
100
+ ): string {
101
+ const lines = [
102
+ "# Conflict Audit",
103
+ "",
104
+ `- Generated: ${options.generatedAt}`,
105
+ `- Filters: status=${options.status || "all"}, canonicalKey=${options.canonicalKey || "all"}, limit=${options.limit}, top=${options.top}`,
106
+ `- Open conflicts: ${summary.openConflicts}/${summary.totalConflicts}`,
107
+ `- Open clusters: ${summary.openClusters}/${summary.totalClusters}`,
108
+ `- Attention: fresh=${summary.attentionCounts.fresh}, aging=${summary.attentionCounts.aging}, stale=${summary.attentionCounts.stale}, escalated=${summary.attentionCounts.escalated}, resolved=${summary.attentionCounts.resolved}`,
109
+ "",
110
+ "## Suggested Actions",
111
+ "",
112
+ ...summary.suggestedActions.map((action) => `- ${action}`),
113
+ "",
114
+ "## Priority Clusters",
115
+ "",
116
+ ];
117
+
118
+ if (summary.priorityClusters.length === 0) {
119
+ lines.push("- None");
120
+ return lines.join("\n");
121
+ }
122
+
123
+ summary.priorityClusters.forEach((cluster, index) => {
124
+ lines.push(`### ${index + 1}. ${cluster.clusterLabel}`);
125
+ lines.push("");
126
+ lines.push(`- Cluster key: ${cluster.clusterKey}`);
127
+ lines.push(`- Attention: ${cluster.attention}`);
128
+ lines.push(`- Open/All: ${cluster.openCount}/${cluster.totalCount}`);
129
+ lines.push(`- Advice: ${cluster.suggestedResolution} (${cluster.confidence})`);
130
+ lines.push(`- Canonical key: ${cluster.canonicalKey}`);
131
+ lines.push(`- Category: ${cluster.category}`);
132
+ lines.push(`- Latest conflict: ${cluster.latestConflictId}`);
133
+ lines.push(`- Updated: ${cluster.latestUpdatedAt}`);
134
+ lines.push("");
135
+ });
136
+
137
+ return lines.join("\n");
138
+ }
139
+
140
+ export function formatConflictRecord(record: ConflictCandidateRecord): string {
141
+ const lifecycle = summarizeConflictLifecycle(record);
142
+ const advice = summarizeConflictAdvice(record);
143
+ const lines = [
144
+ `Conflict ${record.conflictId.slice(0, 8)}`,
145
+ `Status: ${record.status}`,
146
+ `Attention: ${lifecycle.attention}`,
147
+ `Open age days: ${lifecycle.openAgeDays}`,
148
+ `Reopen count: ${lifecycle.reopenCount}`,
149
+ `Escalation count: ${record.escalationCount || 0}`,
150
+ `Suggested resolution: ${advice.suggestedResolution} (${advice.confidence})`,
151
+ `Similarity: ${advice.similarity}`,
152
+ `Cluster key: ${advice.clusterKey}`,
153
+ `Reason: ${record.reason}`,
154
+ `Canonical key: ${record.canonicalKey}`,
155
+ `Incoming category: ${record.category}`,
156
+ `Existing category: ${record.existing.category}`,
157
+ `Created: ${record.createdAt}`,
158
+ `Updated: ${record.updatedAt}`,
159
+ ...(record.lastReopenedAt ? [`Last reopened: ${record.lastReopenedAt}`] : []),
160
+ ...(record.lastEscalatedAt ? [`Last escalated: ${record.lastEscalatedAt} (${record.lastEscalationAttention})`] : []),
161
+ "",
162
+ `Existing durable (${record.existing.memoryId.slice(0, 8)}): ${record.existing.text}`,
163
+ `Incoming candidate (${record.incoming.sourceMemoryId?.slice(0, 8) || record.incoming.source}): ${record.incoming.text}`,
164
+ ];
165
+
166
+ if (record.resolutionNotes) {
167
+ lines.push(`Notes: ${record.resolutionNotes}`);
168
+ }
169
+ if (advice.mergeSuggestion) {
170
+ lines.push(`Merge suggestion: ${advice.mergeSuggestion}`);
171
+ }
172
+ if (advice.reasons.length > 0) {
173
+ lines.push("");
174
+ lines.push("Advice reasons:");
175
+ for (const reason of advice.reasons) {
176
+ lines.push(`- ${reason}`);
177
+ }
178
+ }
179
+
180
+ return lines.join("\n");
181
+ }
182
+
183
+ export function formatConflictEscalation(result: EscalateConflictsResult): string {
184
+ const lines = [
185
+ result.apply ? "Conflict escalation applied" : "Conflict escalation preview",
186
+ `Scanned : ${result.scanned}`,
187
+ `Eligible : ${result.eligible}`,
188
+ `Escalated : ${result.escalated}`,
189
+ `Skipped : ${result.skipped}`,
190
+ ];
191
+
192
+ if (result.items.length === 0) {
193
+ lines.push("");
194
+ lines.push("No stale or escalated conflicts matched.");
195
+ return lines.join("\n");
196
+ }
197
+
198
+ lines.push("");
199
+ lines.push("Items:");
200
+ for (const item of result.items) {
201
+ lines.push(
202
+ `${item.conflictId.slice(0, 8)} ${item.attention.padEnd(10)} ${item.action.padEnd(17)} ${item.suggestedResolution.padEnd(15)} ${item.canonicalKey}`,
203
+ );
204
+ }
205
+ return lines.join("\n");
206
+ }
207
+
208
+ export function formatConflictResolution(result: ConflictResolutionResult): string {
209
+ const lines = [
210
+ `Conflict ${result.conflictId.slice(0, 8)} resolved`,
211
+ `Status: ${result.status}`,
212
+ `Updated: ${result.updatedAt}`,
213
+ ];
214
+ if (result.updatedMemoryId) {
215
+ lines.push(`Updated memory: ${result.updatedMemoryId.slice(0, 8)}`);
216
+ }
217
+ return lines.join("\n");
218
+ }