@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
package/src/tags.mjs ADDED
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Continuity tags — sidecar T labels over turn ranges.
3
+ *
4
+ * Pure index operations only: resolve, find, tag, retag, and a cheap list.
5
+ * No session text is read here and no [TX] is ever written into the raw log.
6
+ */
7
+ export const TAG_ZERO = 'T0';
8
+
9
+ /** T0 是桶,不是话题:它可以出现在索引里,但**永不参与匹配/候选**。 */
10
+ export function matchableTags(rows) {
11
+ return (Array.isArray(rows) ? rows : []).filter((row) => String(row?.id ?? row?.alias ?? "") !== TAG_ZERO);
12
+ }
13
+ export const TAG_DEFAULT_LABEL = '未分类';
14
+ export const FIND_DEFAULT_LIMIT = 20;
15
+ export const FIND_MAX_LIMIT = 100;
16
+
17
+ function asInteger(value) {
18
+ const number = Number(value);
19
+ return Number.isInteger(number) ? number : null;
20
+ }
21
+
22
+ function asId(value) {
23
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
24
+ }
25
+
26
+ function normalizeEntry(id, value) {
27
+ const entry = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
28
+ const label = asId(entry.label) ?? (id === TAG_ZERO ? TAG_DEFAULT_LABEL : id);
29
+ const status = entry.status === 'open' || entry.status === 'closed' ? entry.status : undefined;
30
+ const lifecycle = ['provisional', 'active', 'merged', 'archived'].includes(entry.lifecycle)
31
+ ? entry.lifecycle
32
+ : undefined;
33
+ const keywords = Array.isArray(entry.keywords)
34
+ ? [...new Set(entry.keywords.map((item) => String(item).trim()).filter(Boolean))]
35
+ : [];
36
+ const alias = asId(entry.alias);
37
+ const legacyAliases = Array.isArray(entry.legacyAliases)
38
+ ? [...new Set(entry.legacyAliases.map((item) => String(item).trim()).filter(Boolean))]
39
+ : [];
40
+ const mergedFrom = Array.isArray(entry.mergedFrom)
41
+ ? [...new Set(entry.mergedFrom.map(asId).filter((item) => item !== null))]
42
+ : [];
43
+ const mergedInto = asId(entry.mergedInto);
44
+ const useCount = Number.isInteger(Number(entry.useCount)) ? Math.max(0, Number(entry.useCount)) : undefined;
45
+ return {
46
+ label,
47
+ ...(alias === null ? {} : { alias }),
48
+ ...(status === undefined ? {} : { status }),
49
+ ...(lifecycle === undefined ? {} : { lifecycle }),
50
+ ...(mergedInto === null ? {} : { mergedInto }),
51
+ ...(legacyAliases.length === 0 ? {} : { legacyAliases }),
52
+ ...(mergedFrom.length === 0 ? {} : { mergedFrom }),
53
+ ...(useCount === undefined ? {} : { useCount }),
54
+ ...(keywords.length === 0 ? {} : { keywords }),
55
+ };
56
+ }
57
+
58
+ /** Follow mergedInto to the canonical tag id. Never follows cycles. */
59
+ export function canonicalTagId(tags, id) {
60
+ const state = tags !== null && typeof tags === 'object' && tags.tags !== undefined ? tags : normalizeTags(tags);
61
+ let current = asId(id);
62
+ if (current === null || current === TAG_ZERO) return current;
63
+ if (state.tags?.[current]?.lifecycle === 'archived') return null;
64
+ const seen = new Set();
65
+ while (current !== null && state.tags?.[current]?.mergedInto !== undefined) {
66
+ if (seen.has(current)) return current;
67
+ seen.add(current);
68
+ const next = asId(state.tags[current].mergedInto);
69
+ if (next === null || next === current) return current;
70
+ current = next;
71
+ }
72
+ return current;
73
+ }
74
+
75
+ /** Resolve old id or legacy alias to a canonical tag id, or null. */
76
+ export function canonicalTagRef(tags, token) {
77
+ const state = tags !== null && typeof tags === 'object' && tags.tags !== undefined ? tags : normalizeTags(tags);
78
+ const key = asId(token);
79
+ if (key === null) return null;
80
+ if (state.tags?.[key] !== undefined) return canonicalTagId(state, key);
81
+ const lower = key.toLowerCase();
82
+ for (const [id, entry] of Object.entries(state.tags ?? {})) {
83
+ if (entry?.lifecycle === 'archived') continue;
84
+ if (String(entry?.alias ?? '').toLowerCase() === lower) return canonicalTagId(state, id);
85
+ if (Array.isArray(entry?.legacyAliases)
86
+ && entry.legacyAliases.some((alias) => String(alias).toLowerCase() === lower)) {
87
+ return canonicalTagId(state, id);
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ export function emptyTags() {
93
+ return {
94
+ version: 1,
95
+ tags: { [TAG_ZERO]: { label: TAG_DEFAULT_LABEL } },
96
+ runs: [],
97
+ overrides: {},
98
+ updatedAt: null,
99
+ memberships: {}
100
+ };
101
+ }
102
+
103
+ /** Read-compatible normalization; corrupt partial input never throws. */
104
+ export function normalizeTags(input) {
105
+ const source = input !== null && typeof input === 'object' && !Array.isArray(input) ? input : {};
106
+ const tags = {};
107
+ const sourceTags = source.tags !== null && typeof source.tags === 'object' && !Array.isArray(source.tags)
108
+ ? source.tags
109
+ : {};
110
+ for (const [key, value] of Object.entries(sourceTags)) {
111
+ const id = asId(key);
112
+ if (id !== null) tags[id] = normalizeEntry(id, value);
113
+ }
114
+ if (tags[TAG_ZERO] === undefined) tags[TAG_ZERO] = { label: TAG_DEFAULT_LABEL };
115
+
116
+ const runs = [];
117
+ for (const entry of Array.isArray(source.runs) ? source.runs : []) {
118
+ const from = asInteger(Array.isArray(entry) ? entry[0] : entry?.from);
119
+ const to = asInteger(Array.isArray(entry) ? entry[1] : entry?.to);
120
+ const id = asId(Array.isArray(entry) ? entry[2] : entry?.id);
121
+ if (from === null || to === null || id === null) continue;
122
+ runs.push([Math.min(from, to), Math.max(from, to), id]);
123
+ }
124
+
125
+ // 多标签成员:seq -> [id...]。可选的附加层,旧数据没有这个键。
126
+ const memberships = {};
127
+ const sourceMemberships = source.memberships !== null && typeof source.memberships === 'object' && !Array.isArray(source.memberships)
128
+ ? source.memberships
129
+ : {};
130
+ for (const [key, value] of Object.entries(sourceMemberships)) {
131
+ const seq = asInteger(key);
132
+ if (seq === null || !Array.isArray(value)) continue;
133
+ const ids = value.map(asId).filter((id) => id !== null);
134
+ if (ids.length > 0) memberships[String(seq)] = [...new Set(ids)];
135
+ }
136
+
137
+ const overrides = {};
138
+ const sourceOverrides = source.overrides !== null && typeof source.overrides === 'object' && !Array.isArray(source.overrides)
139
+ ? source.overrides
140
+ : {};
141
+ for (const [key, value] of Object.entries(sourceOverrides)) {
142
+ const seq = asInteger(key);
143
+ const id = asId(value);
144
+ if (seq === null || id === null) continue;
145
+ overrides[String(seq)] = id;
146
+ }
147
+
148
+ for (const id of new Set([...runs.map((run) => run[2]), ...Object.values(overrides)])) {
149
+ if (tags[id] === undefined) tags[id] = normalizeEntry(id, {});
150
+ }
151
+ const updatedAt = typeof source.updatedAt === 'string' && source.updatedAt.length > 0
152
+ ? source.updatedAt
153
+ : null;
154
+ return { version: 1, tags, runs, overrides, memberships, updatedAt };
155
+ }
156
+
157
+ /** Fast resolver over a normalized state; avoids re-normalizing per seq. */
158
+ export function tagResolver(tags) {
159
+ const state = normalizeTags(tags);
160
+ return (seq) => {
161
+ const key = String(Number(seq));
162
+ if (state.overrides[key] !== undefined) return canonicalTagId(state, state.overrides[key]) ?? TAG_ZERO;
163
+ const number = Number(seq);
164
+ for (let index = state.runs.length - 1; index >= 0; index -= 1) {
165
+ const [from, to, id] = state.runs[index];
166
+ if (number >= from && number <= to) return canonicalTagId(state, id) ?? TAG_ZERO;
167
+ }
168
+ return TAG_ZERO;
169
+ };
170
+ }
171
+
172
+ /** Display labels for a folded range: alias → short label → short id; T0 only if all T0. */
173
+ export function tagCoverageForRange(tags, fromSeq, toSeq, { threads = [] } = {}) {
174
+ const state = normalizeTags(tags);
175
+ const from = Number(fromSeq);
176
+ const to = Number(toSeq);
177
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from > to) {
178
+ return { ids: [TAG_ZERO], label: TAG_ZERO, counts: {}, items: [] };
179
+ }
180
+ const counts = new Map();
181
+ const resolve = tagResolver(state);
182
+ for (let seq = from; seq <= to; seq += 1) {
183
+ const id = resolve(seq);
184
+ counts.set(id, (counts.get(id) ?? 0) + 1);
185
+ }
186
+ const threadById = new Map();
187
+ const aliasToThread = new Map();
188
+ for (const thread of Array.isArray(threads) ? threads : []) {
189
+ if (thread?.id === undefined || thread.id === null) continue;
190
+ threadById.set(String(thread.id), thread);
191
+ const alias = asId(thread.alias);
192
+ if (alias !== null) aliasToThread.set(alias, thread);
193
+ }
194
+ const short = (value) => {
195
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
196
+ return text.length > 12 ? `${text.slice(0, 12)}…` : text;
197
+ };
198
+ const ids = [...counts.entries()]
199
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
200
+ .map(([id]) => id);
201
+ const items = ids
202
+ .filter((id) => id !== TAG_ZERO)
203
+ .map((id) => {
204
+ const thread = threadById.get(id) ?? aliasToThread.get(id) ?? null;
205
+ const entry = state.tags[id];
206
+ const alias = asId(entry?.alias) ?? asId(thread?.alias);
207
+ const label = typeof entry?.label === 'string' && entry.label.length > 0 && entry.label !== id
208
+ ? entry.label
209
+ : typeof thread?.label === 'string' && thread.label.length > 0
210
+ ? thread.label
211
+ : null;
212
+ const shortId = id.length > 8 ? `${id.slice(0, 8)}…` : id;
213
+ return {
214
+ id,
215
+ count: counts.get(id) ?? 0,
216
+ display: alias ?? (label === null ? shortId : short(label)),
217
+ };
218
+ });
219
+ let label;
220
+ if (items.length === 0) {
221
+ label = TAG_ZERO;
222
+ } else {
223
+ const primary = items[0];
224
+ const secondary = items.slice(1).reduce((sum, item) => sum + item.count, 0);
225
+ label = `${primary.display}${secondary > 0 ? ` +${secondary}` : ''}`;
226
+ }
227
+ return {
228
+ ids: ids.length > 0 ? ids : [TAG_ZERO],
229
+ label,
230
+ counts: Object.fromEntries(counts),
231
+ items,
232
+ };
233
+ }
234
+
235
+ /** Start of the latest tag run containing seq; null when seq is the implicit T0 tail. */
236
+ export function tagRunStartAt(tags, seq) {
237
+ const state = normalizeTags(tags);
238
+ const number = Number(seq);
239
+ if (!Number.isInteger(number)) return null;
240
+ for (let index = state.runs.length - 1; index >= 0; index -= 1) {
241
+ const [from, to] = state.runs[index];
242
+ if (number >= from && number <= to) return from;
243
+ }
244
+ return null;
245
+ }
246
+
247
+ /** overrides -> last containing run -> T0. */
248
+ export function resolveTag(tags, seq) {
249
+ const state = normalizeTags(tags);
250
+ const key = String(Number(seq));
251
+ if (state.overrides[key] !== undefined) return canonicalTagId(state, state.overrides[key]) ?? TAG_ZERO;
252
+ const number = Number(seq);
253
+ for (let index = state.runs.length - 1; index >= 0; index -= 1) {
254
+ const [from, to, id] = state.runs[index];
255
+ if (number >= from && number <= to) return canonicalTagId(state, id) ?? TAG_ZERO;
256
+ }
257
+ return TAG_ZERO;
258
+ }
259
+
260
+ function messageSeqUniverse(events) {
261
+ const list = [];
262
+ for (const event of Array.isArray(events) ? events : []) {
263
+ if (event?.type !== 'user/message' && event?.type !== 'assistant/message') continue;
264
+ const sourceKind = event?.data?.source?.kind;
265
+ if (sourceKind === 'runtime-context' || sourceKind === 'compact-checkpoint') continue;
266
+ const seq = asInteger(event.seq);
267
+ if (seq !== null) list.push(seq);
268
+ }
269
+ return [...new Set(list)].sort((left, right) => left - right);
270
+ }
271
+
272
+ /**
273
+ * Pure tag index. `seqs` is the message-seq universe (or pass `events`);
274
+ * overrides for this id are included even when they are outside that universe.
275
+ * Returns { seqs, count, first, last, span } with span from event times.
276
+ */
277
+ export function findTagSeqs({
278
+ tags,
279
+ id,
280
+ seqs = null,
281
+ events = null,
282
+ after = undefined,
283
+ before = undefined,
284
+ limit = FIND_DEFAULT_LIMIT,
285
+ } = {}) {
286
+ const state = normalizeTags(tags);
287
+ const tagId = asId(id);
288
+ if (tagId === null) throw new Error('find: id is required');
289
+ const universe = Array.isArray(seqs)
290
+ ? [...new Set(seqs.map(asInteger).filter((seq) => seq !== null))].sort((left, right) => left - right)
291
+ : messageSeqUniverse(events);
292
+ const bySeq = new Map();
293
+ for (const event of Array.isArray(events) ? events : []) {
294
+ const seq = asInteger(event?.seq);
295
+ if (seq !== null) bySeq.set(seq, event);
296
+ }
297
+
298
+ const found = new Set();
299
+ for (const seq of universe) {
300
+ // 主标签命中,或该 seq 显式登记为这个标签的附加成员
301
+ if (resolveTag(state, seq) === tagId || (state.memberships?.[String(seq)] ?? []).includes(tagId)) found.add(seq);
302
+ }
303
+ for (const [key, value] of Object.entries(state.overrides)) {
304
+ if (value === tagId) found.add(Number(key));
305
+ }
306
+
307
+ let list = [...found].sort((left, right) => left - right);
308
+ if (after !== undefined && after !== null) list = list.filter((seq) => seq > Number(after));
309
+ if (before !== undefined && before !== null) list = list.filter((seq) => seq < Number(before));
310
+ const max = Number.isFinite(Number(limit)) && Number(limit) > 0
311
+ ? Math.min(FIND_MAX_LIMIT, Math.trunc(Number(limit)))
312
+ : null;
313
+ if (max !== null) list = list.slice(0, max);
314
+
315
+ const first = list.length > 0 ? list[0] : null;
316
+ const last = list.length > 0 ? list[list.length - 1] : null;
317
+ let span = null;
318
+ if (first !== null && last !== null) {
319
+ const start = Number(bySeq.get(first)?.time);
320
+ const end = Number(bySeq.get(last)?.time);
321
+ if (Number.isFinite(start) && Number.isFinite(end)) {
322
+ span = { start: new Date(start).toISOString(), end: new Date(end).toISOString() };
323
+ }
324
+ }
325
+ return { seqs: list, count: list.length, first, last, span };
326
+ }
327
+
328
+ /**
329
+ * 一个 seq 属于哪些标签:主解析 + 所有覆盖它的 run + memberships。
330
+ * 检索用这个(多标签),覆盖率/T0 仍用 resolveTag(单标签分区)。
331
+ */
332
+ export function tagsAtSeq(tags, seq) {
333
+ const state = normalizeTags(tags);
334
+ const number = Number(seq);
335
+ const out = new Set();
336
+ const key = String(number);
337
+ // 主标签:override -> 最后一个覆盖 run -> T0(与 resolveTag 一致)
338
+ if (state.overrides[key] !== undefined) out.add(state.overrides[key]);
339
+ else {
340
+ let primary = null;
341
+ for (let index = state.runs.length - 1; index >= 0; index -= 1) {
342
+ const [from, to, id] = state.runs[index];
343
+ if (number >= from && number <= to) { primary = id; break; }
344
+ }
345
+ out.add(primary ?? TAG_ZERO);
346
+ }
347
+ // 附加成员:只认显式 memberships(重叠 run 是父子/修正,不是多标签)
348
+ for (const id of state.memberships?.[key] ?? []) out.add(id);
349
+ return out;
350
+ }
351
+
352
+ /** tag({seqs,id,label?,keywords?,alias?}): exact overrides; runs are untouched. */
353
+ export function tagSeqs(tags, { seqs, id, label, keywords, alias } = {}) {
354
+ const state = normalizeTags(tags);
355
+ const tagId = asId(id);
356
+ if (tagId === null) throw new Error('tag: id is required');
357
+ if (!Array.isArray(seqs) || seqs.length === 0) throw new Error('tag: seqs must be a non-empty array');
358
+ const values = [...new Set(seqs.map(asInteger).filter((seq) => seq !== null))];
359
+ if (values.length === 0) throw new Error('tag: seqs must contain integer seqs');
360
+ const requestedLabel = asId(label);
361
+ if (state.tags[tagId] === undefined) state.tags[tagId] = normalizeEntry(tagId, {});
362
+ if (requestedLabel !== null) state.tags[tagId].label = requestedLabel;
363
+ if (Array.isArray(keywords)) {
364
+ state.tags[tagId].keywords = [...new Set(keywords.map((item) => String(item).trim()).filter(Boolean))];
365
+ }
366
+ const requestedAlias = asId(alias);
367
+ if (requestedAlias !== null) state.tags[tagId].alias = requestedAlias;
368
+ for (const seq of values) state.overrides[String(seq)] = tagId;
369
+ state.updatedAt = new Date().toISOString();
370
+ return { state, count: values.length, seqs: values };
371
+ }
372
+
373
+ /** retag({from,to}): move every run and override from one tag to another. */
374
+ export function retagTags(tags, { from, to } = {}) {
375
+ const state = normalizeTags(tags);
376
+ const fromId = asId(from);
377
+ const toId = asId(to);
378
+ if (fromId === null || toId === null) throw new Error('retag: from and to are required');
379
+ if (fromId === toId) throw new Error('retag: from and to must differ');
380
+ const source = state.tags[fromId];
381
+ if (state.tags[toId] === undefined) {
382
+ state.tags[toId] = normalizeEntry(toId, source ?? {});
383
+ }
384
+ let moved = 0;
385
+ state.runs = state.runs.map(([start, end, id]) => {
386
+ if (id !== fromId) return [start, end, id];
387
+ moved += 1;
388
+ return [start, end, toId];
389
+ });
390
+ for (const [key, value] of Object.entries(state.overrides)) {
391
+ if (value !== fromId) continue;
392
+ state.overrides[key] = toId;
393
+ moved += 1;
394
+ }
395
+ if (fromId !== TAG_ZERO) delete state.tags[fromId];
396
+ state.updatedAt = new Date().toISOString();
397
+ return { state, moved };
398
+ }
399
+
400
+ /** tags() index: label/status/keywords, coverage, entries, display, active, alias mapping. */
401
+ export function tagsView(tags, { activeId = null, threads = [], maxSeq = 0 } = {}) {
402
+ const state = normalizeTags(tags);
403
+ const ids = new Set([
404
+ ...Object.keys(state.tags),
405
+ ...state.runs.map((run) => run[2]),
406
+ ...Object.values(state.overrides),
407
+ ]);
408
+ for (const [id, entry] of Object.entries(state.tags)) {
409
+ if (entry?.lifecycle === 'merged' || entry?.lifecycle === 'archived') ids.delete(id);
410
+ }
411
+ const threadById = new Map();
412
+ const aliasToThread = new Map();
413
+ for (const thread of Array.isArray(threads) ? threads : []) {
414
+ if (thread?.id === undefined || thread.id === null) continue;
415
+ threadById.set(String(thread.id), thread);
416
+ const alias = typeof thread.alias === 'string' && thread.alias.trim().length > 0 ? thread.alias.trim() : null;
417
+ if (alias !== null) aliasToThread.set(alias, thread);
418
+ }
419
+ const activeThread = activeId === null ? null : threadById.get(String(activeId)) ?? null;
420
+ const activeAlias = activeThread?.alias !== undefined && activeThread?.alias !== null
421
+ ? String(activeThread.alias)
422
+ : null;
423
+ let max = Number.isFinite(Number(maxSeq)) ? Math.max(0, Math.trunc(Number(maxSeq))) : 0;
424
+ for (const [, end] of state.runs) if (end > max) max = end;
425
+ for (const key of Object.keys(state.overrides)) {
426
+ const seq = Number(key);
427
+ if (Number.isInteger(seq) && seq > max) max = seq;
428
+ }
429
+ const coverage = new Map();
430
+ for (let seq = 0; seq <= max; seq += 1) {
431
+ const key = String(seq);
432
+ let resolved = state.overrides[key];
433
+ if (resolved === undefined) {
434
+ resolved = TAG_ZERO;
435
+ for (let index = state.runs.length - 1; index >= 0; index -= 1) {
436
+ const [from, to, id] = state.runs[index];
437
+ if (seq >= from && seq <= to) { resolved = id; break; }
438
+ }
439
+ }
440
+ coverage.set(resolved, (coverage.get(resolved) ?? 0) + 1);
441
+ }
442
+ const rows = [...ids].sort().map((id) => {
443
+ const runs = state.runs.filter((run) => run[2] === id);
444
+ const overrides = Object.entries(state.overrides).filter(([, value]) => value === id);
445
+ const lastRun = runs.length > 0 ? runs[runs.length - 1] : null;
446
+ const lastOverride = overrides.length > 0
447
+ ? Math.max(...overrides.map(([key]) => Number(key)))
448
+ : null;
449
+ const lastSeq = Math.max(
450
+ lastRun === null ? Number.NEGATIVE_INFINITY : lastRun[1],
451
+ lastOverride === null ? Number.NEGATIVE_INFINITY : lastOverride,
452
+ );
453
+ const thread = threadById.get(id) ?? aliasToThread.get(id) ?? null;
454
+ const explicitLabel = state.tags[id]?.label;
455
+ const threadLabel = typeof thread?.label === 'string' && thread.label.length > 0 ? thread.label : null;
456
+ const label = typeof explicitLabel === 'string' && explicitLabel.length > 0 && explicitLabel !== id
457
+ ? explicitLabel
458
+ : threadLabel;
459
+ const alias = typeof state.tags[id]?.alias === 'string' && state.tags[id].alias.trim().length > 0
460
+ ? state.tags[id].alias.trim()
461
+ : typeof thread?.alias === 'string' && thread.alias.trim().length > 0
462
+ ? thread.alias.trim()
463
+ : null;
464
+ const display = alias ?? label ?? (id.length > 8 ? id.slice(0, 8) : id);
465
+ const entries = runs.length + overrides.length;
466
+ const events = coverage.get(id) ?? 0;
467
+ return {
468
+ id,
469
+ ...(label === null ? {} : { label }),
470
+ ...(alias === null ? {} : { alias }),
471
+ ...(thread === null ? {} : { threadId: String(thread.id) }),
472
+ display,
473
+ ...(state.tags[id]?.status === undefined ? {} : { status: state.tags[id].status }),
474
+ ...(state.tags[id]?.keywords === undefined ? {} : { keywords: state.tags[id].keywords }),
475
+ count: events,
476
+ entries,
477
+ events,
478
+ runs: runs.length,
479
+ overrides: overrides.length,
480
+ lastSeq: Number.isFinite(lastSeq) ? lastSeq : null,
481
+ lastRun,
482
+ active: (activeId !== null && String(activeId) === id)
483
+ || (activeAlias !== null && activeAlias === id),
484
+ };
485
+ });
486
+ for (const thread of Array.isArray(threads) ? threads : []) {
487
+ if (thread?.id === undefined || thread.id === null) continue;
488
+ const id = String(thread.id);
489
+ const alias = typeof thread.alias === 'string' && thread.alias.trim().length > 0 ? thread.alias.trim() : null;
490
+ if (ids.has(id) || (alias !== null && ids.has(alias))) continue;
491
+ const label = typeof thread.label === 'string' && thread.label.length > 0 ? thread.label : null;
492
+ rows.push({
493
+ id,
494
+ ...(label === null ? {} : { label }),
495
+ ...(alias === null ? {} : { alias }),
496
+ threadId: id,
497
+ display: alias ?? label ?? (id.length > 8 ? id.slice(0, 8) : id),
498
+ count: 0,
499
+ entries: 0,
500
+ events: 0,
501
+ runs: 0,
502
+ overrides: 0,
503
+ lastSeq: null,
504
+ lastRun: null,
505
+ active: activeId !== null && String(activeId) === id,
506
+ });
507
+ }
508
+ rows.sort((left, right) => left.id.localeCompare(right.id));
509
+ return { tags: rows, updatedAt: state.updatedAt };
510
+ }
511
+
512
+ /** Merge alias-named tag records into their canonical thread tag, then delete them. */
513
+ export function migrateAliasTags(tags, threads = []) {
514
+ const state = normalizeTags(tags);
515
+ const aliasToThread = new Map();
516
+ for (const thread of Array.isArray(threads) ? threads : []) {
517
+ if (thread?.id === undefined || thread.id === null) continue;
518
+ const alias = asId(thread.alias);
519
+ if (alias !== null && alias !== String(thread.id)) aliasToThread.set(alias, thread);
520
+ }
521
+ let movedRuns = 0;
522
+ let movedOverrides = 0;
523
+ let changed = false;
524
+ for (const [alias, thread] of aliasToThread) {
525
+ const canonical = String(thread.id);
526
+ const aliasEntry = state.tags[alias];
527
+ const aliasRuns = state.runs.filter((run) => run[2] === alias);
528
+ const aliasOverrides = Object.entries(state.overrides).filter(([, id]) => id === alias);
529
+ if (aliasEntry === undefined && aliasRuns.length === 0 && aliasOverrides.length === 0) continue;
530
+ if (state.tags[canonical] === undefined) {
531
+ state.tags[canonical] = normalizeEntry(canonical, {
532
+ label: thread.label ?? aliasEntry?.label ?? canonical,
533
+ ...(aliasEntry?.keywords === undefined ? {} : { keywords: aliasEntry.keywords }),
534
+ alias,
535
+ });
536
+ } else {
537
+ state.tags[canonical].alias = alias;
538
+ if ((state.tags[canonical].label === undefined
539
+ || state.tags[canonical].label.length === 0
540
+ || state.tags[canonical].label === canonical)
541
+ && typeof thread.label === 'string' && thread.label.length > 0) {
542
+ state.tags[canonical].label = thread.label;
543
+ }
544
+ }
545
+ state.runs = state.runs.map(([from, to, id]) => {
546
+ if (id !== alias) return [from, to, id];
547
+ movedRuns += 1;
548
+ return [from, to, canonical];
549
+ });
550
+ for (const [key, id] of Object.entries(state.overrides)) {
551
+ if (id !== alias) continue;
552
+ state.overrides[key] = canonical;
553
+ movedOverrides += 1;
554
+ }
555
+ delete state.tags[alias];
556
+ changed = true;
557
+ }
558
+ if (changed) state.updatedAt = new Date().toISOString();
559
+ return { state, changed, movedRuns, movedOverrides };
560
+ }
561
+
562
+ /** Fixed-template tags.md view. tags.json remains the only input source. */
563
+ export function renderTagsMd(tags, { threads = [], maxSeq = 0, activeId = null } = {}) {
564
+ const view = tagsView(tags, { threads, maxSeq, activeId });
565
+ const lines = [
566
+ '# Continuity tags (generated)',
567
+ '',
568
+ '> Generated from tags.json by dsh-continuity. Do not edit this file; hand edits are overwritten.',
569
+ '',
570
+ '| 符号 | 名称 | 关键词 | 事件 | 条目 | 状态 | 线程 id |',
571
+ '|---|---|---|---|---|---|---|',
572
+ ];
573
+ for (const row of view.tags) {
574
+ if (row.events <= 0) continue;
575
+ const symbol = row.alias ?? row.id;
576
+ lines.push(`| ${symbol} | ${row.label ?? row.display} | ${(row.keywords ?? []).join(', ')} | ${row.events} | ${row.entries} | ${row.status ?? ''} | ${row.threadId ?? ''} |`);
577
+ }
578
+ lines.push('', `updatedAt: ${view.updatedAt ?? ''}`);
579
+ return `${lines.join('\n')}\n`;
580
+ }
581
+
582
+ /** Contiguous ranges where resolve(seq) === id, over 0..max seq. */
583
+ export function findResolvedRanges({
584
+ tags,
585
+ id,
586
+ events = [],
587
+ after = undefined,
588
+ before = undefined,
589
+ limit = FIND_DEFAULT_LIMIT,
590
+ } = {}) {
591
+ const state = normalizeTags(tags);
592
+ const tagId = asId(id);
593
+ if (tagId === null) throw new Error('find: id is required');
594
+ const bySeq = new Map();
595
+ let maxSeq = 0;
596
+ for (const event of Array.isArray(events) ? events : []) {
597
+ const seq = asInteger(event?.seq);
598
+ if (seq === null) continue;
599
+ bySeq.set(seq, event);
600
+ if (seq > maxSeq) maxSeq = seq;
601
+ }
602
+ for (const [, end] of state.runs) if (end > maxSeq) maxSeq = end;
603
+ for (const key of Object.keys(state.overrides)) {
604
+ const seq = asInteger(key);
605
+ if (seq !== null && seq > maxSeq) maxSeq = seq;
606
+ }
607
+ const lower = after !== undefined && after !== null ? Number(after) + 1 : 0;
608
+ const upper = before !== undefined && before !== null ? Number(before) - 1 : maxSeq;
609
+ const ranges = [];
610
+ let current = null;
611
+ let count = 0;
612
+ let first = null;
613
+ let last = null;
614
+ for (let seq = Math.max(0, lower); seq <= upper; seq += 1) {
615
+ if (resolveTag(state, seq) !== tagId) continue;
616
+ if (first === null) first = seq;
617
+ last = seq;
618
+ count += 1;
619
+ if (current !== null && seq === current[1] + 1) current[1] = seq;
620
+ else {
621
+ current = [seq, seq];
622
+ ranges.push(current);
623
+ }
624
+ }
625
+ const maxRanges = Number.isFinite(Number(limit)) && Number(limit) > 0
626
+ ? Math.min(FIND_MAX_LIMIT, Math.trunc(Number(limit)))
627
+ : null;
628
+ const visibleRanges = maxRanges === null ? ranges : ranges.slice(0, maxRanges);
629
+ let span = null;
630
+ if (first !== null && last !== null) {
631
+ const start = Number(bySeq.get(first)?.time);
632
+ const end = Number(bySeq.get(last)?.time);
633
+ if (Number.isFinite(start) && Number.isFinite(end)) {
634
+ span = { start: new Date(start).toISOString(), end: new Date(end).toISOString() };
635
+ }
636
+ }
637
+ return { ranges: visibleRanges, count, first, last, span };
638
+ }
639
+
640
+ /** Complement of all non-T0 coverage over 0..max seq. Used only by find(T0). */
641
+ export function findT0Ranges(options = {}) {
642
+ const result = findResolvedRanges({ ...options, id: TAG_ZERO });
643
+ return {
644
+ seqs: [],
645
+ ranges: result.ranges,
646
+ count: result.count,
647
+ first: result.first,
648
+ last: result.last,
649
+ span: result.span,
650
+ complement: true,
651
+ };
652
+ }
653
+
654
+ /** Text for a tag/seq selection load; no thread-specific fields. */
655
+ export function selectionLoadText({
656
+ id = null,
657
+ label = null,
658
+ display = null,
659
+ count = 0,
660
+ quote = '',
661
+ indexOnly = false,
662
+ mode = 'tag',
663
+ first = 3,
664
+ last = 5,
665
+ foldedNote = null,
666
+ withReasoning = false,
667
+ reasoningInjected = 0,
668
+ skipped = 0,
669
+ } = {}) {
670
+ const title = display
671
+ ?? (label !== null && label !== undefined && label !== id && label !== ''
672
+ ? label
673
+ : (id ?? label ?? '所选内容'));
674
+ const policy = mode === 'tag'
675
+ ? (first === 3 && last === 5
676
+ ? '默认最早3+最新5'
677
+ : first > 0 && last > 0
678
+ ? `最早${first}+最新${last}`
679
+ : first > 0 ? `最早${first}` : `最新${last}`)
680
+ : '按指定 seqs';
681
+ const reasoningText = withReasoning
682
+ ? `含 reasoning${reasoningInjected} 条 · skipped ${skipped}`
683
+ : '已去 reasoning';
684
+ return [
685
+ `[取] ${title} · 实际 ${count} 条 · ${policy} · ${reasoningText}`,
686
+ ...(foldedNote !== null && foldedNote !== undefined && String(foldedNote).length > 0
687
+ ? [`[已折叠于 gist seq=${foldedNote}]`]
688
+ : []),
689
+ indexOnly ? '—— 原文不可用;只有索引 ——' : '—— 以下是从原文取回的所选内容 ——',
690
+ String(quote ?? ''),
691
+ ].join('\n');
692
+ }