@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.2

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 (258) hide show
  1. package/INSTALL.md +25 -0
  2. package/PANEL.md +92 -0
  3. package/README.md +286 -2
  4. package/RELEASE-NOTES.md +732 -0
  5. package/bin/bc-acceptance-full.mjs +91 -0
  6. package/bin/bc-acceptance-tight.mjs +66 -0
  7. package/bin/bc-acceptance.mjs +22 -0
  8. package/bin/bc-add-keywords.mjs +21 -0
  9. package/bin/bc-demo.mjs +47 -0
  10. package/bin/bc-expand-hint.mjs +179 -0
  11. package/bin/bc-ingest-index.mjs +35 -0
  12. package/bin/bc-ingest.mjs +31 -0
  13. package/bin/bc-label-demo.mjs +18 -0
  14. package/bin/bc-relabel.mjs +150 -0
  15. package/bin/bc-sessionize.mjs +59 -0
  16. package/bin/coref_pipeline.py +47 -0
  17. package/bin/coref_service.py +57 -0
  18. package/bin/debug_ckip.py +16 -0
  19. package/bin/debug_ner_zh.py +18 -0
  20. package/bin/doctor.mjs +275 -0
  21. package/bin/en_ner_error_audit.py +33 -0
  22. package/bin/eval-label-rules.mjs +342 -0
  23. package/bin/export-turns.py +68 -0
  24. package/bin/extract-user-paragraph.mjs +14 -0
  25. package/bin/extraction-composite-smoke.mjs +28 -0
  26. package/bin/extraction-latency.mjs +58 -0
  27. package/bin/extraction-user-smoke.mjs +28 -0
  28. package/bin/extraction_service.py +164 -0
  29. package/bin/facet-active-experiment.mjs +110 -0
  30. package/bin/facet-covid.mjs +8 -0
  31. package/bin/facet-curve.mjs +27 -0
  32. package/bin/facet-embedding-train.mjs +20 -0
  33. package/bin/facet-embedding-validate.mjs +83 -0
  34. package/bin/facet-eval-roleplay.mjs +42 -0
  35. package/bin/facet-eval.mjs +38 -0
  36. package/bin/facet-fold.mjs +31 -0
  37. package/bin/facet-load.mjs +16 -0
  38. package/bin/facet-mlp-scan.mjs +45 -0
  39. package/bin/facet-mvp.mjs +34 -0
  40. package/bin/facet-online-experiments.mjs +78 -0
  41. package/bin/facet-online-race.mjs +99 -0
  42. package/bin/facet-online-validate.mjs +51 -0
  43. package/bin/facet-panel.mjs +22 -0
  44. package/bin/facet-passage.mjs +41 -0
  45. package/bin/facet-random-validate.mjs +48 -0
  46. package/bin/facet-roleplay.mjs +33 -0
  47. package/bin/facet-scan.mjs +56 -0
  48. package/bin/facet-segment.mjs +34 -0
  49. package/bin/facet-train.mjs +19 -0
  50. package/bin/facet-validate.mjs +103 -0
  51. package/bin/facet-zeroshot.mjs +14 -0
  52. package/bin/install-templates.mjs +72 -0
  53. package/bin/memory-bench.mjs +53 -0
  54. package/bin/memory-build-summary.mjs +43 -0
  55. package/bin/memory-build-vector.mjs +31 -0
  56. package/bin/memory-build.mjs +41 -0
  57. package/bin/memory-calibration.mjs +70 -0
  58. package/bin/memory-closed-loop.mjs +119 -0
  59. package/bin/memory-dry-run.mjs +70 -0
  60. package/bin/memory-eval.mjs +104 -0
  61. package/bin/memory-event-alias-test.mjs +89 -0
  62. package/bin/memory-extraction-bench.mjs +58 -0
  63. package/bin/memory-load-gate-replay.mjs +137 -0
  64. package/bin/memory-loop-test.mjs +90 -0
  65. package/bin/memory-pre-turn-harness.mjs +63 -0
  66. package/bin/memory-query.mjs +39 -0
  67. package/bin/memory-replay-hints.mjs +84 -0
  68. package/bin/memory-scale.mjs +58 -0
  69. package/bin/memory-scenario-test.mjs +146 -0
  70. package/bin/memory-user-loop.mjs +95 -0
  71. package/bin/ner_bench_tiny_zh.py +59 -0
  72. package/bin/ner_bench_zh.py +60 -0
  73. package/bin/ner_conll_en.py +139 -0
  74. package/bin/ner_direct_en.py +123 -0
  75. package/bin/ner_direct_zh.py +100 -0
  76. package/bin/ner_pipeline.py +208 -0
  77. package/bin/noise-selfcheck.mjs +50 -0
  78. package/bin/pack-integrity.mjs +47 -0
  79. package/bin/panel-check.mjs +57 -0
  80. package/bin/plugin-integrity.mjs +74 -0
  81. package/bin/preview-live.mjs +13 -0
  82. package/bin/public-install-check.mjs +222 -0
  83. package/bin/relabel-apply.mjs +272 -0
  84. package/bin/relabel-dry-run.mjs +381 -0
  85. package/bin/relik_pipeline.py +39 -0
  86. package/bin/relik_service.py +52 -0
  87. package/bin/srl_pipeline.py +55 -0
  88. package/bin/srl_service.py +68 -0
  89. package/bin/summary-index.mjs +151 -0
  90. package/bin/summary-query.mjs +79 -0
  91. package/bin/vec-check.mjs +27 -0
  92. package/bin/zh_ner_error_audit.py +36 -0
  93. package/bin/zh_ner_slot_eval.py +38 -0
  94. package/client.js +760 -0
  95. package/cordis.patch.yml +378 -0
  96. package/docs/API-NOTES.md +1743 -0
  97. package/docs/ARCHITECTURE.md +355 -0
  98. package/docs/CACHE.md +253 -0
  99. package/docs/CONDUCT.md +30 -0
  100. package/docs/CREATOR-HANDOFF.md +102 -0
  101. package/docs/EXPERIMENT.md +136 -0
  102. package/docs/FACET-MVP-REPORT.md +473 -0
  103. package/docs/GUIDE-0.4.66.md +195 -0
  104. package/docs/GUIDE.md +127 -0
  105. package/docs/MECHANISM.md +243 -0
  106. package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
  107. package/docs/README.md +37 -0
  108. package/docs/SESSION-LAYOUT.md +151 -0
  109. package/docs/history-map.md +249 -0
  110. package/docs/relabel-migration-wiring.md +366 -0
  111. package/guides/00-READ-FIRST.md +206 -0
  112. package/guides/01-INSTALL.md +207 -0
  113. package/guides/02-TABLES.md +225 -0
  114. package/guides/03-AUDIT.md +201 -0
  115. package/guides/04-REBUILD.md +306 -0
  116. package/guides/05-FIRST-RUN.md +191 -0
  117. package/guides/06-EMBEDDING.md +146 -0
  118. package/guides/07-PTC-AND-ASSETS.md +221 -0
  119. package/icon.svg +7 -0
  120. package/locale/en.json +6 -0
  121. package/locale/zh.json +6 -0
  122. package/models/README.md +13 -0
  123. package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
  124. package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
  125. package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
  126. package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
  127. package/package.json +139 -4
  128. package/src/asset-tool.mjs +237 -0
  129. package/src/assets.mjs +244 -0
  130. package/src/backfill.mjs +74 -0
  131. package/src/backup.mjs +80 -0
  132. package/src/deadend.mjs +95 -0
  133. package/src/deleted-store.mjs +35 -0
  134. package/src/engine.mjs +3831 -0
  135. package/src/facets/classifier.mjs +146 -0
  136. package/src/facets/embedding-service.mjs +53 -0
  137. package/src/facets/embedding-worker.mjs +27 -0
  138. package/src/facets/embedding.mjs +163 -0
  139. package/src/facets/extract.mjs +80 -0
  140. package/src/facets/fold-plan.mjs +84 -0
  141. package/src/facets/hybrid.mjs +29 -0
  142. package/src/facets/index.mjs +113 -0
  143. package/src/facets/linear-probe.mjs +43 -0
  144. package/src/facets/load.mjs +37 -0
  145. package/src/facets/mlp-probe.mjs +54 -0
  146. package/src/facets/mlp-service.mjs +53 -0
  147. package/src/facets/mlp-worker.mjs +26 -0
  148. package/src/facets/online-knn.mjs +58 -0
  149. package/src/facets/online-models.mjs +227 -0
  150. package/src/facets/panel-state.mjs +23 -0
  151. package/src/facets/panel.mjs +73 -0
  152. package/src/facets/random.mjs +26 -0
  153. package/src/facets/scanner.mjs +35 -0
  154. package/src/facets/schema.mjs +75 -0
  155. package/src/facets/seed-examples.mjs +58 -0
  156. package/src/facets/sensitivity.mjs +98 -0
  157. package/src/facets/service.mjs +53 -0
  158. package/src/facets/store.mjs +39 -0
  159. package/src/facets/worker.mjs +23 -0
  160. package/src/folds-store.mjs +109 -0
  161. package/src/groups.mjs +167 -0
  162. package/src/handoff-tool.mjs +51 -0
  163. package/src/handoff.mjs +127 -0
  164. package/src/index.mjs +649 -0
  165. package/src/inject-policy.mjs +17 -0
  166. package/src/learning.mjs +88 -0
  167. package/src/memory/alias-proposal.mjs +40 -0
  168. package/src/memory/bc-audit.mjs +245 -0
  169. package/src/memory/bc-index-updater.mjs +404 -0
  170. package/src/memory/bc-index.mjs +48 -0
  171. package/src/memory/bc-label.mjs +220 -0
  172. package/src/memory/bc-panel-state.mjs +107 -0
  173. package/src/memory/bc-pre-step.mjs +463 -0
  174. package/src/memory/bc-recall.mjs +470 -0
  175. package/src/memory/bc-shift.mjs +123 -0
  176. package/src/memory/bc-store.mjs +41 -0
  177. package/src/memory/bm25.mjs +112 -0
  178. package/src/memory/calibration.mjs +54 -0
  179. package/src/memory/chatter.mjs +29 -0
  180. package/src/memory/cmap-file.mjs +107 -0
  181. package/src/memory/concepts-file.mjs +41 -0
  182. package/src/memory/coref-slots.mjs +40 -0
  183. package/src/memory/event-alias-ledger.mjs +87 -0
  184. package/src/memory/event-log.mjs +95 -0
  185. package/src/memory/event-model.mjs +132 -0
  186. package/src/memory/event-segmenter.mjs +36 -0
  187. package/src/memory/event-summary-tree.mjs +122 -0
  188. package/src/memory/extraction-adapter.mjs +76 -0
  189. package/src/memory/extraction-composite.mjs +75 -0
  190. package/src/memory/extraction-core.mjs +76 -0
  191. package/src/memory/extraction-pre-step.mjs +161 -0
  192. package/src/memory/extraction-provider.mjs +300 -0
  193. package/src/memory/extraction-score.mjs +36 -0
  194. package/src/memory/facet-stabilizer.mjs +59 -0
  195. package/src/memory/history-map.mjs +139 -0
  196. package/src/memory/language-router.mjs +30 -0
  197. package/src/memory/load-gate.mjs +161 -0
  198. package/src/memory/mechanical-note.mjs +104 -0
  199. package/src/memory/onboarding.mjs +163 -0
  200. package/src/memory/persona.mjs +78 -0
  201. package/src/memory/planner.mjs +249 -0
  202. package/src/memory/pre-turn.mjs +80 -0
  203. package/src/memory/query-aliases.mjs +39 -0
  204. package/src/memory/recall-cue.mjs +43 -0
  205. package/src/memory/recall.mjs +65 -0
  206. package/src/memory/referent-candidates.mjs +55 -0
  207. package/src/memory/referent-ledger.mjs +86 -0
  208. package/src/memory/relabel-migrate.mjs +376 -0
  209. package/src/memory/relik-adapter.mjs +25 -0
  210. package/src/memory/special-keywords.mjs +60 -0
  211. package/src/memory/structured-adapters.mjs +75 -0
  212. package/src/memory/summary-index.mjs +335 -0
  213. package/src/memory/summary-tree.mjs +110 -0
  214. package/src/memory/term-extractor.mjs +44 -0
  215. package/src/memory/tokenize.mjs +9 -0
  216. package/src/memory/vector-index.mjs +54 -0
  217. package/src/memory/window-summary.mjs +133 -0
  218. package/src/mood.mjs +133 -0
  219. package/src/peer-tool.mjs +63 -0
  220. package/src/peer.mjs +144 -0
  221. package/src/policy.mjs +170 -0
  222. package/src/preflight.mjs +113 -0
  223. package/src/provenance.mjs +94 -0
  224. package/src/range.mjs +75 -0
  225. package/src/recall-exact-tool.mjs +267 -0
  226. package/src/recall-tool.mjs +521 -0
  227. package/src/recheck.mjs +99 -0
  228. package/src/save-start.mjs +29 -0
  229. package/src/session-guard.mjs +140 -0
  230. package/src/session-log-tool.mjs +322 -0
  231. package/src/session-log.mjs +606 -0
  232. package/src/skills.mjs +111 -0
  233. package/src/split-range.mjs +115 -0
  234. package/src/state.mjs +198 -0
  235. package/src/t0.mjs +149 -0
  236. package/src/tag-confirm.mjs +78 -0
  237. package/src/tag-detect.mjs +42 -0
  238. package/src/tag-maintenance.mjs +285 -0
  239. package/src/tag-reject.mjs +73 -0
  240. package/src/tags-store.mjs +94 -0
  241. package/src/tags.mjs +692 -0
  242. package/src/threads-backend.mjs +26 -0
  243. package/src/threads-core.mjs +1887 -0
  244. package/src/threads-store.mjs +207 -0
  245. package/src/threads-tool.mjs +522 -0
  246. package/src/threads.mjs +929 -0
  247. package/src/tool-preview.mjs +427 -0
  248. package/src/topic.mjs +63 -0
  249. package/src/training.mjs +75 -0
  250. package/src/turn-tag.mjs +62 -0
  251. package/src/user-turn.mjs +39 -0
  252. package/templates/cmap.md +11 -0
  253. package/templates/concepts.md +12 -0
  254. package/templates/habits.md +40 -0
  255. package/templates/keywords.md +6 -0
  256. package/templates/rebuild.md +134 -0
  257. package/templates/rules-safe.md +184 -0
  258. package/templates/rules.md +177 -0
@@ -0,0 +1,470 @@
1
+ /**
2
+ * B+C recall scaffold.
3
+ *
4
+ * B is a stable generic concept layer. C is a per-user term map.
5
+ * This module is pure and has no model, network, or live dependency.
6
+ */
7
+
8
+ export const GENERIC_CONCEPTS = Object.freeze([
9
+ '游戏', '故事', '对话', '技术', '设备', '规则', '记忆', '剧情',
10
+ '病毒', '公共卫生', '科学',
11
+ ]);
12
+
13
+ /** No hidden C entries: every term must come from cmap.md. */
14
+ export const DEFAULT_C_SEED = Object.freeze([]);
15
+
16
+ function normalizeTerm(term) {
17
+ return String(term ?? '').trim();
18
+ }
19
+
20
+ export function createCMap(entries = []) {
21
+ const terms = new Map();
22
+ const metadata = new Map();
23
+ for (const entry of entries) {
24
+ const term = normalizeTerm(entry?.term);
25
+ if (!term) continue;
26
+ const generic = [...new Set((entry.generic ?? []).map(normalizeTerm).filter(Boolean))];
27
+ terms.set(term, generic);
28
+ metadata.set(term, {
29
+ generic,
30
+ keywords: [...new Set((entry.keywords ?? []).map(normalizeTerm).filter(Boolean))],
31
+ minB: Number.isInteger(entry.minB) ? entry.minB : 0,
32
+ limit: Number.isInteger(entry.limit) && entry.limit > 0 ? entry.limit : 20,
33
+ summary: typeof entry.summary === 'string' ? entry.summary : '',
34
+ });
35
+ }
36
+ return { terms, asked: new Set(), metadata };
37
+ }
38
+
39
+ export function serializeCMap(cmap) {
40
+ return {
41
+ terms: [...(cmap?.terms ?? new Map()).entries()].map(([term, generic]) => {
42
+ const meta = cmap?.metadata?.get(term) ?? {};
43
+ return {
44
+ term,
45
+ generic: [...generic],
46
+ keywords: [...(meta.keywords ?? [])],
47
+ minB: meta.minB ?? 0,
48
+ summary: meta.summary ?? '',
49
+ };
50
+ }),
51
+ asked: [...(cmap?.asked ?? new Set())],
52
+ };
53
+ }
54
+
55
+ export function deserializeCMap(raw = {}) {
56
+ const cmap = createCMap(Array.isArray(raw.terms) ? raw.terms : []);
57
+ for (const term of raw.asked ?? []) {
58
+ const key = normalizeTerm(term);
59
+ if (key) cmap.asked.add(key);
60
+ }
61
+ return cmap;
62
+ }
63
+
64
+ export function learnTerm(cmap, term, generic, { source = 'user' } = {}) {
65
+ const key = normalizeTerm(term);
66
+ const values = [...new Set((Array.isArray(generic) ? generic : [generic]).map(normalizeTerm).filter(Boolean))];
67
+ if (!key || values.length === 0) return cmap;
68
+ cmap.terms.set(key, values);
69
+ cmap.metadata?.set(key, {
70
+ generic: values,
71
+ keywords: cmap.metadata?.get(key)?.keywords ?? [],
72
+ minB: cmap.metadata?.get(key)?.minB ?? 0,
73
+ limit: cmap.metadata?.get(key)?.limit ?? 20,
74
+ summary: cmap.metadata?.get(key)?.summary ?? '',
75
+ });
76
+ cmap.asked.delete(key);
77
+ cmap.lastLearn = { term: key, generic: values, source };
78
+ return cmap;
79
+ }
80
+
81
+ export function shouldAsk(cmap, term) {
82
+ const key = normalizeTerm(term);
83
+ return key.length > 0 && !cmap.terms.has(key) && !cmap.asked.has(key);
84
+ }
85
+
86
+ export function markAsked(cmap, term) {
87
+ const key = normalizeTerm(term);
88
+ if (key) cmap.asked.add(key);
89
+ return cmap;
90
+ }
91
+
92
+ /** Pick the longest known term/concept present in a sentence. */
93
+ export function queryFor(text, { records = [], cmap = createCMap() } = {}) {
94
+ const source = String(text ?? '');
95
+ const candidates = new Set([
96
+ ...GENERIC_CONCEPTS,
97
+ ...(cmap?.terms?.keys?.() ?? []),
98
+ ...records.flatMap((record) => record.terms ?? []),
99
+ ]);
100
+ const present = [...candidates].filter((term) => term.length > 0 && source.includes(term));
101
+ present.sort((a, b) => b.length - a.length);
102
+ return present[0] ?? source.trim();
103
+ }
104
+
105
+ /** Learn from a user's explanation, e.g. "就是游戏、故事、对话". */
106
+ export function learnFromExplanation(cmap, term, explanation, { records = [] } = {}) {
107
+ const mentioned = GENERIC_CONCEPTS.filter((concept) => String(explanation ?? '').includes(concept));
108
+ if (mentioned.length === 0) return { ok: false, reason: 'no-generic-concept', generic: [] };
109
+ learnTerm(cmap, term, mentioned, { source: 'user-explanation' });
110
+ return {
111
+ ok: true,
112
+ term: normalizeTerm(term),
113
+ generic: mentioned,
114
+ records: backfillRecords(records, term, mentioned),
115
+ };
116
+ }
117
+
118
+ /** Parse "前两句/上一句/刚才/前面三句" into a reference count. */
119
+ export function parseReference(text) {
120
+ const source = String(text ?? '');
121
+ const match = source.match(/前(?:面)?\s*([一二两三四五六七八九十\d]+)\s*句/) ?? source.match(/([一二两三四五六七八九十\d]+)\s*句前/);
122
+ if (match) {
123
+ const token = match[1];
124
+ const digits = { 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10 };
125
+ const count = /^\d+$/.test(token) ? Number(token) : (digits[token] ?? Number(token));
126
+ return { kind: 'count', count };
127
+ }
128
+ if (/上一句|刚才|刚刚/.test(source)) return { kind: 'count', count: 1 };
129
+ if (/前面|之前/.test(source)) return { kind: 'recent', count: 3 };
130
+ return null;
131
+ }
132
+
133
+ export function referencedRecords(records = [], count = 1) {
134
+ return records.slice(Math.max(0, records.length - Math.max(1, count)));
135
+ }
136
+
137
+ /**
138
+ * Intersection of the generic labels across evidence records.
139
+ * Background concepts are removed when provided.
140
+ */
141
+ export function intersectionConcepts(evidence = [], { background = [] } = {}) {
142
+ if (evidence.length === 0) return { allOf: [], anyOf: [] };
143
+ const sets = evidence.map((record) => new Set(record.generic ?? []));
144
+ const backgroundSet = new Set(background);
145
+ const all = [...sets[0]].filter((concept) => sets.every((set) => set.has(concept)) && !backgroundSet.has(concept));
146
+ const any = [...new Set(evidence.flatMap((record) => record.generic ?? []))].filter((concept) => !backgroundSet.has(concept));
147
+ return { allOf: all, anyOf: any };
148
+ }
149
+
150
+ /** User points at prior turns: "前两句说的就是跑团". */
151
+ export function learnFromReference(cmap, term, evidence, records = [], { background = [] } = {}) {
152
+ const { allOf, anyOf } = intersectionConcepts(evidence, { background });
153
+ const generic = allOf.length > 0 ? allOf : anyOf;
154
+ if (generic.length === 0) return { ok: false, reason: 'empty-intersection', allOf, anyOf, generic: [] };
155
+ learnTerm(cmap, term, generic, { source: 'referenced-turns' });
156
+ const learnedRecords = backfillRecords(records, term, generic);
157
+ return { ok: true, term: normalizeTerm(term), allOf, anyOf, generic, records: learnedRecords };
158
+ }
159
+
160
+ export function makeRecord({ turn, seqStart, seqEnd, text, generic = [], genericTopic = null, genericWorkflow = null, topicScore = null, topicConfidence = null, topicMinScore = null, labelStamp = null, labelVersion = 1, terms = [], confidence = 1, status = 'confirmed', conceptScores = {}, keywords = [], backfilled = [], vec = null, chatter = null, sessionId = null }) {
161
+ return {
162
+ turn: Number.isInteger(turn) ? turn : null,
163
+ seqStart: Number.isInteger(seqStart) ? seqStart : null,
164
+ seqEnd: Number.isInteger(seqEnd) ? seqEnd : seqStart ?? null,
165
+ text: String(text ?? ''),
166
+ generic: [...new Set(generic.map(normalizeTerm).filter(Boolean))],
167
+ ...(Array.isArray(genericTopic) ? { genericTopic: [...new Set(genericTopic.map(normalizeTerm).filter(Boolean))] } : {}),
168
+ ...(Array.isArray(genericWorkflow) ? { genericWorkflow: [...new Set(genericWorkflow.map(normalizeTerm).filter(Boolean))] } : {}),
169
+ ...(Number.isFinite(topicScore) ? { topicScore: Number(topicScore) } : {}),
170
+ ...(typeof topicConfidence === 'string' && topicConfidence ? { topicConfidence } : {}),
171
+ ...(Number.isFinite(topicMinScore) ? { topicMinScore: Number(topicMinScore) } : {}),
172
+ ...(typeof labelStamp === 'string' && labelStamp ? { labelStamp } : {}),
173
+ ...(Number.isInteger(labelVersion) ? { labelVersion } : {}),
174
+ terms: [...new Set(terms.map(normalizeTerm).filter(Boolean))],
175
+ keywords: [...new Set(keywords.map(normalizeTerm).filter(Boolean))],
176
+ backfilled: [...new Set(backfilled.map(normalizeTerm).filter(Boolean))],
177
+ confidence: Number(confidence) || 0,
178
+ conceptScores: Object.fromEntries(Object.entries(conceptScores ?? {}).map(([key, value]) => [normalizeTerm(key), Number(value) || 0])),
179
+ status,
180
+ ...(typeof vec === 'string' && vec ? { vec } : {}),
181
+ ...(Number.isFinite(chatter) ? { chatter: Number(chatter) } : {}),
182
+ ...(typeof sessionId === 'string' && sessionId ? { sessionId } : {}),
183
+ };
184
+ }
185
+
186
+ const KEYWORD_STOP = new Set([
187
+ '我们', '你们', '他们', '这个', '那个', '就是', '然后', '因为', '所以', '如果', '但是',
188
+ '已经', '还是', '可以', '没有', '一个', '自己', '什么', '这样', '那样', '现在', '这里',
189
+ '那里', '时候', '一下', '不是', '就是', '以及', '还有', '进行', '东西', '事情',
190
+ ]);
191
+
192
+ /** Small, storage-friendly keyword extractor. Not a model. */
193
+ export function extractKeywords(text, { limit = 12 } = {}) {
194
+ const source = String(text ?? '').toLowerCase();
195
+ const counts = new Map();
196
+ const add = (word) => {
197
+ if (word.length < 2 || KEYWORD_STOP.has(word)) return;
198
+ counts.set(word, (counts.get(word) ?? 0) + 1);
199
+ };
200
+ for (const word of source.match(/[a-z0-9_]{2,}/g) ?? []) add(word);
201
+ for (const chunk of source.match(/[\u4e00-\u9fff]+/g) ?? []) {
202
+ const chars = [...chunk];
203
+ for (let size = 2; size <= 4; size += 1) {
204
+ for (let index = 0; index + size <= chars.length; index += 1) add(chars.slice(index, index + size).join(''));
205
+ }
206
+ }
207
+ return [...counts.entries()]
208
+ .sort((a, b) => (b[1] * b[0].length) - (a[1] * a[0].length))
209
+ .slice(0, limit)
210
+ .map(([word]) => word);
211
+ }
212
+
213
+ /**
214
+ * Expand a hit by neighboring turns as long as the next turn is adjacent and
215
+ * either shares a high-confidence facet or at least two keywords.
216
+ * No fixed ±3; a generous safety cap prevents runaway growth.
217
+ */
218
+ export function expandWindow(records = [], hit, { maxTurns = 100, minFacetScore = 0.4, minKeywordOverlap = 2 } = {}) {
219
+ if (!hit) return { seedTurn: null, turns: [], seqStart: null, seqEnd: null, expanded: false };
220
+ const sorted = [...records].filter((record) => Number.isInteger(record.turn)).sort((a, b) => a.turn - b.turn);
221
+ const index = sorted.findIndex((record) => record.turn === hit.turn && record.seqStart === hit.seqStart);
222
+ if (index < 0) return { seedTurn: hit.turn ?? null, turns: [hit.turn], seqStart: hit.seqStart ?? null, seqEnd: hit.seqEnd ?? null, expanded: false };
223
+ const shared = (a, b) => {
224
+ const facet = a.generic.some((concept) => b.generic.includes(concept)
225
+ && (a.conceptScores?.[concept] ?? 0) >= minFacetScore
226
+ && (b.conceptScores?.[concept] ?? 0) >= minFacetScore);
227
+ const keywordsA = new Set(a.keywords ?? []);
228
+ const overlap = (b.keywords ?? []).filter((word) => keywordsA.has(word)).length;
229
+ return facet || overlap >= minKeywordOverlap;
230
+ };
231
+ let left = index;
232
+ let right = index;
233
+ while (index - left < maxTurns && left > 0) {
234
+ const previous = sorted[left - 1];
235
+ const current = sorted[left];
236
+ if (current.turn - previous.turn > 1 || !shared(previous, current)) break;
237
+ left -= 1;
238
+ }
239
+ while (right - index < maxTurns && right + 1 < sorted.length) {
240
+ const current = sorted[right];
241
+ const next = sorted[right + 1];
242
+ if (next.turn - current.turn > 1 || !shared(current, next)) break;
243
+ right += 1;
244
+ }
245
+ const window = sorted.slice(left, right + 1);
246
+ return {
247
+ seedTurn: hit.turn ?? null,
248
+ turns: window.map((record) => record.turn),
249
+ seqStart: window[0]?.seqStart ?? null,
250
+ seqEnd: window[window.length - 1]?.seqEnd ?? null,
251
+ expanded: window.length > 1,
252
+ };
253
+ }
254
+
255
+ function recordsForConcepts(records, concepts, { minConceptScore = 0, minTopScore = 0, maxTopGap = 0.06, term = null, requireTerm = false } = {}) {
256
+ const wanted = new Set(concepts);
257
+ return records.filter((record) => {
258
+ if (requireTerm && term !== null && !record.terms.includes(term)) return false;
259
+ return record.generic.some((value) => {
260
+ if (!wanted.has(value)) return false;
261
+ const scores = record.conceptScores ?? {};
262
+ const values = Object.values(scores);
263
+ if (values.length === 0) return true;
264
+ const score = scores[value];
265
+ if (score === undefined) return values.length === 0 || (record.backfilled ?? []).includes(value);
266
+ const top = Math.max(...values);
267
+ if (minConceptScore > 0 && score < minConceptScore) return false;
268
+ if (minTopScore > 0 && top < minTopScore) return false;
269
+ if (score < top - maxTopGap) return false;
270
+ return true;
271
+ });
272
+ });
273
+ }
274
+
275
+ /** Learning a term may backfill old records that carry the term but lack the generic label. */
276
+ export function backfillRecords(records = [], term, generic = []) {
277
+ const key = normalizeTerm(term);
278
+ const values = [...new Set(generic.map(normalizeTerm).filter(Boolean))];
279
+ if (!key || values.length === 0) return records;
280
+ return records.map((record) => (
281
+ record.terms.includes(key)
282
+ ? {
283
+ ...record,
284
+ generic: [...new Set([...record.generic, ...values])],
285
+ backfilled: [...new Set([...(record.backfilled ?? []), ...values])],
286
+ }
287
+ : record
288
+ ));
289
+ }
290
+
291
+ export function retrieve({ records = [], cmap = createCMap(), query = '', specialKeywords = [], minConceptScore = 0.5, minTopScore = 0.5, maxTopGap = 0.03 } = {}) {
292
+ const term = normalizeTerm(query);
293
+ const specials = [...new Set(specialKeywords.map(normalizeTerm).filter(Boolean))];
294
+ if (specials.length > 0) {
295
+ const wanted = new Set(specials.map((word) => word.toLowerCase()));
296
+ let hits = records.filter((record) => (record.keywords ?? [])
297
+ .some((word) => wanted.has(String(word).toLowerCase())));
298
+ if (hits.length > 0) {
299
+ if (GENERIC_CONCEPTS.includes(term)) {
300
+ const facet = recordsForConcepts(hits, [term], { minConceptScore: 0, minTopScore: 0, maxTopGap: 1 });
301
+ if (facet.length > 0) hits = facet;
302
+ } else if (cmap.terms.has(term)) {
303
+ const facet = recordsForConcepts(hits, cmap.terms.get(term), { minConceptScore: 0, minTopScore: 0, maxTopGap: 1 });
304
+ if (facet.length > 0) hits = facet;
305
+ }
306
+ const facetConcepts = GENERIC_CONCEPTS.includes(term)
307
+ ? [term]
308
+ : (cmap.terms.get(term) ?? []);
309
+ return { kind: 'recall', via: 'K', keywords: specials, concepts: facetConcepts, hits };
310
+ }
311
+ if (!term) return { kind: 'silent', reason: 'no-keyword-record', keywords: specials };
312
+ }
313
+ if (!term) return { kind: 'silent', reason: 'empty-query' };
314
+ if (GENERIC_CONCEPTS.includes(term)) {
315
+ const hits = recordsForConcepts(records, [term], { minConceptScore, minTopScore, maxTopGap });
316
+ return hits.length > 0
317
+ ? { kind: 'recall', via: 'B', concepts: [term], hits }
318
+ : { kind: 'silent', reason: 'no-record', concepts: [term] };
319
+ }
320
+ if (cmap.terms.has(term)) {
321
+ const concepts = cmap.terms.get(term);
322
+ const meta = cmap.metadata?.get(term) ?? { generic: concepts, keywords: [], minB: 0, summary: '' };
323
+ const minB = Number.isInteger(meta.minB) ? meta.minB : 0;
324
+ const kWords = new Set((meta.keywords ?? []).map((word) => String(word).toLowerCase()));
325
+ const hits = records
326
+ .filter((record) => {
327
+ const inTerms = (record.terms ?? []).includes(term);
328
+ const inKeywords = (record.keywords ?? []).includes(term);
329
+ return inTerms || inKeywords;
330
+ })
331
+ .map((record) => {
332
+ const declared = meta.generic ?? concepts;
333
+ const scoreOf = (concept) => {
334
+ const score = record.conceptScores?.[concept];
335
+ if (score !== undefined) return score;
336
+ // Legacy records without conceptScores: fall back to the displayed label.
337
+ return (record.generic ?? []).includes(concept) ? 1 : 0;
338
+ };
339
+ const hasAny = declared.some((concept) => scoreOf(concept) > 0);
340
+ const bCount = declared.filter((concept) => scoreOf(concept) >= 0.5).length;
341
+ const lowB = bCount < minB;
342
+ const kHits = (record.keywords ?? []).filter((word) => kWords.has(String(word).toLowerCase())).length;
343
+ const byTerm = (record.terms ?? []).includes(term);
344
+ const byKeyword = (record.keywords ?? []).includes(term);
345
+ return { ...record, hasAny, lowB, bCount, kHits, byTerm, byKeyword };
346
+ })
347
+ .filter((record) => (minB === 0
348
+ ? (record.hasAny || record.byKeyword)
349
+ : (record.bCount >= minB || record.byTerm || record.byKeyword)))
350
+ .sort((a, b) => (
351
+ Number(b.byTerm) - Number(a.byTerm)
352
+ || Number(a.lowB) - Number(b.lowB)
353
+ || (b.bCount + (b.kHits > 0 ? 0.3 : 0)) - (a.bCount + (a.kHits > 0 ? 0.3 : 0))
354
+ || b.kHits - a.kHits
355
+ || (b.turn ?? 0) - (a.turn ?? 0)
356
+ ));
357
+ const normal = hits.filter((record) => !record.lowB);
358
+ const low = hits.filter((record) => record.lowB);
359
+ const selected = (normal.length >= 3 ? normal : [...normal, ...low.slice(0, 5)])
360
+ .slice(0, Number.isInteger(meta.limit) && meta.limit > 0 ? meta.limit : 20);
361
+ return selected.length > 0
362
+ ? { kind: 'recall', via: 'C', term, concepts, summary: meta.summary ?? '', hits: selected }
363
+ : { kind: 'silent', reason: 'no-record', term, concepts };
364
+ }
365
+ if (shouldAsk(cmap, term)) {
366
+ return { kind: 'ask', term, reason: 'cold-start' };
367
+ }
368
+ return { kind: 'silent', reason: 'already-asked', term };
369
+ }
370
+
371
+ /**
372
+ * High-confidence intersection seeds: every requested concept must be present
373
+ * and above the score floor. This is the narrow seed before any expansion.
374
+ */
375
+ export function selectSeeds(records = [], concepts = [], { minConceptScore = 0.3 } = {}) {
376
+ if (concepts.length === 0) return [];
377
+ return records.filter((record) => concepts.every((concept) => {
378
+ if (!record.generic?.includes(concept)) return false;
379
+ const score = record.conceptScores?.[concept];
380
+ return score === undefined || score >= minConceptScore;
381
+ }));
382
+ }
383
+
384
+ /**
385
+ * Group matching turns into intervals. The gap is adaptive: dense hits merge,
386
+ * large gaps between sessions split. No fixed ±N window.
387
+ */
388
+ export function groupHits(hits = [], { minGap = 3, maxGap = 30 } = {}) {
389
+ const sorted = [...hits]
390
+ .filter((hit) => Number.isInteger(hit?.turn) || Number.isInteger(hit?.seqStart))
391
+ .sort((a, b) => (a.turn ?? a.seqStart) - (b.turn ?? b.seqStart));
392
+ if (sorted.length === 0) return [];
393
+ const gaps = [];
394
+ for (let i = 1; i < sorted.length; i += 1) {
395
+ const prev = sorted[i - 1].turn ?? sorted[i - 1].seqStart;
396
+ const next = sorted[i].turn ?? sorted[i].seqStart;
397
+ gaps.push(Math.max(1, next - prev));
398
+ }
399
+ const sortedGaps = [...gaps].sort((a, b) => a - b);
400
+ const median = sortedGaps.length === 0 ? 1 : sortedGaps[Math.floor(sortedGaps.length / 2)];
401
+ const threshold = Math.max(minGap, Math.min(maxGap, median * 2));
402
+ const groups = [];
403
+ for (let i = 0; i < sorted.length; i += 1) {
404
+ if (i === 0 || gaps[i - 1] > threshold) {
405
+ groups.push({ hits: [sorted[i]] });
406
+ } else {
407
+ groups[groups.length - 1].hits.push(sorted[i]);
408
+ }
409
+ }
410
+ return groups.map((group) => {
411
+ const first = group.hits[0];
412
+ const last = group.hits[group.hits.length - 1];
413
+ return {
414
+ fromTurn: first.turn,
415
+ toTurn: last.turn,
416
+ fromSeq: first.seqStart,
417
+ toSeq: last.seqEnd ?? last.seqStart,
418
+ hits: group.hits,
419
+ turns: group.hits.length,
420
+ weak: group.hits.some((hit) => hit.status === 'weak' || (hit.backfilled?.length ?? 0) > 0),
421
+ };
422
+ });
423
+ }
424
+
425
+ function intervalLine(group) {
426
+ if (group.fromTurn === null || group.fromTurn === undefined) {
427
+ return `seq ${group.fromSeq}-${group.toSeq}`;
428
+ }
429
+ return group.fromTurn === group.toTurn
430
+ ? `turn ${group.fromTurn} / seq ${group.fromSeq}-${group.toSeq}`
431
+ : `turn ${group.fromTurn}-${group.toTurn} / seq ${group.fromSeq}-${group.toSeq}`;
432
+ }
433
+
434
+ /** Keep the injected note bounded; callers still receive every interval. */
435
+ export function formatIntervals(groups, { max = 8, showRemainder = true } = {}) {
436
+ const shown = groups.slice(0, max).map((group) => `- ${intervalLine(group)}`);
437
+ if (showRemainder && groups.length > max) shown.push(`- … 还有 ${groups.length - max} 个区间`);
438
+ return shown.join('\n');
439
+ }
440
+
441
+ /** Warn when a C mapping expands to too many disjoint intervals. */
442
+ export function widthWarning(result, groups, { max = 20 } = {}) {
443
+ if (result?.via !== 'C' || groups.length <= max) return null;
444
+ return `[宽命中] 这条 C 映射产生 ${groups.length} 个区间,建议改 K(精确名字)或拆分 cmap 条目。`;
445
+ }
446
+
447
+ export function renderNote(result) {
448
+ if (result?.kind === 'ask') {
449
+ return `[问] 你说的“${result.term}”是指什么?可以简单说几个近义词。`;
450
+ }
451
+ if (result?.kind !== 'recall' || result.hits.length === 0) return null;
452
+ const label = result.via === 'C'
453
+ ? result.term
454
+ : result.via === 'K'
455
+ ? `${result.keywords.join(' + ')}${result.concepts?.length ? ' · ' + result.concepts.join(' / ') : ''}`
456
+ : result.concepts.join(' / ');
457
+ const groups = groupHits(result.hits);
458
+ const weak = groups.some((group) => group.weak) ? ' · weak' : '';
459
+ const warning = widthWarning(result, groups);
460
+ const tail = (warning ? `\n${warning}` : '') + '\n如需上下文,可按需前后读更多 turn。';
461
+ if (groups.length === 1) {
462
+ return `[追忆] ${label}${weak} -> ${intervalLine(groups[0])}${tail}`;
463
+ }
464
+ return `[追忆] ${label}${weak} -> ${groups.length} 个区间 / ${result.hits?.length ?? 0} 条记录\n${formatIntervals(groups)}${tail}`;
465
+ }
466
+
467
+ /** Public tool alias: the LLM-facing name is recall(). */
468
+ export function recall(options = {}) {
469
+ return retrieve(options);
470
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Topic-shift detector for the B+C trigger layer.
3
+ *
4
+ * Compares the previous and current turn states. The LLM does not decide
5
+ * whether a shift happened; it only sees one note after the detector fires.
6
+ */
7
+
8
+ function setOf(values) {
9
+ return new Set((values ?? []).map((value) => String(value).trim()).filter(Boolean));
10
+ }
11
+
12
+ export function jaccard(a, b) {
13
+ const left = setOf(a);
14
+ const right = setOf(b);
15
+ if (left.size === 0 && right.size === 0) return 1;
16
+ let intersection = 0;
17
+ for (const value of left) if (right.has(value)) intersection += 1;
18
+ return intersection / (left.size + right.size - intersection);
19
+ }
20
+
21
+ /** Normalize a recall record into the fields used by the detector. */
22
+ export function shiftState(record, { turn = null } = {}) {
23
+ return {
24
+ turn: turn ?? record?.turn ?? null,
25
+ generic: record?.generic ?? [],
26
+ keywords: record?.keywords ?? [],
27
+ terms: record?.terms ?? [],
28
+ };
29
+ }
30
+
31
+ export function detectShift(previous, current, {
32
+ genericWeight = 0.6,
33
+ keywordWeight = 0.4,
34
+ threshold = 0.55,
35
+ } = {}) {
36
+ if (!previous || !current) return { kind: 'unknown', score: 0, reasons: ['missing-state'] };
37
+ const genericSimilarity = jaccard(previous.generic, current.generic);
38
+ const keywordSimilarity = jaccard(previous.keywords, current.keywords);
39
+ const similarity = genericWeight * genericSimilarity + keywordWeight * keywordSimilarity;
40
+ const score = 1 - similarity;
41
+ const reasons = [];
42
+ if (genericSimilarity < 0.25) reasons.push('generic-change');
43
+ if (keywordSimilarity < 0.25) reasons.push('keyword-change');
44
+ return {
45
+ kind: score >= threshold ? 'shift' : 'same',
46
+ score: Number(score.toFixed(3)),
47
+ genericSimilarity: Number(genericSimilarity.toFixed(3)),
48
+ keywordSimilarity: Number(keywordSimilarity.toFixed(3)),
49
+ reasons,
50
+ };
51
+ }
52
+
53
+ export function shouldTrigger({ previous, current, cue = false, threshold = 0.55 } = {}) {
54
+ if (cue) return { trigger: true, reason: 'explicit-recall-cue' };
55
+ const shift = detectShift(previous, current, { threshold });
56
+ if (shift.kind === 'shift') return { trigger: true, reason: 'topic-shift', shift };
57
+ return { trigger: false, reason: 'same-topic', shift };
58
+ }
59
+
60
+ function aggregate(records = []) {
61
+ const generic = new Set();
62
+ const keywords = new Set();
63
+ for (const record of records) {
64
+ for (const value of record.generic ?? []) generic.add(value);
65
+ for (const value of record.keywords ?? []) keywords.add(value);
66
+ }
67
+ return { generic: [...generic], keywords: [...keywords] };
68
+ }
69
+
70
+ /**
71
+ * TextTiling-style boundary: compare a window on each side of the boundary,
72
+ * not two single turns. This smooths turn-to-turn B-label drift.
73
+ */
74
+ export function detectWindowShift(records = [], boundary, {
75
+ window = 3,
76
+ genericWeight = 0.6,
77
+ keywordWeight = 0.4,
78
+ threshold = 0.55,
79
+ } = {}) {
80
+ if (boundary <= 0 || boundary >= records.length) return { kind: 'unknown', score: 0, reasons: ['no-boundary'] };
81
+ const leftRecords = records.slice(Math.max(0, boundary - window), boundary);
82
+ const rightRecords = records.slice(boundary, Math.min(records.length, boundary + window));
83
+ if (leftRecords.length === 0 || rightRecords.length === 0) return { kind: 'unknown', score: 0, reasons: ['short-window'] };
84
+ const left = aggregate(leftRecords);
85
+ const right = aggregate(rightRecords);
86
+ const genericSimilarity = jaccard(left.generic, right.generic);
87
+ const keywordSimilarity = jaccard(left.keywords, right.keywords);
88
+ const similarity = genericWeight * genericSimilarity + keywordWeight * keywordSimilarity;
89
+ const score = 1 - similarity;
90
+ const reasons = [];
91
+ if (genericSimilarity < 0.25) reasons.push('generic-change');
92
+ if (keywordSimilarity < 0.25) reasons.push('keyword-change');
93
+ return {
94
+ kind: score >= threshold ? 'shift' : 'same',
95
+ score: Number(score.toFixed(3)),
96
+ genericSimilarity: Number(genericSimilarity.toFixed(3)),
97
+ keywordSimilarity: Number(keywordSimilarity.toFixed(3)),
98
+ left: [leftRecords[0]?.turn, leftRecords.at(-1)?.turn],
99
+ right: [rightRecords[0]?.turn, rightRecords.at(-1)?.turn],
100
+ reasons,
101
+ };
102
+ }
103
+
104
+ export function detectSequence(records = [], options = {}) {
105
+ const boundaries = [];
106
+ for (let index = 1; index < records.length; index += 1) {
107
+ if (records[index].turn - records[index - 1].turn !== 1) continue;
108
+ const shift = detectWindowShift(records, index, options);
109
+ if (shift.kind === 'shift') boundaries.push({ turn: records[index].turn, ...shift });
110
+ }
111
+ return boundaries;
112
+ }
113
+
114
+ export function renderShiftNote({ previous, current, shift, sources = {} } = {}) {
115
+ const from = previous?.turn ?? '?';
116
+ const to = current?.turn ?? '?';
117
+ const lines = [`[追忆] 话题变化 turn ${from} -> ${to}`];
118
+ if (shift?.reasons?.length) lines.push(`原因:${shift.reasons.join(' / ')}`);
119
+ if (sources.gist) lines.push(`gist:${sources.gist}`);
120
+ if (sources.facets?.length) lines.push(`B+C facet:${sources.facets.join(' / ')}`);
121
+ if (sources.keywords?.length) lines.push(`BM25:${sources.keywords.slice(0, 6).join(' / ')}`);
122
+ return lines.join('\n');
123
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * JSON store for B+C records and the per-user C map.
3
+ * No model, no network, no LLM.
4
+ */
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { createCMap, deserializeCMap, serializeCMap } from './bc-recall.mjs';
9
+
10
+ export function defaultBCStorePath(home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir()) {
11
+ return path.join(home, '.dsh', 'continuity', 'bc-index.jsonl');
12
+ }
13
+
14
+ export function emptyStore() {
15
+ return { version: 1, records: [], cmap: createCMap() };
16
+ }
17
+
18
+ export function loadBCStore(file) {
19
+ if (typeof file !== 'string' || file.length === 0) return emptyStore();
20
+ if (!fs.existsSync(file)) return emptyStore();
21
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
22
+ return {
23
+ version: Number(raw.version ?? 1),
24
+ records: Array.isArray(raw.records) ? raw.records : [],
25
+ cmap: deserializeCMap(raw.cmap ?? {}),
26
+ };
27
+ }
28
+
29
+ export function saveBCStore(file, store) {
30
+ const dir = path.dirname(file);
31
+ fs.mkdirSync(dir, { recursive: true });
32
+ const temp = `${file}.tmp-${process.pid}`;
33
+ const payload = {
34
+ version: 1,
35
+ records: store.records ?? [],
36
+ cmap: serializeCMap(store.cmap ?? createCMap()),
37
+ };
38
+ fs.writeFileSync(temp, JSON.stringify(payload, null, 2), 'utf8');
39
+ fs.renameSync(temp, file);
40
+ return file;
41
+ }