@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,140 @@
1
+ /**
2
+ * Best-effort tool-layer path guard.
3
+ *
4
+ * This is not an OS security boundary: arbitrary code with full access can
5
+ * still read/write files. It blocks the normal model tools from touching
6
+ * another session's continuity directory.
7
+ *
8
+ * Design: only *path fields* are inspected. Message bodies, file contents and
9
+ * source text are never scanned — editing a document that merely mentions the
10
+ * continuity path must not be blocked.
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { continuityRoot, isPersonaEnabled, resolvePersona, sessionDir } from './memory/persona.mjs';
15
+
16
+ const norm = (value) => String(value ?? '').replace(/\\/g, '/').toLowerCase();
17
+
18
+ /** Explicit operator allowlist. This is the only sanctioned cross-session path. */
19
+ function isOperatorSession(sessionId) {
20
+ try {
21
+ const file = path.join(continuityRoot(), 'operator-sessions.json');
22
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
23
+ return Array.isArray(parsed) && parsed.map(String).includes(String(sessionId));
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ const ROOT_MARK = '.dsh/continuity';
30
+ const ROOT_TILDE = '~/.dsh/continuity';
31
+
32
+ /** Root-level shared read files are readable by any enabled persona. */
33
+ const SHARED_READ_FILES = new Set([
34
+ norm(path.join(continuityRoot(), 'session-persona.json')),
35
+ norm(path.join(continuityRoot(), 'operator-sessions.json')),
36
+ norm(path.join(continuityRoot(), 'context-log.jsonl')),
37
+ norm(path.join(continuityRoot(), 'base-rules.md')),
38
+ norm('~/.dsh/continuity/base-rules.md'),
39
+ norm('.dsh/continuity/base-rules.md'),
40
+ ]);
41
+
42
+ function isSharedIdentityRead(toolName, hits) {
43
+ if (toolName !== 'read' || hits.length === 0) return false;
44
+ return hits.every((hit) => SHARED_READ_FILES.has(norm(hit)));
45
+ }
46
+
47
+ /** Argument keys that actually carry a filesystem path. */
48
+ const PATH_KEYS = new Set([
49
+ 'file_path', 'path', 'cwd', 'dir', 'directory', 'target', 'destination', 'source', 'file',
50
+ ]);
51
+
52
+ /** Argument keys that are prose or file bodies; never treated as paths. */
53
+ const TEXT_KEYS = new Set([
54
+ 'content', 'old_string', 'new_string', 'text', 'message', 'prompt', 'body',
55
+ 'summary', 'reason', 'note', 'query', 'pattern',
56
+ ]);
57
+
58
+ /** Tools whose arguments are addressed to a human, not the filesystem. */
59
+ const MESSAGE_TOOLS = new Set(['send_message', 'peer_message', 'present']);
60
+
61
+ /** Shell tools: only the command string can name a path. */
62
+ const SHELL_TOOLS = new Set(['pwsh', 'bash', 'shell', 'powershell']);
63
+
64
+ function isPathValue(value) {
65
+ const n = norm(value);
66
+ return n.includes(ROOT_MARK) || n.includes(ROOT_TILDE);
67
+ }
68
+
69
+ function collectPathValues(value, key = '', out = []) {
70
+ if (typeof value === 'string') {
71
+ if (PATH_KEYS.has(key) || key === '') out.push(value);
72
+ return out;
73
+ }
74
+ if (Array.isArray(value)) {
75
+ for (const item of value) collectPathValues(item, key, out);
76
+ return out;
77
+ }
78
+ if (value && typeof value === 'object') {
79
+ for (const [k, v] of Object.entries(value)) {
80
+ if (TEXT_KEYS.has(k)) continue;
81
+ collectPathValues(v, k, out);
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function targetPaths(toolName, args) {
88
+ if (SHELL_TOOLS.has(toolName)) {
89
+ return typeof args?.command === 'string' ? [args.command] : [];
90
+ }
91
+ return collectPathValues(args, 'file_path', []);
92
+ }
93
+
94
+ export function installSessionGuard(ctx) {
95
+ if (!ctx || typeof ctx.inject !== 'function') return false;
96
+ ctx.inject(['tools'], (scoped) => {
97
+ if (!scoped?.tools || typeof scoped.tools.guard !== 'function') return;
98
+ scoped.effect(() => scoped.tools.guard((exec) => {
99
+ const sessionId = exec?.agent?.session?.id;
100
+ if (sessionId === undefined || sessionId === null) return undefined;
101
+
102
+ const toolName = String(exec?.name ?? '');
103
+ if (MESSAGE_TOOLS.has(toolName)) return undefined;
104
+
105
+ const hits = targetPaths(toolName, exec?.arguments ?? {}).filter(isPathValue);
106
+ if (!hits.length) return undefined;
107
+
108
+ // Reading the root identity map is explicitly part of the rules. It is
109
+ // read-only and safe for any enabled persona; writes stay blocked.
110
+ if (isSharedIdentityRead(toolName, hits)) return undefined;
111
+
112
+ // An explicit operator may cross session boundaries even when the
113
+ // operator's own persona is disabled. Keep this list small and auditable.
114
+ if (isOperatorSession(sessionId)) return undefined;
115
+
116
+ // Only paths under the continuity root reach the persona gate: a disabled
117
+ // persona loses continuity access, not every tool.
118
+ const personaId = resolvePersona(sessionId);
119
+ if (!isPersonaEnabled(personaId)) return 'continuity: persona is disabled';
120
+
121
+ const own = norm(sessionDir(sessionId));
122
+ const ownForms = [
123
+ own,
124
+ norm('~/.dsh/continuity' + '/sessions/' + sessionId),
125
+ norm('.dsh/continuity' + '/sessions/' + sessionId),
126
+ ];
127
+ for (const hit of hits) {
128
+ let withoutOwn = norm(hit);
129
+ for (const form of ownForms) withoutOwn = withoutOwn.split(form).join('');
130
+ if (withoutOwn.includes(ROOT_MARK) || withoutOwn.includes(ROOT_TILDE)) {
131
+ return "continuity: a session may not touch another session's continuity files";
132
+ }
133
+ }
134
+ return undefined;
135
+ }));
136
+ });
137
+ return true;
138
+ }
139
+
140
+ export default installSessionGuard;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * `session_log` — read-only retrieval tool, scoped to the Continuity preset.
3
+ *
4
+ * It is a plain ToolDefinition (no `defineTool` import), so the linked
5
+ * package needs no `@deepseek-ai/dsh-tools` resolution from the workspace.
6
+ * Host data comes from `ctx.sessionQuery.readSession()`: live-preferred,
7
+ * replay-validated, and read-only. Nothing here appends, replaces, or
8
+ * compacts.
9
+ */
10
+ import {
11
+ buildIndex,
12
+ buildStats,
13
+ eventRole,
14
+ eventText,
15
+ getRange,
16
+ grepRange,
17
+ parsePointer,
18
+ SessionLogError,
19
+ tailSeqs,
20
+ } from './session-log.mjs';
21
+ import { readAssetIndex, assetMarker } from './assets.mjs';
22
+ import { createSessionAssetTool, createChildAssetTool } from './asset-tool.mjs';
23
+ import { createHandoffTool } from './handoff-tool.mjs';
24
+ import { join } from 'node:path';
25
+ import { continuityState, markSessionLogTool } from './state.mjs';
26
+
27
+ export const name = 'dsh-continuity/session-log-tool';
28
+ export const inject = ['tools', 'sessionQuery', 'tokenMeter'];
29
+
30
+ const DESCRIPTION = [
31
+ 'Read-only access to this machine\'s DSH session log.',
32
+ 'Use it when a [gist] block\'s header says the original is retrievable and the exact',
33
+ 'wording, date, number, promise, or name matters.',
34
+ 'index lists gist ranges for a session; get reads one seq or a start-end range',
35
+ '(a gist header pointer is a range); grep finds literal text and returns seqs for get;',
36
+ 'stats reports surface token share by block (reasoning, tool result, assistant text,',
37
+ 'tool-call, user) and can report the full raw log with scope="raw".',
38
+ 'Pass a session id or a `log:<id>#seq=A-B` pointer; the current session is the default.',
39
+ 'Output is truncated with a manifest and a next seq; never dump a large range.',
40
+ 'Never writes, never changes the surface, never triggers compaction.',
41
+ 'If the original is unavailable, say you only remember the gist; stop after 2-3 fetches.',
42
+ ].join(' ');
43
+
44
+ const PARAMETERS = {
45
+ type: 'object',
46
+ additionalProperties: false,
47
+ properties: {
48
+ action: {
49
+ type: 'string',
50
+ enum: ['index', 'get', 'grep', 'stats'],
51
+ description: 'index = list gist ranges; get = read seq or start-end; grep = literal text search; stats = per-block context share.',
52
+ },
53
+ scope: {
54
+ type: 'string',
55
+ enum: ['surface', 'raw'],
56
+ description: 'stats: surface (default, current context via tokenMeter) or raw (whole historical log, chars/4 estimate).',
57
+ },
58
+ session: {
59
+ type: 'string',
60
+ description: 'Session id, or a gist pointer `log:<id>#seq=A-B`. Default: the current session.',
61
+ },
62
+ seq: {
63
+ type: 'integer',
64
+ description: 'get: one event seq.',
65
+ },
66
+ start: {
67
+ type: 'integer',
68
+ description: 'get: inclusive start event seq (pair with end; a gist pointer fills both).',
69
+ },
70
+ end: {
71
+ type: 'integer',
72
+ description: 'get: inclusive end event seq.',
73
+ },
74
+ query: {
75
+ type: 'string',
76
+ description: 'grep: literal, case-insensitive text. At least 2 characters.',
77
+ },
78
+ limit: {
79
+ type: 'integer',
80
+ description: 'Maximum gists/events/matches to return (default 20, max 100).',
81
+ },
82
+ context: {
83
+ type: 'integer',
84
+ description: 'get seq / grep match: neighboring events before and after (default 0, max 10).',
85
+ },
86
+ },
87
+ required: ['action'],
88
+ };
89
+
90
+ /**
91
+ * Format stats for the live surface or the raw historical log. Surface mode
92
+ * uses the host token meter; raw mode is an explicit chars/4 estimate.
93
+ */
94
+ async function statsText({ liveSession, sessionId, scope, sessionQuery, tokenMeter }) {
95
+ const live = liveSession !== null && liveSession !== undefined
96
+ && String(liveSession.id) === String(sessionId);
97
+ if (scope === 'surface' && live) {
98
+ const events = [];
99
+ for (const seq of liveSession.surface?.nodes ?? []) {
100
+ const event = liveSession.eventAt(seq);
101
+ if (event !== undefined) events.push(event);
102
+ }
103
+ let measurement = null;
104
+ try {
105
+ measurement = tokenMeter?.measure?.(liveSession) ?? null;
106
+ } catch {
107
+ measurement = null;
108
+ }
109
+ const tokensBySeq = new Map();
110
+ for (const node of measurement?.nodes ?? []) {
111
+ if (node?.seq !== undefined) tokensBySeq.set(Number(node.seq), Number(node.tokens));
112
+ }
113
+ const stats = buildStats(events, { tokensBySeq });
114
+ const surfaceTokens = Number.isFinite(measurement?.surfaceTokens)
115
+ ? measurement.surfaceTokens
116
+ : stats.totalTokens;
117
+ const totalTokens = Number.isFinite(measurement?.totalTokens)
118
+ ? measurement.totalTokens
119
+ : surfaceTokens;
120
+ const baseline = Math.max(0, totalTokens - surfaceTokens);
121
+ const source = tokensBySeq.size > 0 ? 'tokenMeter' : 'chars4 estimate';
122
+
123
+ const header = typeof liveSession.requestHeader === 'function' ? liveSession.requestHeader() : null;
124
+ const tools = Array.isArray(header?.tools) ? header.tools : [];
125
+ const toolSchemaChars = tools.length > 0 ? JSON.stringify(tools).length : 0;
126
+
127
+ const lines = [
128
+ `session_log stats · session=${sessionId} · scope=surface · source=${source}`,
129
+ `events=${events.length} surfaceTokens=${Math.round(surfaceTokens)} baseline=${Math.round(baseline)} totalTokens=${Math.round(totalTokens)}`,
130
+ ];
131
+ const denom = stats.totalTokens > 0 ? stats.totalTokens : 1;
132
+ for (const row of stats.rows) {
133
+ if (row.chars <= 0) continue;
134
+ const share = (100 * row.tokens / denom).toFixed(1);
135
+ lines.push(`${row.category.padEnd(14)} count=${String(row.count).padEnd(5)} chars=${String(row.chars).padEnd(9)} tokens=${String(Math.round(row.tokens)).padEnd(8)} share=${share}%`);
136
+ }
137
+ if (tools.length > 0) {
138
+ lines.push(`tools_schema count=${String(tools.length).padEnd(5)} chars=${String(toolSchemaChars).padEnd(9)} ~tokens=${Math.ceil(toolSchemaChars / 4)} (baseline)`);
139
+ }
140
+
141
+ const policy = continuityState.engine?.policy;
142
+ const context = continuityState.context;
143
+ const windowTokens = context?.sessionId === String(sessionId) ? context.windowTokens : null;
144
+ const protect = Number.isFinite(policy?.protect) ? policy.protect : null;
145
+ const tailBudget = Number.isFinite(windowTokens) && protect !== null
146
+ ? Math.round(windowTokens * protect)
147
+ : null;
148
+ if (tailBudget !== null && tokensBySeq.size > 0) {
149
+ const tail = tailSeqs(liveSession.surface.nodes, tokensBySeq, tailBudget);
150
+ const tailEvents = events.filter((event) => tail.has(event.seq));
151
+ const tailStats = buildStats(tailEvents, { tokensBySeq });
152
+ const tailDenom = tailStats.totalTokens > 0 ? tailStats.totalTokens : 1;
153
+ lines.push(`tail · protect=${(protect * 100).toFixed(0)}% × W=${windowTokens} = ${tailBudget} tokens · events=${tailEvents.length}`);
154
+ for (const row of tailStats.rows) {
155
+ if (row.chars <= 0) continue;
156
+ const share = (100 * row.tokens / tailDenom).toFixed(1);
157
+ lines.push(` ${row.category.padEnd(12)} tokens=${String(Math.round(row.tokens)).padEnd(8)} share=${share}%`);
158
+ }
159
+ }
160
+ return lines.join('\n');
161
+ }
162
+
163
+ let snapshot;
164
+ try {
165
+ snapshot = await sessionQuery.readSession(sessionId);
166
+ } catch (error) {
167
+ throw new Error(
168
+ `session_log: cannot read session ${sessionId}: ${error?.message ?? error}. `
169
+ + 'The original may be unavailable; if so, say you only remember the gist.',
170
+ );
171
+ }
172
+ const events = Array.isArray(snapshot?.events) ? snapshot.events : [];
173
+ const stats = buildStats(events);
174
+ const note = scope === 'surface'
175
+ ? 'surface unavailable; raw fallback; historical; may overlap summaries; not context'
176
+ : 'historical; may overlap summaries; not context';
177
+ const lines = [
178
+ `session_log stats · session=${sessionId} · scope=raw · source=chars4 estimate (${note})`,
179
+ `events=${events.length} chars=${stats.totalChars} ~tokens=${Math.ceil(stats.totalChars / 4)}`,
180
+ ];
181
+ const denom = stats.totalTokens > 0 ? stats.totalTokens : 1;
182
+ for (const row of stats.rows) {
183
+ if (row.chars <= 0) continue;
184
+ const share = (100 * row.tokens / denom).toFixed(1);
185
+ lines.push(`${row.category.padEnd(14)} count=${String(row.count).padEnd(5)} chars=${String(row.chars).padEnd(9)} ~tokens=${String(Math.ceil(row.tokens)).padEnd(8)} share=${share}%`);
186
+ }
187
+ return lines.join('\n');
188
+ }
189
+
190
+ /**
191
+ * Build the tool definition. Exported for tests with a fake `sessionQuery`.
192
+ * @param {{ sessionQuery: { readSession(sessionId: unknown): Promise<{ events?: unknown[] }> }, tokenMeter?: object }} deps
193
+ */
194
+ export function createSessionLogTool({ sessionQuery, tokenMeter }) {
195
+ if (sessionQuery === undefined || sessionQuery === null) {
196
+ throw new Error('session_log: sessionQuery service is required');
197
+ }
198
+ return {
199
+ name: 'session_log',
200
+ description: DESCRIPTION,
201
+ parameters: PARAMETERS,
202
+ output: {
203
+ schema: { type: 'string' },
204
+ render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
205
+ },
206
+ timeoutMs: 20_000,
207
+ isConcurrencySafe: () => true,
208
+ async execute(args, exec) {
209
+ const input = args !== null && typeof args === 'object' ? args : {};
210
+ const action = input.action;
211
+ if (action !== 'index' && action !== 'get' && action !== 'grep' && action !== 'stats') {
212
+ throw new Error('session_log: action must be "index", "get", "grep", or "stats".');
213
+ }
214
+ exec?.signal?.throwIfAborted?.();
215
+
216
+ const pointer = input.session === undefined || input.session === null
217
+ ? { sessionId: null, start: null, end: null }
218
+ : parsePointer(input.session);
219
+ const sessionId = pointer.sessionId ?? exec?.agent?.id ?? null;
220
+ if (sessionId === null) {
221
+ throw new Error('session_log: no session id. Pass session=<id or gist pointer>, or run from an agent.');
222
+ }
223
+
224
+ if (action === 'stats') {
225
+ try {
226
+ return await statsText({
227
+ liveSession: exec?.agent?.session ?? null,
228
+ sessionId,
229
+ scope: input.scope === 'raw' ? 'raw' : 'surface',
230
+ sessionQuery,
231
+ tokenMeter,
232
+ });
233
+ } catch (error) {
234
+ if (error instanceof SessionLogError) {
235
+ throw new Error(`session_log stats: ${error.message}`);
236
+ }
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ let snapshot;
242
+ try {
243
+ snapshot = await sessionQuery.readSession(sessionId);
244
+ } catch (error) {
245
+ throw new Error(
246
+ `session_log: cannot read session ${sessionId}: ${error?.message ?? error}. `
247
+ + 'The original may be unavailable; if so, say you only remember the gist.',
248
+ );
249
+ }
250
+ exec?.signal?.throwIfAborted?.();
251
+ const events = Array.isArray(snapshot?.events) ? snapshot.events : [];
252
+ const start = input.start ?? pointer.start ?? undefined;
253
+ const end = input.end ?? pointer.end ?? undefined;
254
+
255
+ try {
256
+ if (action === 'index') {
257
+ const text = buildIndex({ sessionId, events, limit: input.limit }).text;
258
+ let childRows = [];
259
+ try {
260
+ const home = join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity');
261
+ childRows = (await readAssetIndex({ home, sessionId }))
262
+ .filter((row) => row.owner === 'child')
263
+ .slice(-20)
264
+ .reverse();
265
+ } catch { /* child assets are optional */ }
266
+ return childRows.length === 0
267
+ ? text
268
+ : [text, 'child assets (use child_asset):', ...childRows.map((row) => assetMarker(row, 'child'))].join('\n');
269
+ }
270
+ if (action === 'get') {
271
+ let assets = [];
272
+ try {
273
+ const home = join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity');
274
+ assets = (await readAssetIndex({ home, sessionId }))
275
+ .filter((row) => row.owner === 'parent');
276
+ } catch { /* markers are optional */ }
277
+ return getRange({
278
+ sessionId,
279
+ events,
280
+ seq: input.seq,
281
+ start,
282
+ end,
283
+ context: input.context,
284
+ limit: input.limit,
285
+ assets,
286
+ }).text;
287
+ }
288
+ return grepRange({
289
+ sessionId,
290
+ events,
291
+ query: input.query,
292
+ context: input.context,
293
+ limit: input.limit,
294
+ }).text;
295
+ } catch (error) {
296
+ if (error instanceof SessionLogError) {
297
+ throw new Error(`session_log ${action}: ${error.message}`);
298
+ }
299
+ throw error;
300
+ }
301
+ },
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Preset-scoped plugin entry. The row lives inside the `continuity` preset, so
307
+ * `standard` never sees the tool.
308
+ */
309
+ export function apply(ctx) {
310
+ const tool = createSessionLogTool({ sessionQuery: ctx.sessionQuery, tokenMeter: ctx.tokenMeter });
311
+ ctx.effect(() => {
312
+ const dispose = ctx.tools.register(tool);
313
+ markSessionLogTool({ name: tool.name, scope: 'continuity preset' });
314
+ return dispose;
315
+ });
316
+ const assetTool = createSessionAssetTool({ sessionQuery: ctx.sessionQuery });
317
+ ctx.effect(() => ctx.tools.register(assetTool));
318
+ const childAssetTool = createChildAssetTool({ sessionQuery: ctx.sessionQuery });
319
+ ctx.effect(() => ctx.tools.register(childAssetTool));
320
+ const handoffTool = createHandoffTool();
321
+ ctx.effect(() => ctx.tools.register(handoffTool));
322
+ }