eklavya 1.18.2 → 1.19.0
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/dist/assets/dashboard.html +559 -14
- package/dist/assets/tutor/references/focus-and-level.md +29 -0
- package/dist/cli.js +648 -20
- package/dist/cli.js.map +1 -1
- package/dist/config-path.js +156 -0
- package/dist/config-path.js.map +1 -0
- package/dist/config.js +231 -2
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +355 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/eval/retrieval-score.js +77 -0
- package/dist/eval/retrieval-score.js.map +1 -0
- package/dist/hooks/capture-tool.js +107 -0
- package/dist/hooks/capture-tool.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +5 -6
- package/dist/hooks/checkpoint-quiz.js.map +1 -1
- package/dist/hooks/lib.js +7 -33
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/memory-lib.js +181 -0
- package/dist/hooks/memory-lib.js.map +1 -0
- package/dist/hooks/prompt-submit-nudge.js +56 -20
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-start.js +73 -74
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +44 -7
- package/dist/hooks/stop-quiz-check.js.map +1 -1
- package/dist/install.js +37 -0
- package/dist/install.js.map +1 -1
- package/dist/memory/capture.js +123 -0
- package/dist/memory/capture.js.map +1 -0
- package/dist/memory/code.js +186 -0
- package/dist/memory/code.js.map +1 -0
- package/dist/memory/collections.js +87 -0
- package/dist/memory/collections.js.map +1 -0
- package/dist/memory/embed.js +81 -0
- package/dist/memory/embed.js.map +1 -0
- package/dist/memory/hosts.js +71 -0
- package/dist/memory/hosts.js.map +1 -0
- package/dist/memory/identity.js +74 -0
- package/dist/memory/identity.js.map +1 -0
- package/dist/memory/import.js +916 -0
- package/dist/memory/import.js.map +1 -0
- package/dist/memory/learning.js +148 -0
- package/dist/memory/learning.js.map +1 -0
- package/dist/memory/notify.js +220 -0
- package/dist/memory/notify.js.map +1 -0
- package/dist/memory/privacy.js +117 -0
- package/dist/memory/privacy.js.map +1 -0
- package/dist/memory/provider.js +155 -0
- package/dist/memory/provider.js.map +1 -0
- package/dist/memory/recall.js +327 -0
- package/dist/memory/recall.js.map +1 -0
- package/dist/memory/replay.js +174 -0
- package/dist/memory/replay.js.map +1 -0
- package/dist/memory/search.js +143 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/spool.js +93 -0
- package/dist/memory/spool.js.map +1 -0
- package/dist/memory/store.js +399 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/summarize.js +178 -0
- package/dist/memory/summarize.js.map +1 -0
- package/dist/memory/sync.js +595 -0
- package/dist/memory/sync.js.map +1 -0
- package/dist/memory/tokens.js +55 -0
- package/dist/memory/tokens.js.map +1 -0
- package/dist/memory/worker.js +207 -0
- package/dist/memory/worker.js.map +1 -0
- package/dist/migrations/009_memory.sql +226 -0
- package/dist/migrations/010_import.sql +26 -0
- package/dist/migrations/011_sync.sql +76 -0
- package/dist/migrations/012_job_backoff.sql +13 -0
- package/dist/migrations/013_batch_provenance.sql +18 -0
- package/dist/migrations/014_batch_events_index.sql +18 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/tutor.md +3 -1
- package/dist/plugin/hooks/CLAUDE.md +30 -6
- package/dist/plugin/hooks/hooks.json +10 -0
- package/dist/plugin/skills/CLAUDE.md +17 -7
- package/dist/plugin/skills/memory/SKILL.md +88 -0
- package/dist/plugin/skills/setup/SKILL.md +16 -2
- package/dist/plugin/skills/tutor/references/focus-and-level.md +29 -0
- package/dist/time.js +41 -0
- package/dist/time.js.map +1 -0
- package/dist/tools/code_tools.js +73 -0
- package/dist/tools/code_tools.js.map +1 -0
- package/dist/tools/collection_tools.js +92 -0
- package/dist/tools/collection_tools.js.map +1 -0
- package/dist/tools/config_tools.js +104 -1
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory_read_tools.js +277 -0
- package/dist/tools/memory_read_tools.js.map +1 -0
- package/dist/tools/memory_write_tools.js +115 -0
- package/dist/tools/memory_write_tools.js.map +1 -0
- package/dist/user-skill/eklavya/SKILL.md +64 -8
- package/package.json +2 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { identityFor } from './identity.js';
|
|
5
|
+
import { prepare } from './capture.js';
|
|
6
|
+
import { appendEvent } from './store.js';
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code stores transcripts under a directory named after the project
|
|
9
|
+
* path with every separator replaced by a dash.
|
|
10
|
+
*/
|
|
11
|
+
export function transcriptDirFor(cwd) {
|
|
12
|
+
const encoded = path.resolve(cwd).replace(/[/\\]/g, '-');
|
|
13
|
+
return path.join(os.homedir(), '.claude', 'projects', encoded);
|
|
14
|
+
}
|
|
15
|
+
export function transcriptsFor(cwd) {
|
|
16
|
+
const dir = transcriptDirFor(cwd);
|
|
17
|
+
try {
|
|
18
|
+
return fs
|
|
19
|
+
.readdirSync(dir)
|
|
20
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
21
|
+
.map((f) => path.join(dir, f))
|
|
22
|
+
.sort();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function textOf(content) {
|
|
29
|
+
if (typeof content === 'string')
|
|
30
|
+
return content;
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return '';
|
|
33
|
+
return content
|
|
34
|
+
.map((block) => {
|
|
35
|
+
if (!block || typeof block !== 'object')
|
|
36
|
+
return '';
|
|
37
|
+
const b = block;
|
|
38
|
+
return b.type === 'text' && typeof b.text === 'string' ? b.text : '';
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function toolUsesOf(content) {
|
|
44
|
+
if (!Array.isArray(content))
|
|
45
|
+
return [];
|
|
46
|
+
const uses = [];
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
if (!block || typeof block !== 'object')
|
|
49
|
+
continue;
|
|
50
|
+
const b = block;
|
|
51
|
+
if (b.type === 'tool_use' && typeof b.name === 'string') {
|
|
52
|
+
uses.push({ name: b.name, input: b.input ?? {} });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return uses;
|
|
56
|
+
}
|
|
57
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);
|
|
58
|
+
const READ_TOOLS = new Set(['Read', 'NotebookRead', 'Glob', 'Grep']);
|
|
59
|
+
function kindFor(tool) {
|
|
60
|
+
if (EDIT_TOOLS.has(tool))
|
|
61
|
+
return 'file_edit';
|
|
62
|
+
if (READ_TOOLS.has(tool))
|
|
63
|
+
return 'file_read';
|
|
64
|
+
return 'tool_use';
|
|
65
|
+
}
|
|
66
|
+
function bodyFor(input) {
|
|
67
|
+
const parts = [];
|
|
68
|
+
for (const key of ['command', 'description', 'prompt', 'pattern', 'query']) {
|
|
69
|
+
if (typeof input[key] === 'string')
|
|
70
|
+
parts.push(input[key]);
|
|
71
|
+
}
|
|
72
|
+
if (typeof input.old_string === 'string' && typeof input.new_string === 'string') {
|
|
73
|
+
parts.push(`- ${input.old_string.slice(0, 600)}`, `+ ${input.new_string.slice(0, 600)}`);
|
|
74
|
+
}
|
|
75
|
+
if (!parts.length) {
|
|
76
|
+
const keys = Object.keys(input).filter((k) => k !== 'content');
|
|
77
|
+
if (keys.length)
|
|
78
|
+
parts.push(keys.map((k) => `${k}=${String(input[k]).slice(0, 200)}`).join(' '));
|
|
79
|
+
}
|
|
80
|
+
return parts.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function filesFrom(input) {
|
|
83
|
+
return [input.file_path, input.notebook_path, input.path].filter((v) => typeof v === 'string' && v.length > 0);
|
|
84
|
+
}
|
|
85
|
+
function alreadyHave(db, sessionId, kind, tool, body) {
|
|
86
|
+
const row = db
|
|
87
|
+
.prepare(`SELECT 1 AS hit FROM evidence_events
|
|
88
|
+
WHERE session_id = ? AND kind = ? AND IFNULL(tool, '') = ? AND body = ? LIMIT 1`)
|
|
89
|
+
.get(sessionId, kind, tool ?? '', body);
|
|
90
|
+
return Boolean(row);
|
|
91
|
+
}
|
|
92
|
+
/** Replays one transcript file. Idempotent: running it twice captures nothing new. */
|
|
93
|
+
export function replayTranscript(db, config, file, opts = {}) {
|
|
94
|
+
const result = { file, read: 0, captured: 0, duplicates: 0, excluded: 0 };
|
|
95
|
+
let text;
|
|
96
|
+
try {
|
|
97
|
+
text = fs.readFileSync(file, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
const lines = text.split('\n').filter(Boolean).slice(0, opts.maxLines ?? 20_000);
|
|
103
|
+
for (const raw of lines) {
|
|
104
|
+
let line;
|
|
105
|
+
try {
|
|
106
|
+
line = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// A sidechain is a subagent's own transcript; the parent's line already
|
|
112
|
+
// records that the delegation happened, and replaying both files records
|
|
113
|
+
// the same work twice under two identities.
|
|
114
|
+
if (line.isSidechain)
|
|
115
|
+
continue;
|
|
116
|
+
if (line.type !== 'user' && line.type !== 'assistant')
|
|
117
|
+
continue;
|
|
118
|
+
result.read++;
|
|
119
|
+
const identity = identityFor({
|
|
120
|
+
cwd: line.cwd ?? opts.cwd ?? process.cwd(),
|
|
121
|
+
sessionId: line.sessionId ?? 'replay',
|
|
122
|
+
host: 'claude-code',
|
|
123
|
+
});
|
|
124
|
+
const events = [];
|
|
125
|
+
if (line.type === 'user') {
|
|
126
|
+
const prompt = textOf(line.message?.content);
|
|
127
|
+
// A user line carrying a tool result is the host echoing the agent's own
|
|
128
|
+
// work back, not the developer typing. Only a real prompt is one.
|
|
129
|
+
if (prompt.trim() && !line.toolUseResult) {
|
|
130
|
+
events.push({ kind: 'prompt', title: 'prompt', body: prompt, occurredAt: line.timestamp, source: 'replay' });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
for (const use of toolUsesOf(line.message?.content)) {
|
|
135
|
+
const body = bodyFor(use.input);
|
|
136
|
+
if (!body.trim())
|
|
137
|
+
continue;
|
|
138
|
+
events.push({
|
|
139
|
+
kind: kindFor(use.name),
|
|
140
|
+
tool: use.name,
|
|
141
|
+
title: use.name,
|
|
142
|
+
body,
|
|
143
|
+
files: filesFrom(use.input),
|
|
144
|
+
occurredAt: line.timestamp,
|
|
145
|
+
source: 'replay',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const event of events) {
|
|
150
|
+
const input = prepare(config, identity, event);
|
|
151
|
+
if (!input) {
|
|
152
|
+
result.excluded++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (alreadyHave(db, input.sessionId, input.kind, input.tool ?? null, input.body)) {
|
|
156
|
+
result.duplicates++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const { inserted } = appendEvent(db, input);
|
|
160
|
+
if (inserted)
|
|
161
|
+
result.captured++;
|
|
162
|
+
else
|
|
163
|
+
result.duplicates++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/** Replays every transcript Claude Code has for this checkout. */
|
|
169
|
+
export function replayProject(db, config, cwd, opts = {}) {
|
|
170
|
+
return transcriptsFor(cwd)
|
|
171
|
+
.slice(-(opts.limit ?? 20))
|
|
172
|
+
.map((file) => replayTranscript(db, config, file, { cwd }));
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/memory/replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { cosine, embedLocal, fromBlob, LOCAL_EMBEDDER } from './embed.js';
|
|
2
|
+
import { entriesByIds } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* FTS5 has a query language, and a developer's search box does not. Feeding
|
|
5
|
+
* `fix: auth (retry?)` straight to MATCH is a syntax error, and quoting the
|
|
6
|
+
* whole string turns a two-word search into a phrase search that matches almost
|
|
7
|
+
* nothing. So: split into terms, quote each one, and combine them.
|
|
8
|
+
*/
|
|
9
|
+
function ftsQuery(raw, join) {
|
|
10
|
+
const terms = raw
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.split(/[^\p{L}\p{N}_]+/u)
|
|
13
|
+
.filter((t) => t.length > 1)
|
|
14
|
+
.map((t) => `"${t.replace(/"/g, '')}"`);
|
|
15
|
+
if (!terms.length)
|
|
16
|
+
return null;
|
|
17
|
+
return terms.join(` ${join} `);
|
|
18
|
+
}
|
|
19
|
+
function scopeClause(filter, alias = 'e') {
|
|
20
|
+
const where = [`${alias}.deleted_at IS NULL`, `${alias}.superseded_by IS NULL`];
|
|
21
|
+
const args = [];
|
|
22
|
+
if (!filter.allProjects && filter.project) {
|
|
23
|
+
where.push(`${alias}.project = ?`);
|
|
24
|
+
args.push(filter.project);
|
|
25
|
+
}
|
|
26
|
+
if (filter.type) {
|
|
27
|
+
where.push(`${alias}.type = ?`);
|
|
28
|
+
args.push(filter.type);
|
|
29
|
+
}
|
|
30
|
+
if (filter.since) {
|
|
31
|
+
where.push(`${alias}.occurred_at >= ?`);
|
|
32
|
+
args.push(filter.since);
|
|
33
|
+
}
|
|
34
|
+
if (filter.until) {
|
|
35
|
+
where.push(`${alias}.occurred_at <= ?`);
|
|
36
|
+
args.push(filter.until);
|
|
37
|
+
}
|
|
38
|
+
if (filter.tag) {
|
|
39
|
+
where.push(`EXISTS (SELECT 1 FROM memory_entry_tags t WHERE t.entry_id = ${alias}.id AND t.tag = ?)`);
|
|
40
|
+
args.push(filter.tag.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
return { sql: where.join(' AND '), args };
|
|
43
|
+
}
|
|
44
|
+
export function keywordSearch(db, query, filter = {}) {
|
|
45
|
+
const limit = filter.limit ?? 20;
|
|
46
|
+
const scope = scopeClause(filter);
|
|
47
|
+
const run = (match) => {
|
|
48
|
+
const rows = db
|
|
49
|
+
.prepare(`SELECT e.*, bm25(memory_fts) AS rank
|
|
50
|
+
FROM memory_fts
|
|
51
|
+
JOIN memory_entries e ON e.id = memory_fts.rowid
|
|
52
|
+
WHERE memory_fts MATCH ? AND ${scope.sql}
|
|
53
|
+
ORDER BY rank LIMIT ?`)
|
|
54
|
+
.all(match, ...scope.args, limit);
|
|
55
|
+
// bm25 returns a negative score where more negative is better; flip it so
|
|
56
|
+
// every mode in this module speaks "higher is better".
|
|
57
|
+
return rows.map(({ rank, ...entry }) => ({ entry: entry, score: -rank, via: 'keyword' }));
|
|
58
|
+
};
|
|
59
|
+
const andQuery = ftsQuery(query, 'AND');
|
|
60
|
+
if (!andQuery)
|
|
61
|
+
return [];
|
|
62
|
+
const exact = run(andQuery);
|
|
63
|
+
if (exact.length || filter.strict)
|
|
64
|
+
return exact;
|
|
65
|
+
// Nothing matched every term. Widen rather than return nothing: a developer
|
|
66
|
+
// searching "refresh cookie rotation" wants the rotation note even if the
|
|
67
|
+
// wording differs.
|
|
68
|
+
const orQuery = ftsQuery(query, 'OR');
|
|
69
|
+
return orQuery ? run(orQuery) : [];
|
|
70
|
+
}
|
|
71
|
+
/** Bounded scan: cosine over the project's most recent vectors (ADR-03). */
|
|
72
|
+
export function semanticSearch(db, query, filter = {}, scanLimit = 5000) {
|
|
73
|
+
const scope = scopeClause(filter);
|
|
74
|
+
const rows = db
|
|
75
|
+
.prepare(`SELECT e.id, v.vec FROM memory_entries e
|
|
76
|
+
JOIN memory_vectors v ON v.entry_id = e.id AND v.embedder_id = ?
|
|
77
|
+
WHERE ${scope.sql}
|
|
78
|
+
ORDER BY e.occurred_at DESC LIMIT ?`)
|
|
79
|
+
.all(LOCAL_EMBEDDER, ...scope.args, scanLimit);
|
|
80
|
+
if (!rows.length)
|
|
81
|
+
return [];
|
|
82
|
+
const q = embedLocal(query);
|
|
83
|
+
const scored = rows
|
|
84
|
+
.map((r) => ({ id: r.id, score: cosine(q, fromBlob(r.vec)) }))
|
|
85
|
+
.filter((r) => r.score > 0.05)
|
|
86
|
+
.sort((a, b) => b.score - a.score)
|
|
87
|
+
.slice(0, filter.limit ?? 20);
|
|
88
|
+
const entries = new Map(entriesByIds(db, scored.map((s) => s.id)).map((e) => [e.id, e]));
|
|
89
|
+
return scored
|
|
90
|
+
.map((s) => ({ entry: entries.get(s.id), score: s.score, via: 'semantic' }))
|
|
91
|
+
.filter((h) => h.entry);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reciprocal rank fusion of the two lists.
|
|
95
|
+
*
|
|
96
|
+
* RRF rather than a weighted sum of scores because bm25 and cosine are not on a
|
|
97
|
+
* common scale, and normalising them per query invents a calibration the data
|
|
98
|
+
* does not support. Rank is the only thing the two agree on.
|
|
99
|
+
*/
|
|
100
|
+
export function hybridSearch(db, query, filter = {}) {
|
|
101
|
+
const limit = filter.limit ?? 20;
|
|
102
|
+
const wide = { ...filter, limit: Math.max(limit * 3, 30) };
|
|
103
|
+
const keyword = keywordSearch(db, query, wide);
|
|
104
|
+
const semantic = semanticSearch(db, query, wide);
|
|
105
|
+
const K = 60;
|
|
106
|
+
const fused = new Map();
|
|
107
|
+
const add = (hits) => {
|
|
108
|
+
hits.forEach((hit, index) => {
|
|
109
|
+
const prev = fused.get(hit.entry.id);
|
|
110
|
+
const contribution = 1 / (K + index + 1);
|
|
111
|
+
if (prev)
|
|
112
|
+
prev.score += contribution;
|
|
113
|
+
else
|
|
114
|
+
fused.set(hit.entry.id, { entry: hit.entry, score: contribution });
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
add(keyword);
|
|
118
|
+
add(semantic);
|
|
119
|
+
return [...fused.values()]
|
|
120
|
+
.sort((a, b) => b.score - a.score)
|
|
121
|
+
.slice(0, limit)
|
|
122
|
+
.map((f) => ({ entry: f.entry, score: f.score, via: 'hybrid' }));
|
|
123
|
+
}
|
|
124
|
+
export function search(db, query, mode, filter = {}) {
|
|
125
|
+
if (!query.trim())
|
|
126
|
+
return [];
|
|
127
|
+
if (mode === 'keyword')
|
|
128
|
+
return keywordSearch(db, query, filter);
|
|
129
|
+
if (mode === 'semantic')
|
|
130
|
+
return semanticSearch(db, query, filter);
|
|
131
|
+
return hybridSearch(db, query, filter);
|
|
132
|
+
}
|
|
133
|
+
/** Every entry that touched a file, newest first (PRD RET-01 file history). */
|
|
134
|
+
export function fileHistory(db, file, filter = {}) {
|
|
135
|
+
const scope = scopeClause(filter);
|
|
136
|
+
return db
|
|
137
|
+
.prepare(`SELECT e.* FROM memory_entries e
|
|
138
|
+
WHERE ${scope.sql} AND e.files IS NOT NULL
|
|
139
|
+
AND EXISTS (SELECT 1 FROM json_each(e.files) WHERE json_each.value LIKE ?)
|
|
140
|
+
ORDER BY e.occurred_at DESC LIMIT ?`)
|
|
141
|
+
.all(...scope.args, `%${file}%`, filter.limit ?? 20);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/memory/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { eklavyaHome } from '../paths.js';
|
|
4
|
+
import { nowIso } from '../time.js';
|
|
5
|
+
/**
|
|
6
|
+
* The bounded local spool (PRD CAP-03).
|
|
7
|
+
*
|
|
8
|
+
* The database is normally available — the hooks open it directly — but "the
|
|
9
|
+
* disk is full", "another writer holds the lock past the busy timeout" and "the
|
|
10
|
+
* file is mid-upgrade" all happen, and a session must keep working through
|
|
11
|
+
* them. Events that cannot be committed are appended here as JSON lines and
|
|
12
|
+
* replayed on the next healthy open.
|
|
13
|
+
*
|
|
14
|
+
* Bounded, and honest about the bound: past `MAX_BYTES` new events are dropped
|
|
15
|
+
* and counted rather than allowed to fill the disk that is already full. A
|
|
16
|
+
* dropped event is reported by `eklavya doctor`, never silently promised.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
export function spoolPath() {
|
|
20
|
+
return path.join(eklavyaHome(), 'spool', 'events.jsonl');
|
|
21
|
+
}
|
|
22
|
+
function droppedPath() {
|
|
23
|
+
return path.join(eklavyaHome(), 'spool', 'dropped.json');
|
|
24
|
+
}
|
|
25
|
+
export function spoolEvent(record) {
|
|
26
|
+
const file = spoolPath();
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
30
|
+
if (size > MAX_BYTES) {
|
|
31
|
+
noteDropped();
|
|
32
|
+
return 'dropped';
|
|
33
|
+
}
|
|
34
|
+
// One `appendFileSync` of a single line under the pipe buffer is atomic
|
|
35
|
+
// enough for concurrent hook processes: neither interleaves a partial line.
|
|
36
|
+
fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
37
|
+
return 'spooled';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'dropped';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function noteDropped() {
|
|
44
|
+
try {
|
|
45
|
+
const file = droppedPath();
|
|
46
|
+
const prev = fs.existsSync(file)
|
|
47
|
+
? JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
48
|
+
: {};
|
|
49
|
+
fs.writeFileSync(file, JSON.stringify({ count: (prev.count ?? 0) + 1, last: nowIso() }), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// The spool is already the degraded path; a failure to count a drop is not
|
|
53
|
+
// worth taking a session down for.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function droppedCount() {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(droppedPath(), 'utf8'));
|
|
59
|
+
return parsed.count ?? 0;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads and clears the spool. The rename-then-read order matters: a hook
|
|
67
|
+
* writing concurrently appends to a fresh file rather than to the one being
|
|
68
|
+
* replayed, so nothing is read twice and nothing written during the replay is
|
|
69
|
+
* lost. Idempotent appends downstream make a crash mid-replay harmless.
|
|
70
|
+
*/
|
|
71
|
+
export function takeSpooled() {
|
|
72
|
+
const file = spoolPath();
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(file))
|
|
75
|
+
return [];
|
|
76
|
+
const taken = `${file}.replay-${process.pid}`;
|
|
77
|
+
fs.renameSync(file, taken);
|
|
78
|
+
const lines = fs.readFileSync(taken, 'utf8').split('\n').filter(Boolean);
|
|
79
|
+
fs.rmSync(taken, { force: true });
|
|
80
|
+
return lines.flatMap((line) => {
|
|
81
|
+
try {
|
|
82
|
+
return [JSON.parse(line)];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=spool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spool.js","sourceRoot":"","sources":["../../src/memory/spool.ts"],"names":[],"mappings":"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"}
|