@dzhechkov/harness-core 0.8.33 → 0.8.35
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/.dz-manifest.json +52 -52
- package/README.md +228 -5
- package/dist/agentdb-index.d.ts +39 -7
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +217 -23
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +197 -6
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +858 -46
- package/dist/apply-leg.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/mutation-gate.d.ts +19 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +37 -1
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +16 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +115 -13
- package/dist/operations.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +72 -0
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +150 -4
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/release.d.ts +72 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +236 -19
- package/dist/release.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +90 -14
- package/dist/setup.js.map +1 -1
- package/dist/skills.d.ts +87 -3
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +266 -15
- package/dist/skills.js.map +1 -1
- package/dist/vector-tier.d.ts +27 -2
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +117 -4
- package/dist/vector-tier.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +51 -51
- package/src/agentdb-index.ts +223 -24
- package/src/apply-leg.ts +875 -46
- package/src/index.ts +18 -2
- package/src/mutation-gate.ts +58 -2
- package/src/operations.ts +117 -14
- package/src/publish-sibling-drift.ts +209 -4
- package/src/release.ts +263 -17
- package/src/setup.ts +81 -16
- package/src/skills.ts +303 -14
- package/src/vector-tier.ts +157 -5
package/src/agentdb-index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @packageDocumentation
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdirSync, copyFileSync, realpathSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
16
16
|
import { join, dirname, resolve, relative, isAbsolute, basename } from 'node:path';
|
|
17
17
|
import { pathToFileURL } from 'node:url';
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
@@ -23,7 +23,7 @@ import { applyReadonlyPragmas } from './sqlite-read-helpers.js';
|
|
|
23
23
|
import { rotatePreReindexSnapshotsUnlocked, type SnapshotRotationReport } from './agentdb-snapshot-rotation.js';
|
|
24
24
|
import { snapshotSqliteDatabase, restoreSqliteSnapshot, type SnapshotMethod, type SnapshotDbCtor } from './agentdb-snapshot.js';
|
|
25
25
|
import { withAgentdbSnapshotLock, writeReindexMarker, clearReindexMarker, markReindexMarkerRecoveryRequired, reindexMarkerPath, msFromBackupPath } from './agentdb-reindex-marker.js';
|
|
26
|
-
import { NamedLockTimeoutError } from './named-lock.js';
|
|
26
|
+
import { NamedLockTimeoutError, withNamedLockSync } from './named-lock.js';
|
|
27
27
|
import type { StoreLockOptions } from './store-lock.js';
|
|
28
28
|
// The backlog dedup embed form (PURE, zero-dep — no cycle): dz-backlog rows must be embedded in the
|
|
29
29
|
// SAME bounded form the dedup query uses, including through the reindex path.
|
|
@@ -48,10 +48,14 @@ export interface AgentdbRow {
|
|
|
48
48
|
readonly avgReward?: number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Outcome of {@link indexPatternsToAgentdb}.
|
|
51
|
+
/** Outcome of {@link indexPatternsToAgentdb}. `generationBumped`/`generationReason` are present only
|
|
52
|
+
* when a store write actually happened (`indexed > 0`) — FR-4: a failed counter write NEVER fails
|
|
53
|
+
* the indexing call itself, it is only reported so a caller (`dz doctor`, telemetry) can see it. */
|
|
52
54
|
export interface AgentdbIndexResult {
|
|
53
55
|
readonly indexed: number;
|
|
54
56
|
readonly error?: string | undefined;
|
|
57
|
+
readonly generationBumped?: boolean;
|
|
58
|
+
readonly generationReason?: string;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
interface NativeDb {
|
|
@@ -135,6 +139,112 @@ export function ensureAgentdbSchema(projectRoot: string, dbPath?: string): { rea
|
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/** `<dbFile>.generation` — a sidecar counter next to the store itself, so it travels with any copy
|
|
143
|
+
* of `.dz/agentdb.db` (backup, mirror sync) without a separate path to keep in sync. */
|
|
144
|
+
function generationFilePath(dbFile: string): string {
|
|
145
|
+
return `${dbFile}.generation`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** AM-5 (fix-round): the ONLY shape {@link readStoreGeneration} trusts — one or more ASCII digits,
|
|
149
|
+
* nothing else. `Number.parseInt` alone accepts a leading-numeric-with-trailing-junk string like
|
|
150
|
+
* `"12junk"` as `12`; that reads a corrupt sidecar as a plausible generation instead of degrading to
|
|
151
|
+
* the documented `0` compatibility floor. */
|
|
152
|
+
const STRICT_GENERATION = /^\d+$/;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* FR-2/FR-3 (`store-generation-counter`): the store's write-generation counter, read back. A
|
|
156
|
+
* missing file (a store that predates this feature, or one that has never been written through
|
|
157
|
+
* {@link bumpStoreGeneration}) reads as `0` — the compatibility floor {@link getOrOpenEngine}'s
|
|
158
|
+
* caller compares against, never an error. A corrupt/non-numeric file degrades the same way (best
|
|
159
|
+
* effort — a bad counter must never crash a read path), never a throw. AM-5: the content must match
|
|
160
|
+
* {@link STRICT_GENERATION} exactly — `Number.parseInt`'s leading-digits-only tolerance is NOT used
|
|
161
|
+
* to decide validity, only to convert an already-validated string.
|
|
162
|
+
*/
|
|
163
|
+
export function readStoreGeneration(projectRoot: string, dbPath?: string): number {
|
|
164
|
+
try {
|
|
165
|
+
const raw = readFileSync(generationFilePath(resolveAgentdbPath(projectRoot, dbPath)), 'utf8').trim();
|
|
166
|
+
if (!STRICT_GENERATION.test(raw)) return 0;
|
|
167
|
+
const n = Number.parseInt(raw, 10);
|
|
168
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
169
|
+
} catch {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* FR-1 (`store-generation-counter`): monotonically advance the store's write-generation counter by
|
|
176
|
+
* one (absent file ⇒ starts at 1), atomically (tmp + rename — the same durability discipline every
|
|
177
|
+
* other sidecar file in this module uses; a reader can never observe a half-written counter) AND
|
|
178
|
+
* under mutual exclusion (AM-2, fix-round after Codex review). A bare read→compute→rename with no
|
|
179
|
+
* lock lets two concurrent writers both read the same current value and both publish `current+1` —
|
|
180
|
+
* one bump is lost — or lets a DELAYED writer overwrite a later value with an earlier one (the
|
|
181
|
+
* counter briefly goes backwards on disk). `withNamedLockSync` (`named-lock.ts`, the repo's
|
|
182
|
+
* advisory lock for a read-modify-write file store — `.claude/rules/cross-runtime-concurrency.md`)
|
|
183
|
+
* serializes the critical section; the counter is RE-READ from disk *inside* the lock (never trusted
|
|
184
|
+
* from before acquisition), so the sequence every process observes is strictly monotonic
|
|
185
|
+
* (`agentdb-reindex-marker.ts`'s `withAgentdbSnapshotLock` is the precedent this mirrors — a lock
|
|
186
|
+
* addressed by `dirname(dbFile)`, a pure function of the store's own directory, never of
|
|
187
|
+
* `process.cwd()`).
|
|
188
|
+
*
|
|
189
|
+
* NEVER throws (FR-4): a write failure (read-only `.dz`, a full disk, a permissions error, an
|
|
190
|
+
* unresolvable path, OR a lock that could not be acquired by its deadline) is reported honestly as
|
|
191
|
+
* `{ok:false, error}` so the caller can log it — the store write it accompanies must stay successful
|
|
192
|
+
* regardless, telemetry is never a gate. AM-4: {@link resolveAgentdbPath} itself now runs INSIDE this
|
|
193
|
+
* function's outer `try` — an unresolvable path can no longer throw OUT of `bumpStoreGeneration`
|
|
194
|
+
* either; "never throws" now covers the whole function, not just the file-write tail.
|
|
195
|
+
*/
|
|
196
|
+
/** Codex round-2 (NEW HIGH): most mutators discard the bump result, so a failed bump must be
|
|
197
|
+
* VISIBLE on its own — one stderr line, written by the helper itself. Telemetry never throws. */
|
|
198
|
+
function reportBumpFailure(error: string): { readonly ok: false; readonly error: string } {
|
|
199
|
+
try { process.stderr.write(`dz: store generation not bumped — ${error}\n`); } catch { /* telemetry never throws */ }
|
|
200
|
+
return { ok: false, error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function bumpStoreGeneration(
|
|
204
|
+
projectRoot: string,
|
|
205
|
+
dbPath?: string,
|
|
206
|
+
): { readonly ok: true; readonly generation: number } | { readonly ok: false; readonly error: string } {
|
|
207
|
+
try {
|
|
208
|
+
const dbFile = resolveAgentdbPath(projectRoot, dbPath);
|
|
209
|
+
const genFile = generationFilePath(dbFile);
|
|
210
|
+
return withNamedLockSync(
|
|
211
|
+
dirname(dbFile),
|
|
212
|
+
'store-generation',
|
|
213
|
+
(): { readonly ok: true; readonly generation: number } | { readonly ok: false; readonly error: string } => {
|
|
214
|
+
// AM-2: re-read the CURRENT value from disk while holding the lock — a value observed before
|
|
215
|
+
// acquisition may already be stale, another holder may have advanced it in the meantime.
|
|
216
|
+
let current = readStoreGeneration(projectRoot, dbPath);
|
|
217
|
+
const tmp = `${genFile}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
218
|
+
try {
|
|
219
|
+
// Codex round-2 (AM-2 residual): a sidecar that EXISTS but is corrupt reads as 0 and would
|
|
220
|
+
// reset the counter to 1 — a value an engine-cache entry may already be keyed on. Floor a
|
|
221
|
+
// corrupt value at a wall-clock stamp instead: still monotonic (ms since epoch exceeds any
|
|
222
|
+
// count reached by bumping) and never colliding with an earlier generation. Absent file ⇒ 1.
|
|
223
|
+
if (current === 0 && existsSync(genFile)) {
|
|
224
|
+
const raw = readFileSync(genFile, 'utf8').trim();
|
|
225
|
+
if (raw !== '0' && !STRICT_GENERATION.test(raw)) current = Date.now();
|
|
226
|
+
}
|
|
227
|
+
const next = current + 1;
|
|
228
|
+
mkdirSync(dirname(genFile), { recursive: true });
|
|
229
|
+
writeFileSync(tmp, String(next), { encoding: 'utf8', flag: 'wx' });
|
|
230
|
+
renameSync(tmp, genFile);
|
|
231
|
+
return { ok: true, generation: next };
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Best-effort cleanup of a half-written temp file (e.g. rename failed after a successful
|
|
234
|
+
// write) so it never lingers as clutter — never lets a cleanup failure mask the real error.
|
|
235
|
+
try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best-effort only */ }
|
|
236
|
+
return reportBumpFailure(`store generation bump failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// AM-2/FR-4: a lock that could not be acquired by its deadline (`NamedLockTimeoutError`) — and
|
|
242
|
+
// any other failure reaching this point (an unresolvable path, AM-4) — degrades to the same
|
|
243
|
+
// honest `{ok:false, error}` shape; it never throws into the store write it accompanies.
|
|
244
|
+
return reportBumpFailure(`store generation bump failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
138
248
|
/**
|
|
139
249
|
* Index `rows` into the shared AgentDB vector store. Returns `{indexed:0}` for an empty input and
|
|
140
250
|
* `{indexed:0, error}` when `agentdb`/`better-sqlite3` cannot be resolved from the project.
|
|
@@ -218,8 +328,19 @@ export async function indexPatternsToAgentdb(
|
|
|
218
328
|
return prepared.length;
|
|
219
329
|
});
|
|
220
330
|
const indexed = commit() as number;
|
|
331
|
+
// AM-3 (fix-round): bump the generation IMMEDIATELY after the commit — BEFORE
|
|
332
|
+
// `writeEmbedManifest` — not after it. The store already changed on disk the instant `commit()`
|
|
333
|
+
// returned; if the manifest write throws (a jammed manifest path, a full disk), the OLD order
|
|
334
|
+
// left the DB changed with no generation ever published — a stale-cache read would then serve
|
|
335
|
+
// an engine that never saw this write, with no signal anywhere that anything went wrong. Moving
|
|
336
|
+
// the bump here makes "the store changed" and "the generation reflects it" atomic in effect:
|
|
337
|
+
// whichever of the two calls below throws, the generation is already correct for the rows that
|
|
338
|
+
// are already on disk.
|
|
339
|
+
const bump = bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
221
340
|
writeEmbedManifest(dbFile, currentEmbedManifest(model, guard.manifest.version, 'agentdb'));
|
|
222
|
-
return
|
|
341
|
+
return bump.ok
|
|
342
|
+
? { indexed, generationBumped: true }
|
|
343
|
+
: { indexed, generationBumped: false, generationReason: bump.error };
|
|
223
344
|
} finally {
|
|
224
345
|
db.close();
|
|
225
346
|
}
|
|
@@ -284,30 +405,43 @@ export interface AgentdbSearchResult {
|
|
|
284
405
|
|
|
285
406
|
const DEPS_MISSING = 'agentdb/better-sqlite3 not installed in project (run: dz setup --memory agentdb)';
|
|
286
407
|
|
|
408
|
+
type Embedder = { embed: (t: string) => Promise<Float32Array> } | { error: string };
|
|
409
|
+
|
|
287
410
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
411
|
+
* `agentdb-embedder-cache` (feature): the transformers pipeline behind `EmbeddingService` is
|
|
412
|
+
* expensive to stand up (MEASURED 2026-09-14: 2-3.6s per `resolveAgentdbEmbedder` call — see
|
|
413
|
+
* `features/agentdb-embedder-cache/00_complexity_assessment.md`), yet the resolved model/dim never
|
|
414
|
+
* changes within one process. Keyed by `${agentdbDir}|${model}|${dim}` so a config/env change (a
|
|
415
|
+
* different `resolveEmbedModel` source) gets its own entry (FR-2) rather than reusing a stale
|
|
416
|
+
* pipeline. The PROMISE is cached, not the awaited result (FR-4): concurrent first callers for the
|
|
417
|
+
* same key join the same in-flight initialization instead of racing two pipelines. An `{error}`
|
|
418
|
+
* outcome (or a rejection) evicts its own entry so the next call retries cleanly (FR-3) — a failure
|
|
419
|
+
* must never "stick".
|
|
291
420
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
421
|
+
const embedderCache = new Map<string, Promise<Embedder>>();
|
|
422
|
+
let embedderCacheInitializations = 0;
|
|
423
|
+
|
|
424
|
+
/** Test-only (and future warm-start) reset — callers (`vector-tier.ts`, `backlog.ts`) are unaffected. */
|
|
425
|
+
export function resetAgentdbEmbedderCache(): void {
|
|
426
|
+
embedderCache.clear();
|
|
427
|
+
embedderCacheInitializations = 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** `entries` = cached keys right now — a SUCCESSFUL pipeline or an IN-FLIGHT initialization (the promise is
|
|
431
|
+
* cached before it settles, FR-4; a failed one is evicted, FR-3); `initializations` = pipelines actually
|
|
432
|
+
* started since the last reset. (Codex round-1, 2026-09-14: the earlier wording said "successful" only.) */
|
|
433
|
+
export function getAgentdbEmbedderCacheStats(): { entries: number; initializations: number } {
|
|
434
|
+
return { entries: embedderCache.size, initializations: embedderCacheInitializations };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function initAgentdbEmbedder(agentdbDir: string, model: string, dim: number): Promise<Embedder> {
|
|
302
438
|
try {
|
|
303
439
|
const { EmbeddingService } = (await import(pathToFileURL(join(agentdbDir, 'controllers', 'EmbeddingService.js')).href)) as {
|
|
304
440
|
EmbeddingService: new (o: object) => { initialize: () => Promise<void>; embed: (t: string) => Promise<Float32Array> };
|
|
305
441
|
};
|
|
306
|
-
const model = resolveEmbedModel(projectRoot);
|
|
307
|
-
if ('error' in model) return { error: model.error };
|
|
308
442
|
const emb = new EmbeddingService({
|
|
309
|
-
model
|
|
310
|
-
dimension:
|
|
443
|
+
model,
|
|
444
|
+
dimension: dim,
|
|
311
445
|
provider: 'transformers',
|
|
312
446
|
// agentdb >= 3.0.0-alpha.20 refuses UNREGISTERED models without an explicit role policy
|
|
313
447
|
// (its built-in registry knows all-MiniLM-L6-v2 but not our multilingual variant — grounded
|
|
@@ -325,6 +459,41 @@ export async function resolveAgentdbEmbedder(
|
|
|
325
459
|
}
|
|
326
460
|
}
|
|
327
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Resolve agentdb's `EmbeddingService` from the PROJECT (same dynamic-resolution discipline as
|
|
464
|
+
* {@link indexPatternsToAgentdb}); every dz call site uses the same resolved model so query and row
|
|
465
|
+
* vectors stay in the same space. Cached per process — see {@link embedderCache} above.
|
|
466
|
+
*/
|
|
467
|
+
export async function resolveAgentdbEmbedder(projectRoot: string): Promise<Embedder> {
|
|
468
|
+
let agentdbDir: string;
|
|
469
|
+
try {
|
|
470
|
+
const req = createRequire(join(projectRoot, 'package.json'));
|
|
471
|
+
agentdbDir = dirname(req.resolve('agentdb'));
|
|
472
|
+
} catch {
|
|
473
|
+
return { error: DEPS_MISSING };
|
|
474
|
+
}
|
|
475
|
+
const model = resolveEmbedModel(projectRoot);
|
|
476
|
+
if ('error' in model) return { error: model.error };
|
|
477
|
+
const key = `${agentdbDir}|${model.model}|${model.dim}`;
|
|
478
|
+
const hit = embedderCache.get(key);
|
|
479
|
+
if (hit !== undefined) return hit;
|
|
480
|
+
embedderCacheInitializations += 1;
|
|
481
|
+
const promise = initAgentdbEmbedder(agentdbDir, model.model, model.dim);
|
|
482
|
+
embedderCache.set(key, promise);
|
|
483
|
+
// FR-3: an init failure must not stick — evict so the next call retries instead of replaying
|
|
484
|
+
// the same {error} forever. `.catch` here only guards a rejection that slips past
|
|
485
|
+
// `initAgentdbEmbedder`'s own try/catch; it never rethrows (this is bookkeeping, not the return path).
|
|
486
|
+
void promise.then(
|
|
487
|
+
(result) => {
|
|
488
|
+
if ('error' in result && embedderCache.get(key) === promise) embedderCache.delete(key);
|
|
489
|
+
},
|
|
490
|
+
() => {
|
|
491
|
+
if (embedderCache.get(key) === promise) embedderCache.delete(key);
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
return promise;
|
|
495
|
+
}
|
|
496
|
+
|
|
328
497
|
/** `absent: true` = no store file yet — an EMPTY mirror is a state, not an error (backfill relies on this). */
|
|
329
498
|
function openReadonly(projectRoot: string, dbPath?: string): { db: ReadonlyDb } | { absent: true } | { error: string } {
|
|
330
499
|
const dbFile = resolveAgentdbPath(projectRoot, dbPath);
|
|
@@ -678,6 +847,12 @@ export async function importVectorsToAgentdb(
|
|
|
678
847
|
return imported;
|
|
679
848
|
});
|
|
680
849
|
const imported = commit();
|
|
850
|
+
// AM-1 (fix-round): `importVectorsToAgentdb` is the write-half of `dz vector import` — it
|
|
851
|
+
// changes `pattern_embeddings`/`reasoning_patterns` exactly like `indexPatternsToAgentdb`, so
|
|
852
|
+
// it must bump the SAME counter (the whole point of a single "did the store change" signal is
|
|
853
|
+
// that every writer feeds it, not just one). Ordered before `writeEmbedManifest`, same AM-3
|
|
854
|
+
// rationale: the rows are already on disk by the time `commit()` returns.
|
|
855
|
+
bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
681
856
|
writeEmbedManifest(dbFile, currentEmbedManifest(model, guard.manifest.version, 'agentdb'));
|
|
682
857
|
return { imported };
|
|
683
858
|
} finally {
|
|
@@ -725,7 +900,14 @@ export function clearAgentdbQuarantine(
|
|
|
725
900
|
}
|
|
726
901
|
return cleared;
|
|
727
902
|
});
|
|
728
|
-
|
|
903
|
+
const cleared = tx();
|
|
904
|
+
// AM-1 (fix-round): a quarantine clear mutates `metadata` on rows the hook daemon reads
|
|
905
|
+
// straight from this mirror (see the doc comment above) — it changes what a query returns, so
|
|
906
|
+
// it must bump too, exactly like every other mutator. Only when something actually changed
|
|
907
|
+
// (`cleared > 0`) — same "no write, no bump" discipline as `indexPatternsToAgentdb`'s
|
|
908
|
+
// empty-rows case (AC-2).
|
|
909
|
+
if (cleared > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
910
|
+
return { cleared };
|
|
729
911
|
} finally {
|
|
730
912
|
db.close();
|
|
731
913
|
}
|
|
@@ -781,7 +963,11 @@ export function deleteAgentdbByDzIds(
|
|
|
781
963
|
}
|
|
782
964
|
return deleted;
|
|
783
965
|
});
|
|
784
|
-
|
|
966
|
+
const deleted = tx();
|
|
967
|
+
// AM-1 (fix-round): a DELETE removes rows from a query's result set exactly as surely as an
|
|
968
|
+
// INSERT adds them — it must bump too. Only when rows actually left the store (`deleted > 0`).
|
|
969
|
+
if (deleted > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
970
|
+
return { deleted };
|
|
785
971
|
} finally {
|
|
786
972
|
db.close();
|
|
787
973
|
}
|
|
@@ -826,7 +1012,13 @@ export function bumpAgentdbUses(
|
|
|
826
1012
|
}
|
|
827
1013
|
return bumped;
|
|
828
1014
|
});
|
|
829
|
-
|
|
1015
|
+
const bumped = tx();
|
|
1016
|
+
// AM-1 (fix-round): `uses`/`avg_reward` feed reward-weighted ranking — a change here is a real
|
|
1017
|
+
// store mutation, so it bumps the SAME counter (deliberately NOT named `bump` — that identifier
|
|
1018
|
+
// is this function's own return value; the counter helper is called by its full name below to
|
|
1019
|
+
// avoid the collision). Only when a row actually changed (`bumped > 0`).
|
|
1020
|
+
if (bumped > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
1021
|
+
return { bumped };
|
|
830
1022
|
} finally {
|
|
831
1023
|
db.close();
|
|
832
1024
|
}
|
|
@@ -1109,6 +1301,13 @@ export async function reindexAgentdbRows(
|
|
|
1109
1301
|
if (!(err instanceof NamedLockTimeoutError)) throw err;
|
|
1110
1302
|
snapshots = { kept: [], removed: [], removedBytes: 0, keep: keepSnapshots, errors: [`lock busy: ${err.message}`] };
|
|
1111
1303
|
}
|
|
1304
|
+
// AM-1 (fix-round): `reindexAgentdbRows` is itself a mutator (the DELETE above rebuilds the
|
|
1305
|
+
// owned task types) — it must not rely SOLELY on `indexPatternsToAgentdb`'s own internal bump,
|
|
1306
|
+
// because that call is a no-op (and bumps nothing) when `rows` is empty, yet the DELETE it ran
|
|
1307
|
+
// just above unconditionally changed the store. Bumping again here when `rows` was non-empty
|
|
1308
|
+
// (the common case, already bumped once inside `indexPatternsToAgentdb`) is harmless — the
|
|
1309
|
+
// counter is a monotonic "did anything change" signal, not a per-operation tally.
|
|
1310
|
+
bumpStoreGeneration(projectRoot, opts.dbPath);
|
|
1112
1311
|
return {
|
|
1113
1312
|
reembedded: indexed.indexed,
|
|
1114
1313
|
model: model.model,
|