@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/index.mjs ADDED
@@ -0,0 +1,649 @@
1
+ /**
2
+ * dsh-continuity — Host half.
3
+ *
4
+ * This row is the plugin's visible face: it serves the small JSON snapshot the
5
+ * left-sidebar panel reads. It does not replace `ctx.compaction`; the engine is
6
+ * mounted inside the `continuity` agent preset (see cordis.patch.yml), where the
7
+ * one compaction service per context can be replaced safely.
8
+ *
9
+ * The snapshot is diagnostic only: policy values, the last recorded passes,
10
+ * the provenance convention, the measured runtime API facts, and a projection
11
+ * of the config-only experiment for the configured model route.
12
+ */
13
+ import { createRequire } from 'node:module';
14
+ import { appendFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { DEFAULT_POLICY } from './policy.mjs';
17
+ import { SYSTEM_CONVENTION, gistHeader } from './provenance.mjs';
18
+ import { continuityState, rebuildDroppedRanges } from './state.mjs';
19
+ import { buildFacetState } from './facets/panel-state.mjs';
20
+ import { PEER_PATH, handlePeerRequest, peerAgents } from './peer.mjs';
21
+ import { migrateThreadsFile } from './threads-store.mjs';
22
+ import { installExtractionPreStep, recordExtractionInstallError } from './memory/extraction-pre-step.mjs';
23
+ import { installBCPreStep } from './memory/bc-pre-step.mjs';
24
+ import { buildBCPanelState } from './memory/bc-panel-state.mjs';
25
+ import { isPersonaEnabled, resolvePersona, sessionFile } from './memory/persona.mjs';
26
+ import { installSessionGuard } from './session-guard.mjs';
27
+ import { createRecallExactTool } from './recall-exact-tool.mjs';
28
+
29
+ export const name = 'dsh-continuity';
30
+ export const inject = [];
31
+
32
+ const require = createRequire(import.meta.url);
33
+ let pkg = {};
34
+ try {
35
+ pkg = require('../package.json');
36
+ } catch {
37
+ pkg = {};
38
+ }
39
+
40
+ const DEFAULT_PATH = '/dsh-continuity/state.json';
41
+ /** The shipped compaction-basic default; the quick-win config also keeps it. */
42
+ const HEADROOM_TOKENS = 65_536;
43
+ /** Adapter defaults verified in the installed `dsh-llm-deepseek` source. */
44
+ const FALLBACK_WINDOW = { contextWindow: 1_000_000, defaultMaxTokens: 256_000 };
45
+
46
+ /** Measured at runtime through `cordis_inspect_query` on the live 0.2.1-rc.3 Host. */
47
+ const FACTS = [
48
+ {
49
+ label: 'compaction row (host)',
50
+ value: 'include:compaction-basic · patchId compaction-basic · enabled=false in the desktop profile',
51
+ },
52
+ {
53
+ label: 'compaction row (live)',
54
+ value: 'include:preset-standard → group compaction (isolate: compaction, toolResultPruner) → child compaction-basic',
55
+ },
56
+ {
57
+ label: 'token meter',
58
+ value: 'ctx.tokenMeter.measure(session, requestHeader?) → { totalTokens, surfaceTokens, nodes[{ seq, tokens, heuristicTokens }], baseline, logRevision }',
59
+ },
60
+ {
61
+ label: 'session surface',
62
+ value: 'session.surface.nodes: readonly SessionSeq[] · session.eventAt(seq) · session.deriveEventMessage(event) · session.requestHeader()',
63
+ },
64
+ {
65
+ label: 'model window',
66
+ value: 'ctx.llm.resolveModelInfo(provider, model) → { context.contextWindow, defaultMaxTokens }',
67
+ },
68
+ {
69
+ label: 'summary result',
70
+ value: 'summarize() → { summary: ContentBlock[], provider, model, maxTokens, usage? }',
71
+ },
72
+ {
73
+ label: 'pairing predicates',
74
+ value: 'toolPairingBalancedBefore(session, seq) · toolPairingBalancedAfter(session, seq)',
75
+ },
76
+ {
77
+ label: 'system section',
78
+ value: 'ctx.systemPrompt.section({ name, order, text, interpolate })',
79
+ },
80
+ {
81
+ label: 'retrieval tool',
82
+ value: 'session_log index / get / grep / stats → readSession + live tokenMeter · read-only · preset-scoped',
83
+ },
84
+ {
85
+ label: 'thread tool',
86
+ value: 'continuity save / load / active / list → validated surface range + append-only [thread] gist · preset-scoped',
87
+ },
88
+ {
89
+ label: 'thread injection',
90
+ value: 'per-turn user/message after the surface tail (active line + user rules.md) · never in the system/tool prefix',
91
+ },
92
+ ];
93
+
94
+ export function apply(ctx, config = {}) {
95
+ const log = ctx.logger ?? console;
96
+ try { rebuildDroppedRanges(); } catch (error) {
97
+ try { log.warn?.(`dsh-continuity: dropped-ranges rebuild skipped: ${error?.message ?? error}`); } catch { /* ignore */ }
98
+ }
99
+ try { installSessionGuard(ctx); } catch (error) {
100
+ try { log.warn?.(`dsh-continuity: session guard not installed: ${error?.message ?? error}`); } catch { /* ignore */ }
101
+ }
102
+ // One-shot: old backfill rows stored the write moment in `at`; normalize the
103
+ // file too, not only the in-memory view, so direct threads.json readers agree.
104
+ try {
105
+ void migrateThreadsFile({ log }).catch((error) => {
106
+ try { log.warn?.(`[dsh-continuity] threads normalization failed: ${error?.message ?? error}`); } catch { /* ignore */ }
107
+ });
108
+ } catch (error) {
109
+ try { log.warn?.(`[dsh-continuity] threads normalization failed: ${error?.message ?? error}`); } catch { /* ignore */ }
110
+ }
111
+ // Target-addressed peer delivery. An explicit `to` session id is required.
112
+ try {
113
+ ctx.on('agent/created', (payload = {}) => {
114
+ const agent = payload?.agent;
115
+ peerAgents.add(agent?.session?.id, agent);
116
+ });
117
+ ctx.on('session/disposed', (session = {}) => {
118
+ peerAgents.remove(session?.id);
119
+ });
120
+ } catch (error) {
121
+ try {
122
+ appendFileSync(join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity', 'gate-errors.jsonl'), JSON.stringify({
123
+ at: new Date().toISOString(), name: error?.name ?? 'Error', code: 'peer-registry-failed',
124
+ message: String(error?.message ?? error),
125
+ }) + '\n', 'utf8');
126
+ } catch { /* registry failure is reported by the route's 404s */ }
127
+ }
128
+ const peerRoute = {
129
+ kind: 'exact',
130
+ path: PEER_PATH,
131
+ handler: (req, res) => handlePeerRequest(req, res, { agents: peerAgents, log }),
132
+ };
133
+ ctx.inject(['webServer'], (scoped) => {
134
+ scoped.effect(() => scoped.webServer.register(peerRoute));
135
+ scoped.logger?.info?.(`[dsh-continuity] peer route mounted at ${PEER_PATH}`);
136
+ });
137
+ // recall_exact is a global read-only tool, not a preset-only feature.
138
+ ctx.inject(['tools', 'sessionQuery'], (scoped) => {
139
+ const tool = createRecallExactTool({ sessionQuery: scoped.sessionQuery });
140
+ scoped.effect(() => scoped.tools.register(tool));
141
+ });
142
+ // Extraction is opt-in by config or environment. Default off means this
143
+ // listener is never registered, so the normal turn path is byte-for-byte unchanged.
144
+ try {
145
+ const extraction = config.extraction ?? {};
146
+ const extractionEnv = { ...process.env, ...(extraction.env ?? {}) };
147
+ if (typeof extraction.mode === 'string') extractionEnv.DSH_EXTRACTION_MODE = extraction.mode;
148
+ if (typeof extraction.config === 'string') extractionEnv.DSH_EXTRACTION_CONFIG = extraction.config;
149
+ if (typeof extraction.cache === 'string') extractionEnv.DSH_EXTRACTION_CACHE = extraction.cache;
150
+ const sessionQuery = typeof ctx.get === 'function' ? ctx.get('sessionQuery') : ctx.sessionQuery;
151
+ const installed = installExtractionPreStep(ctx, extractionEnv, { sessionQuery });
152
+ if (installed.registered) log.info?.(`[dsh-continuity] extraction pre-step registered (${installed.mode})`);
153
+ else if (installed.error) recordExtractionInstallError(installed.error, { home: process.env.USERPROFILE ?? process.env.HOME ?? '', log });
154
+ } catch (error) {
155
+ try {
156
+ appendFileSync(join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity', 'gate-errors.jsonl'), JSON.stringify({
157
+ at: new Date().toISOString(), name: error?.name ?? 'Error', code: 'extraction-pre-step-register',
158
+ message: String(error?.message ?? error),
159
+ }) + '\n', 'utf8');
160
+ } catch { /* registration failure must not block the plugin */ }
161
+ }
162
+ try {
163
+ const bc = config.bcRecall ?? {};
164
+ if (bc.enabled === true) {
165
+ const installed = installBCPreStep(ctx, {
166
+ enabled: true,
167
+ storePath: typeof bc.store === 'string' ? bc.store : null,
168
+ prefix: typeof bc.prefix === 'string' ? bc.prefix : '',
169
+ sessionQuery: typeof ctx.get === 'function' ? ctx.get('sessionQuery') : ctx.sessionQuery,
170
+ });
171
+ if (installed.registered) log.info?.(`[dsh-continuity] B+C pre-step registered (${installed.records} records) indexUpdater=${installed.indexUpdater} sessionQuery=${installed.sessionQuery} store=${installed.store}`);
172
+ } else {
173
+ log.info?.('[dsh-continuity] B+C pre-step disabled by config');
174
+ }
175
+ } catch (error) {
176
+ try {
177
+ appendFileSync(join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity', 'gate-errors.jsonl'), JSON.stringify({
178
+ at: new Date().toISOString(), name: error?.name ?? 'Error', code: 'bc-pre-step-register',
179
+ message: String(error?.message ?? error),
180
+ }) + '\n', 'utf8');
181
+ } catch { /* registration failure must not block the plugin */ }
182
+ }
183
+ // First history-map injection must not wait for compression pressure.
184
+ try {
185
+ ctx.on('agent/pre-step', async (payload, next) => {
186
+ try {
187
+ const engine = continuityState.engineInstance;
188
+ if (engine !== null && typeof engine?.ensureHistoryMap === 'function') {
189
+ await engine.ensureHistoryMap(payload?.agent);
190
+ }
191
+ } catch (error) {
192
+ try {
193
+ log.warn?.(`[dsh-continuity] history-map bootstrap skipped: ${error?.message ?? error}`);
194
+ } catch { /* logging only */ }
195
+ }
196
+ return next();
197
+ });
198
+ } catch (error) {
199
+ try {
200
+ log.warn?.(`[dsh-continuity] history-map pre-step registration failed: ${error?.message ?? error}`);
201
+ } catch { /* ignore */ }
202
+ }
203
+ // 回合闸注册在这里,不在 engine 里:engine 是挂在 agent preset 里的 compaction service,
204
+ // 它自己的 ctx 接不到 agent/* 事件(2026-10-30 实测:重启后 turn-stopping 一次都没触发,
205
+ // 也没有注册错误——引擎那边是可选链,ctx 不对就静默跳过)。插件入口的 ctx 一定在。
206
+ const hookHome = join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity');
207
+ if (config.safeMode === true) {
208
+ log.info?.('[dsh-continuity] safe mode: turn-stopping hook not registered');
209
+ } else try {
210
+ ctx.on('agent/turn-stopping', (payload) => {
211
+ // Dispatch probe: write this before the gate call so a null/foreign
212
+ // engine is distinguishable from "the Host never dispatched".
213
+ const turn = Number.isInteger(payload?.turn) ? payload.turn : null;
214
+ const engine = continuityState.engineInstance;
215
+ const engineCallable = typeof engine?.onTurnStopping === 'function';
216
+ const agentSession = typeof payload?.agent?.session?.id === 'string' ? payload.agent.session.id : null;
217
+ const continuitySession = continuityState.engineSession ?? null;
218
+ const foreignSession = Boolean(agentSession && continuitySession && agentSession !== continuitySession);
219
+ try {
220
+ appendFileSync(join(hookHome, 'dispatch-probe.jsonl'),
221
+ JSON.stringify({
222
+ at: new Date().toISOString(),
223
+ event: 'agent/turn-stopping',
224
+ turn,
225
+ enginePresent: Boolean(engine),
226
+ engineCallable,
227
+ agentSession,
228
+ continuitySession,
229
+ foreignSession,
230
+ }) + '\n', 'utf8');
231
+ } catch { /* probe failure must not block the gate */ }
232
+ if (engineCallable) {
233
+ void engine.onTurnStopping(payload);
234
+ }
235
+ });
236
+ try {
237
+ writeFileSync(join(hookHome, 'hook-registered.json'),
238
+ JSON.stringify({ at: new Date().toISOString(), via: 'index.apply', event: 'agent/turn-stopping' }, null, 2), 'utf8');
239
+ } catch { /* 探针写不进不影响注册 */ }
240
+ } catch (error) {
241
+ try {
242
+ appendFileSync(join(hookHome, 'gate-errors.jsonl'), JSON.stringify({
243
+ at: new Date().toISOString(), name: error?.name ?? 'Error', code: 'hook-register-failed',
244
+ message: String(error?.message ?? error),
245
+ }) + '\n', 'utf8');
246
+ } catch { /* 只能放弃 */ }
247
+ }
248
+ const panel = config.panel ?? {};
249
+ if (panel.enabled === false) {
250
+ log.info?.('[dsh-continuity] panel disabled by config; no route registered');
251
+ return;
252
+ }
253
+
254
+ const path = typeof panel.path === 'string' && panel.path.startsWith('/') ? panel.path : DEFAULT_PATH;
255
+ const projectionConfig = {
256
+ provider: 'deepseek-official',
257
+ model: 'deepseek-flash',
258
+ ...(config.projection ?? {}),
259
+ };
260
+ const cache = { key: '', at: 0, value: null };
261
+ const scopeCache = { at: 0, value: [] };
262
+ const queryCache = { at: 0, value: null };
263
+
264
+ const route = {
265
+ kind: 'exact',
266
+ path,
267
+ handler: async (req, res) => {
268
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
269
+ res.writeHead(405, { allow: 'GET, HEAD' });
270
+ res.end();
271
+ return;
272
+ }
273
+ let payload;
274
+ try {
275
+ payload = await buildSnapshot(ctx, path, projectionConfig, cache, scopeCache, queryCache);
276
+ } catch (error) {
277
+ payload = { error: String(error?.message ?? error), generatedAt: new Date().toISOString() };
278
+ }
279
+ const text = JSON.stringify(payload, null, 2);
280
+ res.writeHead(200, {
281
+ 'content-type': 'application/json; charset=utf-8',
282
+ 'cache-control': 'no-store',
283
+ 'content-length': Buffer.byteLength(text),
284
+ });
285
+ if (req.method === 'HEAD') res.end();
286
+ else res.end(text);
287
+ },
288
+ };
289
+
290
+ // Do not read `ctx.get('webServer')` here: on a cold start this row may
291
+ // activate before the Web server publishes its service, and the early return
292
+ // would leave the row active with no route (the exact bug seen after a
293
+ // restart). `ctx.inject` registers the route as soon as the service is up,
294
+ // and disposes it with the plugin.
295
+ // Legacy `/dsh-continuity/tags.json` is intentionally not registered.
296
+ // The safe UI reads B/C/K from `/dsh-continuity/bc.json` only.
297
+
298
+ const bcPath = '/dsh-continuity/bc.json';
299
+ const requestSessionId = (req) => {
300
+ try {
301
+ const url = new URL(req.url ?? '/', 'http://localhost');
302
+ const query = url.searchParams.get('sessionId');
303
+ if (query) return query;
304
+ } catch { /* ignore */ }
305
+ const referer = String(req.headers?.referer ?? '');
306
+ const match = referer.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
307
+ return match ? match[0] : null;
308
+ };
309
+ const bcRoute = {
310
+ kind: 'exact',
311
+ path: bcPath,
312
+ handler: async (req, res) => {
313
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
314
+ res.writeHead(405, { allow: 'GET, HEAD' });
315
+ res.end();
316
+ return;
317
+ }
318
+ let payload;
319
+ try {
320
+ const sessionId = requestSessionId(req);
321
+ const home = process.env.USERPROFILE ?? process.env.HOME;
322
+ if (!sessionId) {
323
+ payload = { available: false, error: 'session-id-required', generatedAt: new Date().toISOString() };
324
+ } else {
325
+ const personaId = resolvePersona(sessionId, home);
326
+ if (!isPersonaEnabled(personaId, home)) {
327
+ payload = { available: false, error: 'persona-disabled', personaId, generatedAt: new Date().toISOString() };
328
+ } else {
329
+ const sessionDir = join(home, '.dsh', 'continuity', 'sessions', sessionId);
330
+ payload = buildBCPanelState({
331
+ file: process.env.DSH_BC_INDEX ?? join(sessionDir, 'bc-index.jsonl'),
332
+ home,
333
+ conceptsFile: sessionFile(sessionId, 'concepts.md', home),
334
+ keywordsFile: sessionFile(sessionId, 'keywords.md', home),
335
+ cmapFile: sessionFile(sessionId, 'cmap.md', home),
336
+ });
337
+ }
338
+ }
339
+ } catch (error) {
340
+ payload = { available: false, error: String(error?.message ?? error), generatedAt: new Date().toISOString() };
341
+ }
342
+ const text = JSON.stringify(payload, null, 2);
343
+ res.writeHead(200, {
344
+ 'content-type': 'application/json; charset=utf-8',
345
+ 'cache-control': 'no-store',
346
+ 'content-length': Buffer.byteLength(text),
347
+ });
348
+ if (req.method === 'HEAD') res.end();
349
+ else res.end(text);
350
+ },
351
+ };
352
+
353
+ const facetsPath = '/dsh-continuity/facets.json';
354
+ const facetsRoute = {
355
+ kind: 'exact',
356
+ path: facetsPath,
357
+ handler: async (req, res) => {
358
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
359
+ res.writeHead(405, { allow: 'GET, HEAD' });
360
+ res.end();
361
+ return;
362
+ }
363
+ let payload;
364
+ try {
365
+ payload = buildFacetState();
366
+ } catch (error) {
367
+ payload = { available: false, error: String(error?.message ?? error), generatedAt: new Date().toISOString() };
368
+ }
369
+ const text = JSON.stringify(payload, null, 2);
370
+ res.writeHead(200, {
371
+ 'content-type': 'application/json; charset=utf-8',
372
+ 'cache-control': 'no-store',
373
+ 'content-length': Buffer.byteLength(text),
374
+ });
375
+ if (req.method === 'HEAD') res.end();
376
+ else res.end(text);
377
+ },
378
+ };
379
+
380
+ const switchPath = '/dsh-continuity/switch-to-continuity';
381
+ const readJsonBody = async (req) => {
382
+ let raw = '';
383
+ for await (const chunk of req) {
384
+ raw += chunk;
385
+ if (raw.length > 100_000) throw new Error('request body too large');
386
+ }
387
+ return raw.trim() === '' ? {} : JSON.parse(raw);
388
+ };
389
+ const switchRoute = {
390
+ kind: 'exact',
391
+ path: switchPath,
392
+ handler: async (req, res) => {
393
+ const send = (status, payload) => {
394
+ const text = JSON.stringify(payload, null, 2);
395
+ res.writeHead(status, {
396
+ 'content-type': 'application/json; charset=utf-8',
397
+ 'cache-control': 'no-store',
398
+ 'content-length': Buffer.byteLength(text),
399
+ });
400
+ res.end(text);
401
+ };
402
+ if (req.method !== 'POST') {
403
+ res.writeHead(405, { allow: 'POST' });
404
+ res.end();
405
+ return;
406
+ }
407
+ try {
408
+ const body = await readJsonBody(req);
409
+ const sessionId = String(body?.sessionId ?? '').trim();
410
+ if (!sessionId) return send(400, { ok: false, error: 'sessionId required' });
411
+ const registry = ctx.get('agents');
412
+ const presets = ctx.get('agentPresets');
413
+ if (!registry || typeof registry.list !== 'function') return send(503, { ok: false, error: 'agents service unavailable' });
414
+ if (!presets || typeof presets.select !== 'function') return send(503, { ok: false, error: 'agentPresets service unavailable' });
415
+ const agents = registry.list();
416
+ const agent = agents.find((item) => String(item?.session?.id) === sessionId)
417
+ ?? agents.find((item) => String(item?.id) === sessionId);
418
+ if (!agent) return send(404, { ok: false, error: 'live session not found', sessionId });
419
+ const before = agent?.session?.header?.agentPreset ?? null;
420
+ const after = await presets.select(agent, 'continuity');
421
+ return send(200, { ok: true, sessionId, before, after });
422
+ } catch (error) {
423
+ return send(500, { ok: false, error: String(error?.message ?? error) });
424
+ }
425
+ },
426
+ };
427
+
428
+ ctx.inject(['webServer'], (scoped) => {
429
+ scoped.effect(() => scoped.webServer.register(route));
430
+ scoped.effect(() => scoped.webServer.register(bcRoute));
431
+ scoped.effect(() => scoped.webServer.register(facetsRoute));
432
+ scoped.effect(() => scoped.webServer.register(switchRoute));
433
+ scoped.logger?.info?.(`[dsh-continuity] control panel mounted at ${path} (bc: ${bcPath}, facets: ${facetsPath})`);
434
+ });
435
+ }
436
+
437
+ async function buildSnapshot(ctx, path, projectionConfig, cache, scopeCache, queryCache) {
438
+ const engine = continuityState.engine;
439
+ const policy = engine?.policy ?? DEFAULT_POLICY;
440
+ const presets = await listPresets(ctx);
441
+ const projection = await projectWindow(ctx, projectionConfig, cache, policy);
442
+ const scopes = await inspectScopes(ctx, scopeCache);
443
+ const sessionQuery = await probeSessionQuery(ctx, queryCache);
444
+
445
+ return {
446
+ plugin: { name: pkg.name ?? 'dsh-continuity', version: pkg.version ?? '0.0.0' },
447
+ generatedAt: new Date().toISOString(),
448
+ panel: { path },
449
+ engine: engine === null ? null : {
450
+ mountedAt: engine.mountedAt,
451
+ policy: engine.policy,
452
+ provenance: engine.provenance,
453
+ compactionBasic: engine.compactionBasic,
454
+ base: engine.base,
455
+ },
456
+ passes: continuityState.passes,
457
+ context: continuityState.context,
458
+ previews: continuityState.previews,
459
+ lastError: continuityState.lastError,
460
+ presets,
461
+ tools: {
462
+ sessionLog: continuityState.sessionLogTool,
463
+ threads: continuityState.threadsTool,
464
+ threadsState: continuityState.threads,
465
+ scopes,
466
+ },
467
+ sessionQuery,
468
+ provenance: {
469
+ convention: SYSTEM_CONVENTION,
470
+ sample: gistHeader({ sessionId: 'session-…', startSeq: 120, endSeq: 154, count: 34 }),
471
+ labels: ['[verbatim]', '[gist]', '[uncertain]', '[open]'],
472
+ },
473
+ projection,
474
+ facts: FACTS,
475
+ };
476
+ }
477
+
478
+ /** Current preset: the `agentPreset` projection, falling back to the creation header. */
479
+ function currentPresetOf(agent, projections) {
480
+ const headerPreset = agent?.session?.header?.agentPreset ?? null;
481
+ try {
482
+ const projected = projections?.stateOf?.(agent.session, 'agentPreset');
483
+ if (projected !== undefined) return projected;
484
+ } catch {
485
+ return headerPreset;
486
+ }
487
+ return headerPreset;
488
+ }
489
+
490
+ /**
491
+ * Verify the exact-read Host API used by `session_log` before the tool row is
492
+ * mounted: read the live Continuity session through `ctx.sessionQuery`.
493
+ * Cached; this is a diagnostic, not a per-poll replay.
494
+ */
495
+ async function probeSessionQuery(ctx, cache) {
496
+ if (Date.now() - cache.at < 30_000) return cache.value;
497
+ const query = ctx.get('sessionQuery');
498
+ if (query === undefined) {
499
+ cache.value = { available: false, reason: 'sessionQuery service unavailable' };
500
+ cache.at = Date.now();
501
+ return cache.value;
502
+ }
503
+ const projections = ctx.get('sessionProjections');
504
+ const agents = ctx.get('agents')?.list?.() ?? [];
505
+ const continuity = agents.find((agent) => currentPresetOf(agent, projections) === 'continuity');
506
+ if (continuity === undefined) {
507
+ cache.value = { available: true, sessionId: null, reason: 'no live continuity agent' };
508
+ cache.at = Date.now();
509
+ return cache.value;
510
+ }
511
+ const sessionId = String(continuity.session.id);
512
+ try {
513
+ const snapshot = await query.readSession(continuity.session.id);
514
+ const events = Array.isArray(snapshot?.events) ? snapshot.events : [];
515
+ cache.value = {
516
+ available: true,
517
+ source: 'ctx.sessionQuery.readSession',
518
+ sessionId,
519
+ events: events.length,
520
+ firstSeq: events[0]?.seq ?? null,
521
+ lastSeq: events.at(-1)?.seq ?? null,
522
+ };
523
+ } catch (error) {
524
+ cache.value = {
525
+ available: true,
526
+ source: 'ctx.sessionQuery.readSession',
527
+ sessionId,
528
+ error: String(error?.message ?? error),
529
+ };
530
+ }
531
+ cache.at = Date.now();
532
+ return cache.value;
533
+ }
534
+
535
+ /**
536
+ * Verify the preset-scoped capability without asking the model: which live
537
+ * agents see `session_log`, and which see the convention section. Cached: the
538
+ * prompt assembly is the expensive part and the panel polls every few seconds.
539
+ */
540
+ async function inspectScopes(ctx, cache) {
541
+ if (Date.now() - cache.at < 15_000) return cache.value;
542
+ const tools = ctx.get('tools');
543
+ const prompts = ctx.get('systemPrompt');
544
+ const projections = ctx.get('sessionProjections');
545
+ const agents = ctx.get('agents')?.list?.() ?? [];
546
+ const value = [];
547
+ for (const agent of agents.slice(0, 8)) {
548
+ const sessionId = agent?.id === undefined ? null : String(agent.id);
549
+ const headerPreset = agent?.session?.header?.agentPreset ?? null;
550
+ const preset = currentPresetOf(agent, projections);
551
+ const toolNames = tools?.schemas?.(agent)?.map((tool) => tool.name) ?? [];
552
+ let convention = null;
553
+ if (preset === 'continuity' && prompts !== undefined) {
554
+ try {
555
+ const assembly = await prompts.assemble({ scope: agent });
556
+ convention = (assembly?.sections ?? []).some((section) => section.name === 'continuity:provenance');
557
+ } catch {
558
+ convention = null;
559
+ }
560
+ }
561
+ value.push({
562
+ sessionId,
563
+ preset,
564
+ presetHeader: headerPreset,
565
+ sessionLog: toolNames.includes('session_log'),
566
+ threads: toolNames.includes('continuity'),
567
+ convention,
568
+ toolCount: toolNames.length,
569
+ });
570
+ }
571
+ cache.at = Date.now();
572
+ cache.value = value;
573
+ return value;
574
+ }
575
+
576
+ /** Read the roster if the service is present; a missing roster is not an error. */
577
+ async function listPresets(ctx) {
578
+ const service = ctx.get('agentPresets');
579
+ if (service === undefined) return [];
580
+ try {
581
+ const all = await service.list();
582
+ return (all ?? [])
583
+ .map((entry) => ({
584
+ id: entry?.id ?? entry?.definition?.id ?? null,
585
+ name: entry?.name ?? entry?.definition?.name ?? entry?.id ?? null,
586
+ order: entry?.order ?? entry?.definition?.order ?? null,
587
+ }))
588
+ .filter((entry) => typeof entry.id === 'string' && ['band', 'continuity', 'standard'].includes(entry.id))
589
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
590
+ } catch {
591
+ return [];
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Project the shipped compaction-basic policy for one route. This is the
597
+ * arithmetic the config-only experiment turns on; the live request header may
598
+ * reserve fewer completion tokens than the adapter default, which lowers the
599
+ * threshold fraction but never the retention below its ratio.
600
+ */
601
+ async function projectWindow(ctx, config, cache, policy) {
602
+ const key = `${config.provider}/${config.model}`;
603
+ if (cache.key === key && Date.now() - cache.at < 60_000) return cache.value;
604
+
605
+ let value;
606
+ const llm = ctx.get('llm');
607
+ try {
608
+ if (llm === undefined) throw new Error('llm service unavailable');
609
+ const info = await llm.resolveModelInfo(config.provider, config.model);
610
+ const contextWindow = Number.isInteger(info?.context?.contextWindow)
611
+ ? info.context.contextWindow
612
+ : FALLBACK_WINDOW.contextWindow;
613
+ const reservedTokens = Number.isInteger(info?.defaultMaxTokens)
614
+ ? info.defaultMaxTokens
615
+ : FALLBACK_WINDOW.defaultMaxTokens;
616
+ value = project(contextWindow, reservedTokens, policy);
617
+ value.source = 'ctx.llm.resolveModelInfo';
618
+ } catch (error) {
619
+ value = project(FALLBACK_WINDOW.contextWindow, FALLBACK_WINDOW.defaultMaxTokens, policy);
620
+ value.source = `adapter defaults (${error?.message ?? error})`;
621
+ }
622
+ value.provider = config.provider;
623
+ value.model = config.model;
624
+ cache.key = key;
625
+ cache.at = Date.now();
626
+ cache.value = value;
627
+ return value;
628
+ }
629
+
630
+ function project(contextWindow, reservedTokens, policy) {
631
+ const messageBudget = contextWindow - reservedTokens;
632
+ const baseThreshold = Math.floor(Math.min(contextWindow * 0.92, messageBudget - HEADROOM_TOKENS));
633
+ const baseRetain = Math.floor(messageBudget * 0.5);
634
+ return {
635
+ available: true,
636
+ contextWindow,
637
+ reservedTokens,
638
+ messageBudget,
639
+ headroomTokens: HEADROOM_TOKENS,
640
+ baseThresholdRatio: 0.92,
641
+ baseRetainRatio: 0.5,
642
+ baseThresholdTokens: baseThreshold,
643
+ baseRetainTokens: baseRetain,
644
+ basePassTokens: Math.max(0, baseThreshold - baseRetain),
645
+ baseValid: baseRetain < baseThreshold,
646
+ bandSetpointTokens: Math.floor(messageBudget * policy.setpoint),
647
+ bandProtectTokens: Math.floor(messageBudget * policy.protect),
648
+ };
649
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 注入判定(纯函数):什么时候需要把“必须一直可见”的正文补注入。
3
+ * 判据不是“注入过没有”,而是“承载它的消息现在还在不在 surface 里”。
4
+ */
5
+
6
+ /** 规则全文:内容变了、没有承载 seq,或承载 seq 已不在 surface → 需要注入。 */
7
+ export function needsFullInjection({ text, carrierSeq, surfaceSeqs, changed = false }) {
8
+ if (!text || text.length === 0) return false;
9
+ if (changed === true) return true;
10
+ if (carrierSeq === undefined || carrierSeq === null) return true;
11
+ return !surfaceSeqs.has(carrierSeq);
12
+ }
13
+
14
+ /** 技能正文:只在技能集合变化时注入;之后只保留 `[技能] 正在用` 一行。 */
15
+ export function needsSkillInjection({ keyChanged } = {}) {
16
+ return keyChanged === true;
17
+ }