@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,249 @@
1
+ /**
2
+ * Retrieval planner: route a query to the most appropriate layer, with
3
+ * explicit coverage/score gates instead of always fusing every layer.
4
+ */
5
+ import { buildFacetIndex, queryFacets } from '../facets/index.mjs';
6
+ import { DEFAULT_ALIASES, aliasFacets } from '../facets/schema.mjs';
7
+ import { BM25Index } from './bm25.mjs';
8
+
9
+ export function reciprocalRankFusion(lists, { k = 60, weights = {} } = {}) {
10
+ const scores = new Map();
11
+ for (const [layer, hits] of Object.entries(lists)) {
12
+ const weight = weights[layer] ?? 1;
13
+ hits.forEach((hit, index) => {
14
+ const key = hit.turn;
15
+ const add = weight / (k + index + 1);
16
+ const current = scores.get(key) ?? { turn: key, sources: {}, score: 0, hit };
17
+ current.score += add;
18
+ current.sources[layer] = (current.sources[layer] ?? 0) + add;
19
+ scores.set(key, current);
20
+ });
21
+ }
22
+ return [...scores.values()].sort((a, b) => b.score - a.score);
23
+ }
24
+
25
+ function isLastIntent(query) {
26
+ return /上次|最近|刚才|上一次/.test(query);
27
+ }
28
+
29
+ function asOfFilter(hits, asOf) {
30
+ if (!Number.isInteger(asOf)) return hits;
31
+ return hits.filter((hit) => hit.turn <= asOf);
32
+ }
33
+
34
+ function orderHits(hits, last) {
35
+ return last ? [...hits].sort((a, b) => b.turn - a.turn) : hits;
36
+ }
37
+
38
+ export function buildPlanner({ records, bm25, vectorIndex, summaryBm25 }) {
39
+ const facetIndex = buildFacetIndex(records);
40
+ return {
41
+ query(query, { queryVector = null, topK = 20, asOf = null } = {}) {
42
+ const last = isLastIntent(query);
43
+ const cleanQuery = String(query).replace(/^(上次的?|最近的?|刚才的?|上一次的?)\s*/u, '').trim() || String(query);
44
+ const routeSuffix = last ? '-last' : '';
45
+ const facetAliasKey = Object.keys(DEFAULT_ALIASES).find((key) => cleanQuery.includes(key));
46
+ const facetAlias = facetAliasKey === undefined ? {} : aliasFacets(facetAliasKey);
47
+ if (Object.keys(facetAlias).length > 0) {
48
+ const matches = asOfFilter(queryFacets(facetIndex, facetAliasKey, { topK: records.length }).matches, asOf);
49
+ return {
50
+ query,
51
+ route: `facet${routeSuffix}`,
52
+ layers: ['facet'],
53
+ facet: facetAlias,
54
+ fused: orderHits(matches, last).slice(0, topK).map((hit) => ({
55
+ turn: hit.turn,
56
+ seqStart: hit.seqStart,
57
+ seqEnd: hit.seqEnd,
58
+ score: hit.score,
59
+ sources: { facet: hit.score },
60
+ })),
61
+ };
62
+ }
63
+
64
+ const visible = Number.isInteger(asOf) ? records.filter((record) => record.turn <= asOf) : records;
65
+ const lastQuery = cleanQuery.replace(/(是什么|怎么样了|怎么样|呢|吗|是哪轮|是哪一轮)$/u, '').trim();
66
+ if (last) {
67
+ const literalTerms = [...new Set(lastQuery.toLowerCase().match(/[a-z0-9_.-]+|[\u4e00-\u9fff]+/g) ?? [])];
68
+ if (literalTerms.length >= 2) {
69
+ const literalHits = visible.filter((record) => {
70
+ const text = record.text.toLowerCase();
71
+ return literalTerms.every((term) => text.includes(term));
72
+ });
73
+ if (literalHits.length > 0) {
74
+ return {
75
+ query,
76
+ route: `literal${routeSuffix}`,
77
+ layers: ['literal'],
78
+ facet: {},
79
+ fused: orderHits(literalHits, true).slice(0, topK).map((record) => ({
80
+ turn: record.turn,
81
+ seqStart: record.seqStart,
82
+ seqEnd: record.seqEnd,
83
+ score: 1,
84
+ sources: { literal: 1 },
85
+ })),
86
+ };
87
+ }
88
+ }
89
+ }
90
+ if (last && /^[\u4e00-\u9fff]{4,}$/u.test(lastQuery)) {
91
+ const mid = Math.ceil(lastQuery.length / 2);
92
+ const left = lastQuery.slice(0, mid);
93
+ const right = lastQuery.slice(mid);
94
+ const concept = visible.filter((record) => {
95
+ const text = record.text;
96
+ return text.includes(left) && text.includes(right);
97
+ });
98
+ if (concept.length > 0) {
99
+ return {
100
+ query,
101
+ route: `concept${routeSuffix}`,
102
+ layers: ['concept'],
103
+ facet: {},
104
+ fused: orderHits(concept, true).slice(0, topK).map((record) => ({
105
+ turn: record.turn,
106
+ seqStart: record.seqStart,
107
+ seqEnd: record.seqEnd,
108
+ score: 1,
109
+ sources: { concept: 1 },
110
+ })),
111
+ };
112
+ }
113
+ }
114
+ if (last && lastQuery.length >= 2) {
115
+ const textHits = visible.filter((record) => record.text.toLowerCase().includes(lastQuery.toLowerCase()));
116
+ if (textHits.length > 0) {
117
+ return {
118
+ query,
119
+ route: `phrase${routeSuffix}`,
120
+ layers: ['phrase'],
121
+ facet: {},
122
+ fused: orderHits(textHits, true).slice(0, topK).map((record) => ({
123
+ turn: record.turn,
124
+ seqStart: record.seqStart,
125
+ seqEnd: record.seqEnd,
126
+ score: 1,
127
+ sources: { phrase: 1 },
128
+ })),
129
+ };
130
+ }
131
+ }
132
+ const normalized = cleanQuery.toLowerCase();
133
+ const exact = /\.[a-z0-9]{2,5}\b/i.test(cleanQuery)
134
+ ? visible.filter((record) => record.text.toLowerCase().includes(normalized))
135
+ : [];
136
+ if (exact.length > 0 && exact.length <= 50) {
137
+ return {
138
+ query,
139
+ route: `phrase${routeSuffix}`,
140
+ layers: ['phrase'],
141
+ facet: {},
142
+ fused: orderHits(exact, last).slice(0, topK).map((record) => ({
143
+ turn: record.turn,
144
+ seqStart: record.seqStart,
145
+ seqEnd: record.seqEnd,
146
+ score: 1,
147
+ sources: { phrase: 1 },
148
+ })),
149
+ };
150
+ }
151
+
152
+ const terms = cleanQuery.split(/\s+/).map((term) => term.trim().toLowerCase()).filter(Boolean);
153
+ if (terms.length >= 2) {
154
+ const both = visible.filter((record) => {
155
+ const text = record.text.toLowerCase();
156
+ return terms.every((term) => text.includes(term));
157
+ });
158
+ if (both.length > 0 && both.length <= 50) {
159
+ return {
160
+ query,
161
+ route: `and${routeSuffix}`,
162
+ layers: ['and'],
163
+ facet: {},
164
+ fused: orderHits(both, last).slice(0, topK).map((record) => ({
165
+ turn: record.turn,
166
+ seqStart: record.seqStart,
167
+ seqEnd: record.seqEnd,
168
+ score: 1,
169
+ sources: { and: 1 },
170
+ })),
171
+ };
172
+ }
173
+ }
174
+
175
+ const keyword = asOfFilter(bm25.search(lastQuery || cleanQuery, {
176
+ topK: last ? visible.length : topK * 5,
177
+ minCoverage: last ? 0.8 : 0.6,
178
+ }), asOf);
179
+ if (keyword.length > 0 && keyword[0].score >= 2) {
180
+ return {
181
+ query,
182
+ route: `bm25${routeSuffix}`,
183
+ layers: ['bm25'],
184
+ facet: {},
185
+ fused: orderHits(keyword, last).slice(0, topK).map((hit) => ({
186
+ turn: hit.turn,
187
+ seqStart: hit.seqStart,
188
+ seqEnd: hit.seqEnd,
189
+ score: hit.score,
190
+ sources: { bm25: hit.score, coverage: hit.coverage },
191
+ })),
192
+ };
193
+ }
194
+
195
+ if (queryVector !== null) {
196
+ const semantic = asOfFilter(vectorIndex.search(queryVector, { topK: topK * 5 }), asOf);
197
+ if (semantic.length > 0 && semantic[0].score >= (last ? 0.65 : 0.55)) {
198
+ return {
199
+ query,
200
+ route: `vector${routeSuffix}`,
201
+ layers: ['vector'],
202
+ facet: {},
203
+ fused: orderHits(semantic, last).slice(0, topK).map((hit) => ({
204
+ turn: hit.turn,
205
+ seqStart: hit.seqStart,
206
+ seqEnd: hit.seqEnd,
207
+ score: hit.score,
208
+ sources: { vector: hit.score },
209
+ })),
210
+ };
211
+ }
212
+ }
213
+
214
+ if (/后来|经过|总结|发生了什么|时间线/.test(cleanQuery)) {
215
+ const summary = asOfFilter(summaryBm25.search(cleanQuery, { topK: topK * 3, minCoverage: 0.5 }), asOf);
216
+ if (summary.length > 0) {
217
+ return {
218
+ query,
219
+ route: `summary${routeSuffix}`,
220
+ layers: ['summary'],
221
+ facet: {},
222
+ fused: orderHits(summary, last).slice(0, topK).map((hit) => ({
223
+ turn: hit.turn,
224
+ seqStart: hit.seqStart,
225
+ seqEnd: hit.seqEnd,
226
+ score: hit.score,
227
+ sources: { summary: hit.score },
228
+ })),
229
+ };
230
+ }
231
+ }
232
+
233
+ return { query, route: 'empty', layers: [], facet: {}, fused: [] };
234
+ },
235
+ };
236
+ }
237
+
238
+ export function summaryIndex(tree) {
239
+ return new BM25Index().build(tree.nodes
240
+ .filter((node) => node.level !== 'turn')
241
+ .map((node) => ({
242
+ turn: node.firstTurn,
243
+ firstTurn: node.firstTurn,
244
+ lastTurn: node.lastTurn,
245
+ seqStart: node.seqStart,
246
+ seqEnd: node.seqEnd,
247
+ text: node.summary,
248
+ })));
249
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pre-turn integration: one incoming user/Coo message -> facet/event/gate/load.
3
+ * Pure system layer. No live session writes.
4
+ */
5
+ import { MemoryLoadState, latestCandidate, candidateSegment } from './load-gate.mjs';
6
+ import { EventModelState } from './event-model.mjs';
7
+ import { attachStructuredExtraction } from './extraction-adapter.mjs';
8
+
9
+ export class PreTurnController {
10
+ constructor({ records = [], gate = null, eventModel = null } = {}) {
11
+ this.records = records;
12
+ this.gate = gate ?? new MemoryLoadState();
13
+ this.eventModel = eventModel ?? new EventModelState({ slots: this.gate.loadedFacets });
14
+ }
15
+
16
+ async stepAsync({ extractionProvider = null, ...args } = {}) {
17
+ if (!extractionProvider) return this.step(args);
18
+ const extraction = await extractionProvider.extract({ text: args.text });
19
+ if (!extraction.ok) {
20
+ return {
21
+ ok: false,
22
+ extraction,
23
+ gate: this.step({ ...args, eventSlots: args.eventSlots ?? null }),
24
+ };
25
+ }
26
+ const data = extraction.data ?? {};
27
+ const result = attachStructuredExtraction(this, {
28
+ ...args,
29
+ slots: data.slots ?? null,
30
+ srl: data.srl ?? null,
31
+ corefClusters: data.corefClusters ?? [],
32
+ entityLinks: data.entityLinks ?? [],
33
+ });
34
+ return { ok: result.ok, extraction, ...result };
35
+ }
36
+
37
+ step({ text, role = 'user', facets = {}, confidence = 1, eventSlots = null, nowTurn = 0 } = {}) {
38
+ if (role === 'assistant') {
39
+ return { action: 'none', reason: 'assistant-text', telemetry: { role } };
40
+ }
41
+ const observed = this.eventModel.observe({ slots: eventSlots ?? facets });
42
+ const candidate = latestCandidate(this.records, 'activity', facets.activity);
43
+ const segment = candidateSegment(this.records, candidate);
44
+ const decision = this.gate.decide({
45
+ text,
46
+ currentFacets: facets,
47
+ confidence,
48
+ nowTurn,
49
+ hasCandidate: candidate !== null,
50
+ candidate,
51
+ role,
52
+ boundaryScore: observed.score,
53
+ });
54
+ if (decision.action === 'load') {
55
+ this.gate.recordLoad({
56
+ facets,
57
+ seq: segment?.seqStart ?? null,
58
+ nowTurn,
59
+ reason: decision.reason,
60
+ });
61
+ }
62
+ return {
63
+ ...decision,
64
+ boundaryScore: observed.score,
65
+ eventId: observed.eventId,
66
+ eventError: observed.error ?? null,
67
+ candidateTurn: candidate?.turn ?? null,
68
+ segment: decision.action === 'none' ? null : segment,
69
+ telemetry: {
70
+ role,
71
+ activity: facets.activity ?? null,
72
+ topic: facets.topic ?? null,
73
+ confidence,
74
+ eventSlots: observed.slots,
75
+ eventChanged: observed.changed,
76
+ gateReason: decision.reason,
77
+ },
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Query-side aliases for the literal layer.
3
+ *
4
+ * Format (one term per line):
5
+ * 通知 => notification / post_notifications
6
+ * ...
7
+ *
8
+ * This file affects only the literal query path; it does not change the index,
9
+ * B descriptors, or C/K data.
10
+ */
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+
15
+ export function defaultAliasesPath(home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir()) {
16
+ return path.join(home, '.dsh', 'continuity', 'aliases.md');
17
+ }
18
+
19
+ export function parseAliases(text) {
20
+ const aliases = new Map();
21
+ for (const raw of String(text ?? '').split(/\r?\n/)) {
22
+ const line = raw.replace(/#.*$/, '').trim();
23
+ if (!line) continue;
24
+ const match = line.match(/^(.+?)\s*(?:=>|->|→)\s*(.+)$/);
25
+ if (!match) continue;
26
+ const term = match[1].trim();
27
+ const values = match[2].split(/[\/,,、]/).map((word) => word.trim()).filter(Boolean);
28
+ if (term && values.length > 0) aliases.set(term, values);
29
+ }
30
+ return aliases;
31
+ }
32
+
33
+ export function loadQueryAliases(file = defaultAliasesPath()) {
34
+ try {
35
+ return parseAliases(fs.readFileSync(file, 'utf8'));
36
+ } catch {
37
+ return new Map();
38
+ }
39
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * High-precision explicit recall cues.
3
+ * Deliberately does not fire on bare "之前", "那个", or "继续".
4
+ */
5
+ const CUES = [
6
+ /上次/,
7
+ /上回/,
8
+ /上一次/,
9
+ /前一回/,
10
+ /之前我们/,
11
+ /之前那次/,
12
+ /之前那个/,
13
+ /之前我(?:说|提|讲)过?/,
14
+ /以前我们/,
15
+ /以前那次/,
16
+ /还记得/,
17
+ /记得上次/,
18
+ /记得之前/,
19
+ /回到(?:刚才|之前|那个|上次)/,
20
+ /接着(?:上次|之前)/,
21
+ /那个(?:事|项目|话题|东西|办法|决定|问题)/,
22
+ ];
23
+
24
+ export function detectRecallCue(text) {
25
+ // Quoted cue words are usually meta-discussion ("if I say '上次' ..."),
26
+ // not an actual request to recall.
27
+ const source = String(text ?? '')
28
+ .replace(/`[^`]*`/g, ' ')
29
+ .replace(/“[^”]*”/g, ' ')
30
+ .replace(/「[^」]*」/g, ' ')
31
+ .replace(/"[^"]*"/g, ' ');
32
+ for (const pattern of CUES) {
33
+ const match = source.match(pattern);
34
+ if (match) return { hit: true, cue: match[0], index: match.index ?? 0, pattern: pattern.source };
35
+ }
36
+ return { hit: false, cue: null, index: -1, pattern: null };
37
+ }
38
+
39
+ export function shouldTriggerRecall(text) {
40
+ return detectRecallCue(text).hit;
41
+ }
42
+
43
+ export const RECALL_CUE_PATTERNS = CUES.map((pattern) => pattern.source);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Lexical recall over the immutable session event log.
3
+ *
4
+ * This is deliberately small: it only answers "which previous turn/seq
5
+ * contains text close to this query". It does not call the LLM and it does
6
+ * not write anything.
7
+ */
8
+ import { eventText } from '../session-log.mjs';
9
+ import { BM25Index } from './bm25.mjs';
10
+ import { tokenize } from './tokenize.mjs';
11
+
12
+ const INJECTED_PREFIXES = ['[continuity:', '[抽取]', '[工作线]', '[标签]', '[规矩]', '[习惯]', '[技能]', '[归位', '[候选', '[存?]', '[闸', '[gate', '[peer', '[回正]', '[学习', '[话题?]'];
13
+
14
+ function isInjected(text) {
15
+ return INJECTED_PREFIXES.some((prefix) => text.startsWith(prefix));
16
+ }
17
+
18
+ export function collectRecallRecords(events = [], { beforeTurn = Infinity } = {}) {
19
+ const records = [];
20
+ for (const event of events) {
21
+ if (event?.type !== 'user/message') continue;
22
+ const text = eventText(event).trim();
23
+ if (text.length === 0 || isInjected(text)) continue;
24
+ const turn = Number(event?.data?.turn ?? event?.turn);
25
+ if (Number.isFinite(turn) && turn >= beforeTurn) continue;
26
+ const seq = Number(event?.seq);
27
+ if (!Number.isInteger(seq)) continue;
28
+ records.push({
29
+ turn: Number.isFinite(turn) ? turn : null,
30
+ seqStart: seq,
31
+ seqEnd: seq,
32
+ text,
33
+ });
34
+ }
35
+ return records;
36
+ }
37
+
38
+ export function buildRecallIndex(events, options = {}) {
39
+ const records = collectRecallRecords(events, options);
40
+ return { records, index: new BM25Index().build(records) };
41
+ }
42
+
43
+ export function findRecall(recall, query, { topK = 3, minCoverage = 0.3 } = {}) {
44
+ if (!recall || !Array.isArray(recall.records) || recall.records.length === 0) return [];
45
+ const hits = recall.index.search(String(query ?? ''), { topK, minCoverage });
46
+ return hits.map((hit) => ({
47
+ turn: hit.turn,
48
+ seqStart: hit.seqStart,
49
+ seqEnd: hit.seqEnd,
50
+ score: hit.score,
51
+ coverage: hit.coverage,
52
+ text: recall.records.find((record) => record.seqStart === hit.seqStart)?.text ?? '',
53
+ }));
54
+ }
55
+
56
+ export function renderRecall(hit, query = '') {
57
+ const where = hit.turn === null ? `seq ${hit.seqStart}` : `turn ${hit.turn} / seq ${hit.seqStart}`;
58
+ const lower = String(hit.text ?? '').toLowerCase();
59
+ const terms = [...new Set(tokenize(query))]
60
+ .filter((term) => term.length >= 2 && lower.includes(term.toLowerCase()))
61
+ .slice(0, 4);
62
+ const label = terms.length > 0 ? terms.join(' / ') : '旧记录';
63
+ const snippet = String(hit.text ?? '').replace(/\s+/g, ' ').slice(0, 80);
64
+ return `[追忆] ${label} -> ${where}\n${snippet}`;
65
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Referent candidate generation.
3
+ * Deterministic evidence: known aliases, exact term occurrences, retrieval hits.
4
+ * A candidate is not a promotion; it is input to the referent ledger.
5
+ */
6
+ import { DEFAULT_ALIASES } from '../facets/schema.mjs';
7
+
8
+ export function candidateReferents({
9
+ term,
10
+ records = [],
11
+ keywordHits = [],
12
+ semanticHits = [],
13
+ max = 5,
14
+ } = {}) {
15
+ const clean = String(term ?? '').trim();
16
+ const candidates = new Map();
17
+ const add = (referent, target, source, score, role = null, turn = null) => {
18
+ const existing = candidates.get(referent);
19
+ if (existing === undefined || score > existing.score) {
20
+ candidates.set(referent, { referent, target, source, score, role, turn });
21
+ }
22
+ };
23
+
24
+ if (DEFAULT_ALIASES[clean] !== undefined) {
25
+ const alias = DEFAULT_ALIASES[clean];
26
+ add(`facet:${alias.activity ?? alias.topic}`, alias.activity ?? alias.topic, 'alias', 1, 'alias');
27
+ }
28
+
29
+ for (const record of records) {
30
+ if (!String(record.text ?? '').includes(clean)) continue;
31
+ add(`turn:${record.turn}`, record.facets?.activity ?? null, 'event-log', 0.8, 'mention', record.turn);
32
+ }
33
+
34
+ const hitRank = new Map();
35
+ [...keywordHits, ...semanticHits].forEach((hit, index) => {
36
+ const previous = hitRank.get(hit.turn);
37
+ if (previous === undefined) hitRank.set(hit.turn, index);
38
+ else hitRank.set(hit.turn, Math.min(previous, index));
39
+ });
40
+ for (const [turn, rank] of hitRank) {
41
+ const record = records.find((item) => item.turn === turn);
42
+ if (record === undefined) continue;
43
+ add(`turn:${turn}`, record.facets?.activity ?? null, 'retrieval', 0.5 / (rank + 1), 'retrieval', turn);
44
+ }
45
+
46
+ return [...candidates.values()]
47
+ .sort((a, b) => b.score - a.score)
48
+ .slice(0, max);
49
+ }
50
+
51
+ export function bindReferent(candidates, { threshold = 0.5 } = {}) {
52
+ const top = candidates[0] ?? null;
53
+ if (top === null || top.score < threshold) return { referent: 'NIL', candidate: top, reason: 'below-threshold' };
54
+ return { referent: top.referent, candidate: top, reason: 'bound' };
55
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Term-to-referent ledger.
3
+ *
4
+ * An alias is a relation between a term and a stable referent, not merely a
5
+ * repeated word. Same term with two referents is a new sense, not an alias.
6
+ */
7
+ import { DEFAULT_ALIASES } from '../facets/schema.mjs';
8
+
9
+ const NIL_REFERENTS = new Set(['NIL', 'nil', 'unknown', '未知', '未知实体', 'null']);
10
+
11
+ function dominant(counts) {
12
+ const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
13
+ if (entries.length === 0) return null;
14
+ const total = entries.reduce((sum, [, count]) => sum + count, 0);
15
+ return { value: entries[0][0], count: entries[0][1], total, ratio: entries[0][1] / total };
16
+ }
17
+
18
+ export class ReferentLedger {
19
+ constructor({ minUser = 2, minEvents = 2, terms = new Map(), requireConfirmation = false } = {}) {
20
+ this.minUser = minUser;
21
+ this.minEvents = minEvents;
22
+ this.terms = terms;
23
+ this.requireConfirmation = requireConfirmation;
24
+ }
25
+
26
+ observe({ term, source = 'user', eventId, referent = null, role = null, target = null, nowTurn = 0, kind = 'mention', confirmed = false }) {
27
+ const clean = String(term ?? '').trim();
28
+ if (clean.length === 0) return { action: 'none', reason: 'no-term' };
29
+ if (DEFAULT_ALIASES[clean] !== undefined) return { action: 'none', reason: 'known-alias' };
30
+
31
+ const record = this.terms.get(clean) ?? {
32
+ term: clean,
33
+ user: 0,
34
+ assistant: 0,
35
+ events: new Set(),
36
+ referents: {},
37
+ roles: {},
38
+ targets: {},
39
+ firstSeen: nowTurn,
40
+ lastSeen: nowTurn,
41
+ strong: false,
42
+ proposed: false,
43
+ nilCount: 0,
44
+ confirmed: false,
45
+ };
46
+ if (source === 'user' || (source === 'coo' || source === 'peer')) record.user += 1;
47
+ else record.assistant += 1;
48
+ record.events.add(eventId);
49
+ if (confirmed) record.confirmed = true;
50
+ if (referent !== null && NIL_REFERENTS.has(String(referent))) record.nilCount += 1;
51
+ if (referent !== null && !NIL_REFERENTS.has(String(referent))) record.referents[referent] = (record.referents[referent] ?? 0) + 1;
52
+ if (role !== null) record.roles[role] = (record.roles[role] ?? 0) + 1;
53
+ if (target !== null) record.targets[target] = (record.targets[target] ?? 0) + 1;
54
+ if (kind === 'definition' || kind === 'quoted') record.strong = true;
55
+ record.lastSeen = nowTurn;
56
+ this.terms.set(clean, record);
57
+
58
+ if (record.proposed) return { action: 'none', reason: 'already-proposed', record };
59
+ if (record.user < this.minUser) return { action: 'none', reason: 'need-user-repeat', record };
60
+ if (record.events.size < this.minEvents) return { action: 'none', reason: 'need-event-repeat', record };
61
+ if (Object.keys(record.referents).length === 0 && record.nilCount > 0) {
62
+ return { action: 'none', reason: 'nil-referent', record };
63
+ }
64
+
65
+ const dominantRole = dominant(record.roles);
66
+ if (dominantRole !== null && dominantRole.ratio < 0.8) return { action: 'none', reason: 'role-unstable', record };
67
+ const dominantReferent = dominant(record.referents);
68
+ if (dominantReferent === null) return { action: 'none', reason: 'no-referent', record };
69
+ if (dominantReferent.ratio < 0.8) return { action: 'none', reason: 'referent-unstable', record };
70
+ const dominantTarget = dominant(record.targets);
71
+ if (dominantTarget === null) return { action: 'none', reason: 'no-target-facet', record };
72
+ if (this.requireConfirmation && !record.confirmed) return { action: 'none', reason: 'unconfirmed', record };
73
+
74
+ record.proposed = true;
75
+ return {
76
+ action: 'propose',
77
+ reason: 'cross-event-referent',
78
+ term: clean,
79
+ referent: dominantReferent.value,
80
+ role: dominantRole?.value ?? null,
81
+ target: dominantTarget.value,
82
+ text: `[新别名?] ${clean} → ${dominantTarget.value}(referent ${dominantReferent.value})`,
83
+ record,
84
+ };
85
+ }
86
+ }