dsh-continual-evolve 0.15.6 → 0.15.8

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/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,13 +125,15 @@ 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;
101
132
  durationMs?: number | undefined;
102
133
  model?: string | undefined;
103
134
  usage?: unknown;
135
+ /** Pre-gate call payload (extraction cheap gate); `undefined` when the run never reached it. */
136
+ cheapUsage?: unknown;
104
137
  /** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`; a parse-failed run instead carries the raw answer's head. */
105
138
  files?: unknown;
106
139
  }
@@ -205,12 +238,39 @@ export interface MemoryStore {
205
238
  /** Aggregate usage for one skill, undefined when never consumed. */
206
239
  getSkillUse(workspaceId: string, skillId: string, scope: string): SkillUse | undefined;
207
240
  /**
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.
241
+ * Record one real memory consumption (a body served by id, never a list
242
+ * or search hit). Same heartbeat discipline as skill usage: accounting
243
+ * never fails the read it rides on.
244
+ */
245
+ recordMemoryUse(workspaceId: string, memoryId: string): void;
246
+ /** Aggregate usage for one memory, undefined when never consumed. */
247
+ getMemoryUse(workspaceId: string, memoryId: string): SkillUse | undefined;
248
+ /** Append one per-item content-review verdict (the `item-review` account). */
249
+ logReview(entry: ReviewLogEntry): void;
250
+ /**
251
+ * Atomically consume one unit of today's review budget: returns the
252
+ * remaining budget after consumption, or -1 when the day is exhausted
253
+ * (the caller drops the low-priority item and counts it as dropped).
254
+ */
255
+ consumeReviewBudget(day: string, budget: number): number;
256
+ /** Today's rollup row, undefined when nothing ran yet today. */
257
+ getDaily(day: string): DailyStats | undefined;
258
+ /** Merge one run's counters into today's rollup row (upsert, additive). */
259
+ mergeDaily(day: string, budget: number, delta: DailyDelta): void;
260
+ /**
261
+ * Soft-isolate one item (review quarantine verdict): hidden from
262
+ * injection and reads, never deleted — the ledger keeps the before
263
+ * snapshot for undo. Returns false when the row is absent.
264
+ */
265
+ quarantine(workspaceId: string, kind: "memory" | "skill", id: string): boolean;
266
+ /**
267
+ * The review history of one item: last verdict instant and landed
268
+ * rewrite count, undefined when never reviewed (a new item).
212
269
  */
213
- reviewSummary(workspaceId: string, since?: string): ReviewSummary;
270
+ reviewStateOf(workspaceId: string, kind: "memory" | "skill", id: string): {
271
+ lastTs: string | null;
272
+ evolveCount: number;
273
+ } | undefined;
214
274
  }
215
275
  /** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
216
276
  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,9 +67,10 @@ 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
+ cheap_usage TEXT,
73
74
  files TEXT, occurrences INTEGER NOT NULL DEFAULT 1
74
75
  );
75
76
  CREATE TABLE IF NOT EXISTS state (
@@ -111,11 +112,46 @@ CREATE TABLE IF NOT EXISTS skill_usage (
111
112
  last_used_at TEXT,
112
113
  PRIMARY KEY (workspace_id, skill_id, scope)
113
114
  ) STRICT;
115
+ -- Memory usage counters (content-quality loop): one aggregate row per memory,
116
+ -- the same presence-heartbeat discipline as skill_usage — a body served by id
117
+ -- counts, search/list hits stay uncounted.
118
+ CREATE TABLE IF NOT EXISTS memory_usage (
119
+ workspace_id TEXT NOT NULL,
120
+ memory_id TEXT NOT NULL,
121
+ use_count INTEGER NOT NULL DEFAULT 0,
122
+ last_used_at TEXT,
123
+ PRIMARY KEY (workspace_id, memory_id)
124
+ ) STRICT;
125
+ -- Per-item content-review verdicts (the item-review trigger's account):
126
+ -- one row per reviewed item per run. Pruned with the ledgers.
127
+ CREATE TABLE IF NOT EXISTS review_log (
128
+ run_id TEXT, ts TEXT NOT NULL, workspace_id TEXT,
129
+ item_kind TEXT NOT NULL CHECK (item_kind IN ('memory','skill')),
130
+ item_id TEXT NOT NULL,
131
+ verdict TEXT NOT NULL CHECK (verdict IN ('keep','rewrite','quarantine','skip')),
132
+ reason TEXT,
133
+ cheap_usage TEXT, evolve_usage TEXT,
134
+ applied INTEGER NOT NULL DEFAULT 0,
135
+ evolve_count INTEGER NOT NULL DEFAULT 0
136
+ );
137
+ -- One rollup row per UTC day: the hard-cap account (budget vs reviewed vs
138
+ -- backlog vs dropped) plus the token account split by cheap/evolve purpose.
139
+ CREATE TABLE IF NOT EXISTS review_daily (
140
+ day TEXT NOT NULL PRIMARY KEY,
141
+ budget INTEGER NOT NULL,
142
+ reviewed INTEGER NOT NULL DEFAULT 0,
143
+ new_items INTEGER NOT NULL DEFAULT 0,
144
+ requeued INTEGER NOT NULL DEFAULT 0,
145
+ approved INTEGER NOT NULL DEFAULT 0,
146
+ applied INTEGER NOT NULL DEFAULT 0,
147
+ cheap_token INTEGER NOT NULL DEFAULT 0,
148
+ evolve_token INTEGER NOT NULL DEFAULT 0,
149
+ backlog INTEGER NOT NULL DEFAULT 0,
150
+ dropped INTEGER NOT NULL DEFAULT 0,
151
+ skip_top TEXT,
152
+ skip_counts TEXT
153
+ ) STRICT;
114
154
  `;
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
155
  /** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
120
156
  export function defaultStorePath() {
121
157
  const home = process.env.DSH_HOME?.trim();
@@ -133,6 +169,7 @@ function recordOf(row) {
133
169
  sourceSession: row.source_session,
134
170
  sourceSeqs: row.source_seqs,
135
171
  sourceRun: row.source_run,
172
+ corrects: row.corrects ?? null,
136
173
  createdAt: row.created_at,
137
174
  updatedAt: row.updated_at,
138
175
  };
@@ -263,6 +300,8 @@ export async function openMemoryStore(explicitPath) {
263
300
  }
264
301
  database.exec(DDL);
265
302
  ensureLedgerOccurrences(database);
303
+ ensureMemoriesCorrects(database);
304
+ ensureExtractionCheapUsage(database);
266
305
  const stmts = {
267
306
  get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
268
307
  list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
@@ -281,8 +320,8 @@ export async function openMemoryStore(explicitPath) {
281
320
  "WHERE status = 'active' AND workspace_id <> '' GROUP BY workspace_id ORDER BY ts DESC LIMIT 1"),
282
321
  recentSkill: database.prepare("SELECT workspace_id, MAX(ts) AS ts FROM skill_log " +
283
322
  "WHERE workspace_id IS NOT NULL AND workspace_id <> '' GROUP BY workspace_id ORDER BY ts DESC LIMIT 1"),
284
- ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
285
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
323
+ ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, cheap_usage, files) " +
324
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
286
325
  latestSkip: database.prepare("SELECT rowid AS rid, skip_reason AS reason, status FROM extraction_log " +
287
326
  "WHERE workspace_id IS ? AND session_id IS ? ORDER BY ts DESC, rowid DESC LIMIT 1"),
288
327
  bumpSkip: database.prepare(`UPDATE extraction_log SET ts = ?, ${LEDGER_OCCURRENCES} = ${LEDGER_OCCURRENCES} + 1 WHERE rowid = ?`),
@@ -311,10 +350,25 @@ export async function openMemoryStore(explicitPath) {
311
350
  skillActiveRows: database.prepare("SELECT * FROM skills WHERE status = 'active'"),
312
351
  skillQuarantine: database.prepare("UPDATE skills SET status = 'quarantined' WHERE workspace_id = ? AND id = ?"),
313
352
  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
353
  skillUseGet: database.prepare("SELECT use_count, last_used_at FROM skill_usage WHERE workspace_id = ? AND skill_id = ? AND scope = ?"),
316
354
  skillUseUpsert: database.prepare("INSERT INTO skill_usage (workspace_id, skill_id, scope, use_count, last_used_at) VALUES (?, ?, ?, 1, ?) " +
317
355
  "ON CONFLICT (workspace_id, skill_id, scope) DO UPDATE SET use_count = use_count + 1, last_used_at = excluded.last_used_at"),
356
+ memUseGet: database.prepare("SELECT use_count, last_used_at FROM memory_usage WHERE workspace_id = ? AND memory_id = ?"),
357
+ memUseUpsert: database.prepare("INSERT INTO memory_usage (workspace_id, memory_id, use_count, last_used_at) VALUES (?, ?, 1, ?) " +
358
+ "ON CONFLICT (workspace_id, memory_id) DO UPDATE SET use_count = use_count + 1, last_used_at = excluded.last_used_at"),
359
+ 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) " +
360
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
361
+ 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"),
362
+ reviewEvolved: database.prepare("SELECT COUNT(*) AS n FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND applied = 1"),
363
+ reviewPrune: database.prepare("DELETE FROM review_log WHERE ts < ?"),
364
+ dailyGet: database.prepare("SELECT * FROM review_daily WHERE day = ?"),
365
+ 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) " +
366
+ "VALUES (?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL) " +
367
+ "ON CONFLICT (day) DO UPDATE SET budget = excluded.budget"),
368
+ dailyConsume: database.prepare("UPDATE review_daily SET reviewed = reviewed + 1 WHERE day = ? AND reviewed < budget"),
369
+ dailyMerge: database.prepare("UPDATE review_daily SET new_items = new_items + ?, requeued = requeued + ?, " +
370
+ "approved = approved + ?, applied = applied + ?, cheap_token = cheap_token + ?, evolve_token = evolve_token + ?, " +
371
+ "backlog = ?, dropped = dropped + ?, skip_top = ?, skip_counts = ? WHERE day = ?"),
318
372
  };
319
373
  // LIKE fallback: one OR-triplet per token, every token required. Statements
320
374
  // are cached by token count — the query set repeats, the counts don't grow.
@@ -729,7 +783,8 @@ export async function openMemoryStore(explicitPath) {
729
783
  pruneLedger(beforeIso) {
730
784
  const memory = Number(stmts.prune.run(beforeIso).changes);
731
785
  const skills = Number(stmts.skillPrune.run(beforeIso).changes);
732
- return memory + skills;
786
+ const reviews = Number(stmts.reviewPrune.run(beforeIso).changes);
787
+ return memory + skills + reviews;
733
788
  },
734
789
  close() {
735
790
  database.close();
@@ -826,50 +881,86 @@ export async function openMemoryStore(explicitPath) {
826
881
  return undefined;
827
882
  }
828
883
  },
829
- reviewSummary(workspaceId, since) {
884
+ recordMemoryUse(workspaceId, memoryId) {
830
885
  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
- }
886
+ stmts.memUseUpsert.run(workspaceId, memoryId, new Date().toISOString());
887
+ }
888
+ catch {
889
+ // Accounting never fails the read it rides on.
890
+ }
891
+ },
892
+ getMemoryUse(workspaceId, memoryId) {
893
+ try {
894
+ const row = stmts.memUseGet.get(workspaceId, memoryId);
895
+ if (row === undefined) {
896
+ return undefined;
861
897
  }
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;
898
+ return { useCount: row.use_count, lastUsedAt: row.last_used_at };
870
899
  }
871
900
  catch {
872
- return zeroReviewSummary();
901
+ return undefined;
902
+ }
903
+ },
904
+ logReview(entry) {
905
+ 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);
906
+ },
907
+ consumeReviewBudget(day, budget) {
908
+ stmts.dailyUpsert.run(day, budget);
909
+ const result = stmts.dailyConsume.run(day);
910
+ if (Number(result.changes) === 0) {
911
+ return -1;
912
+ }
913
+ const row = stmts.dailyGet.get(day);
914
+ if (row === undefined) {
915
+ return -1;
916
+ }
917
+ return row.budget - row.reviewed;
918
+ },
919
+ getDaily(day) {
920
+ const row = stmts.dailyGet.get(day);
921
+ if (row === undefined) {
922
+ return undefined;
923
+ }
924
+ return {
925
+ day: row.day, budget: row.budget, reviewed: row.reviewed, newItems: row.new_items,
926
+ requeued: row.requeued, approved: row.approved, applied: row.applied,
927
+ cheapToken: row.cheap_token, evolveToken: row.evolve_token,
928
+ backlog: row.backlog, dropped: row.dropped, skipTop: row.skip_top,
929
+ };
930
+ },
931
+ mergeDaily(day, budget, delta) {
932
+ stmts.dailyUpsert.run(day, budget);
933
+ const row = stmts.dailyGet.get(day);
934
+ const counts = parseCounts(row.skip_counts);
935
+ for (const reason of delta.skipReasons ?? []) {
936
+ counts[reason] = (counts[reason] ?? 0) + 1;
937
+ }
938
+ const entries = Object.entries(counts)
939
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
940
+ .slice(0, 3)
941
+ .map(([reason]) => reason);
942
+ 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);
943
+ },
944
+ quarantine(workspaceId, kind, id) {
945
+ try {
946
+ const result = (kind === "memory" ? stmts.quarantine : stmts.skillQuarantine).run(workspaceId, id);
947
+ return Number(result.changes) > 0;
948
+ }
949
+ catch {
950
+ return false;
951
+ }
952
+ },
953
+ reviewStateOf(workspaceId, kind, id) {
954
+ try {
955
+ const last = stmts.reviewLast.get(workspaceId, kind, id);
956
+ if (last === undefined) {
957
+ return undefined;
958
+ }
959
+ const evolved = stmts.reviewEvolved.get(workspaceId, kind, id);
960
+ return { lastTs: last.ts, evolveCount: Number(evolved.n) };
961
+ }
962
+ catch {
963
+ return undefined;
873
964
  }
874
965
  },
875
966
  };
@@ -887,8 +978,54 @@ function ensureLedgerOccurrences(database) {
887
978
  }
888
979
  database.exec(`ALTER TABLE extraction_log ADD COLUMN ${LEDGER_OCCURRENCES} INTEGER NOT NULL DEFAULT 1`);
889
980
  }
981
+ /**
982
+ * Add the memories `corrects` link column to a store created before the
983
+ * correction-linking field existed. Same single-migration discipline as the
984
+ * ledger occurrences column above.
985
+ */
986
+ function ensureMemoriesCorrects(database) {
987
+ const columns = database.prepare("SELECT name FROM pragma_table_info('memories')").all();
988
+ if (columns.some((column) => column.name === "corrects")) {
989
+ return;
990
+ }
991
+ database.exec("ALTER TABLE memories ADD COLUMN corrects TEXT");
992
+ }
993
+ /**
994
+ * Add the extraction ledger `cheap_usage` column to a store created before
995
+ * the cheap pre-gate existed. Same single-migration discipline as the
996
+ * columns above.
997
+ */
998
+ function ensureExtractionCheapUsage(database) {
999
+ const columns = database.prepare("SELECT name FROM pragma_table_info('extraction_log')").all();
1000
+ if (columns.some((column) => column.name === "cheap_usage")) {
1001
+ return;
1002
+ }
1003
+ database.exec("ALTER TABLE extraction_log ADD COLUMN cheap_usage TEXT");
1004
+ }
1005
+ /** Parse the daily skip-counts map; corrupt payloads degrade to empty. */
1006
+ function parseCounts(raw) {
1007
+ if (raw === null) {
1008
+ return {};
1009
+ }
1010
+ try {
1011
+ const parsed = JSON.parse(raw);
1012
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1013
+ return {};
1014
+ }
1015
+ const counts = {};
1016
+ for (const [key, value] of Object.entries(parsed)) {
1017
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1018
+ counts[key] = Math.floor(value);
1019
+ }
1020
+ }
1021
+ return counts;
1022
+ }
1023
+ catch {
1024
+ return {};
1025
+ }
1026
+ }
890
1027
  /** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
891
1028
  function writeLedger(stmt, entry) {
892
- 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));
1029
+ 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.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.files === undefined ? null : JSON.stringify(entry.files));
893
1030
  }
894
1031
  //# sourceMappingURL=store.js.map
@@ -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.8",
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": {