@dzhechkov/harness-core 0.8.30 → 0.8.31
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 +233 -93
- package/README.md +269 -0
- package/dist/agentdb-index.d.ts +45 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +345 -85
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +170 -0
- package/dist/agentdb-reindex-marker.d.ts.map +1 -0
- package/dist/agentdb-reindex-marker.js +293 -0
- package/dist/agentdb-reindex-marker.js.map +1 -0
- package/dist/agentdb-snapshot-rotation.d.ts +169 -0
- package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
- package/dist/agentdb-snapshot-rotation.js +322 -0
- package/dist/agentdb-snapshot-rotation.js.map +1 -0
- package/dist/agentdb-snapshot.d.ts +126 -0
- package/dist/agentdb-snapshot.d.ts.map +1 -0
- package/dist/agentdb-snapshot.js +195 -0
- package/dist/agentdb-snapshot.js.map +1 -0
- package/dist/apply-leg.d.ts +202 -0
- package/dist/apply-leg.d.ts.map +1 -0
- package/dist/apply-leg.js +975 -0
- package/dist/apply-leg.js.map +1 -0
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +57 -1
- package/dist/book-kb.js.map +1 -1
- package/dist/brain.d.ts +3 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +1 -1
- package/dist/brain.js.map +1 -1
- package/dist/cadence.d.ts +30 -1
- package/dist/cadence.d.ts.map +1 -1
- package/dist/cadence.js +66 -2
- package/dist/cadence.js.map +1 -1
- package/dist/guard.d.ts +14 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +36 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-probe-worker.d.ts.map +1 -1
- package/dist/integration-probe-worker.js +4 -6
- package/dist/integration-probe-worker.js.map +1 -1
- package/dist/integrations-verify.d.ts.map +1 -1
- package/dist/integrations-verify.js +3 -1
- package/dist/integrations-verify.js.map +1 -1
- package/dist/managed-hooks.d.ts +11 -2
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +33 -7
- package/dist/managed-hooks.js.map +1 -1
- package/dist/mutation-gate.d.ts +35 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +49 -9
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +23 -4
- package/dist/patterns.js.map +1 -1
- package/dist/repository-origin.d.ts +1 -1
- package/dist/repository-origin.d.ts.map +1 -1
- package/dist/repository-origin.js +7 -1
- package/dist/repository-origin.js.map +1 -1
- package/dist/round-exec.d.ts +41 -0
- package/dist/round-exec.d.ts.map +1 -0
- package/dist/round-exec.js +45 -0
- package/dist/round-exec.js.map +1 -0
- package/dist/round.d.ts +109 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +148 -0
- package/dist/round.js.map +1 -0
- package/dist/run-records.d.ts +8 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +69 -3
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +166 -3
- package/dist/setup.js.map +1 -1
- package/dist/sqlite-read-helpers.d.ts +60 -0
- package/dist/sqlite-read-helpers.d.ts.map +1 -0
- package/dist/sqlite-read-helpers.js +118 -0
- package/dist/sqlite-read-helpers.js.map +1 -0
- package/dist/statusline.d.ts +20 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +40 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +30 -0
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +189 -0
- package/dist/store-counts.js.map +1 -1
- package/dist/usage.d.ts +55 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +184 -33
- package/dist/usage.js.map +1 -1
- package/dist/vector-tier.d.ts +18 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +26 -2
- package/dist/vector-tier.js.map +1 -1
- package/package.json +6 -6
- package/sbom.json +442 -92
- package/src/agentdb-index.ts +315 -32
- package/src/agentdb-reindex-marker.ts +330 -0
- package/src/agentdb-snapshot-rotation.ts +393 -0
- package/src/agentdb-snapshot.ts +222 -0
- package/src/apply-leg.ts +1046 -0
- package/src/book-kb.ts +64 -1
- package/src/brain.ts +11 -2
- package/src/cadence.ts +87 -2
- package/src/guard.ts +46 -0
- package/src/index.ts +58 -5
- package/src/integration-probe-worker.ts +4 -6
- package/src/integrations-verify.ts +3 -1
- package/src/managed-hooks.ts +32 -7
- package/src/mutation-gate.ts +87 -8
- package/src/operations.ts +89 -0
- package/src/patterns.ts +23 -4
- package/src/repository-origin.ts +7 -1
- package/src/round-exec.ts +85 -0
- package/src/round.ts +228 -0
- package/src/run-records.ts +77 -2
- package/src/setup.ts +198 -3
- package/src/sqlite-read-helpers.ts +128 -0
- package/src/statusline.ts +57 -9
- package/src/store-counts.ts +245 -1
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentdb-snapshot-rotation.d.ts","sourceRoot":"","sources":["../src/agentdb-snapshot-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAoBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CA6CpG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAExE;AAaD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7G,oBAAoB,CAmBtB;AAED,yGAAyG;AACzG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,sBAA2B,GAChC,sBAAsB,CA2ExB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,sBAAsB,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,sBAAsB,CAwBxB"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-reindex snapshot rotation (feature `reindex-snapshot-rotation`, FR-1..FR-8).
|
|
3
|
+
*
|
|
4
|
+
* `reindexAgentdbRows` copies the store to `<db>.pre-reindex-<ms>.bak` (+ `.embed-manifest.json` /
|
|
5
|
+
* `-shm` / `-wal` siblings) before every reindex, as an undo point. Nothing has ever pruned them,
|
|
6
|
+
* so they accumulate without bound — 13 snapshots / 50 MB observed on the owner's hub, and no
|
|
7
|
+
* command removed a single one.
|
|
8
|
+
*
|
|
9
|
+
* This module is the PURE planning half (NFR-2) — {@link listPreReindexSnapshots} (fs READ) and
|
|
10
|
+
* {@link planSnapshotRotation} (no fs at all) — plus the thin fs-EFFECT wrapper
|
|
11
|
+
* {@link rotatePreReindexSnapshots} that acts on the plan. A snapshot is a FAMILY of files sharing
|
|
12
|
+
* one `<ms>` timestamp (FR-2): the family is removed or kept as a unit, never partially.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { lstatSync, readdirSync, unlinkSync } from 'node:fs';
|
|
17
|
+
import { basename, dirname, join } from 'node:path';
|
|
18
|
+
import { readLiveReindexMarkers, withAgentdbSnapshotLock } from './agentdb-reindex-marker.js';
|
|
19
|
+
import { NamedLockTimeoutError } from './named-lock.js';
|
|
20
|
+
/** AM-3: a family younger than this survives rotation even beyond `keep` — no override flag exists;
|
|
21
|
+
* the intent is "never delete something from the last few minutes", not a tunable retention window. */
|
|
22
|
+
const DEFAULT_GRACE_MS = 10 * 60 * 1000;
|
|
23
|
+
/** Escape a string for literal use inside a `RegExp` — basenames may carry `.`, `+`, etc. */
|
|
24
|
+
function escapeRegExp(s) {
|
|
25
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The strict per-db snapshot-family pattern (FR-3): only names literally derived from `dbBasename`
|
|
29
|
+
* match — `agentdb.db.bak`, `other.pre-reindex-1.bak`, or anything else in the directory is left
|
|
30
|
+
* alone. Group 1 is the `<ms>` timestamp; group 2 (optional) is the sibling suffix.
|
|
31
|
+
*/
|
|
32
|
+
function snapshotNamePattern(dbBasename) {
|
|
33
|
+
return new RegExp(`^${escapeRegExp(dbBasename)}\\.pre-reindex-(\\d+)\\.bak(\\.embed-manifest\\.json|-shm|-wal)?$`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Scan the directory next to `dbFile` for candidate families — the shared innards of
|
|
37
|
+
* {@link listPreReindexSnapshots} (which drops `scanErrors` for callers that only ever listed) and
|
|
38
|
+
* {@link rotatePreReindexSnapshots} (which must see them, per AM-4: an incomplete scan must never be
|
|
39
|
+
* read as "these are all the old ones").
|
|
40
|
+
*
|
|
41
|
+
* NFR-1: symlinks are never followed and never treated as candidates. `lstatSync` — never `stat` —
|
|
42
|
+
* decides this, so a matching NAME that resolves to a symlink is excluded entirely (AC-3), not
|
|
43
|
+
* "rotated by its link size". An orphaned sibling (e.g. a `-shm` with no `.bak`) still forms its
|
|
44
|
+
* own one-file family under its `<ms>` (AC-5) — FR-2 groups by timestamp, not by which siblings
|
|
45
|
+
* happen to exist.
|
|
46
|
+
*
|
|
47
|
+
* AM-5: a `<ms>` that is not a safe non-negative integer within `Date`'s representable range
|
|
48
|
+
* (`±8.64e15`) is never grouped into a candidate family — so it never reaches `new Date(ms)`, which
|
|
49
|
+
* throws `RangeError` past that bound, and it is never silently treated as "the oldest one".
|
|
50
|
+
*/
|
|
51
|
+
export function scanSnapshotDir(dbFile) {
|
|
52
|
+
const dir = dirname(dbFile);
|
|
53
|
+
const pattern = snapshotNamePattern(basename(dbFile));
|
|
54
|
+
const scanErrors = [];
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = readdirSync(dir);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const code = err.code;
|
|
61
|
+
if (code !== 'ENOENT') {
|
|
62
|
+
scanErrors.push(`readdir ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
return { families: [], scanErrors };
|
|
65
|
+
}
|
|
66
|
+
const byMs = new Map();
|
|
67
|
+
for (const name of entries) {
|
|
68
|
+
const match = pattern.exec(name);
|
|
69
|
+
if (match === null)
|
|
70
|
+
continue;
|
|
71
|
+
const full = join(dir, name);
|
|
72
|
+
let size;
|
|
73
|
+
try {
|
|
74
|
+
const st = lstatSync(full);
|
|
75
|
+
if (!st.isFile())
|
|
76
|
+
continue; // symlinks (and anything else non-regular) are left alone (NFR-1)
|
|
77
|
+
size = st.size;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const code = err.code;
|
|
81
|
+
if (code !== 'ENOENT') {
|
|
82
|
+
scanErrors.push(`lstat ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
}
|
|
84
|
+
continue; // ENOENT: vanished between readdir and lstat — nothing left to rotate, not an error
|
|
85
|
+
}
|
|
86
|
+
const ms = Number(match[1]);
|
|
87
|
+
if (!Number.isSafeInteger(ms) || ms < 0 || ms > 8.64e15)
|
|
88
|
+
continue; // AM-5: not a candidate
|
|
89
|
+
const list = byMs.get(ms) ?? [];
|
|
90
|
+
list.push({ name, bytes: size });
|
|
91
|
+
byMs.set(ms, list);
|
|
92
|
+
}
|
|
93
|
+
const families = [...byMs.entries()]
|
|
94
|
+
.map(([ms, files]) => ({
|
|
95
|
+
ms,
|
|
96
|
+
files: [...files].sort((a, b) => a.name.localeCompare(b.name)),
|
|
97
|
+
bytes: files.reduce((sum, f) => sum + f.bytes, 0),
|
|
98
|
+
}))
|
|
99
|
+
.sort((a, b) => b.ms - a.ms);
|
|
100
|
+
return { families, scanErrors };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* List every pre-reindex snapshot family sitting next to `dbFile`, newest `ms` first (families
|
|
104
|
+
* unsorted internally are sorted here; per-family files are name-sorted for determinism). See
|
|
105
|
+
* {@link scanSnapshotDir} for the scan rules (NFR-1, AM-5); this wrapper drops `scanErrors` — callers
|
|
106
|
+
* that only ever want to LIST (never delete) have no unsafe decision to gate on them.
|
|
107
|
+
*/
|
|
108
|
+
export function listPreReindexSnapshots(dbFile) {
|
|
109
|
+
return scanSnapshotDir(dbFile).families;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve a candidate path (typically the fresh `backupPath` a reindex just wrote) to the `<ms>` it
|
|
113
|
+
* belongs to, under the SAME strict pattern `listPreReindexSnapshots` uses. `undefined` when the
|
|
114
|
+
* path does not name this db's snapshot family at all — e.g. the db never existed, so no backup was
|
|
115
|
+
* ever written.
|
|
116
|
+
*/
|
|
117
|
+
function msOf(dbFile, candidatePath) {
|
|
118
|
+
const match = snapshotNamePattern(basename(dbFile)).exec(basename(candidatePath));
|
|
119
|
+
return match === null ? undefined : Number(match[1]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pure decision (NFR-2, zero fs): given the families found in a directory, decide which survive.
|
|
123
|
+
* The newest `keep` families survive (`keep` clamped to ≥ 0); the freshly created backup is
|
|
124
|
+
* normally the newest family, so it is ordinarily already inside that top-`keep` slice. `protectMs`
|
|
125
|
+
* names it explicitly and is the FALLBACK that fires only when it would otherwise fall OUTSIDE that
|
|
126
|
+
* slice — the boundary case named by FR-1: at `keep=0` the top slice is empty, yet the snapshot this
|
|
127
|
+
* very call just wrote must never be deleted. `families` need not be pre-sorted.
|
|
128
|
+
*
|
|
129
|
+
* AM-3 (fix-round): a family younger than `graceMs` (default {@link DEFAULT_GRACE_MS}, 10 minutes;
|
|
130
|
+
* `ms > now - graceMs`) is rescued exactly like `protectMs`, even past `keep` — a fresh snapshot from
|
|
131
|
+
* a DIFFERENT process/run than the one calling this must never look "old" just because nobody named
|
|
132
|
+
* it via `protectMs`. There is no override: `now`/`graceMs` exist for deterministic tests, not for a
|
|
133
|
+
* CLI knob that would let someone talk the grace period down to zero.
|
|
134
|
+
*/
|
|
135
|
+
export function planSnapshotRotation(families, opts) {
|
|
136
|
+
const sorted = [...families].sort((a, b) => b.ms - a.ms);
|
|
137
|
+
const keepCount = Math.max(0, opts.keep);
|
|
138
|
+
const topKeep = sorted.slice(0, keepCount);
|
|
139
|
+
const protectMs = opts.protectMs;
|
|
140
|
+
const alreadyKept = protectMs !== undefined && topKeep.some((f) => f.ms === protectMs);
|
|
141
|
+
const rescued = protectMs !== undefined && !alreadyKept ? sorted.find((f) => f.ms === protectMs) : undefined;
|
|
142
|
+
const kept = rescued !== undefined ? [rescued, ...topKeep].sort((a, b) => b.ms - a.ms) : topKeep;
|
|
143
|
+
const keptMs = new Set(kept.map((f) => f.ms));
|
|
144
|
+
const removed = sorted.filter((f) => !keptMs.has(f.ms));
|
|
145
|
+
const now = opts.now ?? Date.now();
|
|
146
|
+
const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
|
|
147
|
+
const graced = removed.filter((f) => f.ms > now - graceMs);
|
|
148
|
+
if (graced.length === 0)
|
|
149
|
+
return { kept, removed };
|
|
150
|
+
const gracedMs = new Set(graced.map((f) => f.ms));
|
|
151
|
+
return {
|
|
152
|
+
kept: [...kept, ...graced].sort((a, b) => b.ms - a.ms),
|
|
153
|
+
removed: removed.filter((f) => !gracedMs.has(f.ms)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** A lock timeout is reported exactly like any other rotation refusal (FR-4): nothing removed,
|
|
157
|
+
* `errors: ['lock busy: …']` — never a throw out of the public rotation entry point. */
|
|
158
|
+
function lockBusyReport(keep, err) {
|
|
159
|
+
return { kept: [], removed: [], removedBytes: 0, keep, errors: [`lock busy: ${err.message}`] };
|
|
160
|
+
}
|
|
161
|
+
/** AM-1's guarantee ("an invalid `keep` reads nothing, touches nothing") checked ONCE and shared by
|
|
162
|
+
* both the unlocked primitive and the public locked wrapper — so an invalid `keep` short-circuits
|
|
163
|
+
* before the wrapper's own pre-scan `readdir`, not just before the unlocked primitive's. */
|
|
164
|
+
function invalidKeepReport(keep) {
|
|
165
|
+
if (!Number.isSafeInteger(keep) || keep < 0) {
|
|
166
|
+
return { kept: [], removed: [], removedBytes: 0, keep, errors: [`invalid keep: ${keep} (must be a non-negative safe integer)`] };
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The fs-effect half: list, plan, delete. A per-file `unlink` failure lands in `errors[]` and never
|
|
172
|
+
* stops the rest of the rotation (FR-5) — by the time this runs, the caller has already produced a
|
|
173
|
+
* successful reindex, and a rotation hiccup must never be read as a reindex failure. `removedBytes`
|
|
174
|
+
* counts only bytes of files ACTUALLY unlinked, so a partial failure never overstates what was freed.
|
|
175
|
+
*
|
|
176
|
+
* AM-1 (fix-round, Codex review Grade D): `keep` is validated HERE, before any fs read or delete.
|
|
177
|
+
* The bug this closes: `Math.max(0, NaN)` is `NaN`, and `sorted.slice(0, NaN)` is `[]` — an EMPTY
|
|
178
|
+
* kept slice, so every existing family looked "older than keep" and got removed. A `keep` that is
|
|
179
|
+
* not a non-negative safe integer is refused outright; nothing is read, nothing is touched.
|
|
180
|
+
*
|
|
181
|
+
* AM-4: a scan error (readdir/lstat failing with anything but ENOENT) makes the candidate list
|
|
182
|
+
* INCOMPLETE — deciding "these are the old ones" off a partial list is exactly the class of mistake
|
|
183
|
+
* this rotation exists to avoid for the snapshots themselves, so this call removes NOTHING.
|
|
184
|
+
*
|
|
185
|
+
* AM-2: within a removed family, siblings unlink FIRST and the `.bak` LAST, and only once every
|
|
186
|
+
* sibling actually unlinked — a failed sibling leaves the `.bak` in place (the one file that alone
|
|
187
|
+
* still proves the snapshot ever existed) and names the family in `partialFamilies` rather than
|
|
188
|
+
* guessing the family is gone.
|
|
189
|
+
*
|
|
190
|
+
* FR-3 (agentdb-snapshot-lock): a family named by a LIVE reindex-in-progress marker
|
|
191
|
+
* ({@link readLiveReindexMarkers}) is rescued from `removed` exactly like `protectPath`, even past
|
|
192
|
+
* `keep` and even past `graceMs` — a reindex that is still running must never lose the undo point it
|
|
193
|
+
* is relying on. An EXPIRED marker is not a scan error: it is removed and named in `notes`.
|
|
194
|
+
*
|
|
195
|
+
* UNLOCKED by design (NFR-2): this is the internal primitive `rotatePreReindexSnapshots` (the public,
|
|
196
|
+
* locked entry point) and `reindexAgentdbRows`'s own success-path rotation both call — each under
|
|
197
|
+
* their OWN `withAgentdbSnapshotLock`, so the lock is never acquired twice in one call stack.
|
|
198
|
+
*/
|
|
199
|
+
export function rotatePreReindexSnapshotsUnlocked(dbFile, opts = {}) {
|
|
200
|
+
const keep = opts.keep ?? 3;
|
|
201
|
+
const invalid = invalidKeepReport(keep);
|
|
202
|
+
if (invalid !== undefined)
|
|
203
|
+
return invalid;
|
|
204
|
+
const dir = dirname(dbFile);
|
|
205
|
+
const { families, scanErrors } = scanSnapshotDir(dbFile);
|
|
206
|
+
if (scanErrors.length > 0) {
|
|
207
|
+
return {
|
|
208
|
+
kept: families.flatMap((f) => f.files.map((file) => file.name)),
|
|
209
|
+
removed: [],
|
|
210
|
+
removedBytes: 0,
|
|
211
|
+
keep,
|
|
212
|
+
scanErrors,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const protectMs = opts.protectPath !== undefined ? msOf(dbFile, opts.protectPath) : undefined;
|
|
216
|
+
let { kept, removed } = planSnapshotRotation(families, {
|
|
217
|
+
keep,
|
|
218
|
+
...(protectMs !== undefined ? { protectMs } : {}),
|
|
219
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
220
|
+
...(opts.graceMs !== undefined ? { graceMs: opts.graceMs } : {}),
|
|
221
|
+
});
|
|
222
|
+
// FR-3: rescue any family a LIVE reindex marker names, however `planSnapshotRotation` above
|
|
223
|
+
// decided — this protection is orthogonal to keep/grace/protectPath and must win over all of them.
|
|
224
|
+
const { protectedMs: markerProtectedMs, notes } = readLiveReindexMarkers(dbFile, opts.now);
|
|
225
|
+
if (markerProtectedMs.length > 0) {
|
|
226
|
+
const protect = new Set(markerProtectedMs);
|
|
227
|
+
const rescued = removed.filter((f) => protect.has(f.ms));
|
|
228
|
+
if (rescued.length > 0) {
|
|
229
|
+
removed = removed.filter((f) => !protect.has(f.ms));
|
|
230
|
+
kept = [...kept, ...rescued].sort((a, b) => b.ms - a.ms);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const errors = [];
|
|
234
|
+
const partialFamilies = [];
|
|
235
|
+
const removedNames = [];
|
|
236
|
+
let removedBytes = 0;
|
|
237
|
+
for (const family of removed) {
|
|
238
|
+
const bakFile = family.files.find((f) => f.name.endsWith('.bak'));
|
|
239
|
+
const siblings = family.files.filter((f) => f.name !== bakFile?.name);
|
|
240
|
+
let allOk = true;
|
|
241
|
+
for (const file of siblings) {
|
|
242
|
+
try {
|
|
243
|
+
unlinkSync(join(dir, file.name));
|
|
244
|
+
removedNames.push(file.name);
|
|
245
|
+
removedBytes += file.bytes;
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
allOk = false;
|
|
249
|
+
errors.push(`${file.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (bakFile !== undefined && allOk) {
|
|
253
|
+
try {
|
|
254
|
+
unlinkSync(join(dir, bakFile.name));
|
|
255
|
+
removedNames.push(bakFile.name);
|
|
256
|
+
removedBytes += bakFile.bytes;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
allOk = false;
|
|
260
|
+
errors.push(`${bakFile.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!allOk)
|
|
264
|
+
partialFamilies.push(family.ms);
|
|
265
|
+
}
|
|
266
|
+
const keptNames = kept.flatMap((f) => f.files.map((file) => file.name));
|
|
267
|
+
return {
|
|
268
|
+
kept: keptNames,
|
|
269
|
+
removed: removedNames,
|
|
270
|
+
removedBytes,
|
|
271
|
+
keep,
|
|
272
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
273
|
+
...(partialFamilies.length > 0 ? { partialFamilies } : {}),
|
|
274
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The PUBLIC, locked rotation entry point (FR-1) — used by `dz brain snapshots --prune` and by any
|
|
279
|
+
* other caller outside `reindexAgentdbRows`'s own success path (which calls
|
|
280
|
+
* {@link rotatePreReindexSnapshotsUnlocked} directly under its own lock, per NFR-2). A
|
|
281
|
+
* `NamedLockTimeoutError` — the lock stayed held past the deadline — is reported exactly like any
|
|
282
|
+
* other rotation refusal: `{ removed: [], errors: ['lock busy: …'] }` (FR-4), never a throw.
|
|
283
|
+
*
|
|
284
|
+
* `lockTimeoutMs` is test/tuning-only: omitted, the lock uses its ordinary default deadline.
|
|
285
|
+
*
|
|
286
|
+
* AM-4 (carried forward): `withNamedLockSync` creates `<dirname(dbFile)>/.dz/locks/` before it ever
|
|
287
|
+
* runs `fn` — if `dirname(dbFile)` exists but is NOT a directory (a pathological store path, exactly
|
|
288
|
+
* the shape AM-4's own fixture uses), that `mkdirSync` throws `ENOTDIR` before any lock is even
|
|
289
|
+
* attempted. Scanning is therefore checked FIRST, outside the lock: an unreadable directory is a
|
|
290
|
+
* property of the directory, not of contention over it, and must report the SAME honest
|
|
291
|
+
* `scanErrors` (never touching, never throwing) whether or not locking is even reachable. MEASURED
|
|
292
|
+
* (fix round, this feature): omitting this pre-check turned the pre-existing
|
|
293
|
+
* `rotatePreReindexSnapshots(brokenDbFile, …)` / `dz brain snapshots --prune` "unscannable
|
|
294
|
+
* directory" fixtures from a clean `scanErrors` report into an uncaught `ENOTDIR` thrown out of the
|
|
295
|
+
* lock's own `mkdirSync` — reddening both `test/agentdb-snapshot-rotation.test.ts`'s AM-4 case and
|
|
296
|
+
* `harness-cli/test/brain-snapshots.test.ts`'s "prune mode: an unscannable store directory" case.
|
|
297
|
+
*/
|
|
298
|
+
export function rotatePreReindexSnapshots(dbFile, opts = {}) {
|
|
299
|
+
const keep = opts.keep ?? 3;
|
|
300
|
+
const invalid = invalidKeepReport(keep);
|
|
301
|
+
if (invalid !== undefined)
|
|
302
|
+
return invalid;
|
|
303
|
+
const preScan = scanSnapshotDir(dbFile);
|
|
304
|
+
if (preScan.scanErrors.length > 0) {
|
|
305
|
+
return {
|
|
306
|
+
kept: preScan.families.flatMap((f) => f.files.map((file) => file.name)),
|
|
307
|
+
removed: [],
|
|
308
|
+
removedBytes: 0,
|
|
309
|
+
keep,
|
|
310
|
+
scanErrors: preScan.scanErrors,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
return withAgentdbSnapshotLock(dbFile, () => rotatePreReindexSnapshotsUnlocked(dbFile, opts), opts.lockTimeoutMs !== undefined ? { timeoutMs: opts.lockTimeoutMs } : {});
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
if (err instanceof NamedLockTimeoutError)
|
|
318
|
+
return lockBusyReport(keep, err);
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=agentdb-snapshot-rotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentdb-snapshot-rotation.js","sourceRoot":"","sources":["../src/agentdb-snapshot-rotation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consistent sqlite snapshot + restore for the pre-reindex undo point (feature
|
|
3
|
+
* `snapshot-wal-consistency`, FR-1..FR-4; fix round AM-1..AM-4).
|
|
4
|
+
*
|
|
5
|
+
* A WAL-mode sqlite database's committed data can live in TWO files: the main `dbFile` and its
|
|
6
|
+
* `-wal` sidecar (frames not yet checkpointed into the main file). The snapshot this module
|
|
7
|
+
* replaces was a bare `copyFileSync(dbFile, backupPath)` — one file, no `-wal` — so a reindex that
|
|
8
|
+
* ran while any writer held the WAL open (autocheckpoint disabled, or simply a live connection
|
|
9
|
+
* between commits) copied a "backup" that was MISSING committed rows. MEASURED (scratch repro,
|
|
10
|
+
* 2026-09-13, `node` + real `better-sqlite3`): a schema + one row checkpointed, then a second row
|
|
11
|
+
* inserted on a connection kept open (`wal_autocheckpoint = 0`) — `copyFileSync` alone yields a
|
|
12
|
+
* backup with 1 row; `VACUUM INTO` on a fresh read-only connection to the SAME live db yields 2.
|
|
13
|
+
* The live db's main file hash and `-wal` size are unchanged before/after the `VACUUM INTO` (the
|
|
14
|
+
* read-only connection never checkpoints or writes) — the safety property NFR-2 names.
|
|
15
|
+
*
|
|
16
|
+
* {@link snapshotSqliteDatabase} (FR-1/FR-2) takes the snapshot through `VACUUM INTO` on a fresh
|
|
17
|
+
* READ-ONLY connection — one self-contained output file with every committed transaction, WAL
|
|
18
|
+
* frames included. If `VACUUM INTO` cannot run (older sqlite, a locked/foreign file, no free disk)
|
|
19
|
+
* — or a caller forces it for a test — it falls back to copying `dbFile` plus its `-wal` sibling
|
|
20
|
+
* (present and non-empty) to `backupPath`/`backupPath-wal`; still strictly better than the old
|
|
21
|
+
* single-file copy, and the fallback is never silent: `method`/`note` name it honestly so a report
|
|
22
|
+
* downstream (`reindexAgentdbRows`'s `snapshotMethod`) never claims a guarantee it did not get.
|
|
23
|
+
*
|
|
24
|
+
* **AM-1 (fix round).** Codex review, Grade C: reusing a `backupPath` left a stale `-wal`/`-shm`
|
|
25
|
+
* sidecar from an EARLIER, unrelated snapshot family sitting next to the new one; `restoreSqliteSnapshot`
|
|
26
|
+
* used to decide whether to restore `-wal` by checking whether `backupPath-wal` merely EXISTS, so that
|
|
27
|
+
* stale sidecar could ride along onto a `vacuum-into` restore that never produced a matching `-wal` of
|
|
28
|
+
* its own. Fixed two ways: `snapshotSqliteDatabase` now clears any `backupPath-wal`/`-shm` sidecar
|
|
29
|
+
* BEFORE either strategy writes anything, and again right after a successful `VACUUM INTO` (whose
|
|
30
|
+
* output is one self-contained file and must never be shadowed); `restoreSqliteSnapshot` now takes the
|
|
31
|
+
* `method` the caller already knows and restores `-wal` ONLY when `method === 'copy+wal'` — never by
|
|
32
|
+
* re-deriving it from file presence.
|
|
33
|
+
*
|
|
34
|
+
* **AM-2 (fix round).** The old WAL-size probe was `existsSync` + best-effort `statSync` inside a
|
|
35
|
+
* `catch { walSize = 0 }` — ANY stat failure (`EACCES`, `EIO`, a raced deletion) was silently read as
|
|
36
|
+
* "no WAL", so a snapshot could report the honest-sounding `method: 'copy'` while actually having
|
|
37
|
+
* skipped a WAL it could not even check. {@link statSizeIfExists} now calls `statSync` directly and
|
|
38
|
+
* treats only a confirmed `ENOENT` as "does not exist" — every other error propagates and aborts the
|
|
39
|
+
* whole snapshot, so `reindexAgentdbRows` reports "snapshot failed", never a falsely-successful `copy`.
|
|
40
|
+
*
|
|
41
|
+
* **AM-4 (fix round).** `snapshotSqliteDatabase` used to happily write into (overwrite) an existing
|
|
42
|
+
* `backupPath`, silently discarding whatever it held — including, combined with AM-1's stale-sidecar
|
|
43
|
+
* bug, a backup that looked complete but carried a wrong-generation `-wal`. It now refuses up front
|
|
44
|
+
* (before `VACUUM INTO` or any copy) when `backupPath` already exists and is non-empty.
|
|
45
|
+
*
|
|
46
|
+
* {@link restoreSqliteSnapshot} (FR-4) is the paired rollback. MEASURED (same repro): copying the
|
|
47
|
+
* old main file back over `dbFile` WITHOUT removing a `-wal` left over from the aborted operation
|
|
48
|
+
* — reopening the "restored" db returned ZERO rows, not the restored one, because sqlite replayed
|
|
49
|
+
* the stale WAL frames (a since-superseded DELETE) on top of the reverted main file. Removing
|
|
50
|
+
* `dbFile-wal`/`dbFile-shm` as part of the restore fixed this: the reopened db then returned
|
|
51
|
+
* exactly the restored row. The caller MUST close its own write connection to `dbFile` before
|
|
52
|
+
* calling this — restoring a file sqlite still holds open is undefined behaviour.
|
|
53
|
+
*
|
|
54
|
+
* **AM-3 (fix round).** `restoreSqliteSnapshot` used to return `void` — a failing `copyFileSync`/
|
|
55
|
+
* `rmSync` inside it threw straight out of the best-effort `rollback()` wrapper in
|
|
56
|
+
* `agentdb-index.ts`, which swallowed it in an empty `catch` and reported nothing: "absence of a
|
|
57
|
+
* receipt is not success" applies to a rollback exactly as much as to a forward operation. It now
|
|
58
|
+
* returns `{ ok: true } | { ok: false, error }` so the caller can name a failed rollback as failed,
|
|
59
|
+
* never as restored.
|
|
60
|
+
*
|
|
61
|
+
* @packageDocumentation
|
|
62
|
+
*/
|
|
63
|
+
/** How a snapshot was actually taken — always reported by the caller, never assumed. */
|
|
64
|
+
export type SnapshotMethod = 'vacuum-into' | 'copy+wal' | 'copy';
|
|
65
|
+
/** Outcome of {@link snapshotSqliteDatabase}. */
|
|
66
|
+
export interface SnapshotOutcome {
|
|
67
|
+
readonly method: SnapshotMethod;
|
|
68
|
+
/** Present only when a fallback method was used — the reason `vacuum-into` did not run. */
|
|
69
|
+
readonly note?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Outcome of {@link restoreSqliteSnapshot} (AM-3) — a failed restore is reported, never assumed. */
|
|
72
|
+
export interface RestoreOutcome {
|
|
73
|
+
readonly ok: boolean;
|
|
74
|
+
/** Present only when `ok` is false — why the restore did not fully complete. */
|
|
75
|
+
readonly error?: string;
|
|
76
|
+
}
|
|
77
|
+
/** Minimal better-sqlite3 constructor surface this module needs (read-only open + exec + close). */
|
|
78
|
+
export interface SnapshotDbCtor {
|
|
79
|
+
new (path: string, opts?: {
|
|
80
|
+
readonly?: boolean;
|
|
81
|
+
}): {
|
|
82
|
+
exec: (sql: string) => void;
|
|
83
|
+
close: () => void;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Take a consistent snapshot of `dbFile` into `backupPath` (FR-1/FR-2). Default: open `dbFile`
|
|
88
|
+
* READ-ONLY and run `VACUUM INTO <backupPath>` — one output file holding every committed
|
|
89
|
+
* transaction (including `-wal` frames), with no write to the live database (NFR-2 — MEASURED:
|
|
90
|
+
* main-file hash and `-wal` size are unchanged across the call). On any failure — an older sqlite
|
|
91
|
+
* without `VACUUM INTO`, a locked file, no disk space — or when `opts.strategy` forces it (tests),
|
|
92
|
+
* falls back to `copyFileSync(dbFile, backupPath)` plus a copy of `dbFile-wal` to `backupPath-wal`
|
|
93
|
+
* when the WAL sibling exists and is non-empty; `method` is `'copy+wal'` when the sibling was
|
|
94
|
+
* copied, `'copy'` when there was none to copy. `note` names the fallback reason (FR-2) — set even
|
|
95
|
+
* for a forced-strategy test call, so a caller never has to guess why the fast path was skipped.
|
|
96
|
+
* The read-only connection opened for `VACUUM INTO` is always closed before returning.
|
|
97
|
+
*
|
|
98
|
+
* Throws (no snapshot taken, or an incomplete one left in a fully-cleared state) when: `backupPath`
|
|
99
|
+
* already names an existing, non-empty file (AM-4); or a `-wal` stat probe hits a non-ENOENT error
|
|
100
|
+
* (AM-2, inside the fallback path). Never silently overwrites, never mis-reports a lesser guarantee
|
|
101
|
+
* as a stronger one.
|
|
102
|
+
*/
|
|
103
|
+
export declare function snapshotSqliteDatabase(Database: SnapshotDbCtor, dbFile: string, backupPath: string, opts?: {
|
|
104
|
+
strategy?: 'vacuum-into' | 'copy+wal';
|
|
105
|
+
}): SnapshotOutcome;
|
|
106
|
+
/**
|
|
107
|
+
* Restore `dbFile` from a snapshot taken by {@link snapshotSqliteDatabase} (FR-4). The caller MUST
|
|
108
|
+
* close its write connection to `dbFile` BEFORE calling this — this restore is a plain file copy,
|
|
109
|
+
* not a sqlite-mediated rollback, and a live handle can reintroduce exactly the frames being undone.
|
|
110
|
+
*
|
|
111
|
+
* `method` is the EXACT {@link SnapshotMethod} the paired `snapshotSqliteDatabase` call returned
|
|
112
|
+
* (AM-1) — never re-derived from whether `backupPath-wal` happens to exist on disk, which a stale
|
|
113
|
+
* sidecar from an unrelated earlier snapshot at the same path could satisfy. Copies `backupPath`
|
|
114
|
+
* over `dbFile`; when `method === 'copy+wal'`, also restores `backupPath-wal` to `dbFile-wal`.
|
|
115
|
+
* Otherwise (`'vacuum-into'` or `'copy'`, neither of which produced a matching `-wal` of its own) any
|
|
116
|
+
* LIVE `dbFile-wal` — left over from the operation being undone — is removed instead: MEASURED,
|
|
117
|
+
* leaving it in place made a reopened "restored" db replay that WAL's frames (a since-superseded
|
|
118
|
+
* write) on top of the reverted main file, returning zero rows instead of the restored set.
|
|
119
|
+
* `dbFile-shm` is always removed — its offsets are valid only for the `-wal` that no longer matches.
|
|
120
|
+
*
|
|
121
|
+
* Returns `{ ok: true }`, or `{ ok: false, error }` (AM-3) when any step throws — a caller that
|
|
122
|
+
* reported a failed restore as a successful one would leave the store worse than before rollback was
|
|
123
|
+
* attempted, with nothing on record to say so.
|
|
124
|
+
*/
|
|
125
|
+
export declare function restoreSqliteSnapshot(dbFile: string, backupPath: string, method: SnapshotMethod): RestoreOutcome;
|
|
126
|
+
//# sourceMappingURL=agentdb-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentdb-snapshot.d.ts","sourceRoot":"","sources":["../src/agentdb-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAIH,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjE,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qGAAqG;AACrG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,oGAAoG;AACpG,MAAM,WAAW,cAAc;IAC7B,KAAK,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACvG;AA6DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,aAAa,GAAG,UAAU,CAAA;CAAO,GACnD,eAAe,CAkBjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,cAAc,CAchH"}
|