@try-works/dsh-memory 0.1.19 → 0.1.21

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 (55) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +585 -585
  3. package/STATUS.md +294 -287
  4. package/cordis.patch.yml +69 -69
  5. package/package.json +1 -1
  6. package/scripts/check-package.mjs +76 -76
  7. package/scripts/inspect-log.ts +40 -0
  8. package/scripts/repair-ignorable.ts +130 -0
  9. package/scripts/scan-sessions.ts +43 -0
  10. package/scripts/verify-log.ts +26 -0
  11. package/src/SYSTEM.template.md +311 -311
  12. package/src/capture.js +242 -242
  13. package/src/client/index.js +886 -886
  14. package/src/config.js +57 -57
  15. package/src/consolidate-run.js +132 -132
  16. package/src/consolidate.js +330 -330
  17. package/src/context.js +88 -88
  18. package/src/decisions.js +106 -106
  19. package/src/defaults.js +93 -93
  20. package/src/digest.js +255 -255
  21. package/src/entry.js +179 -179
  22. package/src/eval.js +72 -72
  23. package/src/files.js +60 -60
  24. package/src/fixture.js +56 -56
  25. package/src/forget.js +71 -71
  26. package/src/freshness.js +65 -65
  27. package/src/fts.js +166 -166
  28. package/src/git.js +53 -53
  29. package/src/history.js +226 -226
  30. package/src/host.js +884 -866
  31. package/src/index.js +115 -94
  32. package/src/inject.js +108 -108
  33. package/src/layers.js +178 -178
  34. package/src/ledger.js +19 -19
  35. package/src/map.js +184 -184
  36. package/src/models.js +136 -136
  37. package/src/queue.js +602 -602
  38. package/src/rank.js +31 -31
  39. package/src/recover.js +91 -91
  40. package/src/refresh.js +80 -80
  41. package/src/remember.js +58 -58
  42. package/src/rerank.js +90 -90
  43. package/src/search.js +75 -75
  44. package/src/select.js +57 -57
  45. package/src/session.js +60 -60
  46. package/src/state.js +240 -240
  47. package/src/store.js +128 -128
  48. package/src/summarize.js +195 -195
  49. package/src/system-doc.js +171 -171
  50. package/src/tasks.js +617 -617
  51. package/src/telemetry.js +122 -122
  52. package/src/tools.js +828 -818
  53. package/src/working.js +288 -288
  54. package/src/workspace-key.js +68 -68
  55. package/src/workspace.js +87 -87
package/src/telemetry.js CHANGED
@@ -1,122 +1,122 @@
1
- // Operational telemetry: a durable trail of what the memory system did, and what it cost.
2
- //
3
- // Two gaps this closes. First, the cadences (capture, consolidation, refresh, history ingest,
4
- // prune, sync) run unattended and previously left no trace: a pass that silently did nothing was
5
- // indistinguishable from one that silently failed. Second, nothing counted model calls, so the
6
- // running cost of memory was invisible.
7
- //
8
- // Both live in logs/console.log - derived, gitignored, and safe to delete. The counters are
9
- // deliberately plain JSON rather than a database: they must never be a reason a memory write can
10
- // fail.
11
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
12
- import { join } from 'node:path'
13
-
14
- /** Rotate the run log once it passes this size. */
15
- export const LOG_MAX_BYTES = 2 * 1024 * 1024
16
- /** Keep the activity feed to this many entries. */
17
- export const ACTIVITY_MAX = 200
18
-
19
- export function logsDir(root) { return join(root, 'logs') }
20
- export function runLogPath(root) { return join(logsDir(root), 'memory.log') }
21
- export function metricsPath(root) { return join(root, 'state', 'metrics.json') }
22
- export function activityPath(root) { return join(root, 'state', 'activity.jsonl') }
23
-
24
- /** One ISO timestamp, taken once per call so a record is internally consistent. */
25
- function nowIso(now) { return (now ?? new Date()).toISOString() }
26
-
27
- /**
28
- * Append one line to the run log, rotating first when it has grown too large.
29
- * Best-effort by design: telemetry must never break a memory operation.
30
- */
31
- export function logRun(root, event, detail = {}, options = {}) {
32
- try {
33
- mkdirSync(logsDir(root), { recursive: true })
34
- const path = runLogPath(root)
35
- try {
36
- if (existsSync(path) && statSync(path).size > (options.maxBytes ?? LOG_MAX_BYTES)) {
37
- renameSync(path, path + '.1')
38
- }
39
- } catch {}
40
- const line = JSON.stringify({ at: nowIso(options.now), event, ...detail })
41
- appendFileSync(path, line + '\n')
42
- return true
43
- } catch { return false }
44
- }
45
-
46
- /** Read the tail of the run log, newest last. */
47
- export function readRunLog(root, limit = 50) {
48
- try {
49
- const lines = readFileSync(runLogPath(root), 'utf8').split('\n').filter(Boolean)
50
- return lines.slice(-limit).map((line) => { try { return JSON.parse(line) } catch { return { raw: line } } })
51
- } catch { return [] }
52
- }
53
-
54
- /** An empty counter set. */
55
- function emptyMetrics() {
56
- return { since: undefined, events: {}, modelCalls: 0, modelPurpose: {}, savedBy: {}, lastAt: undefined }
57
- }
58
-
59
- export function readMetrics(root) {
60
- try { return { ...emptyMetrics(), ...JSON.parse(readFileSync(metricsPath(root), 'utf8')) } }
61
- catch { return emptyMetrics() }
62
- }
63
-
64
- function writeMetrics(root, value) {
65
- try {
66
- mkdirSync(join(root, 'state'), { recursive: true })
67
- writeFileSync(metricsPath(root), JSON.stringify(value, null, 2) + '\n')
68
- } catch {}
69
- }
70
-
71
- /**
72
- * Record one operational event.
73
- * @param root - store root.
74
- * @param event - a short event name, e.g. 'capture', 'consolidate', 'prune', 'sync'.
75
- * @param options.count - increment the event counter by this much (default 1).
76
- * @param options.modelCalls - model calls spent.
77
- * @param options.purpose - what a model call was for ('summary', 'consolidation', 'refresh').
78
- * @param options.saved - a call AVOIDED, by reason ('trivial', 'thin', 'no-pending').
79
- * @param options.detail - extra fields for the run log.
80
- * @param options.log - set false to skip the run-log line (default true).
81
- * @returns the record that was written.
82
- */
83
- export function record(root, event, options = {}) {
84
- const at = nowIso(options.now)
85
- const metrics = readMetrics(root)
86
- metrics.since = metrics.since ?? at
87
- metrics.lastAt = at
88
- metrics.events[event] = (metrics.events[event] ?? 0) + (options.count ?? 1)
89
- if (options.modelCalls) {
90
- metrics.modelCalls += options.modelCalls
91
- const purpose = options.purpose ?? 'other'
92
- metrics.modelPurpose[purpose] = (metrics.modelPurpose[purpose] ?? 0) + options.modelCalls
93
- }
94
- if (options.saved) metrics.savedBy[options.saved] = (metrics.savedBy[options.saved] ?? 0) + 1
95
- writeMetrics(root, metrics)
96
- if (options.log !== false) logRun(root, event, { ...(options.detail ?? {}), count: options.count ?? 1 })
97
- return { at, event, count: options.count ?? 1 }
98
- }
99
-
100
- /**
101
- * Append to the human-readable activity feed: what memory did, newest last.
102
- * Kept short and bounded, so it can be shown to an agent without flooding it.
103
- */
104
- export function noteActivity(root, text, options = {}) {
105
- try {
106
- mkdirSync(join(root, 'state'), { recursive: true })
107
- const line = JSON.stringify({ at: nowIso(options.now), text: String(text ?? '').slice(0, 300), ...(options.detail ?? {}) })
108
- appendFileSync(activityPath(root), line + '\n')
109
- // compact when it has clearly outgrown the cap
110
- const lines = readFileSync(activityPath(root), 'utf8').split('\n').filter(Boolean)
111
- if (lines.length > ACTIVITY_MAX * 2) writeFileSync(activityPath(root), lines.slice(-ACTIVITY_MAX).join('\n') + '\n')
112
- return true
113
- } catch { return false }
114
- }
115
-
116
- /** Recent activity, newest last. */
117
- export function readActivity(root, limit = ACTIVITY_MAX) {
118
- try {
119
- return readFileSync(activityPath(root), 'utf8').split('\n').filter(Boolean).slice(-limit)
120
- .map((line) => { try { return JSON.parse(line) } catch { return { text: line } } })
121
- } catch { return [] }
122
- }
1
+ // Operational telemetry: a durable trail of what the memory system did, and what it cost.
2
+ //
3
+ // Two gaps this closes. First, the cadences (capture, consolidation, refresh, history ingest,
4
+ // prune, sync) run unattended and previously left no trace: a pass that silently did nothing was
5
+ // indistinguishable from one that silently failed. Second, nothing counted model calls, so the
6
+ // running cost of memory was invisible.
7
+ //
8
+ // Both live in logs/console.log - derived, gitignored, and safe to delete. The counters are
9
+ // deliberately plain JSON rather than a database: they must never be a reason a memory write can
10
+ // fail.
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+
14
+ /** Rotate the run log once it passes this size. */
15
+ export const LOG_MAX_BYTES = 2 * 1024 * 1024
16
+ /** Keep the activity feed to this many entries. */
17
+ export const ACTIVITY_MAX = 200
18
+
19
+ export function logsDir(root) { return join(root, 'logs') }
20
+ export function runLogPath(root) { return join(logsDir(root), 'memory.log') }
21
+ export function metricsPath(root) { return join(root, 'state', 'metrics.json') }
22
+ export function activityPath(root) { return join(root, 'state', 'activity.jsonl') }
23
+
24
+ /** One ISO timestamp, taken once per call so a record is internally consistent. */
25
+ function nowIso(now) { return (now ?? new Date()).toISOString() }
26
+
27
+ /**
28
+ * Append one line to the run log, rotating first when it has grown too large.
29
+ * Best-effort by design: telemetry must never break a memory operation.
30
+ */
31
+ export function logRun(root, event, detail = {}, options = {}) {
32
+ try {
33
+ mkdirSync(logsDir(root), { recursive: true })
34
+ const path = runLogPath(root)
35
+ try {
36
+ if (existsSync(path) && statSync(path).size > (options.maxBytes ?? LOG_MAX_BYTES)) {
37
+ renameSync(path, path + '.1')
38
+ }
39
+ } catch {}
40
+ const line = JSON.stringify({ at: nowIso(options.now), event, ...detail })
41
+ appendFileSync(path, line + '\n')
42
+ return true
43
+ } catch { return false }
44
+ }
45
+
46
+ /** Read the tail of the run log, newest last. */
47
+ export function readRunLog(root, limit = 50) {
48
+ try {
49
+ const lines = readFileSync(runLogPath(root), 'utf8').split('\n').filter(Boolean)
50
+ return lines.slice(-limit).map((line) => { try { return JSON.parse(line) } catch { return { raw: line } } })
51
+ } catch { return [] }
52
+ }
53
+
54
+ /** An empty counter set. */
55
+ function emptyMetrics() {
56
+ return { since: undefined, events: {}, modelCalls: 0, modelPurpose: {}, savedBy: {}, lastAt: undefined }
57
+ }
58
+
59
+ export function readMetrics(root) {
60
+ try { return { ...emptyMetrics(), ...JSON.parse(readFileSync(metricsPath(root), 'utf8')) } }
61
+ catch { return emptyMetrics() }
62
+ }
63
+
64
+ function writeMetrics(root, value) {
65
+ try {
66
+ mkdirSync(join(root, 'state'), { recursive: true })
67
+ writeFileSync(metricsPath(root), JSON.stringify(value, null, 2) + '\n')
68
+ } catch {}
69
+ }
70
+
71
+ /**
72
+ * Record one operational event.
73
+ * @param root - store root.
74
+ * @param event - a short event name, e.g. 'capture', 'consolidate', 'prune', 'sync'.
75
+ * @param options.count - increment the event counter by this much (default 1).
76
+ * @param options.modelCalls - model calls spent.
77
+ * @param options.purpose - what a model call was for ('summary', 'consolidation', 'refresh').
78
+ * @param options.saved - a call AVOIDED, by reason ('trivial', 'thin', 'no-pending').
79
+ * @param options.detail - extra fields for the run log.
80
+ * @param options.log - set false to skip the run-log line (default true).
81
+ * @returns the record that was written.
82
+ */
83
+ export function record(root, event, options = {}) {
84
+ const at = nowIso(options.now)
85
+ const metrics = readMetrics(root)
86
+ metrics.since = metrics.since ?? at
87
+ metrics.lastAt = at
88
+ metrics.events[event] = (metrics.events[event] ?? 0) + (options.count ?? 1)
89
+ if (options.modelCalls) {
90
+ metrics.modelCalls += options.modelCalls
91
+ const purpose = options.purpose ?? 'other'
92
+ metrics.modelPurpose[purpose] = (metrics.modelPurpose[purpose] ?? 0) + options.modelCalls
93
+ }
94
+ if (options.saved) metrics.savedBy[options.saved] = (metrics.savedBy[options.saved] ?? 0) + 1
95
+ writeMetrics(root, metrics)
96
+ if (options.log !== false) logRun(root, event, { ...(options.detail ?? {}), count: options.count ?? 1 })
97
+ return { at, event, count: options.count ?? 1 }
98
+ }
99
+
100
+ /**
101
+ * Append to the human-readable activity feed: what memory did, newest last.
102
+ * Kept short and bounded, so it can be shown to an agent without flooding it.
103
+ */
104
+ export function noteActivity(root, text, options = {}) {
105
+ try {
106
+ mkdirSync(join(root, 'state'), { recursive: true })
107
+ const line = JSON.stringify({ at: nowIso(options.now), text: String(text ?? '').slice(0, 300), ...(options.detail ?? {}) })
108
+ appendFileSync(activityPath(root), line + '\n')
109
+ // compact when it has clearly outgrown the cap
110
+ const lines = readFileSync(activityPath(root), 'utf8').split('\n').filter(Boolean)
111
+ if (lines.length > ACTIVITY_MAX * 2) writeFileSync(activityPath(root), lines.slice(-ACTIVITY_MAX).join('\n') + '\n')
112
+ return true
113
+ } catch { return false }
114
+ }
115
+
116
+ /** Recent activity, newest last. */
117
+ export function readActivity(root, limit = ACTIVITY_MAX) {
118
+ try {
119
+ return readFileSync(activityPath(root), 'utf8').split('\n').filter(Boolean).slice(-limit)
120
+ .map((line) => { try { return JSON.parse(line) } catch { return { text: line } } })
121
+ } catch { return [] }
122
+ }