@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,606 @@
1
+ /**
2
+ * Read-only retrieval over one session's raw event log.
3
+ *
4
+ * Pure functions: the tool layer supplies the `SessionEvent[]` from
5
+ * `ctx.sessionQuery.readSession()`. Nothing here appends, replaces, projects,
6
+ * or otherwise mutates a session. Tests can feed frozen fixtures.
7
+ *
8
+ * Sequence semantics: `seq` is the session event seq, the same number a
9
+ * `[gist · log:...#seq=A-B]` pointer carries. Surface spans can be
10
+ * non-monotonic (a replacement inserts a new high seq at an old position), so
11
+ * a range that exactly matches a compaction's `shadowedRange` is resolved
12
+ * through that compaction's `shadowedSeqs` instead of a numeric filter.
13
+ */
14
+
15
+ import { assetMarker } from './assets.mjs';
16
+
17
+ export const DEFAULT_LIMIT = 20;
18
+ export const MAX_LIMIT = 100;
19
+ export const DEFAULT_MAX_CHARS = 16_000;
20
+ export const INDEX_MAX_CHARS = 12_000;
21
+ export const GREP_MAX_CHARS = 12_000;
22
+ export const MAX_EVENT_CHARS = 2_400;
23
+ export const MAX_SNIPPET_CHARS = 240;
24
+ export const MAX_CONTEXT = 10;
25
+
26
+ export class SessionLogError extends Error {
27
+ constructor(code, message) {
28
+ super(message);
29
+ this.name = 'SessionLogError';
30
+ this.code = code;
31
+ }
32
+ }
33
+
34
+ const ROLE_BY_TYPE = Object.freeze({
35
+ 'user/message': 'user',
36
+ 'assistant/message': 'assistant',
37
+ 'tool/result': 'tool',
38
+ 'tool/call': 'tool-call',
39
+ 'system/message': 'system',
40
+ 'developer/message': 'developer',
41
+ 'compaction/summary': 'gist',
42
+ 'compaction/start': 'compaction',
43
+ 'compaction/end': 'compaction',
44
+ 'compaction/prune': 'compaction',
45
+ 'turn/start': 'turn',
46
+ 'turn/end': 'turn',
47
+ 'step/start': 'step',
48
+ 'step/end': 'step',
49
+ 'request/header': 'request',
50
+ 'request/context': 'request',
51
+ });
52
+
53
+ /** Clamp to an integer in [min, max]. */
54
+ function clampInt(value, min, max, fallback = min) {
55
+ const number = Number(value);
56
+ if (!Number.isFinite(number)) return fallback;
57
+ return Math.min(max, Math.max(min, Math.trunc(number)));
58
+ }
59
+
60
+ function formatTime(time) {
61
+ const number = Number(time);
62
+ if (!Number.isFinite(number)) return 'time=?';
63
+ try {
64
+ return new Date(number).toISOString();
65
+ } catch {
66
+ return 'time=?';
67
+ }
68
+ }
69
+
70
+ function truncate(text, max) {
71
+ const value = String(text ?? '');
72
+ if (value.length <= max) return value;
73
+ return `${value.slice(0, max)}… [truncated ${value.length - max} chars]`;
74
+ }
75
+
76
+ function oneLine(text, max) {
77
+ return truncate(String(text ?? '').replace(/\s+/g, ' ').trim(), max);
78
+ }
79
+
80
+ function blockText(blocks, options = {}) {
81
+ const includeReasoning = options.includeReasoning !== false;
82
+ const parts = [];
83
+ for (const block of Array.isArray(blocks) ? blocks : []) {
84
+ if (block == null || typeof block !== 'object') continue;
85
+ if (block.type === 'text' && typeof block.text === 'string') parts.push(block.text);
86
+ else if (block.type === 'reasoning' && typeof block.text === 'string' && includeReasoning) parts.push(`[reasoning] ${block.text}`);
87
+ else if (block.type === 'tool-call') parts.push(`[tool-call ${block.name ?? ''}] ${block.arguments ?? ''}`);
88
+ else if (block.type === 'file') parts.push(`[file ${block.attachment?.name ?? ''}]`);
89
+ else if (block.type === 'image') parts.push('[image]');
90
+ }
91
+ return parts.join('\n');
92
+ }
93
+
94
+ /** Model-visible text carried by one event, or '' for marker-only events.
95
+ * `includeReasoning: false` drops reasoning blocks; the scan/load default. */
96
+ export function eventText(event, options = {}) {
97
+ const data = event?.data;
98
+ if (data == null) return '';
99
+ if (Array.isArray(data)) return blockText(data, options);
100
+ if (Array.isArray(data.summary)) return blockText(data.summary, options);
101
+ if (Array.isArray(data.content)) return blockText(data.content, options);
102
+ if (Array.isArray(data.message?.content)) return blockText(data.message.content, options);
103
+ if (typeof data.arguments === 'string') return `${data.name ?? ''} ${data.arguments}`;
104
+ if (typeof data.text === 'string') return data.text;
105
+ return '';
106
+ }
107
+
108
+ /** Short role bucket used in the manifest. */
109
+ export function eventRole(event) {
110
+ return ROLE_BY_TYPE[event?.type] ?? String(event?.type ?? 'event');
111
+ }
112
+
113
+ /** One-line description for marker events; text events return their text. */
114
+ export function eventSummary(event) {
115
+ const data = event?.data ?? {};
116
+ switch (event?.type) {
117
+ case 'turn/start':
118
+ return `turn ${data.turn ?? '?'} start`;
119
+ case 'turn/end':
120
+ return `turn ${data.turn ?? '?'} end ${data.reason?.kind ?? '?'}`;
121
+ case 'step/start':
122
+ return `turn ${data.turn ?? '?'} step ${data.step ?? '?'} start`;
123
+ case 'step/end':
124
+ return `turn ${data.turn ?? '?'} step ${data.step ?? '?'} end`;
125
+ case 'request/header':
126
+ return `request ${data.header?.config?.provider ?? '?'}/${data.header?.config?.model ?? '?'}`
127
+ + ` maxTokens=${data.header?.config?.maxTokens ?? '?'} tools=${data.header?.tools?.length ?? 0}`;
128
+ case 'request/context':
129
+ return `request context ${data.provider ?? '?'}/${data.model ?? '?'} window=${data.contextWindow ?? '?'}`;
130
+ case 'compaction/start':
131
+ return `compaction ${data.compactionId ?? '?'} start`;
132
+ case 'compaction/end':
133
+ return `compaction ${data.compactionId ?? '?'} end${data.error ? ` error=${data.error.message ?? data.error}` : ''}`;
134
+ case 'compaction/prune':
135
+ return `prune ${data.shadowedRange?.start ?? '?'}-${data.shadowedRange?.end ?? '?'}`
136
+ + ` ~${data.shadowedTokenCount ?? '?'} tokens`;
137
+ case 'compaction/summary':
138
+ return `compaction ${data.compactionId ?? '?'} summary`;
139
+ default:
140
+ return String(event?.type ?? 'event');
141
+ }
142
+ }
143
+
144
+ /** First `[gist …]` line in a compaction summary, if any. */
145
+ export function gistHeaderOf(event) {
146
+ const text = eventText(event);
147
+ for (const line of text.split('\n')) {
148
+ const trimmed = line.trim();
149
+ if (trimmed.startsWith('[gist') || trimmed.startsWith('[thread')) return trimmed;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ const ROLE_ORDER = Object.freeze([
155
+ 'user', 'assistant', 'tool', 'tool-call', 'gist', 'developer', 'system',
156
+ 'compaction', 'turn', 'step', 'request',
157
+ ]);
158
+
159
+ function rolesOf(events) {
160
+ const counts = new Map();
161
+ for (const event of events) {
162
+ const role = eventRole(event);
163
+ counts.set(role, (counts.get(role) ?? 0) + 1);
164
+ }
165
+ const ordered = ROLE_ORDER.filter((role) => counts.has(role));
166
+ const rest = [...counts.keys()].filter((role) => !ROLE_ORDER.includes(role)).sort();
167
+ return [...ordered, ...rest]
168
+ .map((role) => `${role}:${counts.get(role)}`)
169
+ .join(' ');
170
+ }
171
+
172
+ function seqBounds(events) {
173
+ if (!Array.isArray(events) || events.length === 0) return { min: null, max: null };
174
+ let min = Infinity;
175
+ let max = -Infinity;
176
+ for (const event of events) {
177
+ const seq = Number(event?.seq);
178
+ if (!Number.isFinite(seq)) continue;
179
+ if (seq < min) min = seq;
180
+ if (seq > max) max = seq;
181
+ }
182
+ if (min === Infinity) return { min: null, max: null };
183
+ return { min, max };
184
+ }
185
+
186
+ function findGist(events, start, end) {
187
+ return events
188
+ .filter((event) => event?.type === 'compaction/summary'
189
+ && Number(event.data?.shadowedRange?.start) === start
190
+ && Number(event.data?.shadowedRange?.end) === end)
191
+ .at(-1) ?? null;
192
+ }
193
+
194
+ function formatEventForGet(event, assetRows = []) {
195
+ const text = eventText(event);
196
+ const body = text.trim().length > 0 ? text : eventSummary(event);
197
+ const markers = assetRows
198
+ .map((row) => assetMarker(row, row.owner === 'child' ? 'child' : 'parent'))
199
+ .join('\n');
200
+ return `seq=${event.seq} [${eventRole(event)}] ${formatTime(event.time)}\n${truncate(body, MAX_EVENT_CHARS)}`
201
+ + (markers.length > 0 ? `\n${markers}` : '');
202
+ }
203
+
204
+ /**
205
+ * List compaction/gist ranges, newest first.
206
+ * @returns {{ text: string, total: number, returned: number, truncated: boolean }}
207
+ */
208
+ export function buildIndex({ sessionId, events, limit = DEFAULT_LIMIT, maxChars = INDEX_MAX_CHARS }) {
209
+ const list = Array.isArray(events) ? events : [];
210
+ const gists = list
211
+ .filter((event) => event?.type === 'compaction/summary')
212
+ .sort((left, right) => Number(right.seq) - Number(left.seq));
213
+ const lines = [
214
+ `session_log index · session=${sessionId} · events=${list.length} · gists=${gists.length}`,
215
+ ];
216
+ if (gists.length === 0) {
217
+ lines.push('no gist records in this session.');
218
+ lines.push('Use grep with a literal term to locate raw events, then get <seq> or get start-end.');
219
+ return { text: lines.join('\n'), total: 0, returned: 0, truncated: false };
220
+ }
221
+
222
+ const take = clampInt(limit, 1, MAX_LIMIT, DEFAULT_LIMIT);
223
+ const shown = [];
224
+ let used = lines.join('\n').length;
225
+ for (const gist of gists.slice(0, take)) {
226
+ const data = gist.data ?? {};
227
+ const header = gistHeaderOf(gist) ?? '(manual compaction; no gist header)';
228
+ const line = `- seq=${gist.seq} · surface ${data.shadowedRange?.start ?? '?'}-${data.shadowedRange?.end ?? '?'}`
229
+ + ` · ${data.shadowedSeqs?.length ?? '?'} msgs · ~${data.shadowedTokenCount ?? '?'} tok`
230
+ + ` · ${formatTime(gist.time)} · ${data.provider ?? '?'}/${data.model ?? '?'}`
231
+ + `\n ${header}`;
232
+ if (used + line.length > maxChars) break;
233
+ shown.push(line);
234
+ used += line.length + 1;
235
+ }
236
+ lines.push(...shown);
237
+ if (shown.length < gists.length) {
238
+ lines.push(`[showing ${shown.length} of ${gists.length} gists, newest first; raise limit or grep to narrow]`);
239
+ }
240
+ return {
241
+ text: lines.join('\n'),
242
+ total: gists.length,
243
+ returned: shown.length,
244
+ truncated: shown.length < gists.length,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Read one event, one seq ± context, or one inclusive range.
250
+ *
251
+ * A range matching a compaction's `shadowedRange` is resolved to that
252
+ * compaction's exact `shadowedSeqs` (surface order, possibly non-monotonic).
253
+ * @returns {{ text: string, total: number, returned: number, truncated: boolean }}
254
+ */
255
+ export function getRange({
256
+ sessionId,
257
+ events,
258
+ seq,
259
+ start,
260
+ end,
261
+ context = 0,
262
+ limit = DEFAULT_LIMIT,
263
+ maxChars = DEFAULT_MAX_CHARS,
264
+ assets = [],
265
+ }) {
266
+ const list = Array.isArray(events) ? events : [];
267
+ const assetsBySeq = new Map();
268
+ for (const row of Array.isArray(assets) ? assets : []) {
269
+ const key = Number(row?.seq);
270
+ if (!Number.isInteger(key)) continue;
271
+ if (!assetsBySeq.has(key)) assetsBySeq.set(key, []);
272
+ assetsBySeq.get(key).push(row);
273
+ }
274
+ const bounds = seqBounds(list);
275
+ const hasSeq = seq !== undefined && seq !== null && seq !== '';
276
+ const hasStart = start !== undefined && start !== null && start !== '';
277
+ const hasEnd = end !== undefined && end !== null && end !== '';
278
+ if (!hasSeq && !(hasStart && hasEnd)) {
279
+ throw new SessionLogError(
280
+ 'RANGE_REQUIRED',
281
+ 'get needs seq, or start and end. Use index to list gist ranges; without a pointer use grep then get <seq>.',
282
+ );
283
+ }
284
+ if (!hasSeq && hasStart !== hasEnd) {
285
+ throw new SessionLogError('RANGE_PARTIAL', 'get needs both start and end, or a single seq.');
286
+ }
287
+
288
+ let selected = [];
289
+ let label = '';
290
+ let resolvedGist = null;
291
+
292
+ if (hasSeq) {
293
+ const wanted = Number(seq);
294
+ const index = list.findIndex((event) => Number(event.seq) === wanted);
295
+ if (index < 0) {
296
+ throw new SessionLogError(
297
+ 'SEQ_NOT_FOUND',
298
+ `seq ${seq} is not in this session (valid event seqs ${bounds.min ?? '?'}-${bounds.max ?? '?'}).`
299
+ + ' If the original is unavailable, say you only remember the gist.',
300
+ );
301
+ }
302
+ const span = clampInt(context, 0, MAX_CONTEXT, 0);
303
+ selected = list.slice(Math.max(0, index - span), Math.min(list.length, index + span + 1));
304
+ label = `seq=${wanted}${span > 0 ? ` ±${span}` : ''}`;
305
+ } else {
306
+ const from = Number(start);
307
+ const to = Number(end);
308
+ if (!Number.isInteger(from) || !Number.isInteger(to)) {
309
+ throw new SessionLogError('RANGE_INVALID', 'start and end must be integer event seqs.');
310
+ }
311
+ const gist = findGist(list, from, to);
312
+ if (gist !== null) {
313
+ const seqs = Array.isArray(gist.data?.shadowedSeqs) ? gist.data.shadowedSeqs : [];
314
+ const bySeq = new Map(list.map((event) => [Number(event.seq), event]));
315
+ selected = seqs.map((value) => bySeq.get(Number(value))).filter((event) => event !== undefined);
316
+ if (selected.length > 0) {
317
+ resolvedGist = gist.data?.compactionId ?? 'unknown';
318
+ label = `surface ${from}-${to}`;
319
+ }
320
+ }
321
+ if (selected.length === 0 && from <= to) {
322
+ selected = list.filter((event) => Number(event.seq) >= from && Number(event.seq) <= to);
323
+ label = `event seqs ${from}-${to}`;
324
+ }
325
+ if (selected.length === 0 && from > to) {
326
+ throw new SessionLogError(
327
+ 'RANGE_ORDER',
328
+ `surface range ${from}-${to} is not numerically ordered and matches no compaction summary.`
329
+ + ' Pass the exact gist pointer (session=log:<id>#seq=A-B) or a start <= end range.',
330
+ );
331
+ }
332
+ }
333
+
334
+ if (selected.length === 0) {
335
+ throw new SessionLogError(
336
+ 'RANGE_EMPTY',
337
+ `no events in ${label || 'that range'} (valid event seqs ${bounds.min ?? '?'}-${bounds.max ?? '?'}).`
338
+ + ' If the original is unavailable, say you only remember the gist.',
339
+ );
340
+ }
341
+
342
+ const take = clampInt(limit, 1, MAX_LIMIT, DEFAULT_LIMIT);
343
+ const manifest = [
344
+ `session_log get · session=${sessionId} · ${label} · events=${selected.length}`
345
+ + (resolvedGist === null ? '' : ` · resolved via gist ${resolvedGist}`),
346
+ `roles: ${rolesOf(selected)}`,
347
+ ];
348
+ const body = [];
349
+ let used = manifest.join('\n').length;
350
+ let returned = 0;
351
+ for (const event of selected.slice(0, take)) {
352
+ const text = formatEventForGet(event, assetsBySeq.get(Number(event.seq)) ?? []);
353
+ if (used + text.length > maxChars) {
354
+ if (returned === 0) {
355
+ body.push(truncate(text, maxChars));
356
+ returned = 1;
357
+ }
358
+ break;
359
+ }
360
+ body.push(text);
361
+ used += text.length + 2;
362
+ returned += 1;
363
+ }
364
+ if (returned < selected.length) {
365
+ const next = selected[returned]?.seq;
366
+ manifest.push(
367
+ `[truncated: showing ${returned} of ${selected.length}; next seq=${next ?? '?'};`
368
+ + ' get seq=<next> ±context, raise limit, or grep to narrow]',
369
+ );
370
+ }
371
+ return {
372
+ text: [...manifest, ...body].join('\n\n'),
373
+ total: selected.length,
374
+ returned,
375
+ truncated: returned < selected.length,
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Literal case-insensitive search over raw event text.
381
+ * @returns {{ text: string, total: number, returned: number, truncated: boolean }}
382
+ */
383
+ export function grepRange({
384
+ sessionId,
385
+ events,
386
+ query,
387
+ context = 0,
388
+ limit = DEFAULT_LIMIT,
389
+ maxChars = GREP_MAX_CHARS,
390
+ }) {
391
+ const list = Array.isArray(events) ? events : [];
392
+ const needle = String(query ?? '').trim();
393
+ if (needle.length < 2) {
394
+ throw new SessionLogError('QUERY_REQUIRED', 'grep needs a query of at least 2 characters.');
395
+ }
396
+ const lower = needle.toLowerCase();
397
+ const matches = [];
398
+ for (const event of list) {
399
+ // The system prompt is not conversation; searching it produces noise
400
+ // (including this plugin's own convention text). get <seq> can still read it.
401
+ if (event?.type === 'system/message') continue;
402
+ const text = eventText(event);
403
+ if (text.length === 0) continue;
404
+ const index = text.toLowerCase().indexOf(lower);
405
+ if (index < 0) continue;
406
+ const before = text.slice(Math.max(0, index - 80), index).replace(/\s+/g, ' ');
407
+ const after = text.slice(index + needle.length, index + needle.length + 120).replace(/\s+/g, ' ');
408
+ matches.push({
409
+ seq: event.seq,
410
+ role: eventRole(event),
411
+ time: event.time,
412
+ snippet: `…${before}${text.slice(index, index + needle.length)}${after}…`,
413
+ });
414
+ }
415
+ matches.sort((left, right) => Number(left.seq) - Number(right.seq));
416
+
417
+ const lines = [`session_log grep · session=${sessionId} · query="${needle}" · matches=${matches.length}`];
418
+ if (matches.length === 0) {
419
+ lines.push('no matches; try another term or a shorter phrase.');
420
+ lines.push('If the original is unavailable, say you only remember the gist.');
421
+ return { text: lines.join('\n'), total: 0, returned: 0, truncated: false };
422
+ }
423
+
424
+ const span = clampInt(context, 0, MAX_CONTEXT, 0);
425
+ const take = clampInt(limit, 1, MAX_LIMIT, DEFAULT_LIMIT);
426
+ const shown = matches.slice(0, take);
427
+ const body = [];
428
+ let used = lines.join('\n').length;
429
+ for (const match of shown) {
430
+ const line = `seq=${match.seq} [${match.role}] ${formatTime(match.time)}: ${truncate(match.snippet, MAX_SNIPPET_CHARS)}`;
431
+ let chunk = line;
432
+ if (span > 0) {
433
+ const index = list.findIndex((event) => Number(event.seq) === Number(match.seq));
434
+ for (let offset = -span; offset <= span; offset += 1) {
435
+ if (offset === 0) continue;
436
+ const neighbor = list[index + offset];
437
+ if (neighbor === undefined) continue;
438
+ const text = eventText(neighbor);
439
+ const preview = text.trim().length > 0 ? text : eventSummary(neighbor);
440
+ chunk += `\n ${offset < 0 ? 'before' : 'after'} seq=${neighbor.seq} [${eventRole(neighbor)}] ${formatTime(neighbor.time)}: ${oneLine(preview, 120)}`;
441
+ }
442
+ }
443
+ if (used + chunk.length > maxChars) {
444
+ if (body.length === 0) body.push(truncate(chunk, maxChars));
445
+ break;
446
+ }
447
+ body.push(chunk);
448
+ used += chunk.length + 1;
449
+ }
450
+ lines.push(...body);
451
+ if (body.length < matches.length) {
452
+ lines.push(`[truncated: showing ${body.length} of ${matches.length} matches; raise limit or refine the query]`);
453
+ }
454
+ return {
455
+ text: lines.join('\n'),
456
+ total: matches.length,
457
+ returned: body.length,
458
+ truncated: body.length < matches.length,
459
+ };
460
+ }
461
+
462
+ /** Parse `log:<id>#seq=A-B` (also inside a full gist header) or a plain id. */
463
+ export function parsePointer(value) {
464
+ const text = String(value ?? '').trim();
465
+ if (text.length === 0) return { sessionId: null, start: null, end: null, pointer: false };
466
+ const match = /log:([^\s#·]+)#seq=(\d+)-(\d+)/.exec(text);
467
+ if (match !== null) {
468
+ return {
469
+ sessionId: match[1],
470
+ start: Number(match[2]),
471
+ end: Number(match[3]),
472
+ pointer: true,
473
+ };
474
+ }
475
+ return { sessionId: text, start: null, end: null, pointer: false };
476
+ }
477
+
478
+ /** Ordered categories used by `session_log stats`. */
479
+ export const STATS_CATEGORIES = Object.freeze([
480
+ 'reasoning',
481
+ 'tool_result',
482
+ 'assistant_text',
483
+ 'tool_call',
484
+ 'user',
485
+ 'system',
486
+ 'developer',
487
+ 'gist',
488
+ ]);
489
+
490
+ function pushTextBlocks(blocks, category, source) {
491
+ if (!Array.isArray(blocks)) return;
492
+ let chars = 0;
493
+ let count = 0;
494
+ for (const block of blocks) {
495
+ if (block?.type === 'text' && typeof block.text === 'string') {
496
+ chars += block.text.length;
497
+ count += 1;
498
+ }
499
+ }
500
+ if (chars > 0 || count > 0) source.push({ category, chars, count });
501
+ }
502
+
503
+ /**
504
+ * Classify one event into stats categories. Assistant text, reasoning and
505
+ * tool-call arguments are separate; `tool/call` markers are skipped because the
506
+ * assistant message already carries the call arguments.
507
+ * @returns {{ category: string, chars: number, count: number }[]}
508
+ */
509
+ export function classifyStatsEvent(event) {
510
+ const out = [];
511
+ const data = event?.data;
512
+ if (data === null || data === undefined) return out;
513
+ switch (event?.type) {
514
+ case 'user/message':
515
+ pushTextBlocks(data.content, 'user', out);
516
+ break;
517
+ case 'developer/message':
518
+ pushTextBlocks(data.content, 'developer', out);
519
+ break;
520
+ case 'system/message':
521
+ pushTextBlocks(data.message?.content ?? data.content, 'system', out);
522
+ break;
523
+ case 'assistant/message': {
524
+ const blocks = data.message?.content;
525
+ if (!Array.isArray(blocks)) break;
526
+ let text = 0;
527
+ let reasoning = 0;
528
+ let toolCall = 0;
529
+ let textBlocks = 0;
530
+ let reasoningBlocks = 0;
531
+ let callBlocks = 0;
532
+ for (const block of blocks) {
533
+ if (block?.type === 'text' && typeof block.text === 'string') {
534
+ text += block.text.length;
535
+ textBlocks += 1;
536
+ } else if (block?.type === 'reasoning' && typeof block.text === 'string') {
537
+ reasoning += block.text.length;
538
+ reasoningBlocks += 1;
539
+ } else if (block?.type === 'tool-call') {
540
+ toolCall += String(block.name ?? '').length + String(block.arguments ?? '').length;
541
+ callBlocks += 1;
542
+ }
543
+ }
544
+ if (reasoning > 0 || reasoningBlocks > 0) out.push({ category: 'reasoning', chars: reasoning, count: reasoningBlocks });
545
+ if (text > 0 || textBlocks > 0) out.push({ category: 'assistant_text', chars: text, count: textBlocks });
546
+ if (toolCall > 0 || callBlocks > 0) out.push({ category: 'tool_call', chars: toolCall, count: callBlocks });
547
+ break;
548
+ }
549
+ case 'tool/result':
550
+ pushTextBlocks(data.message?.content, 'tool_result', out);
551
+ break;
552
+ case 'compaction/summary':
553
+ pushTextBlocks(data.summary, 'gist', out);
554
+ break;
555
+ default:
556
+ break;
557
+ }
558
+ return out;
559
+ }
560
+
561
+ /**
562
+ * Aggregate events into token/char rows. `tokensBySeq` (surface mode) provides
563
+ * route-priced tokens per event; within a multi-block assistant event the event
564
+ * token count is split across categories by chars. Without it, chars/4 is used
565
+ * and the caller must label the result as an estimate.
566
+ * @returns {{ totalChars: number, totalTokens: number, rows: object[] }}
567
+ */
568
+ export function buildStats(events, { tokensBySeq = null } = {}) {
569
+ const byCategory = new Map();
570
+ let totalChars = 0;
571
+ let totalTokens = 0;
572
+ for (const event of Array.isArray(events) ? events : []) {
573
+ const categories = classifyStatsEvent(event);
574
+ if (categories.length === 0) continue;
575
+ const eventChars = categories.reduce((sum, entry) => sum + entry.chars, 0);
576
+ const nodeTokens = tokensBySeq?.get(Number(event.seq));
577
+ const eventTokens = Number.isFinite(nodeTokens) ? nodeTokens : eventChars / 4;
578
+ for (const entry of categories) {
579
+ const row = byCategory.get(entry.category) ?? { category: entry.category, count: 0, chars: 0, tokens: 0 };
580
+ row.count += entry.count;
581
+ row.chars += entry.chars;
582
+ row.tokens += eventChars > 0 ? eventTokens * (entry.chars / eventChars) : 0;
583
+ byCategory.set(entry.category, row);
584
+ }
585
+ totalChars += eventChars;
586
+ totalTokens += eventTokens;
587
+ }
588
+ const rows = [...byCategory.values()].sort((left, right) => right.tokens - left.tokens);
589
+ return { totalChars, totalTokens, rows };
590
+ }
591
+
592
+ /** Surface seqs in the newest `tailTokens` route-priced budget. */
593
+ export function tailSeqs(surfaceSeqs, tokensBySeq, tailTokens) {
594
+ const selected = new Set();
595
+ if (!Array.isArray(surfaceSeqs) || !Number.isFinite(tailTokens) || tailTokens <= 0) return selected;
596
+ let accumulated = 0;
597
+ for (let index = surfaceSeqs.length - 1; index >= 0; index -= 1) {
598
+ const seq = surfaceSeqs[index];
599
+ const tokens = Number(tokensBySeq?.get(Number(seq)) ?? 0);
600
+ if (accumulated + tokens > tailTokens && accumulated > 0) break;
601
+ selected.add(seq);
602
+ accumulated += tokens;
603
+ }
604
+ return selected;
605
+ }
606
+