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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token accounting for the savings ledger (PRD MET-01).
|
|
3
|
+
*
|
|
4
|
+
* The contract is not "be exact" — it is "be the same method on both sides of
|
|
5
|
+
* the subtraction, and say which method it was". A receipt stores `method`, and
|
|
6
|
+
* a percentage is only computed from two counts that share it. That is what
|
|
7
|
+
* stops the classic dishonest number: a generous estimate of what was avoided
|
|
8
|
+
* against a tight count of what was sent.
|
|
9
|
+
*/
|
|
10
|
+
/** Identity of the estimator, stored on every receipt. */
|
|
11
|
+
export const ESTIMATOR = 'chars4-v1';
|
|
12
|
+
/**
|
|
13
|
+
* Four characters per token, the usual English-code approximation. It is an
|
|
14
|
+
* estimate and every surface that shows a number derived from it says so.
|
|
15
|
+
*/
|
|
16
|
+
export function estimateTokens(text) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return 0;
|
|
19
|
+
return Math.ceil(text.length / 4);
|
|
20
|
+
}
|
|
21
|
+
export function estimateTokensOf(parts) {
|
|
22
|
+
return estimateTokens(parts.filter(Boolean).join('\n'));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The one place the percentage is computed. Every caller — banner, dashboard,
|
|
26
|
+
* CLI — goes through it, so they cannot drift into three different arithmetics.
|
|
27
|
+
*
|
|
28
|
+
* Negative saving is reported as overhead rather than clamped to zero: reuse
|
|
29
|
+
* that costs more than it saves is a real outcome and hiding it is how a
|
|
30
|
+
* measurement becomes marketing.
|
|
31
|
+
*/
|
|
32
|
+
export function savingsFrom(input) {
|
|
33
|
+
const { baseTokens: base, deliveredTokens: delivered, delivery } = input;
|
|
34
|
+
if (base <= 0 && delivered <= 0)
|
|
35
|
+
return { kind: 'none' };
|
|
36
|
+
if (delivery !== 'confirmed' || base <= 0)
|
|
37
|
+
return { kind: 'unavailable' };
|
|
38
|
+
if (delivered > base)
|
|
39
|
+
return { kind: 'overhead', tokens: delivered - base, base, delivered };
|
|
40
|
+
return { kind: 'saving', percent: Math.round((100 * (base - delivered)) / base), base, delivered };
|
|
41
|
+
}
|
|
42
|
+
/** The banner line for a savings result (UX-01 wording). */
|
|
43
|
+
export function savingsLine(s) {
|
|
44
|
+
switch (s.kind) {
|
|
45
|
+
case 'none':
|
|
46
|
+
return 'Your savings: — no context reused yet';
|
|
47
|
+
case 'unavailable':
|
|
48
|
+
return 'Your savings: — unavailable';
|
|
49
|
+
case 'overhead':
|
|
50
|
+
return `Reuse overhead: ${s.tokens} tokens (estimated)`;
|
|
51
|
+
case 'saving':
|
|
52
|
+
return `Your savings: ${s.percent}% less context from reuse (estimated)`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/memory/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AAErC;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAA+B;IAC5D,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAoC;IACnE,OAAO,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC;AAcD;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAmB;IAC7C,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACzE,IAAI,IAAI,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACzD,IAAI,QAAQ,KAAK,WAAW,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAC1E,IAAI,SAAS,GAAG,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC7F,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AACrG,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,uCAAuC,CAAC;QACjD,KAAK,aAAa;YAChB,OAAO,6BAA6B,CAAC;QACvC,KAAK,UAAU;YACb,OAAO,mBAAmB,CAAC,CAAC,MAAM,qBAAqB,CAAC;QAC1D,KAAK,QAAQ;YACX,OAAO,iBAAiB,CAAC,CAAC,OAAO,uCAAuC,CAAC;IAC7E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { nowIso } from '../time.js';
|
|
3
|
+
import { ProviderError, ProviderSummarizer } from './provider.js';
|
|
4
|
+
import { LocalSummarizer, summarizeSession } from './summarize.js';
|
|
5
|
+
import { addCandidate, batchById, batchEvents, batchSession, claimJob, failJob, finishJob, insertEntry, replaceEntry, timeline, } from './store.js';
|
|
6
|
+
/**
|
|
7
|
+
* The job worker (ADR-05).
|
|
8
|
+
*
|
|
9
|
+
* There is no daemon. Hooks and the CLI call `processPending` at the seams
|
|
10
|
+
* where work naturally arrives, under a lease that makes concurrent callers
|
|
11
|
+
* safe. The failure this avoids is the reference's worst class: an orphaned
|
|
12
|
+
* background process holding a port, restarted by self-healing after the
|
|
13
|
+
* developer uninstalled it.
|
|
14
|
+
*
|
|
15
|
+
* `processPending` is bounded by `maxJobs` and never throws: a memory failure
|
|
16
|
+
* must not take a coding session with it.
|
|
17
|
+
*/
|
|
18
|
+
export function summarizerFor(config) {
|
|
19
|
+
return config.providers.observer ? new ProviderSummarizer(config.providers.observer) : new LocalSummarizer();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What governed one summarisation run (PRD MEM-01: "a versioned input batch and
|
|
23
|
+
* prompt/config identity").
|
|
24
|
+
*
|
|
25
|
+
* `generator` on an entry names the summariser, but only after the fact and
|
|
26
|
+
* only for a batch that produced something — a batch that came out wrong, or
|
|
27
|
+
* empty, leaves no row to ask. Recorded on the batch, it is what makes "re-run
|
|
28
|
+
* this batch against the prompt that produced it" an answerable question.
|
|
29
|
+
*
|
|
30
|
+
* The digest covers the three settings that change what a summary comes out as:
|
|
31
|
+
* what capture accepted, what redaction removed from it, and which model read
|
|
32
|
+
* it. `ProviderConfig` names an environment variable rather than carrying a key
|
|
33
|
+
* (SEC-01), so nothing secret is hashed.
|
|
34
|
+
*
|
|
35
|
+
* ponytail: the provider's system prompt is versioned by `provider.ts` alone —
|
|
36
|
+
* `summarizer.id` carries the model, not the prompt. Fold a prompt version into
|
|
37
|
+
* `ProviderSummarizer.id` once that prompt starts changing between releases.
|
|
38
|
+
*/
|
|
39
|
+
function configDigest(config) {
|
|
40
|
+
return crypto
|
|
41
|
+
.createHash('sha256')
|
|
42
|
+
.update(JSON.stringify([config.memory, config.privacy, config.providers.observer]))
|
|
43
|
+
.digest('hex')
|
|
44
|
+
.slice(0, 12);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The roll-up's own generator id, versioned like any other summariser, so an
|
|
48
|
+
* entry always says which code wrote it.
|
|
49
|
+
*/
|
|
50
|
+
const SESSION_SUMMARY_GENERATOR = 'session-rollup-v1';
|
|
51
|
+
/**
|
|
52
|
+
* Writes — or refreshes — this session's summary. Returns its entry id, or null
|
|
53
|
+
* when the session has nothing worth summarising.
|
|
54
|
+
*
|
|
55
|
+
* Called at the session seam, which fires at the end of every turn rather than
|
|
56
|
+
* once at the end of a session, so the row is rewritten in place as the session
|
|
57
|
+
* grows instead of one summary being written per turn.
|
|
58
|
+
*
|
|
59
|
+
* ponytail: with `providers.observer` set, the seam only *queues* the batch, so
|
|
60
|
+
* the last turn's observations land after this ran and the summary trails them
|
|
61
|
+
* by one turn until the next seam. Move the call after `processPending` if the
|
|
62
|
+
* provider path ever stops being asynchronous.
|
|
63
|
+
*/
|
|
64
|
+
export function writeSessionSummary(db, project, sessionId) {
|
|
65
|
+
// Bounded: the oldest observations of a day-long session are not where it
|
|
66
|
+
// left off, and an unbounded roll-up is an unbounded row in every recall.
|
|
67
|
+
const observations = timeline(db, { project, sessionId, kind: 'observation', limit: 24 });
|
|
68
|
+
const draft = summarizeSession(observations);
|
|
69
|
+
if (!draft)
|
|
70
|
+
return null;
|
|
71
|
+
const fields = {
|
|
72
|
+
project,
|
|
73
|
+
sessionId,
|
|
74
|
+
kind: 'session_summary',
|
|
75
|
+
type: draft.type,
|
|
76
|
+
title: draft.title,
|
|
77
|
+
narrative: draft.narrative,
|
|
78
|
+
facts: draft.facts,
|
|
79
|
+
files: draft.files,
|
|
80
|
+
tags: draft.tags,
|
|
81
|
+
generator: SESSION_SUMMARY_GENERATOR,
|
|
82
|
+
confidence: draft.confidence,
|
|
83
|
+
// The newest observation's time, so the summary sorts above the work it
|
|
84
|
+
// covers instead of below the session's first batch.
|
|
85
|
+
occurredAt: observations[0].occurred_at,
|
|
86
|
+
eventIds: draft.eventIds,
|
|
87
|
+
};
|
|
88
|
+
const existing = timeline(db, { project, sessionId, kind: 'session_summary', limit: 1 })[0];
|
|
89
|
+
if (existing) {
|
|
90
|
+
replaceEntry(db, existing.id, fields);
|
|
91
|
+
return existing.id;
|
|
92
|
+
}
|
|
93
|
+
return insertEntry(db, fields);
|
|
94
|
+
}
|
|
95
|
+
export async function processPending(db, config, opts = {}) {
|
|
96
|
+
const owner = opts.owner ?? `${process.pid}-${crypto.randomUUID().slice(0, 8)}`;
|
|
97
|
+
const maxJobs = opts.maxJobs ?? 3;
|
|
98
|
+
const summarizer = summarizerFor(config);
|
|
99
|
+
const result = { processed: 0, entries: 0, failed: 0, skipped: 0 };
|
|
100
|
+
for (let i = 0; i < maxJobs; i++) {
|
|
101
|
+
const job = claimJob(db, owner);
|
|
102
|
+
if (!job)
|
|
103
|
+
break;
|
|
104
|
+
const batch = batchById(db, job.batch_id);
|
|
105
|
+
const events = batchEvents(db, job.batch_id);
|
|
106
|
+
if (!batch || events.length === 0) {
|
|
107
|
+
// A batch with no events cannot produce anything and will never succeed.
|
|
108
|
+
finishJob(db, job.id, owner);
|
|
109
|
+
result.skipped++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Before the call, not after it: a batch that fails is the one whose
|
|
113
|
+
// provenance someone comes looking for.
|
|
114
|
+
db.prepare('UPDATE memory_batches SET summarizer = ?, config_digest = ? WHERE id = ?').run(summarizer.id, configDigest(config), batch.id);
|
|
115
|
+
try {
|
|
116
|
+
const drafts = await summarizer.summarize({
|
|
117
|
+
project: batch.project,
|
|
118
|
+
sessionId: batch.session_id,
|
|
119
|
+
events,
|
|
120
|
+
});
|
|
121
|
+
// One transaction for the entries, the candidates and the event status:
|
|
122
|
+
// a crash between them would leave events marked done with nothing to
|
|
123
|
+
// show for them, which is the one loss the batch cannot recover from.
|
|
124
|
+
db.transaction(() => {
|
|
125
|
+
for (const draft of drafts) {
|
|
126
|
+
const entryId = insertEntry(db, {
|
|
127
|
+
project: batch.project,
|
|
128
|
+
sessionId: batch.session_id,
|
|
129
|
+
batchId: batch.id,
|
|
130
|
+
type: draft.type,
|
|
131
|
+
title: draft.title,
|
|
132
|
+
narrative: draft.narrative,
|
|
133
|
+
facts: draft.facts,
|
|
134
|
+
files: draft.files,
|
|
135
|
+
tags: draft.tags,
|
|
136
|
+
generator: summarizer.id,
|
|
137
|
+
confidence: draft.confidence,
|
|
138
|
+
occurredAt: events[0].occurred_at,
|
|
139
|
+
eventIds: draft.eventIds,
|
|
140
|
+
});
|
|
141
|
+
result.entries++;
|
|
142
|
+
for (const concept of draft.concepts ?? []) {
|
|
143
|
+
addCandidate(db, {
|
|
144
|
+
entryId,
|
|
145
|
+
slug: concept.slug,
|
|
146
|
+
name: concept.name,
|
|
147
|
+
domain: concept.domain,
|
|
148
|
+
confidence: draft.confidence,
|
|
149
|
+
project: batch.project,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
db.prepare("UPDATE evidence_events SET status = 'summarized' WHERE batch_id = ?").run(batch.id);
|
|
154
|
+
})();
|
|
155
|
+
finishJob(db, job.id, owner);
|
|
156
|
+
result.processed++;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const errorClass = error instanceof ProviderError ? error.errorClass : 'transient';
|
|
160
|
+
failJob(db, job.id, owner, errorClass, error instanceof Error ? error.message : String(error));
|
|
161
|
+
result.failed++;
|
|
162
|
+
// A paused provider will fail the next job the same way; stop rather than
|
|
163
|
+
// burn the remaining budget re-learning that the key is rejected.
|
|
164
|
+
if (errorClass === 'auth' || errorClass === 'quota')
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The session seam: close the open batch and drain the queue.
|
|
172
|
+
*
|
|
173
|
+
* Called by the Stop hook and by `eklavya memory flush`. Batching at the seam
|
|
174
|
+
* rather than per tool call is what keeps one provider request from riding on
|
|
175
|
+
* every edit (PRD MEM-01).
|
|
176
|
+
*/
|
|
177
|
+
export async function flushSession(db, config, project, sessionId, reason = 'session_seam') {
|
|
178
|
+
batchSession(db, { project, sessionId, reason, maxEvents: config.memory.batch_max_events });
|
|
179
|
+
return processPending(db, config, { maxJobs: 4 });
|
|
180
|
+
}
|
|
181
|
+
/** Age of the oldest unprocessed job, for the health surfaces. */
|
|
182
|
+
export function queueDepth(db) {
|
|
183
|
+
const row = db
|
|
184
|
+
.prepare(`SELECT
|
|
185
|
+
COALESCE(SUM(CASE WHEN status IN ('pending','claimed') THEN 1 ELSE 0 END), 0) AS pending,
|
|
186
|
+
COALESCE(SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END), 0) AS paused,
|
|
187
|
+
COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) AS failed,
|
|
188
|
+
MIN(CASE WHEN status IN ('pending','claimed') THEN created_at END) AS oldest
|
|
189
|
+
FROM memory_jobs`)
|
|
190
|
+
.get();
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
/** Retention sweep (PRD SEC-02). Raw evidence ages out; entries are kept. */
|
|
194
|
+
export function pruneEvidence(db, config) {
|
|
195
|
+
const days = config.memory.retention_days;
|
|
196
|
+
if (!days)
|
|
197
|
+
return 0;
|
|
198
|
+
const cutoff = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
199
|
+
const info = db
|
|
200
|
+
.prepare(`DELETE FROM evidence_events
|
|
201
|
+
WHERE occurred_at < ? AND status = 'summarized'
|
|
202
|
+
AND id NOT IN (SELECT event_id FROM memory_entry_events)`)
|
|
203
|
+
.run(cutoff);
|
|
204
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('memory_pruned_at', ?)").run(nowIso());
|
|
205
|
+
return info.changes;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/memory/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
-- Memory: the evidence half of the unified plugin (.plan/unified-memory-learning).
|
|
2
|
+
--
|
|
3
|
+
-- Learning already records what the developer was *asked* and what they knew.
|
|
4
|
+
-- These tables record what actually *happened* -- the prompts, tool uses, edits
|
|
5
|
+
-- and failures a session produced -- and the observations distilled from them.
|
|
6
|
+
-- They live in the same file as the learning tables (ADR-02) so one transaction
|
|
7
|
+
-- can say "this evidence produced this concept candidate", and they hold no
|
|
8
|
+
-- foreign key into learning except that one explicit link table.
|
|
9
|
+
--
|
|
10
|
+
-- Nothing here is trusted instruction material. Everything an observer or an
|
|
11
|
+
-- import writes is derived data with provenance, quoted back to the model as
|
|
12
|
+
-- evidence and never as a system prompt (PRD SEC-01).
|
|
13
|
+
|
|
14
|
+
-- Raw accepted evidence. One row per host event that survived exclusion and
|
|
15
|
+
-- sanitisation. `event_uid` is the idempotency key: hooks and transcript replay
|
|
16
|
+
-- both converge here and must not record the same turn twice (PRD CAP-02).
|
|
17
|
+
CREATE TABLE IF NOT EXISTS evidence_events (
|
|
18
|
+
id INTEGER PRIMARY KEY,
|
|
19
|
+
event_uid TEXT NOT NULL UNIQUE,
|
|
20
|
+
project TEXT NOT NULL, -- projectKey: worktrees folded into their checkout
|
|
21
|
+
checkout TEXT, -- the unfolded root, so a worktree stays distinguishable
|
|
22
|
+
session_id TEXT NOT NULL,
|
|
23
|
+
agent_id TEXT, -- subagent identity, NULL for the main thread
|
|
24
|
+
host TEXT NOT NULL DEFAULT 'claude-code',
|
|
25
|
+
source TEXT NOT NULL DEFAULT 'hook', -- 'hook' | 'replay' | 'manual' | 'import'
|
|
26
|
+
kind TEXT NOT NULL, -- prompt|tool_use|tool_error|file_edit|file_read|assistant|lifecycle|note
|
|
27
|
+
tool TEXT,
|
|
28
|
+
title TEXT,
|
|
29
|
+
body TEXT NOT NULL DEFAULT '',
|
|
30
|
+
files TEXT, -- JSON array of repo-relative paths
|
|
31
|
+
occurred_at TEXT NOT NULL, -- when it happened on the host
|
|
32
|
+
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
redacted INTEGER NOT NULL DEFAULT 0, -- 1 when the privacy filter removed something
|
|
34
|
+
batch_id INTEGER, -- set once a batch claims it
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'accepted' -- accepted|batched|summarized
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- The immutable input to one observation job. A provider request must be
|
|
39
|
+
-- reconstructible from storage after a crash, not from a lost in-memory
|
|
40
|
+
-- conversation (PRD DATA-03), so the batch is written before the job runs.
|
|
41
|
+
CREATE TABLE IF NOT EXISTS memory_batches (
|
|
42
|
+
id INTEGER PRIMARY KEY,
|
|
43
|
+
project TEXT NOT NULL,
|
|
44
|
+
session_id TEXT NOT NULL,
|
|
45
|
+
reason TEXT NOT NULL, -- 'size' | 'time' | 'session_seam' | 'manual'
|
|
46
|
+
event_count INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
byte_count INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Durable jobs with claim leases. A crashed worker's lease expires and the job
|
|
52
|
+
-- is retried; `attempts` and `last_error` classify why, so a permanent failure
|
|
53
|
+
-- (bad auth) is distinguishable from a transient one (timeout).
|
|
54
|
+
CREATE TABLE IF NOT EXISTS memory_jobs (
|
|
55
|
+
id INTEGER PRIMARY KEY,
|
|
56
|
+
batch_id INTEGER NOT NULL REFERENCES memory_batches(id) ON DELETE CASCADE,
|
|
57
|
+
kind TEXT NOT NULL DEFAULT 'summarize',
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending|claimed|done|failed|paused
|
|
59
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
lease_owner TEXT,
|
|
61
|
+
lease_until TEXT,
|
|
62
|
+
last_error TEXT,
|
|
63
|
+
error_class TEXT, -- transient|auth|quota|overflow|malformed|permanent
|
|
64
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
65
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- An observation: the distilled, searchable unit of memory. `kind` separates a
|
|
69
|
+
-- generated observation from a session summary and from a note the developer
|
|
70
|
+
-- wrote by hand. Corrections supersede rather than overwrite, so the audit
|
|
71
|
+
-- trail survives (PRD MEM-03).
|
|
72
|
+
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
73
|
+
id INTEGER PRIMARY KEY,
|
|
74
|
+
entry_uid TEXT NOT NULL UNIQUE,
|
|
75
|
+
project TEXT NOT NULL,
|
|
76
|
+
session_id TEXT,
|
|
77
|
+
batch_id INTEGER REFERENCES memory_batches(id) ON DELETE SET NULL,
|
|
78
|
+
kind TEXT NOT NULL DEFAULT 'observation', -- observation|session_summary|note
|
|
79
|
+
type TEXT, -- bugfix|feature|refactor|decision|discovery|...
|
|
80
|
+
title TEXT NOT NULL,
|
|
81
|
+
narrative TEXT NOT NULL DEFAULT '',
|
|
82
|
+
facts TEXT, -- JSON array of strings
|
|
83
|
+
files TEXT, -- JSON array of repo-relative paths
|
|
84
|
+
generator TEXT NOT NULL DEFAULT 'local', -- summarizer id, e.g. 'local-v1' or a provider id
|
|
85
|
+
confidence REAL,
|
|
86
|
+
occurred_at TEXT NOT NULL,
|
|
87
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
+
superseded_by INTEGER REFERENCES memory_entries(id) ON DELETE SET NULL,
|
|
89
|
+
deleted_at TEXT,
|
|
90
|
+
import_source TEXT -- provenance when the row came from an importer
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- Which raw events an entry came from. Drill-down from an observation to the
|
|
94
|
+
-- evidence behind it, and the reason an entry can be rebuilt after a bad batch.
|
|
95
|
+
CREATE TABLE IF NOT EXISTS memory_entry_events (
|
|
96
|
+
entry_id INTEGER NOT NULL REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
97
|
+
event_id INTEGER NOT NULL REFERENCES evidence_events(id) ON DELETE CASCADE,
|
|
98
|
+
PRIMARY KEY (entry_id, event_id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- Tags are a table rather than JSON because filtering by tag is a first-class
|
|
102
|
+
-- retrieval path and `json_each` over the whole corpus is not an index.
|
|
103
|
+
CREATE TABLE IF NOT EXISTS memory_entry_tags (
|
|
104
|
+
entry_id INTEGER NOT NULL REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
105
|
+
tag TEXT NOT NULL,
|
|
106
|
+
PRIMARY KEY (entry_id, tag)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
-- Keyword retrieval. Contentless FTS5: the row content lives in memory_entries
|
|
110
|
+
-- and the index stores only the terms, so there is one copy of the text.
|
|
111
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
112
|
+
title, narrative, facts, files,
|
|
113
|
+
content = 'memory_entries',
|
|
114
|
+
content_rowid = 'id',
|
|
115
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
-- Semantic retrieval (ADR-03). One row per entry per embedder: switching
|
|
119
|
+
-- embedders re-embeds in the background and vectors of different models are
|
|
120
|
+
-- never compared, because `embedder_id` is part of the read filter.
|
|
121
|
+
CREATE TABLE IF NOT EXISTS memory_vectors (
|
|
122
|
+
entry_id INTEGER NOT NULL REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
123
|
+
embedder_id TEXT NOT NULL,
|
|
124
|
+
dim INTEGER NOT NULL,
|
|
125
|
+
vec BLOB NOT NULL, -- float32 little-endian, L2-normalised
|
|
126
|
+
PRIMARY KEY (entry_id, embedder_id)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
-- The savings ledger (PRD MET-01). One receipt per injection: what would have
|
|
130
|
+
-- been read (`base_tokens`), what was actually delivered (`delivered_tokens`),
|
|
131
|
+
-- how both were counted, and whether delivery was confirmed. A percentage the
|
|
132
|
+
-- dashboard or the banner shows must be derivable from these rows alone.
|
|
133
|
+
CREATE TABLE IF NOT EXISTS context_receipts (
|
|
134
|
+
id INTEGER PRIMARY KEY,
|
|
135
|
+
receipt_uid TEXT NOT NULL UNIQUE,
|
|
136
|
+
project TEXT NOT NULL,
|
|
137
|
+
session_id TEXT,
|
|
138
|
+
scope TEXT NOT NULL, -- 'session_start' | 'search' | 'resume' | ...
|
|
139
|
+
method TEXT NOT NULL, -- tokenizer/estimator identity
|
|
140
|
+
base_tokens INTEGER NOT NULL DEFAULT 0,
|
|
141
|
+
delivered_tokens INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
item_count INTEGER NOT NULL DEFAULT 0,
|
|
143
|
+
delivery TEXT NOT NULL DEFAULT 'unknown', -- confirmed|unknown|prepared
|
|
144
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
-- Per-item accounting, so a later detail fetch updates the same episode's total
|
|
148
|
+
-- instead of leaving the optimistic index-only percentage standing.
|
|
149
|
+
CREATE TABLE IF NOT EXISTS context_receipt_items (
|
|
150
|
+
receipt_id INTEGER NOT NULL REFERENCES context_receipts(id) ON DELETE CASCADE,
|
|
151
|
+
entry_id INTEGER NOT NULL REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
152
|
+
source_tokens INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
sent_tokens INTEGER NOT NULL DEFAULT 0,
|
|
154
|
+
stage TEXT NOT NULL DEFAULT 'index', -- 'index' | 'detail'
|
|
155
|
+
PRIMARY KEY (receipt_id, entry_id, stage)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
-- The one explicit bridge between the two halves (PRD LRN-02, DATA-01).
|
|
159
|
+
-- An evidence-derived concept candidate. `status` is the gate: a candidate is
|
|
160
|
+
-- not a concept, and no row here ever records an attempt or moves mastery.
|
|
161
|
+
CREATE TABLE IF NOT EXISTS learning_sources (
|
|
162
|
+
id INTEGER PRIMARY KEY,
|
|
163
|
+
entry_id INTEGER REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
164
|
+
event_id INTEGER REFERENCES evidence_events(id) ON DELETE CASCADE,
|
|
165
|
+
concept_id INTEGER REFERENCES concepts(id),
|
|
166
|
+
slug TEXT, -- proposed slug before validation
|
|
167
|
+
name TEXT,
|
|
168
|
+
domain TEXT,
|
|
169
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
170
|
+
status TEXT NOT NULL DEFAULT 'candidate', -- candidate|accepted|rejected|duplicate
|
|
171
|
+
project TEXT,
|
|
172
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
-- Saved focused knowledge collections (PRD RET-04). The filter is stored so a
|
|
176
|
+
-- rebuild reproduces the same query; membership is materialised so a failed
|
|
177
|
+
-- rebuild can be discarded without losing the last good set.
|
|
178
|
+
CREATE TABLE IF NOT EXISTS memory_collections (
|
|
179
|
+
id INTEGER PRIMARY KEY,
|
|
180
|
+
name TEXT NOT NULL UNIQUE,
|
|
181
|
+
description TEXT,
|
|
182
|
+
filter TEXT NOT NULL DEFAULT '{}', -- JSON query descriptor
|
|
183
|
+
project TEXT,
|
|
184
|
+
built_at TEXT,
|
|
185
|
+
status TEXT NOT NULL DEFAULT 'ready', -- ready|building|failed
|
|
186
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE TABLE IF NOT EXISTS memory_collection_items (
|
|
190
|
+
collection_id INTEGER NOT NULL REFERENCES memory_collections(id) ON DELETE CASCADE,
|
|
191
|
+
entry_id INTEGER NOT NULL REFERENCES memory_entries(id) ON DELETE CASCADE,
|
|
192
|
+
rank REAL NOT NULL DEFAULT 0,
|
|
193
|
+
PRIMARY KEY (collection_id, entry_id)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
-- Read paths on a user's critical path: the SessionStart recall, the dashboard
|
|
197
|
+
-- timeline, and the worker's claim scan.
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_events_project_time ON evidence_events(project, occurred_at DESC);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON evidence_events(session_id, occurred_at DESC);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_events_status ON evidence_events(status, project);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_entries_project_time ON memory_entries(project, occurred_at DESC);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_entries_session ON memory_entries(session_id);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON memory_jobs(status, lease_until);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_receipts_project ON context_receipts(project, created_at DESC);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_sources_status ON learning_sources(status, project);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON memory_entry_tags(tag);
|
|
207
|
+
|
|
208
|
+
-- FTS stays in step through triggers rather than through every call site
|
|
209
|
+
-- remembering to update it. External-content FTS5 needs the old values on
|
|
210
|
+
-- delete, which only a trigger has to hand.
|
|
211
|
+
CREATE TRIGGER IF NOT EXISTS memory_entries_ai AFTER INSERT ON memory_entries BEGIN
|
|
212
|
+
INSERT INTO memory_fts(rowid, title, narrative, facts, files)
|
|
213
|
+
VALUES (new.id, new.title, new.narrative, new.facts, new.files);
|
|
214
|
+
END;
|
|
215
|
+
|
|
216
|
+
CREATE TRIGGER IF NOT EXISTS memory_entries_ad AFTER DELETE ON memory_entries BEGIN
|
|
217
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, narrative, facts, files)
|
|
218
|
+
VALUES ('delete', old.id, old.title, old.narrative, old.facts, old.files);
|
|
219
|
+
END;
|
|
220
|
+
|
|
221
|
+
CREATE TRIGGER IF NOT EXISTS memory_entries_au AFTER UPDATE ON memory_entries BEGIN
|
|
222
|
+
INSERT INTO memory_fts(memory_fts, rowid, title, narrative, facts, files)
|
|
223
|
+
VALUES ('delete', old.id, old.title, old.narrative, old.facts, old.files);
|
|
224
|
+
INSERT INTO memory_fts(rowid, title, narrative, facts, files)
|
|
225
|
+
VALUES (new.id, new.title, new.narrative, new.facts, new.files);
|
|
226
|
+
END;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Import provenance: the durable id map that makes an import resumable and
|
|
2
|
+
-- idempotent (PRD MIG-02).
|
|
3
|
+
--
|
|
4
|
+
-- Without it, a run interrupted halfway has no way to tell what it already
|
|
5
|
+
-- wrote, and the only safe second attempt is a full re-import that duplicates
|
|
6
|
+
-- everything. Entry and event uids are deterministic, so duplication is already
|
|
7
|
+
-- prevented at the row level -- but "already present" and "just written" are
|
|
8
|
+
-- indistinguishable without a map, and a report that cannot say which is which
|
|
9
|
+
-- cannot validate its own counts.
|
|
10
|
+
--
|
|
11
|
+
-- Keyed by (source, source_db, source_table, source_id) rather than by source
|
|
12
|
+
-- id alone: two Claude Mem databases (a laptop's and a restored backup) share
|
|
13
|
+
-- their autoincrement ids, and folding them together would silently drop half
|
|
14
|
+
-- of the second import.
|
|
15
|
+
CREATE TABLE IF NOT EXISTS import_id_map (
|
|
16
|
+
source TEXT NOT NULL, -- importer id, e.g. 'claude-mem'
|
|
17
|
+
source_db TEXT NOT NULL, -- identity of the source file, not its path of the day
|
|
18
|
+
source_table TEXT NOT NULL,
|
|
19
|
+
source_id TEXT NOT NULL,
|
|
20
|
+
target_table TEXT NOT NULL, -- 'memory_entries' | 'evidence_events' | 'learning_sources'
|
|
21
|
+
target_id INTEGER NOT NULL,
|
|
22
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
PRIMARY KEY (source, source_db, source_table, source_id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_import_target ON import_id_map(target_table, target_id);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
-- Multi-device sync (ADR-09): a shared *directory*, never a server.
|
|
2
|
+
--
|
|
3
|
+
-- Two devices point at one folder -- Dropbox, iCloud, Syncthing, a mounted
|
|
4
|
+
-- share, a git repo -- and exchange revision records as files. None of those is
|
|
5
|
+
-- a dependency, because the target is a path. The hosted team server in the
|
|
6
|
+
-- PRD's Phase 6 is deliberately not built.
|
|
7
|
+
--
|
|
8
|
+
-- What crosses is memory entries, their tags and their tombstones. Attempts,
|
|
9
|
+
-- mastery, gates, receipts and raw `evidence_events` never do (PRD SEC-02): a
|
|
10
|
+
-- developer's assessment history is theirs, and pointing a team at one shared
|
|
11
|
+
-- folder must not hand them everyone's quiz record. `src/memory/sync.ts`
|
|
12
|
+
-- enforces that with a column allowlist; these tables simply have nowhere to
|
|
13
|
+
-- put anything else.
|
|
14
|
+
|
|
15
|
+
-- Per-device high-water marks, including this device's own counter.
|
|
16
|
+
--
|
|
17
|
+
-- One uniform meaning for every row: "revisions from this device up to and
|
|
18
|
+
-- including `last_revision` are reflected in this database". For a peer that
|
|
19
|
+
-- makes a pull incremental -- only files above the mark are read. For the local
|
|
20
|
+
-- device it is the last revision handed out, so `revision` is monotonic per
|
|
21
|
+
-- device and a pair (device_id, revision) names one version for ever.
|
|
22
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
23
|
+
device_id TEXT PRIMARY KEY,
|
|
24
|
+
last_revision INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
last_sync_at TEXT
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- The version vector, one row per entry uid: which device wrote the version
|
|
29
|
+
-- this database currently holds, at which revision, and what it descended from.
|
|
30
|
+
--
|
|
31
|
+
-- Keyed by `entry_uid` and not by the entry's integer id, and with no foreign
|
|
32
|
+
-- key, on purpose. Row ids are local; `entry_uid` is the only name two devices
|
|
33
|
+
-- agree on. And a tombstone has to outlive the row it buries -- without a
|
|
34
|
+
-- record that survives the delete, the next pull from a device that still had
|
|
35
|
+
-- the entry would resurrect it, which is the exact bug this table prevents.
|
|
36
|
+
--
|
|
37
|
+
-- `base` is the version this one replaced, spelled `<device>:<revision>` (NULL
|
|
38
|
+
-- for a first write). An incoming record whose `base` is what we already hold
|
|
39
|
+
-- is a fast-forward and applies. Anything else is divergence, and divergence is
|
|
40
|
+
-- quarantined rather than merged.
|
|
41
|
+
--
|
|
42
|
+
-- `hash` covers the payload, so the push scan can tell a changed entry from an
|
|
43
|
+
-- untouched one without a write-time hook in `memory/store.ts`.
|
|
44
|
+
CREATE TABLE IF NOT EXISTS sync_records (
|
|
45
|
+
entry_uid TEXT PRIMARY KEY,
|
|
46
|
+
device_id TEXT NOT NULL,
|
|
47
|
+
revision INTEGER NOT NULL,
|
|
48
|
+
base TEXT,
|
|
49
|
+
op TEXT NOT NULL DEFAULT 'upsert', -- 'upsert' | 'delete' (a tombstone)
|
|
50
|
+
hash TEXT NOT NULL,
|
|
51
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
-- Losing versions, kept readable rather than dropped.
|
|
55
|
+
--
|
|
56
|
+
-- Two devices editing one entry between syncs is not something a timestamp can
|
|
57
|
+
-- adjudicate, so nothing here is overwritten silently: the local version stays
|
|
58
|
+
-- live, the incoming one lands here with its whole payload, and a repair call
|
|
59
|
+
-- picks a side. `payload` is the wire record verbatim so that choosing the
|
|
60
|
+
-- remote side needs nothing from the target directory, which may be offline by
|
|
61
|
+
-- the time somebody looks.
|
|
62
|
+
CREATE TABLE IF NOT EXISTS sync_conflicts (
|
|
63
|
+
id INTEGER PRIMARY KEY,
|
|
64
|
+
entry_uid TEXT NOT NULL,
|
|
65
|
+
device_id TEXT NOT NULL, -- the device whose version lost
|
|
66
|
+
revision INTEGER NOT NULL,
|
|
67
|
+
base TEXT,
|
|
68
|
+
local_ref TEXT NOT NULL, -- `<device>:<revision>` held at the time
|
|
69
|
+
payload TEXT NOT NULL, -- the incoming record, as JSON
|
|
70
|
+
detected_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
71
|
+
resolved_at TEXT,
|
|
72
|
+
resolution TEXT -- 'local' | 'remote' | 'superseded'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_sync_records_device ON sync_records(device_id, revision);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_open ON sync_conflicts(resolved_at, entry_uid);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Retry backoff for the observation queue.
|
|
2
|
+
--
|
|
3
|
+
-- A transient provider failure returned the job straight to 'pending', which
|
|
4
|
+
-- the very next hook reclaimed -- so a job failing against a timing-out or
|
|
5
|
+
-- rate-limited provider spun against it once per tool call until it burned its
|
|
6
|
+
-- five attempts, turning one bad minute into a permanent loss. `next_attempt`
|
|
7
|
+
-- is the floor under the next claim: `failJob` sets it, `claimJob` skips a job
|
|
8
|
+
-- whose floor is still in the future (quality.md, "Failure containment").
|
|
9
|
+
--
|
|
10
|
+
-- Nullable, and NULL means claimable now: every job that already exists was
|
|
11
|
+
-- claimable when this ran, and a migration that invented a cooldown for them
|
|
12
|
+
-- would stall a healthy queue on upgrade.
|
|
13
|
+
ALTER TABLE memory_jobs ADD COLUMN next_attempt TEXT;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Batch provenance (PRD MEM-01: "a versioned input batch and prompt/config
|
|
2
|
+
-- identity").
|
|
3
|
+
--
|
|
4
|
+
-- 009 made the input reconstructible -- the events, the reason, the counts --
|
|
5
|
+
-- but not the run. `memory_entries.generator` records the summariser after the
|
|
6
|
+
-- fact and only when the batch produced something, so a batch that came out
|
|
7
|
+
-- wrong, or produced nothing at all, left no way to know which summariser or
|
|
8
|
+
-- which configuration had read it. Both are written when the job is claimed,
|
|
9
|
+
-- which is the point at which they are known and before the call that may fail.
|
|
10
|
+
--
|
|
11
|
+
-- Nullable because every batch written before this migration ran genuinely has
|
|
12
|
+
-- no answer, and a backfilled guess would be worse than an honest NULL.
|
|
13
|
+
ALTER TABLE memory_batches ADD COLUMN summarizer TEXT;
|
|
14
|
+
|
|
15
|
+
-- Digest over the settings that change what a summary comes out as: what
|
|
16
|
+
-- capture accepted, what redaction removed, which model read it. Never a key --
|
|
17
|
+
-- a provider config names an environment variable, not its value (SEC-01).
|
|
18
|
+
ALTER TABLE memory_batches ADD COLUMN config_digest TEXT;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- The worker reads and retires a batch by `batch_id`, and nothing indexed it.
|
|
2
|
+
--
|
|
3
|
+
-- `batchEvents` (`SELECT … WHERE batch_id = ?`) and the worker's
|
|
4
|
+
-- `UPDATE … SET status = 'summarized' WHERE batch_id = ?` both reported
|
|
5
|
+
-- `SCAN evidence_events`, so summarising one forty-event batch cost a full scan
|
|
6
|
+
-- of every event ever captured. The batch does not grow; the table does, which
|
|
7
|
+
-- makes the total summarising work over a database's life quadratic in its own
|
|
8
|
+
-- history. Measured at the fixture `quality.md` asks for: 1.2ms per batch at
|
|
9
|
+
-- 2,000 events, 26ms at 200,000, 613ms at 1,000,000 -- for identical input.
|
|
10
|
+
--
|
|
11
|
+
-- It stayed hidden because the 2k and 20k perf runs wrote a fixed ~2,000 events
|
|
12
|
+
-- whatever the entry count, pinning the one axis that drives it.
|
|
13
|
+
--
|
|
14
|
+
-- `memory.retention_days` defaults to null, so evidence never shrinks by itself
|
|
15
|
+
-- and the curve has nothing to flatten it. This is on the worker's path, at a
|
|
16
|
+
-- session seam, not on a human's -- which is why it was slow rather than
|
|
17
|
+
-- visible.
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_events_batch ON evidence_events(batch_id);
|