@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,521 @@
1
+ /**
2
+ * `recall` — model-facing candidate lookup over the B+C pointer index.
3
+ * Read-only; no LLM; no session writes.
4
+ */
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import { loadIndexJsonl } from './memory/bc-index.mjs';
8
+ import { createCMap, DEFAULT_C_SEED, formatIntervals, GENERIC_CONCEPTS, groupHits, queryFor, recall, renderNote, widthWarning } from './memory/bc-recall.mjs';
9
+ import { loadSpecialKeywords, matchSpecialKeywords } from './memory/special-keywords.mjs';
10
+ import { loadCMapEntries } from './memory/cmap-file.mjs';
11
+ import { loadQueryAliases } from './memory/query-aliases.mjs';
12
+ import { loadConcepts } from './memory/concepts-file.mjs';
13
+ import { querySummaryIndex, readSummaryIndex, summaryIndexPath, summaryIndexStaleness } from './memory/summary-index.mjs';
14
+ import { resolveVocabFile, sessionFile } from './memory/persona.mjs';
15
+ import { eventText } from './session-log.mjs';
16
+
17
+ export const name = 'dsh-continuity/recall-tool';
18
+
19
+ const INJECTED = /^\[(?:抽取|工作线|标签|规则|习惯|技能|归位|候选|存\?|闸|回正|学习|追忆|记忆|continuity:[a-z-]+)\]/;
20
+ const MACHINE_PREFIX = /^(?:Current runtime context|Current working directory|Current DSH file policy|Approval prompts|Time sampled)/;
21
+
22
+ function isHumanEvent(event) {
23
+ const source = event?.data?.source ?? event?.source;
24
+ if (source && typeof source.kind === 'string' && source.kind !== 'user') return false;
25
+ if (source && source.peer === true) return false;
26
+ const text = eventText(event).trim();
27
+ return Boolean(text) && !INJECTED.test(text) && !MACHINE_PREFIX.test(text);
28
+ }
29
+
30
+ function parseDirective(query) {
31
+ const text = String(query ?? '').trim();
32
+ const match = text.match(/^([ABCK])\s*标签\s*[::]?\s*(.*)$/i)
33
+ ?? text.match(/^([ABCK])\s*[::]\s*(.*)$/i);
34
+ if (!match) return null;
35
+ return { kind: match[1].toUpperCase(), value: match[2].trim() };
36
+ }
37
+
38
+ async function currentUserText(sessionQuery, exec) {
39
+ if (!sessionQuery || typeof sessionQuery.readSession !== 'function') return '';
40
+ const sessionId = exec?.agent?.session?.id;
41
+ if (sessionId === undefined || sessionId === null) return '';
42
+ const snapshot = await sessionQuery.readSession(sessionId);
43
+ const events = snapshot?.events ?? [];
44
+ for (let i = events.length - 1; i >= 0; i -= 1) {
45
+ const event = events[i];
46
+ if (event?.type !== 'user/message') continue;
47
+ if (!isHumanEvent(event)) continue;
48
+ const text = eventText(event).trim();
49
+ if (text) return text;
50
+ }
51
+ return '';
52
+ }
53
+
54
+ async function latestSummarySeq(sessionQuery, sessionId) {
55
+ if (!sessionQuery || typeof sessionQuery.observeSession !== 'function') return null;
56
+ let observation = null;
57
+ try {
58
+ observation = await sessionQuery.observeSession(sessionId, { projectionMode: 'none' });
59
+ const events = Array.isArray(observation?.events) ? observation.events : [];
60
+ return events.reduce((max, event) => {
61
+ if (event?.type !== 'compaction/summary') return max;
62
+ const seq = Number(event.seq);
63
+ return Number.isInteger(seq) && seq > max ? seq : max;
64
+ }, -1);
65
+ } catch {
66
+ return null;
67
+ } finally {
68
+ if (typeof observation?.[Symbol.dispose] === 'function') observation[Symbol.dispose]();
69
+ }
70
+ }
71
+
72
+ function inventory(records, kind, specialKeywords, cTerms = [], aConcepts = [], aField = null, aKinds = null) {
73
+ const counts = new Map();
74
+ const add = (word) => counts.set(word, (counts.get(word) ?? 0) + 1);
75
+ if (kind === 'A') {
76
+ const known = new Set(aConcepts);
77
+ const field = aField === 'workflow' ? 'workflow' : 'topic';
78
+ for (const concept of aConcepts) counts.set(concept, 0);
79
+ for (const record of records) {
80
+ const fallback = (record.generic ?? []).filter((word) => (aKinds?.[word] ?? 'topic') === field);
81
+ const words = field === 'workflow'
82
+ ? (record.genericWorkflow ?? fallback)
83
+ : (record.genericTopic ?? fallback);
84
+ for (const word of words) if (known.has(word)) add(word);
85
+ }
86
+ } else if (kind === 'B') {
87
+ for (const record of records) for (const word of record.generic ?? []) add(word);
88
+ } else if (kind === 'C') {
89
+ const known = new Set(cTerms);
90
+ for (const record of records) for (const word of record.terms ?? []) if (known.has(word)) add(word);
91
+ } else {
92
+ for (const record of records) {
93
+ const words = new Set((record.keywords ?? []).map((word) => String(word).toLowerCase()));
94
+ for (const keyword of specialKeywords) {
95
+ if (words.has(keyword.toLowerCase())) add(keyword);
96
+ }
97
+ }
98
+ }
99
+ return [...counts.entries()]
100
+ .map(([label, turns]) => ({ label, turns }))
101
+ .sort((a, b) => b.turns - a.turns || a.label.localeCompare(b.label));
102
+ }
103
+
104
+ const LITERAL_STOP = new Set([
105
+ '的', '是', '了', '和', '与', '在', '有', '我', '你', '他', '这', '那',
106
+ '哪个', '什么', '怎么', '怎么样', '为什么', '上次', '之前', '那个',
107
+ '一个', '可以', '如果', '因为', '所以', '然后',
108
+ '会怎么样', '会怎么', '怎么办', '怎么回事', '什么时候', '么时候开',
109
+ '是不是', '有没有', '能不能', '多少', '多久', '对不对', '行不行',
110
+ '可不可以', '那次', '今天', '明天', '昨天', '方案', '刚才', '事情', '问题',
111
+ ]);
112
+
113
+ const LITERAL_FRAGMENTS = new Set([
114
+ '说的', '件事', '那个', '这个', '一下', '时候', '上次', '之前', '那事',
115
+ '这事的', '什么', '怎么', '哪个', '多少', '多久',
116
+ ]);
117
+
118
+ function isLiteralStop(token) {
119
+ if (LITERAL_FRAGMENTS.has(token)) return true;
120
+ if (/[的了吗呢吧啊嘛]$/.test(token)) return true;
121
+ if (/^[那这哪什怎为多几吧]/.test(token)) return true;
122
+ for (const stop of LITERAL_STOP) {
123
+ if (token === stop || token.startsWith(stop)) return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ function literalTokens(query) {
129
+ const text = String(query ?? '');
130
+ const tokens = new Set();
131
+ for (const word of text.match(/[A-Za-z0-9_]+/g) ?? []) {
132
+ const lower = word.toLowerCase();
133
+ if (lower.length >= 3 && !isLiteralStop(lower)) tokens.add(lower);
134
+ }
135
+ for (let size = 2; size <= 4; size += 1) {
136
+ for (let i = 0; i + size <= text.length; i += 1) {
137
+ const part = text.slice(i, i + size);
138
+ if (!/[\u3400-\u9fff]/.test(part)) continue;
139
+ if (!isLiteralStop(part)) tokens.add(part);
140
+ }
141
+ }
142
+ return [...tokens];
143
+ }
144
+
145
+ function literalCandidates(records, query, idfRecords = records, aliases = new Map()) {
146
+ const tokens = literalTokens(query);
147
+ if (tokens.length === 0) return [];
148
+ const weights = new Map(tokens.map((token) => [token, 1]));
149
+ let aliasExpansions = 0;
150
+ for (const token of tokens) {
151
+ for (const alias of aliases.get(token) ?? []) {
152
+ if (aliasExpansions >= 3) break;
153
+ const key = String(alias).trim().toLowerCase();
154
+ if (!key) continue;
155
+ if (!weights.has(key)) weights.set(key, 0.6);
156
+ aliasExpansions += 1;
157
+ }
158
+ if (aliasExpansions >= 3) break;
159
+ }
160
+ const keywordSet = new Set();
161
+ for (const record of records) {
162
+ for (const word of record.keywords ?? []) keywordSet.add(String(word).toLowerCase());
163
+ }
164
+ const matched = [...weights.keys()].filter((token) => keywordSet.has(token));
165
+ if (matched.length === 0) return [];
166
+ const df = new Map(matched.map((token) => [token, 0]));
167
+ for (const record of idfRecords) {
168
+ const words = new Set((record.keywords ?? []).map((word) => String(word).toLowerCase()));
169
+ for (const token of matched) if (words.has(token)) df.set(token, (df.get(token) ?? 0) + 1);
170
+ }
171
+ const n = Math.max(idfRecords.length, 1);
172
+ const idf = new Map([...df.entries()].map(([token, count]) => [token, Math.log((n + 1) / (count + 1)) + 1]));
173
+ return records.map((record) => {
174
+ const words = new Set((record.keywords ?? []).map((word) => String(word).toLowerCase()));
175
+ const hits = matched.filter((token) => words.has(token));
176
+ if (hits.length === 0) return null;
177
+ const score = hits.reduce((sum, token) => sum + (idf.get(token) ?? 0) * (weights.get(token) ?? 1), 0);
178
+ return { ...record, literalScore: Number(score.toFixed(4)), literalTerms: hits };
179
+ }).filter(Boolean).sort((a, b) => b.literalScore - a.literalScore || (b.turn ?? 0) - (a.turn ?? 0));
180
+ }
181
+
182
+ function literalNote(query, hits) {
183
+ const groups = groupHits(hits);
184
+ const label = `字面 "${query}"`;
185
+ const header = groups.length === 1
186
+ ? `[追忆] ${label} -> ${intervalText(groups[0])}`
187
+ : `[追忆] ${label} -> ${groups.length} 个区间 / ${hits.length} 条记录\n${formatIntervals(groups, { max: 5 })}`;
188
+ return `${header}\n如需上下文,可按需前后读更多 turn。`;
189
+ }
190
+
191
+ function indexPath() {
192
+ return process.env.DSH_BC_INDEX
193
+ ?? path.join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity', 'bc-index.jsonl');
194
+ }
195
+
196
+ function wantsRecent(args, query) {
197
+ if (args.recent === true) return true;
198
+ if (args.recent === false) return false;
199
+ return /上次|最近|刚才|之前|上一次|上回/.test(query);
200
+ }
201
+
202
+ function liveCandidates(events, query, { limit = 10000, sinceTurn = -Infinity, beforeTurn = Infinity } = {}) {
203
+ const hits = [];
204
+ for (const event of events ?? []) {
205
+ const text = eventText(event).trim();
206
+ if (!text || !isHumanEvent(event)) continue;
207
+ if (!text.includes(query)) continue;
208
+ const turn = Number(event?.data?.turn ?? event?.turn);
209
+ const seq = Number(event?.seq);
210
+ if (!Number.isInteger(seq)) continue;
211
+ if (Number.isFinite(turn) && turn < sinceTurn) continue;
212
+ if (Number.isFinite(turn) && turn >= beforeTurn) continue;
213
+ hits.push({
214
+ turn: Number.isFinite(turn) ? turn : null,
215
+ seqStart: seq,
216
+ seqEnd: seq,
217
+ concepts: [],
218
+ status: 'live',
219
+ backfilled: [],
220
+ text: text.replace(/\s+/g, ' ').slice(0, 120),
221
+ });
222
+ }
223
+ return hits.sort((a, b) => (b.turn ?? 0) - (a.turn ?? 0)).slice(0, limit);
224
+ }
225
+
226
+ function intervalText(group) {
227
+ if (group.fromTurn === null || group.fromTurn === undefined) return `seq ${group.fromSeq}-${group.toSeq}`;
228
+ return group.fromTurn === group.toTurn
229
+ ? `turn ${group.fromTurn} / seq ${group.fromSeq}-${group.toSeq}`
230
+ : `turn ${group.fromTurn}-${group.toTurn} / seq ${group.fromSeq}-${group.toSeq}`;
231
+ }
232
+
233
+ function noteForLive(query, hits) {
234
+ const groups = groupHits(hits);
235
+ const head = groups.length === 1
236
+ ? `[追忆] 最近 ${query} -> ${intervalText(groups[0])}`
237
+ : `[追忆] 最近 ${query} -> ${groups.length} 个区间\n${formatIntervals(groups)}`;
238
+ return `${head}\n如需上下文,可按需前后读更多 turn。`;
239
+ }
240
+
241
+ export function createRecallTool({ file = indexPath(), sessionQuery = null, cmapFile = null, conceptsFile = null, keywordsFile = null, literalRecords = null, aliasesFile = null } = {}) {
242
+ return {
243
+ name: 'recall',
244
+ description: 'Look up candidate memory turns. Call with no query to interpret the current user message; call with A标签/B标签/C标签/K标签 (optionally plus a value) to inspect or search one namespace. Returns bounded turn/seq pointers; summaryHits, when available and not stale, point to the middle-layer summary text for a seq range. Open the cited original before relying on it.',
245
+ parameters: {
246
+ type: 'object',
247
+ additionalProperties: false,
248
+ properties: {
249
+ query: { type: 'string', description: 'Text to look up, or a namespace directive: A标签 / B标签 / C标签 / K标签, optionally followed by a value.' },
250
+ recent: { type: 'boolean', description: 'Prefer the current session, newest first. Defaults to true when the text contains 上次/最近/之前.' },
251
+ sinceTurn: { type: 'integer', description: 'Optional lower turn bound.' },
252
+ },
253
+ },
254
+ output: {
255
+ schema: { type: 'string' },
256
+ render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
257
+ },
258
+ timeoutMs: 30_000,
259
+ async execute(args = {}, exec = {}) {
260
+ let raw = String(args.query ?? '').trim();
261
+ if (!raw) {
262
+ try {
263
+ raw = await currentUserText(sessionQuery, exec);
264
+ } catch { /* fall through to the error below */ }
265
+ }
266
+ if (!raw) return JSON.stringify({ ok: false, error: 'query-or-session-required' });
267
+
268
+ const sessionId = exec?.agent?.session?.id;
269
+ const sessionIndexFile = sessionId === undefined || sessionId === null
270
+ ? file
271
+ : path.join(path.dirname(file), 'sessions', String(sessionId), 'bc-index.jsonl');
272
+ const records = fs.existsSync(sessionIndexFile) ? loadIndexJsonl(sessionIndexFile) : [];
273
+ const conceptsPath = conceptsFile ?? (sessionId === undefined || sessionId === null
274
+ ? resolveVocabFile('continuity', 'concepts.md')
275
+ : sessionFile(sessionId, 'concepts.md'));
276
+ const aRows = loadConcepts(conceptsPath);
277
+ const aConcepts = aRows.map((row) => row.label);
278
+ const aKinds = Object.fromEntries(aRows.map((row) => [row.label, row.kind]));
279
+ const topicConcepts = aRows.filter((row) => row.kind !== 'workflow').map((row) => row.label);
280
+ const workflowConcepts = aRows.filter((row) => row.kind === 'workflow').map((row) => row.label);
281
+ const aliasesPath = aliasesFile ?? (sessionId === undefined || sessionId === null
282
+ ? resolveVocabFile('continuity', 'aliases.md')
283
+ : sessionFile(sessionId, 'aliases.md'));
284
+ const cmapPath = cmapFile ?? (sessionId === undefined || sessionId === null
285
+ ? resolveVocabFile('continuity', 'cmap.md')
286
+ : sessionFile(sessionId, 'cmap.md'));
287
+ const keywordsPath = keywordsFile ?? (sessionId === undefined || sessionId === null
288
+ ? resolveVocabFile('continuity', 'keywords.md')
289
+ : sessionFile(sessionId, 'keywords.md'));
290
+ const aliases = loadQueryAliases(aliasesPath);
291
+ const cmap = createCMap([...DEFAULT_C_SEED, ...loadCMapEntries(cmapPath)]);
292
+ const specials = fs.existsSync(keywordsPath) ? loadSpecialKeywords(keywordsPath) : [];
293
+ const directive = parseDirective(raw);
294
+
295
+ if (directive && !directive.value) {
296
+ return JSON.stringify({
297
+ ok: true,
298
+ mode: 'labels',
299
+ kind: directive.kind,
300
+ labels: inventory(
301
+ records,
302
+ directive.kind,
303
+ specials,
304
+ [...cmap.terms.keys()],
305
+ directive.kind === 'A' ? topicConcepts : [],
306
+ directive.kind === 'A' ? 'topic' : null,
307
+ aKinds,
308
+ ),
309
+ });
310
+ }
311
+
312
+ if (directive?.kind === 'A' && ['topic', 'workflow'].includes(String(directive.value).toLowerCase())) {
313
+ const requested = String(directive.value).toLowerCase();
314
+ const selected = requested === 'workflow' ? workflowConcepts : topicConcepts;
315
+ return JSON.stringify({
316
+ ok: true,
317
+ mode: 'labels',
318
+ kind: 'A',
319
+ scope: requested,
320
+ labels: inventory(records, 'A', specials, [...cmap.terms.keys()], selected, requested, aKinds),
321
+ });
322
+ }
323
+
324
+ if (directive?.kind === 'A' && directive.value) {
325
+ const wanted = directive.value.trim();
326
+ const concept = aConcepts.find((label) => label === wanted)
327
+ ?? aConcepts.find((label) => label.includes(wanted) || wanted.includes(label));
328
+ if (!concept) {
329
+ return JSON.stringify({ ok: false, error: 'unknown-a-label', labels: aConcepts });
330
+ }
331
+ const hits = records
332
+ .filter((record) => {
333
+ const field = aKinds[concept] === 'workflow' ? 'genericWorkflow' : 'genericTopic';
334
+ const values = Array.isArray(record[field])
335
+ ? record[field]
336
+ : (record.generic ?? []).filter((word) => (aKinds[word] ?? 'topic') === aKinds[concept]);
337
+ return values.includes(concept);
338
+ })
339
+ .map((record) => ({
340
+ turn: record.turn,
341
+ seqStart: record.seqStart,
342
+ seqEnd: record.seqEnd,
343
+ concepts: record.generic ?? [],
344
+ status: record.status,
345
+ backfilled: record.backfilled ?? [],
346
+ score: null,
347
+ literalTerms: [],
348
+ }));
349
+ const groups = groupHits(hits);
350
+ return JSON.stringify({
351
+ ok: true,
352
+ mode: 'locate',
353
+ query: concept,
354
+ via: 'A',
355
+ kind: 'recall',
356
+ note: hits.length > 0
357
+ ? `[追忆] A:${concept} -> ${groups.length} 个区间 / ${hits.length} 条记录`
358
+ : `[追忆] A:${concept} 暂无记录`,
359
+ intervals: groups.map((group) => ({
360
+ fromTurn: group.fromTurn,
361
+ toTurn: group.toTurn,
362
+ fromSeq: group.fromSeq,
363
+ toSeq: group.toSeq,
364
+ turns: group.turns,
365
+ })),
366
+ candidates: hits,
367
+ });
368
+ }
369
+
370
+ let special = [];
371
+ let facet = '';
372
+ let lookup = raw;
373
+ if (directive?.value) {
374
+ raw = directive.value;
375
+ if (directive.kind === 'K') {
376
+ special = matchSpecialKeywords(raw, specials);
377
+ if (special.length === 0) special = [raw];
378
+ }
379
+ facet = GENERIC_CONCEPTS.find((concept) => raw.includes(concept)) ?? '';
380
+ lookup = facet || (directive.kind === 'B' || directive.kind === 'C' ? raw : special[0] || raw);
381
+ } else {
382
+ special = matchSpecialKeywords(raw, specials);
383
+ facet = GENERIC_CONCEPTS.find((concept) => raw.includes(concept)) ?? '';
384
+ const extracted = queryFor(raw, { records, cmap });
385
+ lookup = facet || (special.length > 0 ? special[0] : (extracted && extracted !== raw ? extracted : raw));
386
+ }
387
+
388
+ const recent = wantsRecent(args, raw);
389
+ const sinceTurn = Number.isInteger(args.sinceTurn) ? args.sinceTurn : -Infinity;
390
+
391
+ if (recent && sessionQuery && typeof sessionQuery.readSession === 'function') {
392
+ const sessionId = exec?.agent?.session?.id;
393
+ if (sessionId !== undefined && sessionId !== null) {
394
+ try {
395
+ const snapshot = await sessionQuery.readSession(sessionId);
396
+ const live = liveCandidates(snapshot?.events ?? [], lookup, {
397
+ sinceTurn,
398
+ beforeTurn: Number.isInteger(exec?.agent?.lastTurn) ? exec.agent.lastTurn : Infinity,
399
+ });
400
+ if (live.length > 0) {
401
+ const groups = groupHits(live);
402
+ return JSON.stringify({
403
+ ok: true,
404
+ mode: 'locate',
405
+ query: raw,
406
+ via: 'live',
407
+ recent: true,
408
+ note: noteForLive(raw, live),
409
+ intervals: groups.map((group) => ({
410
+ fromTurn: group.fromTurn,
411
+ toTurn: group.toTurn,
412
+ fromSeq: group.fromSeq,
413
+ toSeq: group.toSeq,
414
+ turns: group.turns,
415
+ })),
416
+ candidates: live,
417
+ });
418
+ }
419
+ } catch { /* fall back to the static index */ }
420
+ }
421
+ }
422
+
423
+ const result = recall({ records, cmap, query: lookup, specialKeywords: special });
424
+ if (records.length === 0) return JSON.stringify({ ok: false, error: 'index-empty', file });
425
+ let hits = recent
426
+ ? [...(result.hits ?? [])].sort((a, b) => (b.turn ?? 0) - (a.turn ?? 0))
427
+ : (result.hits ?? []);
428
+ let via = result.via ?? null;
429
+ let warning = widthWarning(result, groupHits(hits));
430
+ let note = hits.length > 0 && recent
431
+ ? (() => {
432
+ const groups = groupHits(hits);
433
+ const custom = groups.length === 1
434
+ ? `[追忆] 最近 ${raw} -> ${intervalText(groups[0])}\n如需上下文,可按需前后读更多 turn。`
435
+ : `[追忆] 最近 ${raw} -> ${groups.length} 个区间\n${formatIntervals(groups)}\n如需上下文,可按需前后读更多 turn。`;
436
+ return warning ? `${custom}\n${warning}` : custom;
437
+ })()
438
+ : renderNote(result);
439
+
440
+ // Literal fallback: only when no B/C/K hit at all. Never override an
441
+ // existing exact K/C hit (q2-style) with a broader literal list.
442
+ if ((result.hits?.length ?? 0) === 0) {
443
+ const literal = literalCandidates(records, raw, literalRecords ?? records, aliases).slice(0, 5);
444
+ if (literal.length > 0) {
445
+ const seen = new Set(literal.map((hit) => hit.turn));
446
+ hits = [...literal, ...hits.filter((hit) => !seen.has(hit.turn))];
447
+ via = 'literal';
448
+ warning = null;
449
+ note = literalNote(raw, literal);
450
+ }
451
+ }
452
+
453
+ const groups = groupHits(hits);
454
+ const hitTurns = new Set(hits.map((hit) => hit.turn).filter((turn) => turn !== undefined && turn !== null)).size;
455
+ const kTooWide = via === 'K' && hitTurns > 50;
456
+ const exactHint = (kTooWide || Boolean(warning)) && hits.length > 0
457
+ ? '[提示] 命中过宽;需要字面定位可改用 recall_exact。'
458
+ : null;
459
+ if (exactHint) note = note ? `${note}\n${exactHint}` : exactHint;
460
+ const summaryPath = summaryIndexPath(path.dirname(sessionIndexFile));
461
+ let summaryHits = [];
462
+ let summaryIndexState = { available: false, stale: null, path: summaryPath, maxSummarySeq: null, latestSeq: null };
463
+ if (fs.existsSync(summaryPath)) {
464
+ try {
465
+ const summaryIndex = readSummaryIndex(summaryPath);
466
+ const liveSummarySeq = await latestSummarySeq(sessionQuery, sessionId);
467
+ const stale = summaryIndexStaleness(summaryIndex, records, { latestSummarySeq: liveSummarySeq });
468
+ summaryIndexState = {
469
+ available: true,
470
+ stale: stale.stale,
471
+ staleBy: stale.staleBy,
472
+ path: summaryPath,
473
+ maxSummarySeq: stale.maxSummarySeq,
474
+ latestSeq: stale.latestSeq,
475
+ latestSummaryAt: stale.latestSummaryAt,
476
+ };
477
+ summaryHits = querySummaryIndex(summaryIndex, raw, { limit: 5 }).hits;
478
+ } catch (error) {
479
+ summaryIndexState = {
480
+ available: false,
481
+ stale: null,
482
+ path: summaryPath,
483
+ error: String(error?.message ?? error),
484
+ };
485
+ }
486
+ }
487
+ return JSON.stringify({
488
+ ok: true,
489
+ mode: 'locate',
490
+ query: raw,
491
+ kind: via === 'literal' ? 'recall' : result.kind,
492
+ via,
493
+ recent,
494
+ note,
495
+ hint: exactHint,
496
+ warning,
497
+ intervals: groups.map((group) => ({
498
+ fromTurn: group.fromTurn,
499
+ toTurn: group.toTurn,
500
+ fromSeq: group.fromSeq,
501
+ toSeq: group.toSeq,
502
+ turns: group.turns,
503
+ })),
504
+ candidates: hits.slice(0, 20).map((hit) => ({
505
+ turn: hit.turn,
506
+ seqStart: hit.seqStart,
507
+ seqEnd: hit.seqEnd,
508
+ concepts: hit.generic,
509
+ status: hit.status,
510
+ backfilled: hit.backfilled ?? [],
511
+ score: hit.literalScore ?? null,
512
+ literalTerms: hit.literalTerms ?? [],
513
+ })),
514
+ summaryHits,
515
+ summaryStale: summaryIndexState.stale,
516
+ summaryStaleBy: summaryIndexState.staleBy ?? null,
517
+ summaryIndex: summaryIndexState,
518
+ });
519
+ },
520
+ };
521
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Tail TAG-repeat counting.
3
+ *
4
+ * Count is per T only: a valid `TAG: T` while T is active increments that T's
5
+ * counter. Switching T does not touch the old T; the new T starts at its own
6
+ * count. No header prompt and no cache stamp exist here.
7
+ */
8
+
9
+ export const RECHECK_FIRST_TAGS = 100;
10
+ export const RECHECK_INTERVAL_TAGS = 40;
11
+
12
+ export function emptyRecheckState() {
13
+ return { version: 1, threads: {} };
14
+ }
15
+
16
+ export function normalizeRecheckState(raw) {
17
+ if (raw === null || typeof raw !== 'object') return emptyRecheckState();
18
+ if (raw.threads === null || typeof raw.threads !== 'object') return emptyRecheckState();
19
+ return { version: 1, threads: { ...raw.threads } };
20
+ }
21
+
22
+ function entryOf(state, threadId) {
23
+ const key = String(threadId ?? '');
24
+ const entry = state.threads[key];
25
+ if (entry === null || typeof entry !== 'object') {
26
+ return { key, entry: { count: 0, asked: 0, lastAskedAt: 0 } };
27
+ }
28
+ return {
29
+ key,
30
+ entry: {
31
+ count: Number(entry.count ?? 0),
32
+ asked: Number(entry.asked ?? 0),
33
+ lastAskedAt: Number(entry.lastAskedAt ?? 0),
34
+ },
35
+ };
36
+ }
37
+
38
+ /** Increment one accepted same-T TAG and say whether the threshold is due. */
39
+ export function bumpTagCount(raw, { threadId } = {}) {
40
+ const state = normalizeRecheckState(raw);
41
+ const { key, entry } = entryOf(state, threadId);
42
+ entry.count += 1;
43
+ const due = entry.count >= RECHECK_FIRST_TAGS
44
+ && (entry.lastAskedAt === 0 || entry.count - entry.lastAskedAt >= RECHECK_INTERVAL_TAGS);
45
+ if (due) {
46
+ entry.asked += 1;
47
+ entry.lastAskedAt = entry.count;
48
+ }
49
+ state.threads[key] = entry;
50
+ return { state, due, entry };
51
+ }
52
+
53
+ /** y means "still this T": reset this T's repeat count. */
54
+ export function resetTagCount(raw, { threadId } = {}) {
55
+ const state = normalizeRecheckState(raw);
56
+ const { key, entry } = entryOf(state, threadId);
57
+ entry.count = 0;
58
+ entry.lastAskedAt = 0;
59
+ state.threads[key] = entry;
60
+ return { state, entry };
61
+ }
62
+
63
+ /**
64
+ * The answer line is `[闸·回查] y` / `[闸·回查] n` (old `[回查]` accepted),
65
+ * or a bare y/n as the final non-empty line. A following TAG line is allowed,
66
+ * so look one line back. Prose containing y/n is not accepted.
67
+ */
68
+ export function parseRecheckAnswer(text) {
69
+ const lines = String(text ?? '').trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
70
+ const answerOf = (line) => {
71
+ const tagged = /^\[(?:闸·)?回查\]\s*(y|n)$/i.exec(line);
72
+ if (tagged !== null) return tagged[1].toLowerCase() === 'y' ? 'yes' : 'no';
73
+ if (/^y$/i.test(line)) return 'yes';
74
+ if (/^n$/i.test(line)) return 'no';
75
+ return null;
76
+ };
77
+ const last = lines.length > 0 ? lines[lines.length - 1] : '';
78
+ const direct = answerOf(last);
79
+ if (direct !== null) return direct;
80
+ if (/^TAG\s*[::]/i.test(last) && lines.length > 1) return answerOf(lines[lines.length - 2]);
81
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
82
+ const found = answerOf(lines[index]);
83
+ if (found !== null) return found;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ export function recheckRetryText(alias) {
89
+ return '回查:没有读到 y 或 n。\n'
90
+ + '本轮最后先写一行:\n'
91
+ + '[闸·回查] y — 仍在 ' + String(alias ?? '当前 T') + '\n'
92
+ + '[闸·回查] n — 已离开(然后正常写 TAG 新线)\n'
93
+ + '连续 3 次没有 y/n 会按 y 放行,不会困住你。';
94
+ }
95
+
96
+ export function recheckAutoText(alias) {
97
+ return '回查:连续 3 次没有读到 y/n;这是故意按 y 放行的,不会因此困住你。\n'
98
+ + '请正常写 TAG 收尾。';
99
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 0.8.6 start/skip resolver for the normal (surface) save path.
3
+ * Backfill is deliberately not here: an explicit range below surface[0]
4
+ * folds from surface[0] and reports the skipped hole; it never throws.
5
+ */
6
+ export function resolveSaveStart({ explicitFrom, surfaceFirst }) {
7
+ const floor = Number(surfaceFirst);
8
+ const requested = explicitFrom === undefined || explicitFrom === null ? null : Number(explicitFrom);
9
+ if (!Number.isFinite(floor)) return { startFrom: requested, belowSurface: false };
10
+ if (requested === null) return { startFrom: floor, belowSurface: false };
11
+ if (requested < floor) return { startFrom: floor, belowSurface: true };
12
+ return { startFrom: requested, belowSurface: false };
13
+ }
14
+
15
+ export function detectSkippedRange({ coveredTo, surfaceFirst, events = [] }) {
16
+ const floor = Number(surfaceFirst);
17
+ const covered = Number(coveredTo);
18
+ if (!Number.isFinite(floor) || !Number.isFinite(covered)) return null;
19
+ const from = covered + 1;
20
+ const to = floor - 1;
21
+ if (to < from) return null;
22
+ const count = (Array.isArray(events) ? events : []).filter((event) => {
23
+ const seq = Number(event?.seq);
24
+ return Number.isInteger(seq) && seq >= from && seq <= to
25
+ && (event?.type === 'user/message' || event?.type === 'assistant/message');
26
+ }).length;
27
+ if (count === 0) return null;
28
+ return { from, to, events: count };
29
+ }