dsh-continual-evolve 0.15.6 → 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 +6 -46
- package/lib/card-routes.js +3 -8
- 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 +1 -3
- package/lib/memory-snapshot.js +1 -11
- 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-tools.js +3 -2
- package/lib/store-apply.js +40 -5
- package/lib/store.d.ts +80 -22
- package/lib/store.js +171 -48
- package/lib/token-usage.d.ts +3 -2
- package/lib/token-usage.js +3 -2
- 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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { MemoryProposal } from "./memory-rules.js";
|
|
2
2
|
import { type SkillProposal } from "./skill-rules.js";
|
|
3
3
|
import { type ProposalContext, type ProposalOutcome } from "./store-apply.js";
|
|
4
|
-
|
|
5
|
-
export type { MemoryProposal, ProposalContext, ProposalOutcome, UsageTotals };
|
|
4
|
+
export type { MemoryProposal, ProposalContext, ProposalOutcome };
|
|
6
5
|
/** Thrown when the runtime lacks `node:sqlite` — the degrade signal. */
|
|
7
6
|
export declare class StoreUnavailableError extends Error {
|
|
8
7
|
}
|
|
@@ -50,22 +49,52 @@ export interface SkillUse {
|
|
|
50
49
|
/** ISO instant of the last counted use, `null` when never consumed. */
|
|
51
50
|
lastUsedAt: string | null;
|
|
52
51
|
}
|
|
53
|
-
/** One
|
|
54
|
-
export interface
|
|
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;
|
|
55
77
|
applied: number;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
97
|
}
|
|
67
|
-
/** The empty audit view: no rows, no signal — never a failure. */
|
|
68
|
-
export declare function zeroReviewSummary(): ReviewSummary;
|
|
69
98
|
/** One stored memory record (snake_case columns camelCased). */
|
|
70
99
|
export interface MemoryRecord {
|
|
71
100
|
workspaceId: string;
|
|
@@ -81,6 +110,8 @@ export interface MemoryRecord {
|
|
|
81
110
|
sourceSeqs: string | null;
|
|
82
111
|
/** The extraction run that wrote it; `'explicit'` for tool writes. */
|
|
83
112
|
sourceRun: string | null;
|
|
113
|
+
/** Id of the memory this row corrects/supersedes in the same workspace, `null` when standalone. */
|
|
114
|
+
corrects: string | null;
|
|
84
115
|
createdAt: string;
|
|
85
116
|
updatedAt: string;
|
|
86
117
|
}
|
|
@@ -94,7 +125,7 @@ export interface LedgerEntry {
|
|
|
94
125
|
runId?: string | undefined;
|
|
95
126
|
sessionId?: string | undefined;
|
|
96
127
|
workspaceId?: string | undefined;
|
|
97
|
-
trigger: "turn-debounce" | "compaction" | "close-drain" | "explicit" | "import";
|
|
128
|
+
trigger: "turn-debounce" | "compaction" | "close-drain" | "explicit" | "import" | "item-review";
|
|
98
129
|
decision?: string | undefined;
|
|
99
130
|
skipReason?: string | undefined;
|
|
100
131
|
status?: string | undefined;
|
|
@@ -205,12 +236,39 @@ export interface MemoryStore {
|
|
|
205
236
|
/** Aggregate usage for one skill, undefined when never consumed. */
|
|
206
237
|
getSkillUse(workspaceId: string, skillId: string, scope: string): SkillUse | undefined;
|
|
207
238
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
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).
|
|
212
267
|
*/
|
|
213
|
-
|
|
268
|
+
reviewStateOf(workspaceId: string, kind: "memory" | "skill", id: string): {
|
|
269
|
+
lastTs: string | null;
|
|
270
|
+
evolveCount: number;
|
|
271
|
+
} | undefined;
|
|
214
272
|
}
|
|
215
273
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
216
274
|
export declare function defaultStorePath(): string;
|
package/lib/store.js
CHANGED
|
@@ -17,7 +17,6 @@ import { secretLeakReason } from "./memory-rules.js";
|
|
|
17
17
|
import { globalSkillsDir, projectSkillsDir, renderSkillMarkdown, SKILL_GENERATED_MARKER, skillDir, skillFieldGateReasons, slugifySkillId, validateRenderedSkill, } from "./skill-rules.js";
|
|
18
18
|
import { isValidMemoryId } from "./memory-rules.js";
|
|
19
19
|
import { applyProposals } from "./store-apply.js";
|
|
20
|
-
import { normalizeUsage, sumUsage, zeroUsage } from "./token-usage.js";
|
|
21
20
|
/** Thrown when the runtime lacks `node:sqlite` — the degrade signal. */
|
|
22
21
|
export class StoreUnavailableError extends Error {
|
|
23
22
|
}
|
|
@@ -52,6 +51,7 @@ PRAGMA busy_timeout=5000;CREATE TABLE IF NOT EXISTS memories (
|
|
|
52
51
|
source_session TEXT,
|
|
53
52
|
source_seqs TEXT,
|
|
54
53
|
source_run TEXT,
|
|
54
|
+
corrects TEXT,
|
|
55
55
|
created_at TEXT NOT NULL,
|
|
56
56
|
updated_at TEXT NOT NULL,
|
|
57
57
|
PRIMARY KEY (workspace_id, id)
|
|
@@ -67,7 +67,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
67
67
|
-- patrol() reconciles any residue.
|
|
68
68
|
CREATE TABLE IF NOT EXISTS extraction_log (
|
|
69
69
|
run_id TEXT, ts TEXT NOT NULL, session_id TEXT, workspace_id TEXT,
|
|
70
|
-
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')),
|
|
71
71
|
decision TEXT, skip_reason TEXT,
|
|
72
72
|
status TEXT, duration_ms INTEGER, model TEXT, usage TEXT,
|
|
73
73
|
files TEXT, occurrences INTEGER NOT NULL DEFAULT 1
|
|
@@ -111,11 +111,46 @@ CREATE TABLE IF NOT EXISTS skill_usage (
|
|
|
111
111
|
last_used_at TEXT,
|
|
112
112
|
PRIMARY KEY (workspace_id, skill_id, scope)
|
|
113
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;
|
|
114
153
|
`;
|
|
115
|
-
/** The empty audit view: no rows, no signal — never a failure. */
|
|
116
|
-
export function zeroReviewSummary() {
|
|
117
|
-
return { applied: 0, rejected: 0, skipped: 0, failed: 0, topSkips: [], tokens: zeroUsage() };
|
|
118
|
-
}
|
|
119
154
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
120
155
|
export function defaultStorePath() {
|
|
121
156
|
const home = process.env.DSH_HOME?.trim();
|
|
@@ -133,6 +168,7 @@ function recordOf(row) {
|
|
|
133
168
|
sourceSession: row.source_session,
|
|
134
169
|
sourceSeqs: row.source_seqs,
|
|
135
170
|
sourceRun: row.source_run,
|
|
171
|
+
corrects: row.corrects ?? null,
|
|
136
172
|
createdAt: row.created_at,
|
|
137
173
|
updatedAt: row.updated_at,
|
|
138
174
|
};
|
|
@@ -263,6 +299,7 @@ export async function openMemoryStore(explicitPath) {
|
|
|
263
299
|
}
|
|
264
300
|
database.exec(DDL);
|
|
265
301
|
ensureLedgerOccurrences(database);
|
|
302
|
+
ensureMemoriesCorrects(database);
|
|
266
303
|
const stmts = {
|
|
267
304
|
get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
|
|
268
305
|
list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
|
|
@@ -311,10 +348,25 @@ export async function openMemoryStore(explicitPath) {
|
|
|
311
348
|
skillActiveRows: database.prepare("SELECT * FROM skills WHERE status = 'active'"),
|
|
312
349
|
skillQuarantine: database.prepare("UPDATE skills SET status = 'quarantined' WHERE workspace_id = ? AND id = ?"),
|
|
313
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"),
|
|
314
|
-
reviewRows: database.prepare("SELECT status, skip_reason, usage FROM extraction_log WHERE workspace_id = ? AND ts >= ?"),
|
|
315
351
|
skillUseGet: database.prepare("SELECT use_count, last_used_at FROM skill_usage WHERE workspace_id = ? AND skill_id = ? AND scope = ?"),
|
|
316
352
|
skillUseUpsert: database.prepare("INSERT INTO skill_usage (workspace_id, skill_id, scope, use_count, last_used_at) VALUES (?, ?, ?, 1, ?) " +
|
|
317
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 = ?"),
|
|
318
370
|
};
|
|
319
371
|
// LIKE fallback: one OR-triplet per token, every token required. Statements
|
|
320
372
|
// are cached by token count — the query set repeats, the counts don't grow.
|
|
@@ -729,7 +781,8 @@ export async function openMemoryStore(explicitPath) {
|
|
|
729
781
|
pruneLedger(beforeIso) {
|
|
730
782
|
const memory = Number(stmts.prune.run(beforeIso).changes);
|
|
731
783
|
const skills = Number(stmts.skillPrune.run(beforeIso).changes);
|
|
732
|
-
|
|
784
|
+
const reviews = Number(stmts.reviewPrune.run(beforeIso).changes);
|
|
785
|
+
return memory + skills + reviews;
|
|
733
786
|
},
|
|
734
787
|
close() {
|
|
735
788
|
database.close();
|
|
@@ -826,50 +879,86 @@ export async function openMemoryStore(explicitPath) {
|
|
|
826
879
|
return undefined;
|
|
827
880
|
}
|
|
828
881
|
},
|
|
829
|
-
|
|
882
|
+
recordMemoryUse(workspaceId, memoryId) {
|
|
830
883
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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;
|
|
861
956
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
.slice(0, 3)
|
|
865
|
-
.map(([reason, count]) => ({
|
|
866
|
-
reason: reason.length > 80 ? `${reason.slice(0, 80)}…` : reason,
|
|
867
|
-
count,
|
|
868
|
-
}));
|
|
869
|
-
return summary;
|
|
957
|
+
const evolved = stmts.reviewEvolved.get(workspaceId, kind, id);
|
|
958
|
+
return { lastTs: last.ts, evolveCount: Number(evolved.n) };
|
|
870
959
|
}
|
|
871
960
|
catch {
|
|
872
|
-
return
|
|
961
|
+
return undefined;
|
|
873
962
|
}
|
|
874
963
|
},
|
|
875
964
|
};
|
|
@@ -887,6 +976,40 @@ function ensureLedgerOccurrences(database) {
|
|
|
887
976
|
}
|
|
888
977
|
database.exec(`ALTER TABLE extraction_log ADD COLUMN ${LEDGER_OCCURRENCES} INTEGER NOT NULL DEFAULT 1`);
|
|
889
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
|
+
}
|
|
890
1013
|
/** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
|
|
891
1014
|
function writeLedger(stmt, entry) {
|
|
892
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));
|
package/lib/token-usage.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* `inputTokens`/`outputTokens` are usual, `totalTokens`, `cacheReadTokens`,
|
|
5
5
|
* `cacheWriteTokens`, and `reasoningTokens` come and go — so every call site
|
|
6
6
|
* normalizes through {@link normalizeUsage} and aggregates with
|
|
7
|
-
* {@link sumUsage}. Nothing here touches the store: aggregation over
|
|
8
|
-
* rows lives in
|
|
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).
|
|
9
10
|
*/
|
|
10
11
|
/** Token counts for one LLM call, every field present, zeros when unknown. */
|
|
11
12
|
export interface UsageTotals {
|
package/lib/token-usage.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* `inputTokens`/`outputTokens` are usual, `totalTokens`, `cacheReadTokens`,
|
|
5
5
|
* `cacheWriteTokens`, and `reasoningTokens` come and go — so every call site
|
|
6
6
|
* normalizes through {@link normalizeUsage} and aggregates with
|
|
7
|
-
* {@link sumUsage}. Nothing here touches the store: aggregation over
|
|
8
|
-
* rows lives in
|
|
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).
|
|
9
10
|
*/
|
|
10
11
|
/** The zero value: no signal, not a failure. */
|
|
11
12
|
export function zeroUsage() {
|
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": {
|