dsh-continual-evolve 0.15.13 → 0.15.14

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 CHANGED
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-395%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-397%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-395%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-397%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
package/lib/review.js CHANGED
@@ -21,7 +21,7 @@ import { runChain } from "./chain.js";
21
21
  import { resolveCallRoute } from "./llm-call.js";
22
22
  import { cwdOf } from "./memory-section.js";
23
23
  import { collectCandidates } from "./review-candidates.js";
24
- import { reviewChain } from "./review-ring.js";
24
+ import { failureHead, reviewChain } from "./review-ring.js";
25
25
  import { subagentLoaderFor } from "./subagent.js";
26
26
  import { zeroUsage } from "./token-usage.js";
27
27
  /** Defaults for the review settings the host may override — and edit live. */
@@ -51,51 +51,81 @@ export async function runReviewBatch(deps, candidates, opts) {
51
51
  // order the ring declares. Without a route there is no ring to walk.
52
52
  const ring = route === undefined || llm === undefined ? undefined : assembleRing({ llm, route, sessionId: opts.sessionId });
53
53
  let position = 0;
54
- for (const candidate of batch) {
55
- const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
56
- if (remaining < 0) {
57
- // Hard cap hit: this item and everything after it drops (low
58
- // priority never rolls over — it simply waits for a future run).
59
- summary.dropped += batch.length - position;
60
- for (const item of batch.slice(position)) {
54
+ // The run's account is written whatever happens to the loop: a fault that
55
+ // escapes one item (or the loop itself) is rethrown *after* the rollup is
56
+ // merged, so a dead run can never leave `reviewed` incremented with every
57
+ // other counter at zero and nothing saying why (2026-10-14 incident).
58
+ let escaped;
59
+ let failed = false;
60
+ try {
61
+ for (const candidate of batch) {
62
+ try {
63
+ const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
64
+ if (remaining < 0) {
65
+ // Hard cap hit: this item and everything after it drops (low
66
+ // priority never rolls over — it simply waits for a future run).
67
+ summary.dropped += batch.length - position;
68
+ for (const item of batch.slice(position)) {
69
+ deps.store.logReview({
70
+ runId, workspaceId: item.workspaceId, itemKind: item.kind, itemId: item.id,
71
+ verdict: "skip", reason: "budget-exhausted", applied: 0,
72
+ });
73
+ }
74
+ skipReasons.push("budget-exhausted");
75
+ break;
76
+ }
77
+ summary.reviewed += 1;
78
+ if (candidate.priority === 0) {
79
+ newItems += 1;
80
+ }
81
+ else {
82
+ requeued += 1;
83
+ }
84
+ if (route === undefined || ring === undefined) {
85
+ deps.store.logReview({
86
+ runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
87
+ verdict: "skip", reason: "no-route", applied: 0,
88
+ });
89
+ skipReasons.push("no-route");
90
+ position += 1;
91
+ continue;
92
+ }
93
+ const item = {
94
+ deps, loader: ring.loader, route, runId, candidate, opts, summary, skipReasons,
95
+ gate: { usage: zeroUsage(), raw: undefined },
96
+ };
97
+ await runChain(ring.links, item);
98
+ }
99
+ catch (error) {
100
+ // A fault escaping the chain (a store write, a wiring bug) gets its
101
+ // own row carrying the error text and costs the batch only this
102
+ // item — the model-facing failures already degrade inside the ring.
61
103
  deps.store.logReview({
62
- runId, workspaceId: item.workspaceId, itemKind: item.kind, itemId: item.id,
63
- verdict: "skip", reason: "budget-exhausted", applied: 0,
104
+ runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
105
+ verdict: "skip", reason: `review-failed: ${failureHead(error)}`,
106
+ model: route?.model, applied: 0,
64
107
  });
108
+ skipReasons.push("review-failed");
65
109
  }
66
- skipReasons.push("budget-exhausted");
67
- break;
68
- }
69
- summary.reviewed += 1;
70
- if (candidate.priority === 0) {
71
- newItems += 1;
72
- }
73
- else {
74
- requeued += 1;
75
- }
76
- if (route === undefined || ring === undefined) {
77
- deps.store.logReview({
78
- runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
79
- verdict: "skip", reason: "no-route", applied: 0,
80
- });
81
- skipReasons.push("no-route");
82
110
  position += 1;
83
- continue;
84
111
  }
85
- const item = {
86
- deps, loader: ring.loader, route, runId, candidate, opts, summary, skipReasons,
87
- gate: { usage: zeroUsage(), raw: undefined },
88
- };
89
- await runChain(ring.links, item);
90
- position += 1;
91
112
  }
92
- summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
93
- deps.store.mergeDaily(opts.day, opts.budget, {
94
- newItems, requeued, approved: summary.approved, applied: summary.applied,
95
- cheapToken: summary.cheapToken, evolveToken: summary.evolveToken,
96
- backlog: summary.backlog, dropped: summary.dropped,
97
- skipReasons: skipReasons.length > 0 ? skipReasons : undefined,
98
- });
113
+ catch (error) {
114
+ failed = true;
115
+ escaped = error;
116
+ }
117
+ finally {
118
+ summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
119
+ deps.store.mergeDaily(opts.day, opts.budget, {
120
+ newItems, requeued, approved: summary.approved, applied: summary.applied,
121
+ cheapToken: summary.cheapToken, evolveToken: summary.evolveToken,
122
+ backlog: summary.backlog, dropped: summary.dropped,
123
+ skipReasons: skipReasons.length > 0 ? skipReasons : undefined,
124
+ });
125
+ }
126
+ if (failed) {
127
+ throw escaped;
128
+ }
99
129
  return summary;
100
130
  }
101
131
  /** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
@@ -140,26 +170,39 @@ export function createReviewScheduler(host, deps, config) {
140
170
  const workspace = triggerWorkspace ?? pendingWorkspace;
141
171
  pendingWorkspace = undefined;
142
172
  void (async () => {
173
+ const settings = config.read();
174
+ const day = todayUtc();
143
175
  try {
144
- const settings = config.read();
145
- const day = todayUtc();
146
176
  const daily = deps.store.getDaily(day);
147
177
  const remaining = settings.dailyBudget - (daily?.reviewed ?? 0);
148
178
  if (remaining <= 0) {
179
+ host.logger("continual-evolve").info(`review drain skipped: daily budget spent (${daily?.reviewed ?? 0}/${settings.dailyBudget})`);
149
180
  return;
150
181
  }
151
182
  const candidates = collectCandidates(deps.store, { staleDays: settings.staleDays, limit: settings.batchSize * 2 });
152
183
  const ordered = workspace === undefined
153
184
  ? candidates
154
185
  : [...candidates.filter((c) => c.workspaceId === workspace), ...candidates.filter((c) => c.workspaceId !== workspace)];
155
- await runReviewBatch(deps, ordered.slice(0, settings.batchSize), {
186
+ const summary = await runReviewBatch(deps, ordered.slice(0, settings.batchSize), {
156
187
  day, budget: settings.dailyBudget, dryRun: settings.dryRun,
157
188
  batchSize: settings.batchSize, staleDays: settings.staleDays,
158
189
  provider: lastRoute?.provider, model: lastRoute?.model, sessionId: lastRoute?.sessionId,
159
190
  });
191
+ host.logger("continual-evolve").info(`review drain: reviewed ${summary.reviewed}, approved ${summary.approved}, ` +
192
+ `applied ${summary.applied}, dropped ${summary.dropped}, backlog ${summary.backlog}`);
160
193
  }
161
194
  catch (error) {
162
- host.logger("continual-evolve").warn(`review drain failed: ${error instanceof Error ? error.message : String(error)}`);
195
+ const head = failureHead(error);
196
+ host.logger("continual-evolve").warn(`review drain failed: ${head}`);
197
+ // Two records on purpose: the logger is the live channel, the day's
198
+ // rollup is the durable one every later reading sees. A drain that
199
+ // died must leave a trace in the ledger, not only in a log file.
200
+ try {
201
+ deps.store.mergeDaily(day, settings.dailyBudget, { skipReasons: [`drain-failed: ${head}`] });
202
+ }
203
+ catch (recordError) {
204
+ host.logger("continual-evolve").warn(`review drain failure not ledgered: ${failureHead(recordError)}`);
205
+ }
163
206
  }
164
207
  finally {
165
208
  running = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.15.13",
3
+ "version": "0.15.14",
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": {