@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,161 @@
1
+ /**
2
+ * System-side memory load gate.
3
+ *
4
+ * The gate decides whether to load prior context before the model speaks.
5
+ * It does not ask the LLM for approval. The LLM may still call memory.load
6
+ * manually; that is an optional second path, not the gate.
7
+ */
8
+
9
+ const META_PATTERN = /TAG\s*:|戏外|存档|回执|折叠|gist|```|未决|threads|session/i;
10
+ const META_MENTION = /这个设计|这个机制|挺有意思|是什么|怎么写|规则文档|文档里/u;
11
+
12
+ export function isSceneRecord(record) {
13
+ return record?.facets?.medium !== '代码' && !META_PATTERN.test(record?.text ?? '');
14
+ }
15
+
16
+ function hasActivitySignal(text, activity) {
17
+ const source = String(text ?? '');
18
+ if (source.trim().length < 2) return false;
19
+ if (activity === '跑团') return /跑团|TRPG|开团|骰|团|灯室|钟楼|矿口|角色|剧情/u.test(source);
20
+ if (activity === '开发' || activity === '调试' || activity === '文档' || activity === '测试') {
21
+ return /代码|插件|接口|调试|文档|版本|测试|部署|修|改|实现|bug/iu.test(source);
22
+ }
23
+ if (activity === '对话') return /聊|说|想|问|讨论/u.test(source);
24
+ return source.trim().length >= 8;
25
+ }
26
+
27
+ export class MemoryLoadState {
28
+ constructor({
29
+ loadedFacets = null,
30
+ loadedSeq = null,
31
+ loadedAtTurn = null,
32
+ lastReason = null,
33
+ cooldownTurns = 8,
34
+ autoActivityLoad = false,
35
+ abstainThreshold = 0.5,
36
+ } = {}) {
37
+ this.loadedFacets = loadedFacets;
38
+ this.loadedSeq = loadedSeq;
39
+ this.loadedAtTurn = loadedAtTurn;
40
+ this.lastReason = lastReason;
41
+ this.cooldownTurns = cooldownTurns;
42
+ this.autoActivityLoad = autoActivityLoad;
43
+ this.abstainThreshold = abstainThreshold;
44
+ }
45
+
46
+ decide({ text, currentFacets, confidence = 1, nowTurn = 0, hasCandidate = true, candidate = null, role = 'user', boundaryScore = null }) {
47
+ if (role === 'assistant') return { action: 'none', reason: 'assistant-text' };
48
+ const facets = currentFacets ?? {};
49
+ const activity = facets.activity ?? null;
50
+ if (activity === null) return { action: 'none', reason: 'no-activity' };
51
+ if (confidence < this.abstainThreshold) return { action: 'none', reason: 'low-confidence' };
52
+
53
+ const recall = /上次|之前(?:那个|那件|那次|说的|聊的|提的|发生)|还记得|后来|上一次|刚才|交给谁|在哪|发生了什么|最后(?:呢|怎么|怎么样|是谁|多少|哪|结果)/u.test(text);
54
+ const start = /开始(?:跑团|一局|团|游戏|新的)|开个新团|新团|开新团|来一局|开团|开一局|进入.*(?:跑团|灯室|游戏)|跑团吧|TRPG/u.test(text);
55
+ const activityChanged = this.loadedFacets !== null && this.loadedFacets.activity !== activity;
56
+ const topicChanged = this.loadedFacets !== null
57
+ && this.loadedFacets.activity === activity
58
+ && this.loadedFacets.topic !== facets.topic;
59
+ const entity = /[\u4e00-\u9fff]{2,}|[a-z][a-z0-9_-]{2,}/iu.test(text);
60
+ const cooldown = this.loadedAtTurn !== null && nowTurn - this.loadedAtTurn < this.cooldownTurns;
61
+ const hasBoundary = Number.isFinite(boundaryScore);
62
+ const strongBoundary = hasBoundary && boundaryScore >= 0.35;
63
+
64
+ if (recall) {
65
+ if (!hasCandidate) return { action: 'none', reason: 'recall-no-candidate' };
66
+ return { action: 'load', reason: 'explicit-recall', query: text, facets, candidate };
67
+ }
68
+
69
+ if (start) {
70
+ if (!hasCandidate) return { action: 'none', reason: 'start-no-candidate' };
71
+ return { action: 'load', reason: 'explicit-start', query: text, facets, candidate };
72
+ }
73
+
74
+ if (META_MENTION.test(text)) {
75
+ return { action: 'none', reason: 'meta-mention' };
76
+ }
77
+
78
+ if (this.loadedFacets === null) {
79
+ if (hasCandidate && confidence >= 0.75) {
80
+ return { action: 'load', reason: 'initial-context', query: activity, facets, candidate };
81
+ }
82
+ return { action: 'none', reason: 'initial-low-confidence' };
83
+ }
84
+
85
+ if (strongBoundary) {
86
+ if (!hasCandidate) return { action: 'none', reason: 'boundary-no-candidate' };
87
+ const autoBoundary = this.autoActivityLoad || (role === 'coo' || role === 'peer');
88
+ if (!autoBoundary) {
89
+ return { action: 'hint', reason: 'boundary-hint', query: text, facets, candidate };
90
+ }
91
+ if (cooldown && confidence < 0.85) return { action: 'none', reason: 'cooldown' };
92
+ return { action: 'load', reason: 'boundary-change', query: text, facets, candidate };
93
+ }
94
+
95
+ if (hasBoundary && !strongBoundary) {
96
+ if (boundaryScore > 0 && activityChanged) {
97
+ if (!hasActivitySignal(text, activity)) return { action: 'none', reason: 'no-activity-signal' };
98
+ if ((role === 'coo' || role === 'peer') || this.autoActivityLoad) {
99
+ return { action: 'load', reason: 'activity-change', query: activity, facets, candidate };
100
+ }
101
+ return { action: 'hint', reason: 'activity-change-hint', query: activity, facets, candidate };
102
+ }
103
+ return { action: 'none', reason: 'weak-boundary' };
104
+ }
105
+
106
+ if (activityChanged) {
107
+ if (!hasCandidate) return { action: 'none', reason: 'activity-no-candidate' };
108
+ if (!hasActivitySignal(text, activity)) return { action: 'none', reason: 'no-activity-signal' };
109
+ const autoActivity = this.autoActivityLoad || (role === 'coo' || role === 'peer');
110
+ if (!autoActivity) {
111
+ return { action: 'hint', reason: 'activity-change-hint', query: activity, facets, candidate };
112
+ }
113
+ if (cooldown && confidence < 0.85) return { action: 'none', reason: 'cooldown' };
114
+ return { action: 'load', reason: 'activity-change', query: activity, facets, candidate };
115
+ }
116
+
117
+ if (topicChanged && entity) {
118
+ return { action: 'hint', reason: 'topic-entity', query: text, facets, candidate };
119
+ }
120
+
121
+ return { action: 'none', reason: 'same-context' };
122
+ }
123
+
124
+ recordLoad({ facets, seq = null, nowTurn = 0, reason = null }) {
125
+ this.loadedFacets = facets ?? this.loadedFacets;
126
+ this.loadedSeq = seq;
127
+ this.loadedAtTurn = nowTurn;
128
+ this.lastReason = reason;
129
+ return this;
130
+ }
131
+ }
132
+
133
+ export function latestCandidate(records, facet, value, { sceneOnly = true } = {}) {
134
+ const hits = records.filter((record) => record.facets?.[facet] === value);
135
+ if (hits.length === 0) return null;
136
+ if (sceneOnly) {
137
+ const scenes = hits.filter(isSceneRecord);
138
+ return scenes.length === 0 ? null : scenes[scenes.length - 1];
139
+ }
140
+ return hits[hits.length - 1];
141
+ }
142
+
143
+ export function candidateSegment(records, candidate, { sceneOnly = true } = {}) {
144
+ if (candidate === null) return null;
145
+ const activity = candidate.facets?.activity;
146
+ const index = records.findIndex((record) => record.turn === candidate.turn);
147
+ if (index < 0) return null;
148
+ let start = index;
149
+ let end = index;
150
+ const fits = (record) => record.facets?.activity === activity && (!sceneOnly || isSceneRecord(record));
151
+ while (start > 0 && fits(records[start - 1])) start -= 1;
152
+ while (end + 1 < records.length && fits(records[end + 1])) end += 1;
153
+ const segment = records.slice(start, end + 1);
154
+ return {
155
+ turn: candidate.turn,
156
+ seqStart: segment[0].seqStart,
157
+ seqEnd: segment[segment.length - 1].seqEnd,
158
+ turns: segment.length,
159
+ text: segment.map((record) => record.text).join('\n'),
160
+ };
161
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Deterministic handoff note for the 4.13 mechanical-summary trial.
3
+ *
4
+ * The note is a rendered record, not an LLM summary: user text, tool calls and
5
+ * tool receipts are kept; assistant prose and reasoning are skipped. It is
6
+ * deliberately boring so a fold cannot silently lose the record's shape.
7
+ */
8
+
9
+ const DEFAULT_MAX_TOKENS = 32_000;
10
+ const CHARS_PER_TOKEN = 4;
11
+ const INJECTED = /^(?:\[(?:continuity:|追忆|抽取|工作线|标签|规则|规矩|习惯|技能|归位|候选|存\?|闸|回正|学习|记忆)|<system-reminder>|Current runtime context|Time sampled while preparing)/i;
12
+ const PEER = /^\[(?:创|peer)\]/i;
13
+
14
+ function textOfContent(content) {
15
+ if (typeof content === 'string') return content;
16
+ if (!Array.isArray(content)) return '';
17
+ return content
18
+ .filter((block) => block && typeof block === 'object' && block.type === 'text')
19
+ .map((block) => String(block.text ?? ''))
20
+ .join('\n');
21
+ }
22
+
23
+ function toolCallsOf(message) {
24
+ const calls = [];
25
+ if (Array.isArray(message?.tool_calls)) {
26
+ for (const call of message.tool_calls) {
27
+ const name = call?.function?.name ?? call?.name;
28
+ if (typeof name === 'string') calls.push({ name, args: call?.function?.arguments ?? call?.arguments ?? '' });
29
+ }
30
+ }
31
+ if (Array.isArray(message?.content)) {
32
+ for (const block of message.content) {
33
+ if (block?.type === 'tool-call' || block?.type === 'tool_call') {
34
+ const name = block?.name ?? block?.toolName;
35
+ if (typeof name === 'string') calls.push({ name, args: block?.arguments ?? block?.input ?? '' });
36
+ }
37
+ }
38
+ }
39
+ return calls;
40
+ }
41
+
42
+ function oneLine(text, maxChars) {
43
+ const clean = String(text ?? '').replace(/\s+/g, ' ').trim();
44
+ if (clean.length <= maxChars) return clean;
45
+ return `${clean.slice(0, maxChars).replace(/\s+\S*$/, '')}…`;
46
+ }
47
+
48
+ function normalize(args) {
49
+ if (typeof args === 'string') return args;
50
+ try { return JSON.stringify(args ?? {}); } catch { return String(args ?? ''); }
51
+ }
52
+
53
+ export function renderMechanicalNote({
54
+ messages,
55
+ maxTokens = DEFAULT_MAX_TOKENS,
56
+ header = null,
57
+ }) {
58
+ const list = Array.isArray(messages) ? messages : [];
59
+ const lines = [];
60
+ const budgetChars = Math.max(400, Number(maxTokens) || DEFAULT_MAX_TOKENS) * CHARS_PER_TOKEN;
61
+
62
+ if (typeof header === 'string' && header.trim().length > 0) lines.push(header.trim());
63
+ lines.push('## Mechanical handoff note');
64
+ lines.push('This is a mechanical record of the folded range. It is not an LLM summary; raw session_log remains authoritative.');
65
+
66
+ for (const message of list) {
67
+ const role = String(message?.role ?? '');
68
+ if (role === 'system') continue;
69
+ if (role === 'user') {
70
+ const text = textOfContent(message?.content).trim();
71
+ if (!text || INJECTED.test(text)) continue;
72
+ if (PEER.test(text)) {
73
+ lines.push(`## Peer\n${oneLine(text, 8_000)}`);
74
+ } else {
75
+ lines.push(`## User\n${oneLine(text, 12_000)}`);
76
+ }
77
+ continue;
78
+ }
79
+ if (role === 'assistant') {
80
+ for (const call of toolCallsOf(message)) {
81
+ lines.push(`## Tool call\n${call.name} ${oneLine(normalize(call.args), 2_000)}`);
82
+ }
83
+ continue;
84
+ }
85
+ if (role === 'tool') {
86
+ const text = textOfContent(message?.content).trim();
87
+ if (text) lines.push(`## Tool receipt\n${oneLine(text, 6_000)}`);
88
+ }
89
+ }
90
+
91
+ let text = lines.join('\n\n');
92
+ if (text.length > budgetChars) {
93
+ text = `${text.slice(-budgetChars).replace(/^\s+\S*/, '')}`;
94
+ text = `## Mechanical handoff note\n(older entries trimmed)\n\n${text}`;
95
+ }
96
+ return {
97
+ text,
98
+ chars: text.length,
99
+ approxTokens: Math.ceil(text.length / CHARS_PER_TOKEN),
100
+ maxTokens,
101
+ };
102
+ }
103
+
104
+ export default renderMechanicalNote;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * First-run onboarding and scaffolding.
3
+ *
4
+ * The onboarding is a user/message appended through the same session.append
5
+ * path used by the audit injection. Completion is only recorded after the
6
+ * marker is actually present in the next decision's messages.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { randomUUID } from 'node:crypto';
12
+ import { contextMessage } from '../threads.mjs';
13
+
14
+ const MARKER = '[continuity:boot]';
15
+ const DEFAULT_HOME = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
16
+ const DEFAULT_DIR = path.join(DEFAULT_HOME, '.dsh', 'continuity');
17
+
18
+ export function onboardingText(marker = MARKER) {
19
+ return [
20
+ `${marker} 这个环境有一套连续性记忆,先记住这几件事:`,
21
+ '',
22
+ '0. 以 [continuity:*] 开头的 user 消息是插件注入,不是人说的。',
23
+ '1. 取:需要回忆时调用 recall(query);它返回 [追忆] 指针,不直接给全文。',
24
+ '2. 存:重要结论写进当前 session 的目录或工作区 log,不要只留在上下文里。',
25
+ '3. 长:A 表在当前 session 目录的 concepts.md;它由这个 session 的语料逐步长成,不继承别的 session。',
26
+ '4. 验:用 recall 打开真实 turn/seq 再下结论;候选不是事实。',
27
+ '5. 审计:每 200 轮会自动来一次 [continuity:audit],你不用排期;扫一眼,跳过叙事即可。',
28
+ '6. 找回靠“词”不靠“话题”:给具体名字/术语(跑团、dataplan、换箍)能定位;“之前那个事”基本不中。想让它能找回,就把名字写进 keywords.md 或 cmap.md。',
29
+ '7. B 标签(每轮 top3)只用于展示,不参与找回;真正四条路是 字面+别名 / K / C。',
30
+ '8. 表各管什么:keywords=精确名,cmap=用户词,aliases=中英映射,concepts=概念表(展示+算分,不参与查询)。',
31
+ '9. 重建:如果这个 session 已有很长历史(>200 轮)而记忆是空的,不要从零编概念;看 ~/.dsh/continuity/rebuild.md 按九步建索引。',
32
+ '',
33
+ 'A 表经验边界:topic 条数约 0.6√N,每次最多 +2;两条 topic 覆盖 Jaccard >0.6 就合并;workflow 不参与合并。',
34
+ '只有 session_log 里 role=user 的消息才算真的送达;delivered/日志不算。',
35
+ '',
36
+ '你可以改自己的 rules/habits,也可以拒绝这些建议;插件不替你做价值判断。',
37
+ ].join('\n');
38
+ }
39
+
40
+ export const ONBOARDING = onboardingText(MARKER);
41
+
42
+ const SCAFFOLDS = {
43
+ 'concepts.md': '# 概念表:名字 | 描述词 | 参考覆盖\n# 由你的语料生成;空表不等于回退别人的表。\n',
44
+ 'cmap.md': '# C 用户词:term => B:概念 | K:关键词 | minB:n | summary\n',
45
+ 'keywords.md': '# K 特殊关键词:一行一个\n',
46
+ 'aliases.md': '# 别名:term => alias / alias(只放跨语言标识符)\n',
47
+ 'rebuild.md': '# 重建宫殿(九步)\n# 1 数历史 2 建索引 3 补 vec/chatter 4 种 K 5 种 A\n# 6 重标 7 种 C/别名 8 验收 9 启动审计\n# 细节见包内 templates/rebuild.md;每步先备份,失败回滚。\n',
48
+ };
49
+
50
+ export function bootstrapPaths(home = DEFAULT_HOME) {
51
+ const dir = path.join(home, '.dsh', 'continuity');
52
+ return { dir, stateFile: path.join(dir, 'bootstrap.json') };
53
+ }
54
+
55
+ function readState(file) {
56
+ try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; }
57
+ }
58
+ function writeState(file, value) {
59
+ fs.mkdirSync(path.dirname(file), { recursive: true });
60
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
61
+ }
62
+
63
+ /** Create only missing session files. Never overwrite an existing table. */
64
+ export function ensureSessionScaffolds({ sessionId = null, home = DEFAULT_HOME } = {}) {
65
+ if (!sessionId) return [];
66
+ const dir = path.join(bootstrapPaths(home).dir, 'sessions', String(sessionId));
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ const created = [];
69
+ for (const [name, body] of Object.entries(SCAFFOLDS)) {
70
+ const target = path.join(dir, name);
71
+ if (!fs.existsSync(target)) {
72
+ fs.writeFileSync(target, body, 'utf8');
73
+ created.push(name);
74
+ }
75
+ }
76
+ return created;
77
+ }
78
+
79
+ /** Create only missing files. Never overwrite an existing table. */
80
+ export function ensureScaffolds({ home = DEFAULT_HOME, stateFile = null } = {}) {
81
+ const paths = bootstrapPaths(home);
82
+ const file = stateFile ?? paths.stateFile;
83
+ const state = readState(file);
84
+ if (state?.status === 'done') return [];
85
+ fs.mkdirSync(paths.dir, { recursive: true });
86
+ const created = [];
87
+ for (const [name, body] of Object.entries(SCAFFOLDS)) {
88
+ const target = path.join(paths.dir, name);
89
+ if (!fs.existsSync(target)) {
90
+ fs.writeFileSync(target, body, 'utf8');
91
+ created.push(name);
92
+ }
93
+ }
94
+ return created;
95
+ }
96
+
97
+ function markerNeedles(marker) {
98
+ const needles = [marker];
99
+ const uuid = String(marker).split(':').pop();
100
+ if (uuid && uuid !== marker) needles.push(uuid);
101
+ return needles;
102
+ }
103
+
104
+ function hasMarker(messages = [], marker = MARKER) {
105
+ const needles = markerNeedles(marker);
106
+ for (const message of messages ?? []) {
107
+ const content = message?.content;
108
+ const text = Array.isArray(content)
109
+ ? content.map((part) => part?.type === 'text' ? part.text : '').join('')
110
+ : String(content ?? '');
111
+ if (needles.some((needle) => text.includes(needle))) return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function sessionHasMarker(session, marker = MARKER) {
117
+ if (!session) return false;
118
+ try {
119
+ if (typeof session.deriveMessages === 'function' && hasMarker(session.deriveMessages(), marker)) return true;
120
+ } catch { /* fall through */ }
121
+ const nodes = Array.isArray(session.surface?.nodes) ? session.surface.nodes : [];
122
+ for (const seq of nodes.slice(-80)) {
123
+ try {
124
+ const event = session.eventAt?.(seq);
125
+ const message = event?.data?.message;
126
+ if (hasMarker([message], marker)) return true;
127
+ } catch { /* ignore */ }
128
+ }
129
+ return false;
130
+ }
131
+
132
+ /**
133
+ * Call once per pre-step before the normal note.
134
+ * Returns true when onboarding was appended this step.
135
+ */
136
+ export function maybeOnboard({ session = null, messages = [], home = DEFAULT_HOME, stateFile = null } = {}) {
137
+ const paths = bootstrapPaths(home);
138
+ const file = stateFile ?? paths.stateFile;
139
+ let state = readState(file);
140
+ if (state?.status === 'done') return false;
141
+ if (!state) state = { version: 1, status: 'new', retries: 0 };
142
+ if (!state.marker) state.marker = `${MARKER}:v1:${randomUUID()}`;
143
+ if (hasMarker(messages, state.marker) || sessionHasMarker(session, state.marker)) {
144
+ writeState(file, { ...state, status: 'done', completedAt: new Date().toISOString(), retries: state.retries ?? 0 });
145
+ return false;
146
+ }
147
+ if ((state.retries ?? 0) >= 3) {
148
+ writeState(file, { ...state, status: 'failed', completedAt: new Date().toISOString() });
149
+ return false;
150
+ }
151
+ ensureScaffolds({ home, stateFile: file });
152
+ ensureSessionScaffolds({ sessionId: session?.id, home });
153
+ if (!session || typeof session.append !== 'function') return false;
154
+ session.append('user/message', contextMessage(onboardingText(state.marker)), { surfaceOp: 'append' });
155
+ writeState(file, {
156
+ ...state,
157
+ status: 'pending',
158
+ injectedAt: new Date().toISOString(),
159
+ retries: (state.retries ?? 0) + 1,
160
+ scaffolds: ['concepts.md', 'cmap.md', 'keywords.md', 'aliases.md', 'rebuild.md'],
161
+ });
162
+ return true;
163
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Persona namespace: vocabulary/rules/habits are persona-level, while index,
3
+ * audit state, and hints stay session-level.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const HOME = () => process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
10
+
11
+ export function continuityRoot(home = HOME()) {
12
+ return path.join(home, '.dsh', 'continuity');
13
+ }
14
+
15
+ export function sessionPersonaPath(home = HOME()) {
16
+ return path.join(continuityRoot(home), 'session-persona.json');
17
+ }
18
+
19
+ export function personaDir(personaId, home = HOME()) {
20
+ return path.join(continuityRoot(home), 'personas', String(personaId || 'default'));
21
+ }
22
+
23
+ export function sessionDir(sessionId, home = HOME()) {
24
+ return path.join(continuityRoot(home), 'sessions', String(sessionId));
25
+ }
26
+
27
+ export function sessionFile(sessionId, name, home = HOME()) {
28
+ return path.join(sessionDir(sessionId, home), name);
29
+ }
30
+
31
+ /** Session vocabulary is the source of truth; no persona/global fallback. */
32
+ export function resolveSessionVocabFile(sessionId, name, home = HOME()) {
33
+ return sessionFile(sessionId, name, home);
34
+ }
35
+
36
+ export function loadSessionPersonas(home = HOME()) {
37
+ try {
38
+ const raw = fs.readFileSync(sessionPersonaPath(home), 'utf8').replace(/^\uFEFF/, '');
39
+ const parsed = JSON.parse(raw);
40
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ export function resolvePersona(sessionId, home = HOME()) {
47
+ const map = loadSessionPersonas(home);
48
+ if (sessionId && typeof map[String(sessionId)] === 'string' && map[String(sessionId)]) {
49
+ return map[String(sessionId)];
50
+ }
51
+ if (typeof map.default === 'string' && map.default) return map.default;
52
+ return 'continuity';
53
+ }
54
+
55
+ export function personaFile(personaId, name, home = HOME()) {
56
+ return path.join(personaDir(personaId, home), name);
57
+ }
58
+
59
+ /** Per-persona enable switch; continuity stays enabled for old installs. */
60
+ export function isPersonaEnabled(personaId, home = HOME()) {
61
+ const id = String(personaId || 'default');
62
+ const file = personaFile(id, 'persona.json', home);
63
+ try {
64
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
65
+ return parsed?.enabled !== false;
66
+ } catch {
67
+ return id === 'continuity';
68
+ }
69
+ }
70
+
71
+ /** Persona files win; only continuity may fall back to the archived global table. */
72
+ export function resolveVocabFile(personaId, name, home = HOME(), { allowLegacy = personaId === 'continuity' } = {}) {
73
+ const primary = personaFile(personaId, name, home);
74
+ if (fs.existsSync(primary)) return primary;
75
+ if (!allowLegacy) return primary;
76
+ const legacy = path.join(continuityRoot(home), name);
77
+ return fs.existsSync(legacy) ? legacy : primary;
78
+ }