dsh-continual-evolve 0.15.5 → 0.15.7
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/README.md +19 -8
- package/README.zh.md +19 -8
- package/client/client.js +8 -0
- package/lib/card-routes.js +3 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +45 -0
- package/lib/memory-rules.d.ts +7 -0
- package/lib/memory-snapshot.d.ts +4 -0
- package/lib/memory-snapshot.js +17 -2
- package/lib/memory-tools.js +8 -1
- package/lib/review-prompt.d.ts +55 -0
- package/lib/review-prompt.js +105 -0
- package/lib/review.d.ts +97 -0
- package/lib/review.js +458 -0
- package/lib/skill-rules.d.ts +6 -0
- package/lib/skill-rules.js +8 -0
- package/lib/skill-tools.js +9 -1
- package/lib/store-apply.js +40 -5
- package/lib/store.d.ts +97 -1
- package/lib/store.js +210 -2
- package/lib/token-usage.d.ts +31 -0
- package/lib/token-usage.js +59 -0
- package/package.json +1 -1
package/lib/store-apply.js
CHANGED
|
@@ -11,13 +11,14 @@ function recordOf(row) {
|
|
|
11
11
|
sourceSession: row.source_session,
|
|
12
12
|
sourceSeqs: row.source_seqs,
|
|
13
13
|
sourceRun: row.source_run,
|
|
14
|
+
corrects: row.corrects ?? null,
|
|
14
15
|
createdAt: row.created_at,
|
|
15
16
|
updatedAt: row.updated_at,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
const INSERT_SQL = "INSERT INTO memories (workspace_id, id, type, title, description, body, status, source_session, source_seqs, source_run, created_at, updated_at) " +
|
|
19
|
-
"VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)";
|
|
20
|
-
const UPDATE_SQL = "UPDATE memories SET type = ?, title = ?, description = ?, body = ?, updated_at = ? WHERE workspace_id = ? AND id = ?";
|
|
19
|
+
const INSERT_SQL = "INSERT INTO memories (workspace_id, id, type, title, description, body, status, source_session, source_seqs, source_run, corrects, created_at, updated_at) " +
|
|
20
|
+
"VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)";
|
|
21
|
+
const UPDATE_SQL = "UPDATE memories SET type = ?, title = ?, description = ?, body = ?, corrects = COALESCE(?, corrects), updated_at = ? WHERE workspace_id = ? AND id = ?";
|
|
21
22
|
const DELETE_SQL = "DELETE FROM memories WHERE workspace_id = ? AND id = ?";
|
|
22
23
|
const FTS_DROP_SQL = "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories_fts WHERE workspace_id = ? AND id = ?)";
|
|
23
24
|
const FTS_INSERT_SQL = "INSERT INTO memories_fts (workspace_id, id, title, description, body) " +
|
|
@@ -99,6 +100,10 @@ function applyOne(db, ctx, proposal, stmts, files) {
|
|
|
99
100
|
if ("reason" in fields) {
|
|
100
101
|
return fail(proposal, fields.reason);
|
|
101
102
|
}
|
|
103
|
+
const link = checkCorrects(stmts.get, ctx.workspaceId, proposal, id);
|
|
104
|
+
if ("reason" in link) {
|
|
105
|
+
return fail(proposal, link.reason);
|
|
106
|
+
}
|
|
102
107
|
const reasons = fieldGateReasons(fields.fields);
|
|
103
108
|
if (reasons.length > 0) {
|
|
104
109
|
return fail(proposal, reasons[0] ?? "invalid record");
|
|
@@ -108,7 +113,7 @@ function applyOne(db, ctx, proposal, stmts, files) {
|
|
|
108
113
|
return fail(proposal, `memory "${id}" already exists — update it instead of creating a duplicate`);
|
|
109
114
|
}
|
|
110
115
|
const now = db.now();
|
|
111
|
-
stmts.insert.run(ctx.workspaceId, id, fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, ctx.sessionId ?? null, proposal.sourceSeqs ?? null, ctx.runId ?? (ctx.trigger === "explicit" ? "explicit" : null), now, now);
|
|
116
|
+
stmts.insert.run(ctx.workspaceId, id, fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, ctx.sessionId ?? null, proposal.sourceSeqs ?? null, ctx.runId ?? (ctx.trigger === "explicit" ? "explicit" : null), link.value, now, now);
|
|
112
117
|
syncFts(db, ctx.workspaceId, id);
|
|
113
118
|
files.push({
|
|
114
119
|
id,
|
|
@@ -122,7 +127,7 @@ function applyOne(db, ctx, proposal, stmts, files) {
|
|
|
122
127
|
if (existing === undefined) {
|
|
123
128
|
return fail(proposal, `no memory "${id}" to update — create it instead`);
|
|
124
129
|
}
|
|
125
|
-
stmts.update.run(fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, db.now(), ctx.workspaceId, id);
|
|
130
|
+
stmts.update.run(fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, link.value, db.now(), ctx.workspaceId, id);
|
|
126
131
|
syncFts(db, ctx.workspaceId, id);
|
|
127
132
|
files.push({ id, action, before: existing, after: recordOf(stmts.get.get(ctx.workspaceId, id)) });
|
|
128
133
|
return { id, action, ok: true };
|
|
@@ -131,6 +136,36 @@ function applyOne(db, ctx, proposal, stmts, files) {
|
|
|
131
136
|
function fail(proposal, reason) {
|
|
132
137
|
return { id: proposal.id ?? "", action: proposal.action, ok: false, reason };
|
|
133
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the correction link: absent (or explicitly empty-checked) proposals
|
|
141
|
+
* carry `null`, which keeps an existing link on update and stores null on
|
|
142
|
+
* create. A named target must be a well-formed id in the same workspace,
|
|
143
|
+
* must exist, must be active, and must not be the row itself.
|
|
144
|
+
*/
|
|
145
|
+
function checkCorrects(get, workspaceId, proposal, id) {
|
|
146
|
+
const raw = proposal.corrects;
|
|
147
|
+
if (raw === undefined) {
|
|
148
|
+
return { value: null };
|
|
149
|
+
}
|
|
150
|
+
const target = raw.trim();
|
|
151
|
+
if (target.length === 0) {
|
|
152
|
+
return { reason: "corrects must not be empty — omit it instead" };
|
|
153
|
+
}
|
|
154
|
+
if (!isValidMemoryId(target)) {
|
|
155
|
+
return { reason: `malformed corrects target "${target}"` };
|
|
156
|
+
}
|
|
157
|
+
if (target === id) {
|
|
158
|
+
return { reason: `memory "${id}" cannot correct itself` };
|
|
159
|
+
}
|
|
160
|
+
const row = get.get(workspaceId, target);
|
|
161
|
+
if (row === undefined) {
|
|
162
|
+
return { reason: `no memory "${target}" to correct` };
|
|
163
|
+
}
|
|
164
|
+
if (row.status !== "active") {
|
|
165
|
+
return { reason: `memory "${target}" is not active and cannot be corrected` };
|
|
166
|
+
}
|
|
167
|
+
return { value: target };
|
|
168
|
+
}
|
|
134
169
|
/** The proposal id, derived from the title (slug, CJK-hash fallback) on create. */
|
|
135
170
|
function idFor(proposal) {
|
|
136
171
|
if (proposal.id !== undefined && proposal.id.trim().length > 0) {
|
package/lib/store.d.ts
CHANGED
|
@@ -43,6 +43,58 @@ export interface SkillFileWarning {
|
|
|
43
43
|
id: string;
|
|
44
44
|
warning: string;
|
|
45
45
|
}
|
|
46
|
+
/** Aggregate consumption of one skill (the V2 usage loop's only counter). */
|
|
47
|
+
export interface SkillUse {
|
|
48
|
+
useCount: number;
|
|
49
|
+
/** ISO instant of the last counted use, `null` when never consumed. */
|
|
50
|
+
lastUsedAt: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** One per-item content-review verdict (the `item-review` account's row). */
|
|
53
|
+
export interface ReviewLogEntry {
|
|
54
|
+
runId?: string | undefined;
|
|
55
|
+
workspaceId?: string | undefined;
|
|
56
|
+
itemKind: "memory" | "skill";
|
|
57
|
+
itemId: string;
|
|
58
|
+
verdict: "keep" | "rewrite" | "quarantine" | "skip";
|
|
59
|
+
reason?: string | undefined;
|
|
60
|
+
/** Normalized usage JSON of the cheap gate call, `undefined` when skipped before any call. */
|
|
61
|
+
cheapUsage?: unknown;
|
|
62
|
+
/** Normalized usage JSON of the evolve call, `undefined` unless rewritten. */
|
|
63
|
+
evolveUsage?: unknown;
|
|
64
|
+
/** Whether the rewrite landed (1) or the row is verdict-only (0). */
|
|
65
|
+
applied?: number | undefined;
|
|
66
|
+
/** How many times this item has evolved including this run. */
|
|
67
|
+
evolveCount?: number | undefined;
|
|
68
|
+
}
|
|
69
|
+
/** One day of the hard-cap account (UTC `YYYY-MM-DD`, budget = 100). */
|
|
70
|
+
export interface DailyStats {
|
|
71
|
+
day: string;
|
|
72
|
+
budget: number;
|
|
73
|
+
reviewed: number;
|
|
74
|
+
newItems: number;
|
|
75
|
+
requeued: number;
|
|
76
|
+
approved: number;
|
|
77
|
+
applied: number;
|
|
78
|
+
cheapToken: number;
|
|
79
|
+
evolveToken: number;
|
|
80
|
+
backlog: number;
|
|
81
|
+
dropped: number;
|
|
82
|
+
/** JSON array of the top skip reasons, `null` when none. */
|
|
83
|
+
skipTop: string | null;
|
|
84
|
+
}
|
|
85
|
+
/** Additive counters merged into today's rollup by one run. */
|
|
86
|
+
export interface DailyDelta {
|
|
87
|
+
newItems?: number | undefined;
|
|
88
|
+
requeued?: number | undefined;
|
|
89
|
+
approved?: number | undefined;
|
|
90
|
+
applied?: number | undefined;
|
|
91
|
+
cheapToken?: number | undefined;
|
|
92
|
+
evolveToken?: number | undefined;
|
|
93
|
+
/** Queue depth after the run (set, not added). */
|
|
94
|
+
backlog?: number | undefined;
|
|
95
|
+
dropped?: number | undefined;
|
|
96
|
+
skipReasons?: string[] | undefined;
|
|
97
|
+
}
|
|
46
98
|
/** One stored memory record (snake_case columns camelCased). */
|
|
47
99
|
export interface MemoryRecord {
|
|
48
100
|
workspaceId: string;
|
|
@@ -58,6 +110,8 @@ export interface MemoryRecord {
|
|
|
58
110
|
sourceSeqs: string | null;
|
|
59
111
|
/** The extraction run that wrote it; `'explicit'` for tool writes. */
|
|
60
112
|
sourceRun: string | null;
|
|
113
|
+
/** Id of the memory this row corrects/supersedes in the same workspace, `null` when standalone. */
|
|
114
|
+
corrects: string | null;
|
|
61
115
|
createdAt: string;
|
|
62
116
|
updatedAt: string;
|
|
63
117
|
}
|
|
@@ -71,7 +125,7 @@ export interface LedgerEntry {
|
|
|
71
125
|
runId?: string | undefined;
|
|
72
126
|
sessionId?: string | undefined;
|
|
73
127
|
workspaceId?: string | undefined;
|
|
74
|
-
trigger: "turn-debounce" | "compaction" | "close-drain" | "explicit" | "import";
|
|
128
|
+
trigger: "turn-debounce" | "compaction" | "close-drain" | "explicit" | "import" | "item-review";
|
|
75
129
|
decision?: string | undefined;
|
|
76
130
|
skipReason?: string | undefined;
|
|
77
131
|
status?: string | undefined;
|
|
@@ -173,6 +227,48 @@ export interface MemoryStore {
|
|
|
173
227
|
};
|
|
174
228
|
/** Partitions holding active skills (the card's skill fence). */
|
|
175
229
|
listSkillWorkspaces(): WorkspaceSummary[];
|
|
230
|
+
/**
|
|
231
|
+
* Record one real skill consumption (a body served by id, never a list
|
|
232
|
+
* or search hit). Accounting never fails the read — failures degrade
|
|
233
|
+
* silently, exactly like file sync never fails a write batch.
|
|
234
|
+
*/
|
|
235
|
+
recordSkillUse(workspaceId: string, skillId: string, scope: string): void;
|
|
236
|
+
/** Aggregate usage for one skill, undefined when never consumed. */
|
|
237
|
+
getSkillUse(workspaceId: string, skillId: string, scope: string): SkillUse | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* Record one real memory consumption (a body served by id, never a list
|
|
240
|
+
* or search hit). Same heartbeat discipline as skill usage: accounting
|
|
241
|
+
* never fails the read it rides on.
|
|
242
|
+
*/
|
|
243
|
+
recordMemoryUse(workspaceId: string, memoryId: string): void;
|
|
244
|
+
/** Aggregate usage for one memory, undefined when never consumed. */
|
|
245
|
+
getMemoryUse(workspaceId: string, memoryId: string): SkillUse | undefined;
|
|
246
|
+
/** Append one per-item content-review verdict (the `item-review` account). */
|
|
247
|
+
logReview(entry: ReviewLogEntry): void;
|
|
248
|
+
/**
|
|
249
|
+
* Atomically consume one unit of today's review budget: returns the
|
|
250
|
+
* remaining budget after consumption, or -1 when the day is exhausted
|
|
251
|
+
* (the caller drops the low-priority item and counts it as dropped).
|
|
252
|
+
*/
|
|
253
|
+
consumeReviewBudget(day: string, budget: number): number;
|
|
254
|
+
/** Today's rollup row, undefined when nothing ran yet today. */
|
|
255
|
+
getDaily(day: string): DailyStats | undefined;
|
|
256
|
+
/** Merge one run's counters into today's rollup row (upsert, additive). */
|
|
257
|
+
mergeDaily(day: string, budget: number, delta: DailyDelta): void;
|
|
258
|
+
/**
|
|
259
|
+
* Soft-isolate one item (review quarantine verdict): hidden from
|
|
260
|
+
* injection and reads, never deleted — the ledger keeps the before
|
|
261
|
+
* snapshot for undo. Returns false when the row is absent.
|
|
262
|
+
*/
|
|
263
|
+
quarantine(workspaceId: string, kind: "memory" | "skill", id: string): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* The review history of one item: last verdict instant and landed
|
|
266
|
+
* rewrite count, undefined when never reviewed (a new item).
|
|
267
|
+
*/
|
|
268
|
+
reviewStateOf(workspaceId: string, kind: "memory" | "skill", id: string): {
|
|
269
|
+
lastTs: string | null;
|
|
270
|
+
evolveCount: number;
|
|
271
|
+
} | undefined;
|
|
176
272
|
}
|
|
177
273
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
178
274
|
export declare function defaultStorePath(): string;
|
package/lib/store.js
CHANGED
|
@@ -51,6 +51,7 @@ PRAGMA busy_timeout=5000;CREATE TABLE IF NOT EXISTS memories (
|
|
|
51
51
|
source_session TEXT,
|
|
52
52
|
source_seqs TEXT,
|
|
53
53
|
source_run TEXT,
|
|
54
|
+
corrects TEXT,
|
|
54
55
|
created_at TEXT NOT NULL,
|
|
55
56
|
updated_at TEXT NOT NULL,
|
|
56
57
|
PRIMARY KEY (workspace_id, id)
|
|
@@ -66,7 +67,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
66
67
|
-- patrol() reconciles any residue.
|
|
67
68
|
CREATE TABLE IF NOT EXISTS extraction_log (
|
|
68
69
|
run_id TEXT, ts TEXT NOT NULL, session_id TEXT, workspace_id TEXT,
|
|
69
|
-
trigger TEXT CHECK (trigger IN ('turn-debounce','compaction','close-drain','explicit','import')),
|
|
70
|
+
trigger TEXT CHECK (trigger IN ('turn-debounce','compaction','close-drain','explicit','import','item-review')),
|
|
70
71
|
decision TEXT, skip_reason TEXT,
|
|
71
72
|
status TEXT, duration_ms INTEGER, model TEXT, usage TEXT,
|
|
72
73
|
files TEXT, occurrences INTEGER NOT NULL DEFAULT 1
|
|
@@ -100,6 +101,55 @@ CREATE TABLE IF NOT EXISTS skill_log (
|
|
|
100
101
|
run_id TEXT, ts TEXT NOT NULL, session_id TEXT, workspace_id TEXT,
|
|
101
102
|
trigger TEXT, decision TEXT, status TEXT, files TEXT, warnings TEXT
|
|
102
103
|
);
|
|
104
|
+
-- Skill usage counters (V2 usage loop): one aggregate row per skill, never a
|
|
105
|
+
-- per-hit ledger, so the table cannot grow without bound and needs no prune.
|
|
106
|
+
CREATE TABLE IF NOT EXISTS skill_usage (
|
|
107
|
+
workspace_id TEXT NOT NULL,
|
|
108
|
+
skill_id TEXT NOT NULL,
|
|
109
|
+
scope TEXT NOT NULL CHECK (scope IN ('project','global')),
|
|
110
|
+
use_count INTEGER NOT NULL DEFAULT 0,
|
|
111
|
+
last_used_at TEXT,
|
|
112
|
+
PRIMARY KEY (workspace_id, skill_id, scope)
|
|
113
|
+
) STRICT;
|
|
114
|
+
-- Memory usage counters (content-quality loop): one aggregate row per memory,
|
|
115
|
+
-- the same presence-heartbeat discipline as skill_usage — a body served by id
|
|
116
|
+
-- counts, search/list hits stay uncounted.
|
|
117
|
+
CREATE TABLE IF NOT EXISTS memory_usage (
|
|
118
|
+
workspace_id TEXT NOT NULL,
|
|
119
|
+
memory_id TEXT NOT NULL,
|
|
120
|
+
use_count INTEGER NOT NULL DEFAULT 0,
|
|
121
|
+
last_used_at TEXT,
|
|
122
|
+
PRIMARY KEY (workspace_id, memory_id)
|
|
123
|
+
) STRICT;
|
|
124
|
+
-- Per-item content-review verdicts (the item-review trigger's account):
|
|
125
|
+
-- one row per reviewed item per run. Pruned with the ledgers.
|
|
126
|
+
CREATE TABLE IF NOT EXISTS review_log (
|
|
127
|
+
run_id TEXT, ts TEXT NOT NULL, workspace_id TEXT,
|
|
128
|
+
item_kind TEXT NOT NULL CHECK (item_kind IN ('memory','skill')),
|
|
129
|
+
item_id TEXT NOT NULL,
|
|
130
|
+
verdict TEXT NOT NULL CHECK (verdict IN ('keep','rewrite','quarantine','skip')),
|
|
131
|
+
reason TEXT,
|
|
132
|
+
cheap_usage TEXT, evolve_usage TEXT,
|
|
133
|
+
applied INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
evolve_count INTEGER NOT NULL DEFAULT 0
|
|
135
|
+
);
|
|
136
|
+
-- One rollup row per UTC day: the hard-cap account (budget vs reviewed vs
|
|
137
|
+
-- backlog vs dropped) plus the token account split by cheap/evolve purpose.
|
|
138
|
+
CREATE TABLE IF NOT EXISTS review_daily (
|
|
139
|
+
day TEXT NOT NULL PRIMARY KEY,
|
|
140
|
+
budget INTEGER NOT NULL,
|
|
141
|
+
reviewed INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
new_items INTEGER NOT NULL DEFAULT 0,
|
|
143
|
+
requeued INTEGER NOT NULL DEFAULT 0,
|
|
144
|
+
approved INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
applied INTEGER NOT NULL DEFAULT 0,
|
|
146
|
+
cheap_token INTEGER NOT NULL DEFAULT 0,
|
|
147
|
+
evolve_token INTEGER NOT NULL DEFAULT 0,
|
|
148
|
+
backlog INTEGER NOT NULL DEFAULT 0,
|
|
149
|
+
dropped INTEGER NOT NULL DEFAULT 0,
|
|
150
|
+
skip_top TEXT,
|
|
151
|
+
skip_counts TEXT
|
|
152
|
+
) STRICT;
|
|
103
153
|
`;
|
|
104
154
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
105
155
|
export function defaultStorePath() {
|
|
@@ -118,6 +168,7 @@ function recordOf(row) {
|
|
|
118
168
|
sourceSession: row.source_session,
|
|
119
169
|
sourceSeqs: row.source_seqs,
|
|
120
170
|
sourceRun: row.source_run,
|
|
171
|
+
corrects: row.corrects ?? null,
|
|
121
172
|
createdAt: row.created_at,
|
|
122
173
|
updatedAt: row.updated_at,
|
|
123
174
|
};
|
|
@@ -248,6 +299,7 @@ export async function openMemoryStore(explicitPath) {
|
|
|
248
299
|
}
|
|
249
300
|
database.exec(DDL);
|
|
250
301
|
ensureLedgerOccurrences(database);
|
|
302
|
+
ensureMemoriesCorrects(database);
|
|
251
303
|
const stmts = {
|
|
252
304
|
get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
|
|
253
305
|
list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
|
|
@@ -296,6 +348,25 @@ export async function openMemoryStore(explicitPath) {
|
|
|
296
348
|
skillActiveRows: database.prepare("SELECT * FROM skills WHERE status = 'active'"),
|
|
297
349
|
skillQuarantine: database.prepare("UPDATE skills SET status = 'quarantined' WHERE workspace_id = ? AND id = ?"),
|
|
298
350
|
skillWorkspaces: database.prepare("SELECT workspace_id, COUNT(*) AS n, MAX(updated_at) AS u FROM skills WHERE status = 'active' GROUP BY workspace_id ORDER BY u DESC"),
|
|
351
|
+
skillUseGet: database.prepare("SELECT use_count, last_used_at FROM skill_usage WHERE workspace_id = ? AND skill_id = ? AND scope = ?"),
|
|
352
|
+
skillUseUpsert: database.prepare("INSERT INTO skill_usage (workspace_id, skill_id, scope, use_count, last_used_at) VALUES (?, ?, ?, 1, ?) " +
|
|
353
|
+
"ON CONFLICT (workspace_id, skill_id, scope) DO UPDATE SET use_count = use_count + 1, last_used_at = excluded.last_used_at"),
|
|
354
|
+
memUseGet: database.prepare("SELECT use_count, last_used_at FROM memory_usage WHERE workspace_id = ? AND memory_id = ?"),
|
|
355
|
+
memUseUpsert: database.prepare("INSERT INTO memory_usage (workspace_id, memory_id, use_count, last_used_at) VALUES (?, ?, 1, ?) " +
|
|
356
|
+
"ON CONFLICT (workspace_id, memory_id) DO UPDATE SET use_count = use_count + 1, last_used_at = excluded.last_used_at"),
|
|
357
|
+
reviewLog: database.prepare("INSERT INTO review_log (run_id, ts, workspace_id, item_kind, item_id, verdict, reason, cheap_usage, evolve_usage, applied, evolve_count) " +
|
|
358
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
359
|
+
reviewLast: database.prepare("SELECT ts, applied FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? ORDER BY rowid DESC LIMIT 1"),
|
|
360
|
+
reviewEvolved: database.prepare("SELECT COUNT(*) AS n FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND applied = 1"),
|
|
361
|
+
reviewPrune: database.prepare("DELETE FROM review_log WHERE ts < ?"),
|
|
362
|
+
dailyGet: database.prepare("SELECT * FROM review_daily WHERE day = ?"),
|
|
363
|
+
dailyUpsert: database.prepare("INSERT INTO review_daily (day, budget, reviewed, new_items, requeued, approved, applied, cheap_token, evolve_token, backlog, dropped, skip_top, skip_counts) " +
|
|
364
|
+
"VALUES (?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL) " +
|
|
365
|
+
"ON CONFLICT (day) DO UPDATE SET budget = excluded.budget"),
|
|
366
|
+
dailyConsume: database.prepare("UPDATE review_daily SET reviewed = reviewed + 1 WHERE day = ? AND reviewed < budget"),
|
|
367
|
+
dailyMerge: database.prepare("UPDATE review_daily SET new_items = new_items + ?, requeued = requeued + ?, " +
|
|
368
|
+
"approved = approved + ?, applied = applied + ?, cheap_token = cheap_token + ?, evolve_token = evolve_token + ?, " +
|
|
369
|
+
"backlog = ?, dropped = dropped + ?, skip_top = ?, skip_counts = ? WHERE day = ?"),
|
|
299
370
|
};
|
|
300
371
|
// LIKE fallback: one OR-triplet per token, every token required. Statements
|
|
301
372
|
// are cached by token count — the query set repeats, the counts don't grow.
|
|
@@ -710,7 +781,8 @@ export async function openMemoryStore(explicitPath) {
|
|
|
710
781
|
pruneLedger(beforeIso) {
|
|
711
782
|
const memory = Number(stmts.prune.run(beforeIso).changes);
|
|
712
783
|
const skills = Number(stmts.skillPrune.run(beforeIso).changes);
|
|
713
|
-
|
|
784
|
+
const reviews = Number(stmts.reviewPrune.run(beforeIso).changes);
|
|
785
|
+
return memory + skills + reviews;
|
|
714
786
|
},
|
|
715
787
|
close() {
|
|
716
788
|
database.close();
|
|
@@ -787,6 +859,108 @@ export async function openMemoryStore(explicitPath) {
|
|
|
787
859
|
updatedAt: row.u,
|
|
788
860
|
}));
|
|
789
861
|
},
|
|
862
|
+
recordSkillUse(workspaceId, skillId, scope) {
|
|
863
|
+
try {
|
|
864
|
+
stmts.skillUseUpsert.run(workspaceId, skillId, scope, new Date().toISOString());
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
// Accounting never fails the read it rides on.
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
getSkillUse(workspaceId, skillId, scope) {
|
|
871
|
+
try {
|
|
872
|
+
const row = stmts.skillUseGet.get(workspaceId, skillId, scope);
|
|
873
|
+
if (row === undefined) {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
return { useCount: row.use_count, lastUsedAt: row.last_used_at };
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
recordMemoryUse(workspaceId, memoryId) {
|
|
883
|
+
try {
|
|
884
|
+
stmts.memUseUpsert.run(workspaceId, memoryId, new Date().toISOString());
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
// Accounting never fails the read it rides on.
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
getMemoryUse(workspaceId, memoryId) {
|
|
891
|
+
try {
|
|
892
|
+
const row = stmts.memUseGet.get(workspaceId, memoryId);
|
|
893
|
+
if (row === undefined) {
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
return { useCount: row.use_count, lastUsedAt: row.last_used_at };
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
logReview(entry) {
|
|
903
|
+
stmts.reviewLog.run(entry.runId ?? null, new Date().toISOString(), entry.workspaceId ?? null, entry.itemKind, entry.itemId, entry.verdict, entry.reason ?? null, entry.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.evolveUsage === undefined ? null : JSON.stringify(entry.evolveUsage), entry.applied ?? 0, entry.evolveCount ?? 0);
|
|
904
|
+
},
|
|
905
|
+
consumeReviewBudget(day, budget) {
|
|
906
|
+
stmts.dailyUpsert.run(day, budget);
|
|
907
|
+
const result = stmts.dailyConsume.run(day);
|
|
908
|
+
if (Number(result.changes) === 0) {
|
|
909
|
+
return -1;
|
|
910
|
+
}
|
|
911
|
+
const row = stmts.dailyGet.get(day);
|
|
912
|
+
if (row === undefined) {
|
|
913
|
+
return -1;
|
|
914
|
+
}
|
|
915
|
+
return row.budget - row.reviewed;
|
|
916
|
+
},
|
|
917
|
+
getDaily(day) {
|
|
918
|
+
const row = stmts.dailyGet.get(day);
|
|
919
|
+
if (row === undefined) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
day: row.day, budget: row.budget, reviewed: row.reviewed, newItems: row.new_items,
|
|
924
|
+
requeued: row.requeued, approved: row.approved, applied: row.applied,
|
|
925
|
+
cheapToken: row.cheap_token, evolveToken: row.evolve_token,
|
|
926
|
+
backlog: row.backlog, dropped: row.dropped, skipTop: row.skip_top,
|
|
927
|
+
};
|
|
928
|
+
},
|
|
929
|
+
mergeDaily(day, budget, delta) {
|
|
930
|
+
stmts.dailyUpsert.run(day, budget);
|
|
931
|
+
const row = stmts.dailyGet.get(day);
|
|
932
|
+
const counts = parseCounts(row.skip_counts);
|
|
933
|
+
for (const reason of delta.skipReasons ?? []) {
|
|
934
|
+
counts[reason] = (counts[reason] ?? 0) + 1;
|
|
935
|
+
}
|
|
936
|
+
const entries = Object.entries(counts)
|
|
937
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
|
|
938
|
+
.slice(0, 3)
|
|
939
|
+
.map(([reason]) => reason);
|
|
940
|
+
stmts.dailyMerge.run(delta.newItems ?? 0, delta.requeued ?? 0, delta.approved ?? 0, delta.applied ?? 0, delta.cheapToken ?? 0, delta.evolveToken ?? 0, delta.backlog ?? row.backlog, delta.dropped ?? 0, entries.length > 0 ? JSON.stringify(entries) : row.skip_top, Object.keys(counts).length > 0 ? JSON.stringify(counts) : null, day);
|
|
941
|
+
},
|
|
942
|
+
quarantine(workspaceId, kind, id) {
|
|
943
|
+
try {
|
|
944
|
+
const result = (kind === "memory" ? stmts.quarantine : stmts.skillQuarantine).run(workspaceId, id);
|
|
945
|
+
return Number(result.changes) > 0;
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
reviewStateOf(workspaceId, kind, id) {
|
|
952
|
+
try {
|
|
953
|
+
const last = stmts.reviewLast.get(workspaceId, kind, id);
|
|
954
|
+
if (last === undefined) {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
const evolved = stmts.reviewEvolved.get(workspaceId, kind, id);
|
|
958
|
+
return { lastTs: last.ts, evolveCount: Number(evolved.n) };
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return undefined;
|
|
962
|
+
}
|
|
963
|
+
},
|
|
790
964
|
};
|
|
791
965
|
return store;
|
|
792
966
|
}
|
|
@@ -802,6 +976,40 @@ function ensureLedgerOccurrences(database) {
|
|
|
802
976
|
}
|
|
803
977
|
database.exec(`ALTER TABLE extraction_log ADD COLUMN ${LEDGER_OCCURRENCES} INTEGER NOT NULL DEFAULT 1`);
|
|
804
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Add the memories `corrects` link column to a store created before the
|
|
981
|
+
* correction-linking field existed. Same single-migration discipline as the
|
|
982
|
+
* ledger occurrences column above.
|
|
983
|
+
*/
|
|
984
|
+
function ensureMemoriesCorrects(database) {
|
|
985
|
+
const columns = database.prepare("SELECT name FROM pragma_table_info('memories')").all();
|
|
986
|
+
if (columns.some((column) => column.name === "corrects")) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
database.exec("ALTER TABLE memories ADD COLUMN corrects TEXT");
|
|
990
|
+
}
|
|
991
|
+
/** Parse the daily skip-counts map; corrupt payloads degrade to empty. */
|
|
992
|
+
function parseCounts(raw) {
|
|
993
|
+
if (raw === null) {
|
|
994
|
+
return {};
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
const parsed = JSON.parse(raw);
|
|
998
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
999
|
+
return {};
|
|
1000
|
+
}
|
|
1001
|
+
const counts = {};
|
|
1002
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1003
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1004
|
+
counts[key] = Math.floor(value);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return counts;
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return {};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
805
1013
|
/** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
|
|
806
1014
|
function writeLedger(stmt, entry) {
|
|
807
1015
|
stmt.run(entry.runId ?? null, new Date().toISOString(), entry.sessionId ?? null, entry.workspaceId ?? null, entry.trigger, entry.decision ?? null, entry.skipReason ?? null, entry.status ?? null, entry.durationMs ?? null, entry.model ?? null, entry.usage === undefined ? null : JSON.stringify(entry.usage), entry.files === undefined ? null : JSON.stringify(entry.files));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single accounting entry for direct LLM calls (the `token-usage.ts`
|
|
3
|
+
* the coding standard names). Observed provider payloads disagree on keys —
|
|
4
|
+
* `inputTokens`/`outputTokens` are usual, `totalTokens`, `cacheReadTokens`,
|
|
5
|
+
* `cacheWriteTokens`, and `reasoningTokens` come and go — so every call site
|
|
6
|
+
* normalizes through {@link normalizeUsage} and aggregates with
|
|
7
|
+
* {@link sumUsage}. Nothing here touches the store: per-day aggregation over
|
|
8
|
+
* ledger rows lives in the review daily rollup, which stays read-only and
|
|
9
|
+
* bounded (one row per day).
|
|
10
|
+
*/
|
|
11
|
+
/** Token counts for one LLM call, every field present, zeros when unknown. */
|
|
12
|
+
export interface UsageTotals {
|
|
13
|
+
input: number;
|
|
14
|
+
output: number;
|
|
15
|
+
cacheRead: number;
|
|
16
|
+
cacheWrite: number;
|
|
17
|
+
reasoning: number;
|
|
18
|
+
total: number;
|
|
19
|
+
}
|
|
20
|
+
/** The zero value: no signal, not a failure. */
|
|
21
|
+
export declare function zeroUsage(): UsageTotals;
|
|
22
|
+
/**
|
|
23
|
+
* Normalize one provider usage payload to full totals. Returns `null` when
|
|
24
|
+
* the payload carries no usable numeric signal at all (not an object, or an
|
|
25
|
+
* object with no recognized numeric field) — callers treat that as "no
|
|
26
|
+
* accounting for this run", never as zeros that would dilute real totals.
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeUsage(raw: unknown): UsageTotals | null;
|
|
29
|
+
/** Pure field-wise sum; either side may be a zero value. */
|
|
30
|
+
export declare function sumUsage(a: UsageTotals, b: UsageTotals): UsageTotals;
|
|
31
|
+
//# sourceMappingURL=token-usage.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single accounting entry for direct LLM calls (the `token-usage.ts`
|
|
3
|
+
* the coding standard names). Observed provider payloads disagree on keys —
|
|
4
|
+
* `inputTokens`/`outputTokens` are usual, `totalTokens`, `cacheReadTokens`,
|
|
5
|
+
* `cacheWriteTokens`, and `reasoningTokens` come and go — so every call site
|
|
6
|
+
* normalizes through {@link normalizeUsage} and aggregates with
|
|
7
|
+
* {@link sumUsage}. Nothing here touches the store: per-day aggregation over
|
|
8
|
+
* ledger rows lives in the review daily rollup, which stays read-only and
|
|
9
|
+
* bounded (one row per day).
|
|
10
|
+
*/
|
|
11
|
+
/** The zero value: no signal, not a failure. */
|
|
12
|
+
export function zeroUsage() {
|
|
13
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, total: 0 };
|
|
14
|
+
}
|
|
15
|
+
function num(value) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Normalize one provider usage payload to full totals. Returns `null` when
|
|
20
|
+
* the payload carries no usable numeric signal at all (not an object, or an
|
|
21
|
+
* object with no recognized numeric field) — callers treat that as "no
|
|
22
|
+
* accounting for this run", never as zeros that would dilute real totals.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeUsage(raw) {
|
|
25
|
+
if (typeof raw !== "object" || raw === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const payload = raw;
|
|
29
|
+
const input = num(payload.inputTokens);
|
|
30
|
+
const output = num(payload.outputTokens);
|
|
31
|
+
const cacheRead = num(payload.cacheReadTokens);
|
|
32
|
+
const cacheWrite = num(payload.cacheWriteTokens);
|
|
33
|
+
const reasoning = num(payload.reasoningTokens);
|
|
34
|
+
const recognized = [input, output, cacheRead, cacheWrite, reasoning].some((n) => n > 0);
|
|
35
|
+
const totalRaw = num(payload.totalTokens);
|
|
36
|
+
if (!recognized && totalRaw === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
input,
|
|
41
|
+
output,
|
|
42
|
+
cacheRead,
|
|
43
|
+
cacheWrite,
|
|
44
|
+
reasoning,
|
|
45
|
+
total: totalRaw > 0 ? totalRaw : input + output,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Pure field-wise sum; either side may be a zero value. */
|
|
49
|
+
export function sumUsage(a, b) {
|
|
50
|
+
return {
|
|
51
|
+
input: a.input + b.input,
|
|
52
|
+
output: a.output + b.output,
|
|
53
|
+
cacheRead: a.cacheRead + b.cacheRead,
|
|
54
|
+
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
55
|
+
reasoning: a.reasoning + b.reasoning,
|
|
56
|
+
total: a.total + b.total,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=token-usage.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"description": "ZCode-style workspace memory for DeepSeek Harness: one central SQLite store partitioned by workspace, index injection, and sole-writer gated writes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|