@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.
- package/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +294 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/scripts/inspect-log.ts +40 -0
- package/scripts/repair-ignorable.ts +130 -0
- package/scripts/scan-sessions.ts +43 -0
- package/scripts/verify-log.ts +26 -0
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +884 -866
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- 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
|
+
}
|