-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathread-digests.mjs
More file actions
146 lines (128 loc) · 5.01 KB
/
Copy pathread-digests.mjs
File metadata and controls
146 lines (128 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// read-digests.mjs — the READ side (the economic payoff).
//
// Given a project, assemble its consolidated digests into a compact
// session-start context block, and report the token economics vs. injecting
// raw observations (which is what an append-only store does today).
//
// Token counts are estimates (chars / 4) — fine for an order-of-magnitude
// comparison. For exact counts, call your provider's count_tokens.
//
// bun read-digests.mjs # list projects with digests
// bun read-digests.mjs my-app # context block + economics for my-app
//
// CONFIG (env)
// CLAUDE_MEM_DB path to the source observation store
// (default: ~/.claude-mem/claude-mem.db)
// DIGEST_DB path to the digest DB (default: ./digests.db)
import { Database } from "bun:sqlite";
import { homedir } from "os";
import { join } from "path";
const SOURCE_DB =
process.env.CLAUDE_MEM_DB || join(homedir(), ".claude-mem", "claude-mem.db");
const DIGEST_DB = process.env.DIGEST_DB || join(process.cwd(), "digests.db");
const PROJECT = process.argv[2] || null;
// Logical reading order + human labels for the memory-type facets.
const TYPE_ORDER = [
["why-it-exists", "Why things exist"],
["how-it-works", "How it works"],
["pattern", "Patterns"],
["decision", "Decisions"],
["trade-off", "Trade-offs"],
["problem-solution", "Problems & solutions"],
["gotcha", "Gotchas"],
["what-changed", "Recent changes"],
];
const estTokens = (s) => Math.ceil((s || "").length / 4);
const dg = new Database(DIGEST_DB, { readonly: true });
// No project given: list what's available.
if (!PROJECT) {
const rows = dg
.query(
`SELECT project, COUNT(*) cells, SUM(source_count) sources,
SUM(LENGTH(digest)) chars
FROM digests GROUP BY project ORDER BY sources DESC`
)
.all();
console.log("Projects with digests:\n");
console.log("project".padEnd(24) + "cells sources ~digest-tokens");
for (const r of rows) {
console.log(
r.project.padEnd(24) +
String(r.cells).padStart(5) +
String(r.sources).padStart(9) +
String(Math.ceil(r.chars / 4)).padStart(15)
);
}
console.log("\nRun: bun read-digests.mjs <project>");
dg.close();
process.exit(0);
}
const cells = dg
.query(
`SELECT type, digest, source_count, version
FROM digests WHERE project = ?`
)
.all(PROJECT);
if (cells.length === 0) {
console.error(`No digests for project "${PROJECT}". Run build-digests first.`);
process.exit(1);
}
const byType = new Map(cells.map((c) => [c.type, c]));
// ---------------------------------------------------------------------------
// THE INJECTABLE CONTEXT BLOCK — this is what would be fed at session start.
// ---------------------------------------------------------------------------
let block = `# Memory digest — ${PROJECT}\n`;
block += `_Consolidated understanding from prior sessions. Each section is an evolving digest, not a raw log._\n`;
let digestTokens = 0;
let digestSources = 0;
for (const [type, label] of TYPE_ORDER) {
const cell = byType.get(type);
if (!cell) continue;
block += `\n## ${label}\n${cell.digest}\n`;
digestTokens += estTokens(cell.digest);
digestSources += cell.source_count;
}
// ---------------------------------------------------------------------------
// ECONOMICS — digest vs. raw observations for this project.
// ---------------------------------------------------------------------------
const src = new Database(SOURCE_DB, { readonly: true });
const raw = src
.query(`SELECT title, narrative, facts FROM observations WHERE project = ?`)
.all(PROJECT);
let rawTokens = 0;
for (const r of raw) {
rawTokens += estTokens(r.title) + estTokens(r.narrative) + estTokens(r.facts);
}
// A common naive injection: up to 50 most-recent narratives.
const recent = src
.query(
`SELECT narrative FROM observations WHERE project = ?
ORDER BY created_at_epoch DESC LIMIT 50`
)
.all(PROJECT);
let recentTokens = 0;
for (const r of recent) recentTokens += estTokens(r.narrative);
// Emit the context block to stdout (the deliverable), economics to stderr.
console.log(block);
const pct = (n, d) => (d ? ((100 * n) / d).toFixed(1) + "%" : "—");
console.error(`\n${"=".repeat(64)}`);
console.error(`ECONOMICS — ${PROJECT} (token estimates, chars/4)`);
console.error("=".repeat(64));
console.error(
`Digest block: ~${digestTokens} tokens (${cells.length} cells, ${digestSources} source observations consolidated)`
);
console.error(
`All raw obs: ~${rawTokens} tokens (${raw.length} observations, full narrative+facts)`
);
console.error(
`Naive recent-50: ~${recentTokens} tokens (50 most-recent narratives)`
);
console.error("-".repeat(64));
console.error(
`Digest vs all-raw: ${pct(digestTokens, rawTokens)} of the tokens (${(rawTokens / Math.max(digestTokens, 1)).toFixed(1)}x compression)`
);
console.error(
`Digest vs recent-50: ${pct(digestTokens, recentTokens)} — and the digest covers ALL ${raw.length} observations, not just 50`
);
dg.close();
src.close();