@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,427 @@
1
+ /**
2
+ * Pure A4/B helpers: active-tool pinning and deterministic tool-result previews.
3
+ *
4
+ * A4 keeps a hard pin on the tools of the current open turn (plus every
5
+ * unpaired call). All other `tool/result` nodes may be rewritten to a head +
6
+ * marker + tail preview; user/assistant/reasoning content is never touched.
7
+ * The marker cites the original seq so `session_log get seq=<n>` can recover
8
+ * the full text; the raw log is never rewritten.
9
+ *
10
+ * No DSH imports: the engine supplies live events and the token meter.
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+
15
+ const DEFAULTS = Object.freeze({
16
+ maxChars: 1280,
17
+ headChars: 900,
18
+ tailChars: 200,
19
+ });
20
+
21
+ /** Recursively freeze a freshly built message tree. */
22
+ export function deepFreeze(value) {
23
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
24
+ Object.freeze(value);
25
+ for (const child of Object.values(value)) deepFreeze(child);
26
+ return value;
27
+ }
28
+
29
+ /** Count Unicode code points without splitting a surrogate pair. */
30
+ export function codePointLength(text) {
31
+ return Array.from(String(text ?? '')).length;
32
+ }
33
+
34
+ function textOfBlocks(blocks) {
35
+ if (!Array.isArray(blocks) || blocks.length === 0) return null;
36
+ let text = '';
37
+ for (const block of blocks) {
38
+ if (block === null || typeof block !== 'object') return null;
39
+ if (block.type !== 'text' || typeof block.text !== 'string') return null;
40
+ text += block.text;
41
+ }
42
+ return text;
43
+ }
44
+
45
+ /**
46
+ * Build a preview for a text-only block array.
47
+ * @param {readonly object[]} blocks
48
+ * @param {{ maxChars?: number, headChars?: number, tailChars?: number, originalSeq?: number|string|null, header?: string|null }} [options]
49
+ * @returns {{ content: object[], charsBefore: number, charsAfter: number, omitted: number, marker: string } | null}
50
+ */
51
+ export function previewBlocks(blocks, options = {}) {
52
+ const maxChars = Number.isInteger(options.maxChars) ? options.maxChars : DEFAULTS.maxChars;
53
+ const headChars = Number.isInteger(options.headChars) ? options.headChars : DEFAULTS.headChars;
54
+ const tailChars = Number.isInteger(options.tailChars) ? options.tailChars : DEFAULTS.tailChars;
55
+ const text = textOfBlocks(blocks);
56
+ if (text === null) return null;
57
+
58
+ const points = Array.from(text);
59
+ if (points.length <= maxChars) return null;
60
+ if (headChars < 0 || tailChars < 0 || headChars + tailChars >= points.length) return null;
61
+
62
+ const head = points.slice(0, headChars).join('');
63
+ const tail = points.slice(-tailChars).join('');
64
+ const omitted = points.length - headChars - tailChars;
65
+ const where = options.originalSeq === undefined || options.originalSeq === null
66
+ ? (options.header ? `original retrievable via ${options.header}` : 'original retrievable via session_log get on the gist range')
67
+ : `original retrievable via session_log get seq=${options.originalSeq}`;
68
+ const marker = `\n\n[... tool result preview: omitted ${omitted} chars; ${where} ...]\n\n`;
69
+ const content = [{ type: 'text', text: head + marker + tail }];
70
+ return {
71
+ content,
72
+ charsBefore: points.length,
73
+ charsAfter: codePointLength(content[0].text),
74
+ omitted,
75
+ marker,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * A handle, a pause, or an existing preview must never be previewed again: a
81
+ * second pass would cut the semantic summary/path and repoint the marker at the
82
+ * handle's own replacement seq instead of the original non-text event.
83
+ * @param {readonly object[]} blocks
84
+ * @returns {boolean}
85
+ */
86
+ export function isNonPreviewableBlocks(blocks) {
87
+ if (!Array.isArray(blocks)) return false;
88
+ const text = blocks
89
+ .filter((block) => block?.type === 'text' && typeof block.text === 'string')
90
+ .map((block) => block.text)
91
+ .join('\n');
92
+ return text.includes('[tool result handle')
93
+ || text.includes('[continuity pause')
94
+ || text.includes('[... tool result preview');
95
+ }
96
+
97
+ /**
98
+ * Rewrite one tool/result event into a preview message.
99
+ * @param {object} event
100
+ * @param {number|string} seq
101
+ * @param {object} [options]
102
+ * @returns {{ message: object, charsBefore: number, charsAfter: number, omitted: number, marker: string, seq: number|string } | null}
103
+ */
104
+ export function previewToolResultEvent(event, seq, options = {}) {
105
+ if (event?.type !== 'tool/result') return null;
106
+ const message = event?.data?.message;
107
+ if (message === undefined || message === null) return null;
108
+ if (isNonPreviewableBlocks(message.content)) return null;
109
+ const preview = previewBlocks(message.content, { ...options, originalSeq: seq });
110
+ if (preview === null) return null;
111
+ return {
112
+ ...preview,
113
+ seq,
114
+ message: deepFreeze({ ...message, content: preview.content }),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Copy a summarization input with tool-result messages previewed. Human,
120
+ * assistant and reasoning messages are returned unchanged.
121
+ * @param {object} input
122
+ * @param {{ maxChars?: number, headChars?: number, tailChars?: number, header?: string|null }} [options]
123
+ */
124
+ export function previewSummaryInput(input, options = {}) {
125
+ if (input === null || typeof input !== 'object' || !Array.isArray(input.messages)) return input;
126
+ let changed = false;
127
+ const messages = input.messages.map((message) => {
128
+ if (message?.role !== 'tool' || isNonPreviewableBlocks(message.content)) return message;
129
+ const preview = previewBlocks(message.content, {
130
+ maxChars: options.maxChars,
131
+ headChars: options.headChars,
132
+ tailChars: options.tailChars,
133
+ header: options.header ?? null,
134
+ });
135
+ if (preview === null) return message;
136
+ changed = true;
137
+ return deepFreeze({ ...message, content: preview.content });
138
+ });
139
+ return changed ? { ...input, messages } : input;
140
+ }
141
+
142
+ /**
143
+ * Call ids that are active at compaction time: every tool call in the current
144
+ * open turn, plus every unpaired call in the last `recentTurns` turns. Older
145
+ * unmatched calls are abandoned project debris, not active work.
146
+ * @param {readonly object[]} events full session log in seq order
147
+ * @param {{ recentTurns?: number }} [options]
148
+ * @returns {Set<string>}
149
+ */
150
+ export function activeCallIds(events, { recentTurns = 3 } = {}) {
151
+ if (!Array.isArray(events)) return new Set();
152
+ let openTurn = null;
153
+ const completed = new Set();
154
+ const turns = [];
155
+ for (const event of events) {
156
+ if (event?.type === 'turn/start') {
157
+ openTurn = event.data?.turn ?? null;
158
+ if (!turns.includes(openTurn)) turns.push(openTurn);
159
+ } else if (event?.type === 'turn/end' && event.data?.turn === openTurn) {
160
+ openTurn = null;
161
+ } else if (event?.type === 'tool/result') {
162
+ const callId = event.data?.message?.source?.callId ?? event.data?.message?.toolCallId;
163
+ if (callId !== undefined && callId !== null) completed.add(String(callId));
164
+ }
165
+ }
166
+
167
+ const keep = Math.max(1, Math.trunc(Number(recentTurns) || 3));
168
+ const recent = new Set(turns.slice(-keep));
169
+ const active = new Set();
170
+ for (const event of events) {
171
+ if (event?.type !== 'tool/call') continue;
172
+ const callId = event.data?.callId;
173
+ if (callId === undefined || callId === null) continue;
174
+ const inOpenTurn = openTurn !== null && event.data?.turn === openTurn;
175
+ const recentUnpaired = recent.has(event.data?.turn) && !completed.has(String(callId));
176
+ if (inOpenTurn || recentUnpaired) active.add(String(callId));
177
+ }
178
+ return active;
179
+ }
180
+
181
+ /** Surface seqs holding an active assistant tool-call message or its result. */
182
+ export function activePinSeqs(surfaceSeqs, eventAt, callIds) {
183
+ const pins = new Set();
184
+ if (!Array.isArray(surfaceSeqs) || callIds === undefined || callIds === null || callIds.size === 0) return pins;
185
+ for (const seq of surfaceSeqs) {
186
+ const event = eventAt(seq);
187
+ if (event?.type === 'tool/result') {
188
+ const callId = event.data?.message?.source?.callId ?? event.data?.message?.toolCallId;
189
+ if (callId !== undefined && callId !== null && callIds.has(String(callId))) pins.add(seq);
190
+ } else if (event?.type === 'assistant/message') {
191
+ const content = event.data?.message?.content;
192
+ if (!Array.isArray(content)) continue;
193
+ for (const block of content) {
194
+ if (block?.type === 'tool-call' && block.id !== undefined && callIds.has(String(block.id))) {
195
+ pins.add(seq);
196
+ break;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return pins;
202
+ }
203
+
204
+ /** Index of the oldest active pin, or -1. */
205
+ export function oldestPinIndex(surfaceSeqs, pins) {
206
+ if (!Array.isArray(surfaceSeqs) || pins === undefined || pins === null || pins.size === 0) return -1;
207
+ for (let index = 0; index < surfaceSeqs.length; index += 1) {
208
+ if (pins.has(surfaceSeqs[index])) return index;
209
+ }
210
+ return -1;
211
+ }
212
+
213
+ /**
214
+ * The post-compression target for a triggered plan. A4 always drives a
215
+ * band-high trigger to `low` and an emergency trigger to `emergencyTo`; it does
216
+ * not treat `high` as the target (that would leave the context hovering at the
217
+ * trigger and cause repeated prefix invalidations).
218
+ * @param {'band-high'|'emergency'|string} reason
219
+ * @param {{ low?: number, emergencyTo?: number }} policy
220
+ * @returns {number|null}
221
+ */
222
+ export function targetFractionFor(reason, policy) {
223
+ if (reason === 'emergency') {
224
+ return Number.isFinite(policy?.emergencyTo) ? policy.emergencyTo : null;
225
+ }
226
+ if (reason === 'band-high') {
227
+ return Number.isFinite(policy?.low) ? policy.low : null;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Remaining pass budget after the preview stage. `band-high` keeps the normal
234
+ * `maxPerBatch` cap; `emergency` ignores it, as the policy does. A positive
235
+ * budget here means the pass must continue to the target even if the preview
236
+ * already brought the estimate below `high`.
237
+ * @param {{ reason: string, estTokens: number, windowTokens: number, policy: object }} input
238
+ * @returns {number} 0 when no pass is needed
239
+ */
240
+ export function compressionBudget({ reason, estTokens, windowTokens, policy }) {
241
+ const target = targetFractionFor(reason, policy);
242
+ if (target === null) return 0;
243
+ if (!Number.isFinite(estTokens) || !Number.isFinite(windowTokens) || windowTokens <= 0) return 0;
244
+ const remaining = estTokens - target * windowTokens;
245
+ if (!(remaining > 0)) return 0;
246
+ if (reason === 'emergency') return remaining;
247
+ const minAction = Number.isFinite(policy?.minAction) ? policy.minAction : 0;
248
+ if (remaining < minAction) return 0;
249
+ const cap = policy?.maxPerBatch;
250
+ return Number.isFinite(cap) ? Math.min(remaining, cap) : remaining;
251
+ }
252
+
253
+ /** Map tool call id → { name, arguments, turn } from the raw log. */
254
+ export function toolCallMap(events) {
255
+ const map = new Map();
256
+ for (const event of Array.isArray(events) ? events : []) {
257
+ if (event?.type !== 'tool/call') continue;
258
+ const callId = event.data?.callId;
259
+ if (callId === undefined || callId === null) continue;
260
+ map.set(String(callId), {
261
+ name: String(event.data?.name ?? 'tool'),
262
+ arguments: String(event.data?.arguments ?? ''),
263
+ turn: event.data?.turn,
264
+ });
265
+ }
266
+ return map;
267
+ }
268
+
269
+ const NON_TEXT_TYPES = new Set(['image', 'file', 'audio', 'video']);
270
+ export const MAX_INLINE_IMAGE_BYTES = 4 * 1024 * 1024;
271
+
272
+ /** Content blocks in any of the event shapes used by DSH. */
273
+ export function eventContentBlocks(event) {
274
+ const data = event?.data;
275
+ if (Array.isArray(data)) return data;
276
+ if (Array.isArray(data?.content)) return data.content;
277
+ if (Array.isArray(data?.summary)) return data.summary;
278
+ if (Array.isArray(data?.message?.content)) return data.message.content;
279
+ return null;
280
+ }
281
+
282
+ /**
283
+ * Describe non-text blocks without carrying their payload. Returns null for a
284
+ * text-only tool result.
285
+ * @param {object} event
286
+ * @returns {{ types: string[], names: string[], bytes: number|null, text: string, isError: boolean } | null}
287
+ */
288
+ export function nonTextInfo(event) {
289
+ if (event?.type !== 'tool/result') return null;
290
+ const content = eventContentBlocks(event);
291
+ if (!Array.isArray(content)) return null;
292
+ const types = [];
293
+ const names = [];
294
+ const paths = [];
295
+ let bytes = null;
296
+ const texts = [];
297
+ for (const block of content) {
298
+ if (block?.type === 'text' && typeof block.text === 'string') {
299
+ texts.push(block.text);
300
+ } else if (block?.type !== undefined && NON_TEXT_TYPES.has(block.type)) {
301
+ if (!types.includes(block.type)) types.push(block.type);
302
+ const name = block.attachment?.name ?? block.name ?? block.fileName ?? block.source?.name;
303
+ if (typeof name === 'string' && name.length > 0 && !names.includes(name)) names.push(name);
304
+ const path = block.attachment?.path ?? block.path ?? block.source?.path;
305
+ if (typeof path === 'string' && path.length > 0 && !paths.includes(path)) paths.push(path);
306
+ let size = block.attachment?.size ?? block.attachment?.bytes
307
+ ?? block.size ?? block.bytes;
308
+ if (!Number.isFinite(size) && typeof path === 'string' && path.length > 0) {
309
+ try {
310
+ size = fs.statSync(path).size;
311
+ } catch { /* unknown size remains null */ }
312
+ }
313
+ if (Number.isFinite(size)) bytes = (bytes ?? 0) + size;
314
+ else {
315
+ const data = block.source?.data ?? block.data ?? block.base64;
316
+ if (typeof data === 'string' && data.length > 0) {
317
+ bytes = (bytes ?? 0) + Math.floor((data.length * 3) / 4);
318
+ }
319
+ }
320
+ }
321
+ }
322
+ if (types.length === 0) return null;
323
+ return {
324
+ types,
325
+ names,
326
+ paths,
327
+ bytes,
328
+ text: texts.join('\n').trim(),
329
+ isError: event.data?.message?.isError === true,
330
+ };
331
+ }
332
+
333
+ /** Images stay in the model context; other non-text result types use handles. */
334
+ export function isImageToolResult(event) {
335
+ const info = nonTextInfo(event);
336
+ return info !== null
337
+ && info.types.includes('image')
338
+ && info.bytes !== null
339
+ && info.bytes <= MAX_INLINE_IMAGE_BYTES;
340
+ }
341
+
342
+ function argsPreview(args) {
343
+ return String(args ?? '').replace(/\s+/g, ' ').trim().slice(0, 160);
344
+ }
345
+
346
+ /**
347
+ * Build a text handle for a non-text tool result. The original raw event stays
348
+ * untouched; the handle is the model-visible replacement.
349
+ * @param {object} event
350
+ * @param {number|string} seq
351
+ * @param {{ toolName?: string, toolArgs?: string, semantic?: object|null }} [options]
352
+ */
353
+ export function handleNonTextToolResult(event, seq, options = {}) {
354
+ const info = nonTextInfo(event);
355
+ if (info === null) return null;
356
+ const semantic = options.semantic ?? {};
357
+ const toolName = options.toolName ?? 'tool';
358
+ const args = argsPreview(options.toolArgs);
359
+ const importance = typeof semantic.importance === 'string' && semantic.importance.length > 0
360
+ ? semantic.importance
361
+ : (info.isError ? 'high (error)' : 'unknown');
362
+ const summary = typeof semantic.summary === 'string' && semantic.summary.trim().length > 0
363
+ ? semantic.summary.trim().slice(0, 800)
364
+ : (info.text.length > 0 ? info.text.slice(0, 300) : 'no semantic summary; inspect the original if needed');
365
+ const topic = typeof semantic.topic === 'string' && semantic.topic.trim().length > 0
366
+ ? semantic.topic.trim().slice(0, 200)
367
+ : `${toolName}${args.length > 0 ? ` ${args}` : ''}`;
368
+ const fn = typeof semantic.function === 'string' && semantic.function.trim().length > 0
369
+ ? semantic.function.trim().slice(0, 200)
370
+ : `${toolName} returned ${info.types.join('/')} output`;
371
+ const caveats = typeof semantic.caveats === 'string' && semantic.caveats.trim().length > 0
372
+ ? semantic.caveats.trim().slice(0, 300)
373
+ : 'non-text payload not loaded; original archived';
374
+ const callId = event.data?.message?.source?.callId ?? event.data?.message?.toolCallId ?? '?';
375
+ const names = info.names.length > 0 ? info.names.join(', ') : 'unnamed';
376
+ const paths = info.paths.length > 0 ? info.paths.join(', ') : '';
377
+ const bytes = info.bytes === null ? '?' : String(info.bytes);
378
+ const text = [
379
+ `[tool result handle · seq=${seq} · tool=${toolName} · call=${callId} · types=${info.types.join('/')} · name=${names} · bytes=${bytes}${paths.length > 0 ? ` · path=${paths}` : ''}]`,
380
+ `topic: ${topic}`,
381
+ `function: ${fn}`,
382
+ `importance: ${importance}`,
383
+ `caveats: ${caveats}`,
384
+ `summary: ${summary}`,
385
+ `original archived at seq=${seq}; use session_log get seq=${seq} or the attachment path if present. Do not assume the non-text payload is in context.`,
386
+ ].join('\n');
387
+ const message = deepFreeze({
388
+ ...event.data.message,
389
+ content: [{ type: 'text', text }],
390
+ });
391
+ const charsBefore = info.text.length + info.types.length * 8;
392
+ return {
393
+ message,
394
+ handle: text,
395
+ info,
396
+ charsBefore,
397
+ charsAfter: codePointLength(text),
398
+ };
399
+ }
400
+
401
+ /**
402
+ * The last-resort active-overflow handle. It is still a tool/result with the
403
+ * same call id, so the active pin keeps it; the text tells the instance to
404
+ * pause and ask for a larger window.
405
+ */
406
+ export function pauseActiveToolResult(event, seq, { toolName = 'tool', callId = '?' } = {}) {
407
+ const text = [
408
+ `[continuity pause · active tool call ${callId} was detached because its result alone exceeded the context window.`,
409
+ 'This is a limit problem, not a task failure. Do not retry the same call or keep compressing.',
410
+ `Tell the user the context window is too small for this task and ask them to raise it. The original result is archived at seq=${seq}; preserve continuity and do not discard the rest of the session.]`,
411
+ ].join(' ');
412
+ const message = deepFreeze({
413
+ ...(event?.data?.message ?? { role: 'tool' }),
414
+ content: [{ type: 'text', text }],
415
+ });
416
+ return {
417
+ message,
418
+ handle: text,
419
+ charsBefore: 0,
420
+ charsAfter: codePointLength(text),
421
+ toolName,
422
+ callId,
423
+ };
424
+ }
425
+
426
+ export { DEFAULTS as PREVIEW_DEFAULTS };
427
+
package/src/topic.mjs ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 话题提示的判定逻辑(纯函数,可单测)。
3
+ *
4
+ * 判据(2026-10-30 Continuity 定):
5
+ * 1. 只认 keywords/anchors;标签名不参与打分,只用于显示;
6
+ * 2. 泛词权重 0;至少 1 个专属词(precision ≥ 0.5 且 hits ≥ 5),或 ≥2 个非泛关键词;
7
+ * 3. 与当前线的分差 ≥ 2;最多 1 个目标;
8
+ * 4. 拿不到专属词 → 静默,不猜。
9
+ */
10
+ export function selectTopic({ rows = [], text = '', genericWords = new Set(), wordStats = new Map(), activeId = '' } = {}) {
11
+ const ranked = [];
12
+ for (const row of Array.isArray(rows) ? rows : []) {
13
+ const id = String(row?.id ?? '');
14
+ if (id.length === 0) continue;
15
+ const keywords = Array.isArray(row?.keywords) ? row.keywords : [];
16
+ const anchors = Array.isArray(row?.anchors) ? row.anchors : [];
17
+ const keywordHits = keywords.filter((word) => typeof word === 'string' && word.length > 0 && text.includes(word));
18
+ const anchorHits = anchors.filter((word) => typeof word === 'string' && word.length > 0 && text.includes(word));
19
+ const matched = [...new Set([...keywordHits, ...anchorHits])];
20
+ if (matched.length === 0) continue;
21
+
22
+ // 有显式必需词时,一个都没命中 → 这个标签不参与。
23
+ if (anchors.length > 0 && anchorHits.length === 0) continue;
24
+
25
+ const exclusiveOf = (word) => {
26
+ const stats = wordStats instanceof Map ? wordStats.get(word) : wordStats?.[word];
27
+ return Number(stats?.precision) >= 0.5 && Number(stats?.hits) >= 5;
28
+ };
29
+ // anchors 只负责“必须出现”,不得把泛词升格成强证据。
30
+ const strong = matched.filter((word) => !genericWords.has(word));
31
+ const weak = matched.filter((word) => genericWords.has(word));
32
+ const exclusive = strong.filter(exclusiveOf);
33
+ ranked.push({
34
+ id,
35
+ alias: row.alias ?? id,
36
+ label: row.label,
37
+ matched,
38
+ anchors: anchorHits,
39
+ strong,
40
+ weak,
41
+ exclusive,
42
+ score: strong.length * 3,
43
+ overlap: Number((strong.length / matched.length).toFixed(2)),
44
+ });
45
+ }
46
+ ranked.sort((left, right) => right.score - left.score);
47
+ const top = ranked[0];
48
+ const active = ranked.find((row) => row.id === String(activeId));
49
+ const activeScore = active?.score ?? 0;
50
+ const activeStrong = active?.strong.length ?? 0;
51
+
52
+ if (top === undefined) return { blocked: true, reason: 'no-match', ranked, top: null };
53
+ if (top.strong.length === 0) return { blocked: true, reason: 'only-generic', ranked, top };
54
+ if (top.exclusive.length === 0 && top.strong.length < 2) {
55
+ return { blocked: true, reason: 'weak-evidence', ranked, top };
56
+ }
57
+ if (top.id === String(activeId)) return { blocked: true, reason: 'already-active', ranked, top };
58
+ if (top.score < activeScore + 2) return { blocked: true, reason: 'not-strong-enough', ranked, top };
59
+ if (activeStrong > 0 && top.strong.length <= activeStrong && top.score < activeScore + 2) {
60
+ return { blocked: true, reason: 'tie', ranked, top };
61
+ }
62
+ return { blocked: false, reason: 'switch-suggested', ranked, top };
63
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 训练模式下的自动学习:有候选就直接写进技能文件,不问。
3
+ * 与 skill-append.mjs 同口径:查重、上限 12 条。
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ export function learningDir() {
9
+ return path.join(process.env.USERPROFILE ?? process.env.HOME ?? ".", ".dsh", "continuity");
10
+ }
11
+
12
+ export function trainingMode() {
13
+ try {
14
+ const raw = JSON.parse(fs.readFileSync(path.join(learningDir(), "training.json"), "utf8"));
15
+ return Number(raw?.mode) === 1 ? 1 : 0;
16
+ } catch {
17
+ return 0;
18
+ }
19
+ }
20
+
21
+ function sectionBounds(lines) {
22
+ const start = lines.findIndex((l) => l.startsWith("## 失败分支"));
23
+ if (start < 0) return null;
24
+ let end = lines.length;
25
+ for (let i = start + 1; i < lines.length; i++) if (lines[i].startsWith("## ")) { end = i; break; }
26
+ return { start, end };
27
+ }
28
+
29
+ function tokens(text) {
30
+ return new Set(String(text).toLowerCase().split(/[^a-z0-9\u4e00-\u9fa5_]+/).filter((x) => x.length >= 2));
31
+ }
32
+
33
+ function covered(sig, line) {
34
+ const a = tokens(sig), b = tokens(line);
35
+ if (a.size === 0) return false;
36
+ let hit = 0;
37
+ for (const t of a) if (b.has(t)) hit++;
38
+ if (hit / a.size >= 0.6) return true;
39
+ const norm = (s) => s.replace(/\s+/g, " ").slice(0, 24);
40
+ return line.includes(norm(sig));
41
+ }
42
+
43
+ /**
44
+ * 自动写入一条教训。
45
+ * @returns {{ok:boolean, action:'appended'|'duplicate'|'cap'|'error', message:string}}
46
+ */
47
+ export function autoLearn({ sig, count, skillFile }) {
48
+ try {
49
+ const file = skillFile ?? path.join(learningDir(), "skills", "change-loop.md");
50
+ const raw = fs.readFileSync(file, "utf8");
51
+ const lines = raw.split(/\r?\n/);
52
+ const bounds = sectionBounds(lines);
53
+ if (bounds === null) return { ok: false, action: "error", message: "失败分支段不存在" };
54
+
55
+ const body = lines.slice(bounds.start + 1, bounds.end);
56
+ const bullets = body.filter((l) => l.trim().startsWith("- "));
57
+ for (const b of bullets) if (covered(sig, b)) return { ok: false, action: "duplicate", message: b.trim().slice(0, 80) };
58
+ if (bullets.length >= 12) return { ok: false, action: "cap", message: `已满 ${bullets.length}/12,需合并` };
59
+
60
+ const bullet = `- **${sig}**:重复出现 ${count} 次,自动记入(训练模式)。`;
61
+ let insertAt = bounds.end;
62
+ for (let i = bounds.end - 1; i > bounds.start; i--) {
63
+ if (lines[i].trim() !== "") { insertAt = i + 1; break; }
64
+ }
65
+ lines.splice(insertAt, 0, bullet);
66
+ fs.writeFileSync(file, lines.join("\n"), "utf8");
67
+
68
+ const log = path.join(learningDir(), "learning", "auto-log.jsonl");
69
+ fs.mkdirSync(path.dirname(log), { recursive: true });
70
+ fs.appendFileSync(log, JSON.stringify({ at: new Date().toISOString(), sig, count, action: "appended" }) + "\n", "utf8");
71
+ return { ok: true, action: "appended", message: bullet };
72
+ } catch (error) {
73
+ return { ok: false, action: "error", message: String(error?.message ?? error) };
74
+ }
75
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 轮标签投影(纯函数)。
3
+ *
4
+ * 消息级标签由引擎自动写入 tags.json;turn-tags.jsonl 只是它的“物化视图”。
5
+ * 这里只做投影:不判断、不匹配,只数多数派。
6
+ *
7
+ * 规则:
8
+ * - 忽略 T0(那是“未分类”桶,不能当轮标签);
9
+ * - 需要至少 minShare 的份额(默认 0.6)才算可投影;
10
+ * - 打平或全 T0 → null(交给实例判断)。
11
+ */
12
+ /**
13
+ * 把 surface 顺序的消息归到轮(纯函数)。
14
+ *
15
+ * 为什么需要它:`user/message` 事件没有 `data.turn`,只按 data.turn 分组会把
16
+ * 用户消息全部丢掉,整轮只剩 assistant 消息 → 全是 T0 → 投影永远 no-tag。
17
+ *
18
+ * 规则:用户消息归到“它后面第一条带轮号的 assistant 消息”所属的轮;
19
+ * 末尾没有后续 assistant 的(当轮还没产出)不归属。
20
+ *
21
+ * @param ordered [{ seq, type: 'user/message'|'assistant/message', turn: number|null }]
22
+ * @returns { turnSeqs: Map<number, number[]>, maxTurn: number }
23
+ */
24
+ export function attributeTurnSeqs(ordered = []) {
25
+ const list = Array.isArray(ordered) ? ordered : [];
26
+ const turnSeqs = new Map();
27
+ for (let index = 0; index < list.length; index += 1) {
28
+ const row = list[index];
29
+ if (!Number.isInteger(Number(row?.seq))) continue;
30
+ let turn = Number.isInteger(row?.turn) ? row.turn : null;
31
+ if (turn === null) {
32
+ for (let later = index + 1; later < list.length; later += 1) {
33
+ if (Number.isInteger(list[later]?.turn)) { turn = list[later].turn; break; }
34
+ }
35
+ }
36
+ if (turn === null) continue;
37
+ const bucket = turnSeqs.get(turn) ?? [];
38
+ bucket.push(Number(row.seq));
39
+ turnSeqs.set(turn, bucket);
40
+ }
41
+ const maxTurn = turnSeqs.size === 0 ? 0 : Math.max(...turnSeqs.keys());
42
+ return { turnSeqs, maxTurn };
43
+ }
44
+
45
+ export function deriveTurnTag({ messages = [], minShare = 0.6 } = {}) {
46
+ const counts = new Map();
47
+ let total = 0;
48
+ for (const message of Array.isArray(messages) ? messages : []) {
49
+ const tag = String(message?.tag ?? '');
50
+ if (tag.length === 0 || tag === 'T0') continue;
51
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
52
+ total += 1;
53
+ }
54
+ if (total === 0) return { tag: null, reason: 'no-tag' };
55
+ const ranked = [...counts.entries()].sort((left, right) => right[1] - left[1]);
56
+ const [topTag, topCount] = ranked[0];
57
+ const secondCount = ranked[1]?.[1] ?? 0;
58
+ if (topCount === secondCount) return { tag: null, reason: 'tie' };
59
+ const share = topCount / total;
60
+ if (share < minShare) return { tag: null, reason: 'weak-majority', share: Number(share.toFixed(2)) };
61
+ return { tag: topTag, reason: 'majority', share: Number(share.toFixed(2)) };
62
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Only real user input may be quoted as "what the user just said".
3
+ * runtime-context, time-context, delivery notices and queue splices are not
4
+ * statements by the person, even though they travel as user/message envelopes.
5
+ */
6
+ import { eventText } from './session-log.mjs';
7
+
8
+ export function messageSourceKind(event) {
9
+ const data = event?.data;
10
+ return data?.message?.source?.kind ?? data?.source?.kind ?? null;
11
+ }
12
+
13
+ /** Latest surface text from a real source.kind === "user" message. */
14
+ export function latestUserTurnFromSession(session) {
15
+ const nodes = session?.surface?.nodes ?? [];
16
+ for (let i = nodes.length - 1; i >= 0; i--) {
17
+ let event;
18
+ try { event = session.eventAt(nodes[i]); } catch { continue; }
19
+ let text = '';
20
+ if (event?.type === 'agent/inbox/spliced') {
21
+ const inserted = Array.isArray(event?.data?.inserted) ? event.data.inserted : [];
22
+ text = inserted
23
+ .filter((item) => item?.source?.kind === 'user')
24
+ .map((item) => (Array.isArray(item?.content) ? item.content : []).map((part) => part?.text ?? '').join(' '))
25
+ .join(' ')
26
+ .trim();
27
+ } else if (event?.type === 'user/message') {
28
+ if (messageSourceKind(event) !== 'user') continue;
29
+ try { text = eventText(event, { includeReasoning: false }).trim(); } catch { continue; }
30
+ } else {
31
+ continue;
32
+ }
33
+ if (!text) continue;
34
+ if (/^\[(线|标签|习惯|技能|归位)\]/.test(text)) continue;
35
+ if (/^\s*</.test(text) || text.includes('system-reminder')) continue;
36
+ return { seq: nodes[i], text };
37
+ }
38
+ return null;
39
+ }
@@ -0,0 +1,11 @@
1
+ # C 用户词 -> B 通用概念
2
+
3
+ # 一行一条;LLM 可以自己加,加错删掉即可。
4
+ # 旧格式:用户词 => 概念 / 概念
5
+ # 新格式:用户词 => B:概念/概念 | K:关键词/关键词 | minB:2 | 摘要:一句话(摘要不是原文)
6
+ #
7
+ # 新装实例默认空表。不要凭想象预填“跑团”之类:
8
+ # 用户历史里没有的 C 词,只会变成噪音。
9
+ #
10
+ # 示例(需要时取消注释并改成用户自己的词):
11
+ # 某词 => B:游戏/故事 | K:某名字 | minB:2 | 摘要:一句话