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.
@@ -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
- import { type UsageTotals } from "./token-usage.js";
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 review-loop audit view over the extraction ledger (read-only). */
54
- export interface ReviewSummary {
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
- rejected: number;
57
- skipped: number;
58
- failed: number;
59
- /** Top skip reasons, most frequent first (at most 3, display-truncated). */
60
- topSkips: {
61
- reason: string;
62
- count: number;
63
- }[];
64
- /** Token totals over the same rows (unnormalizable payloads contribute nothing). */
65
- tokens: UsageTotals;
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
- * Read-only audit over the extraction ledger for one workspace: judgment
209
- * counts, top skip reasons, and token totals. Degrades to zeros on any
210
- * fault — the card observes, never breaks. `since` is an inclusive ISO
211
- * lower bound; empty counts every row.
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
- reviewSummary(workspaceId: string, since?: string): ReviewSummary;
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
- return memory + skills;
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
- reviewSummary(workspaceId, since) {
882
+ recordMemoryUse(workspaceId, memoryId) {
830
883
  try {
831
- const rows = stmts.reviewRows.all(workspaceId, since ?? "");
832
- const summary = zeroReviewSummary();
833
- const skips = new Map();
834
- for (const row of rows) {
835
- if (row.status === "applied") {
836
- summary.applied += 1;
837
- }
838
- else if (row.status === "rejected") {
839
- summary.rejected += 1;
840
- }
841
- else if (row.status === "failed") {
842
- summary.failed += 1;
843
- }
844
- else {
845
- summary.skipped += 1;
846
- }
847
- if (row.status !== "applied" && row.status !== "rejected" && typeof row.skip_reason === "string" && row.skip_reason.length > 0) {
848
- skips.set(row.skip_reason, (skips.get(row.skip_reason) ?? 0) + 1);
849
- }
850
- if (typeof row.usage === "string" && row.usage.length > 0) {
851
- try {
852
- const normalized = normalizeUsage(JSON.parse(row.usage));
853
- if (normalized !== null) {
854
- summary.tokens = sumUsage(summary.tokens, normalized);
855
- }
856
- }
857
- catch {
858
- // One corrupt payload must not void the whole audit.
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
- summary.topSkips = [...skips.entries()]
863
- .sort((a, b) => b[1] - a[1])
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 zeroReviewSummary();
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));
@@ -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 ledger
8
- * rows lives in `store.reviewSummary`, which stays read-only and bounded.
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 {
@@ -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 ledger
8
- * rows lives in `store.reviewSummary`, which stays read-only and bounded.
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.6",
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": {