@yemi33/minions 0.1.2114 → 0.1.2116
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/bin/minions.js +21 -0
- package/engine/cleanup.js +10 -3
- package/engine/consolidation.js +1 -1
- package/engine/db/migrate.js +11 -4
- package/engine/db/migrations/010-pr-links.js +61 -0
- package/engine/db/migrations/011-remaining-state.js +116 -0
- package/engine/kb-sweep.js +20 -14
- package/engine/shared.js +156 -9
- package/engine/small-state-store.js +574 -0
- package/package.json +1 -1
package/bin/minions.js
CHANGED
|
@@ -34,6 +34,27 @@
|
|
|
34
34
|
* minions uninstall --confirm Remove Minions and uninstall package
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
// Phase 9.4 (issue #3035 hardening): self-reexec on Node 22.x without
|
|
38
|
+
// --experimental-sqlite so the CLI PARENT process can load `node:sqlite`
|
|
39
|
+
// for in-process store reads (status, doctor, etc.). On Node 24+ this is a
|
|
40
|
+
// no-op (sqlite is unflagged). Must run BEFORE any `require('engine/*')` —
|
|
41
|
+
// engine modules pull in `node:sqlite` transitively via `engine/db`.
|
|
42
|
+
(function _ensureSqliteFlagSelfReexec() {
|
|
43
|
+
const major = parseInt(String(process.versions.node).split('.')[0], 10);
|
|
44
|
+
if (!Number.isFinite(major) || major < 22 || major >= 24) return;
|
|
45
|
+
const opts = String(process.env.NODE_OPTIONS || '');
|
|
46
|
+
if (opts.includes('--experimental-sqlite')) return;
|
|
47
|
+
if (process.env.MINIONS_FORCE_JSON === '1') return; // explicit opt-out
|
|
48
|
+
const { spawnSync } = require('child_process');
|
|
49
|
+
const nextOpts = (opts + ' --experimental-sqlite').trim();
|
|
50
|
+
const res = spawnSync(
|
|
51
|
+
process.execPath,
|
|
52
|
+
['--experimental-sqlite', __filename, ...process.argv.slice(2)],
|
|
53
|
+
{ stdio: 'inherit', env: { ...process.env, NODE_OPTIONS: nextOpts } }
|
|
54
|
+
);
|
|
55
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
56
|
+
})();
|
|
57
|
+
|
|
37
58
|
const fs = require('fs');
|
|
38
59
|
const path = require('path');
|
|
39
60
|
const os = require('os');
|
package/engine/cleanup.js
CHANGED
|
@@ -1410,12 +1410,19 @@ async function runCleanup(config, verbose = false) {
|
|
|
1410
1410
|
} catch (e) { log('warn', 'clean stale PID files: ' + e.message); }
|
|
1411
1411
|
|
|
1412
1412
|
// 13. Prune test-results.json — keep last 200 entries
|
|
1413
|
+
// Phase 9.3: route through shared.mutateTestResults so the read+trim+write
|
|
1414
|
+
// is atomic under the file lock (previously a safeJsonArr → safeWrite race).
|
|
1413
1415
|
try {
|
|
1414
1416
|
const testResultsPath = path.join(ENGINE_DIR, 'test-results.json');
|
|
1415
|
-
const results = shared.safeJsonArr(testResultsPath);
|
|
1416
1417
|
const TEST_RESULTS_CAP = 200;
|
|
1417
|
-
|
|
1418
|
-
|
|
1418
|
+
// Fast path: skip lock acquisition entirely when the file is already
|
|
1419
|
+
// under cap. The atomic re-check inside the mutator handles the race.
|
|
1420
|
+
const probe = shared.safeJsonArr(testResultsPath);
|
|
1421
|
+
if (probe.length > TEST_RESULTS_CAP) {
|
|
1422
|
+
shared.mutateTestResults((curr) => {
|
|
1423
|
+
if (curr.length > TEST_RESULTS_CAP) return curr.slice(-TEST_RESULTS_CAP);
|
|
1424
|
+
return undefined; // already trimmed by a concurrent writer
|
|
1425
|
+
});
|
|
1419
1426
|
}
|
|
1420
1427
|
} catch { /* optional — file may not exist */ }
|
|
1421
1428
|
|
package/engine/consolidation.js
CHANGED
|
@@ -849,7 +849,7 @@ function classifyToKnowledgeBase(items, config) {
|
|
|
849
849
|
const dir = path.join(KNOWLEDGE_DIR, cat);
|
|
850
850
|
if (fs.existsSync(dir)) count += fs.readdirSync(dir).length;
|
|
851
851
|
}
|
|
852
|
-
|
|
852
|
+
shared.mutateKbCheckpoint(() => ({ count, updatedAt: ts() }));
|
|
853
853
|
} catch (err) { log('warn', `KB checkpoint: ${err.message}`); }
|
|
854
854
|
}
|
|
855
855
|
|
package/engine/db/migrate.js
CHANGED
|
@@ -35,17 +35,24 @@ function runMigrations(db) {
|
|
|
35
35
|
)
|
|
36
36
|
`);
|
|
37
37
|
|
|
38
|
-
const currentRow = db.prepare('SELECT COALESCE(MAX(version), 0) AS v FROM schema_version').get();
|
|
39
|
-
const current = Number(currentRow.v) || 0;
|
|
40
38
|
const migrations = _loadMigrations();
|
|
41
39
|
|
|
42
40
|
for (const m of migrations) {
|
|
43
41
|
if (typeof m.version !== 'number' || typeof m.up !== 'function') {
|
|
44
42
|
throw new Error(`engine/db/migrate: migration missing { version, up }: ${JSON.stringify(Object.keys(m))}`);
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Concurrent-startup safety: take the SQLite write lock BEFORE re-reading
|
|
45
|
+
// schema_version. With plain `BEGIN` (deferred), two processes booting at
|
|
46
|
+
// the same time can both observe schema_version=N, both decide to apply
|
|
47
|
+
// N+1, and one then fails with "table already exists" while the other
|
|
48
|
+
// succeeds — leaving the loser stuck on `_dbInitError` for the lifetime
|
|
49
|
+
// of its process. `BEGIN IMMEDIATE` serializes the version check itself
|
|
50
|
+
// so the second arrival simply sees the updated schema_version and skips.
|
|
51
|
+
db.exec('BEGIN IMMEDIATE');
|
|
48
52
|
try {
|
|
53
|
+
const currentRow = db.prepare('SELECT COALESCE(MAX(version), 0) AS v FROM schema_version').get();
|
|
54
|
+
const current = Number(currentRow.v) || 0;
|
|
55
|
+
if (m.version <= current) { db.exec('COMMIT'); continue; }
|
|
49
56
|
// eslint-disable-next-line no-console
|
|
50
57
|
console.log(`[db-migrate] Applying v${m.version}: ${m.description || '(no description)'}`);
|
|
51
58
|
m.up(db, { fs, path });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// engine/db/migrations/010-pr-links.js
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.1: move `engine/pr-links.json` into SQL.
|
|
4
|
+
//
|
|
5
|
+
// pr-links.json is the *fallback* PR → work-item linkage store. The
|
|
6
|
+
// primary source is `pull_requests.prdItems[]` (managed via the
|
|
7
|
+
// pull-requests-store). Stray PR IDs that aren't covered by a project
|
|
8
|
+
// pull-requests record still need to be discoverable — that's what this
|
|
9
|
+
// table holds.
|
|
10
|
+
//
|
|
11
|
+
// Shape: { [prId]: <value> } where <value> is typically an array of item IDs
|
|
12
|
+
// but may also be a bare string from legacy writes. Readers normalize.
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
function _resolveMinionsDir() {
|
|
18
|
+
const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
|
|
19
|
+
if (envHome) return envHome;
|
|
20
|
+
try { return require('../../shared').MINIONS_DIR; } catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _readJsonOr(filePath, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch { return fallback; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
version: 10,
|
|
32
|
+
description: 'pr_links: schema + pr-links.json backfill (fallback static store)',
|
|
33
|
+
up(db) {
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE pr_links (
|
|
36
|
+
pr_id TEXT PRIMARY KEY,
|
|
37
|
+
data TEXT NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
const minionsDir = _resolveMinionsDir();
|
|
43
|
+
if (!minionsDir) return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
let inserted = 0;
|
|
46
|
+
|
|
47
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'pr-links.json'), null);
|
|
48
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
49
|
+
const ins = db.prepare(`INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?)`);
|
|
50
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
51
|
+
try {
|
|
52
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
53
|
+
inserted += 1;
|
|
54
|
+
} catch { /* duplicate / corrupt — skip */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.log(`[db-migrate] v10: backfilled ${inserted} pr-links rows; pr-links.json kept as dual-write mirror`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// engine/db/migrations/011-remaining-state.js
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.2: finish the JSON→SQL migration. Four remaining engine state
|
|
4
|
+
// files move into their own tables; the JSON files are kept as dual-write
|
|
5
|
+
// mirrors for back-compat.
|
|
6
|
+
//
|
|
7
|
+
// engine/cooldowns.json -> cooldowns
|
|
8
|
+
// engine/pending-rebases.json -> pending_rebases
|
|
9
|
+
// engine/cc-sessions.json -> cc_sessions
|
|
10
|
+
// engine/doc-sessions.json -> doc_sessions
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
function _resolveMinionsDir() {
|
|
16
|
+
const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
|
|
17
|
+
if (envHome) return envHome;
|
|
18
|
+
try { return require('../../shared').MINIONS_DIR; } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _readJsonOr(filePath, fallback) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch { return fallback; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
version: 11,
|
|
30
|
+
description: 'cooldowns + pending_rebases + cc_sessions + doc_sessions',
|
|
31
|
+
up(db) {
|
|
32
|
+
db.exec(`
|
|
33
|
+
CREATE TABLE cooldowns (
|
|
34
|
+
key TEXT PRIMARY KEY,
|
|
35
|
+
data TEXT NOT NULL,
|
|
36
|
+
updated_at INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE pending_rebases (
|
|
40
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
+
data TEXT NOT NULL,
|
|
42
|
+
updated_at INTEGER NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE cc_sessions (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
data TEXT NOT NULL,
|
|
48
|
+
updated_at INTEGER NOT NULL
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE doc_sessions (
|
|
52
|
+
key TEXT PRIMARY KEY,
|
|
53
|
+
data TEXT NOT NULL,
|
|
54
|
+
updated_at INTEGER NOT NULL
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const minionsDir = _resolveMinionsDir();
|
|
59
|
+
if (!minionsDir) return;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
let inserted = 0;
|
|
62
|
+
|
|
63
|
+
// ── cooldowns ──────────────────────────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'cooldowns.json'), null);
|
|
66
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
67
|
+
const ins = db.prepare(`INSERT INTO cooldowns (key, data, updated_at) VALUES (?, ?, ?)`);
|
|
68
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
69
|
+
try { ins.run(String(k), JSON.stringify(v), now); inserted += 1; }
|
|
70
|
+
catch { /* duplicate / corrupt — skip */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── pending_rebases ────────────────────────────────────────────────────
|
|
76
|
+
{
|
|
77
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'pending-rebases.json'), null);
|
|
78
|
+
if (Array.isArray(raw)) {
|
|
79
|
+
const ins = db.prepare(`INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)`);
|
|
80
|
+
for (const entry of raw) {
|
|
81
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
82
|
+
try { ins.run(JSON.stringify(entry), now); inserted += 1; }
|
|
83
|
+
catch { /* corrupt — skip */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── cc_sessions ────────────────────────────────────────────────────────
|
|
89
|
+
{
|
|
90
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'cc-sessions.json'), null);
|
|
91
|
+
if (Array.isArray(raw)) {
|
|
92
|
+
const ins = db.prepare(`INSERT INTO cc_sessions (id, data, updated_at) VALUES (?, ?, ?)`);
|
|
93
|
+
for (const entry of raw) {
|
|
94
|
+
if (!entry || typeof entry !== 'object' || !entry.id) continue;
|
|
95
|
+
try { ins.run(String(entry.id), JSON.stringify(entry), now); inserted += 1; }
|
|
96
|
+
catch { /* duplicate id — skip */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── doc_sessions ───────────────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'doc-sessions.json'), null);
|
|
104
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
105
|
+
const ins = db.prepare(`INSERT INTO doc_sessions (key, data, updated_at) VALUES (?, ?, ?)`);
|
|
106
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
107
|
+
try { ins.run(String(k), JSON.stringify(v), now); inserted += 1; }
|
|
108
|
+
catch { /* duplicate / corrupt — skip */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.log(`[db-migrate] v11: backfilled ${inserted} small-state rows; JSON files kept as dual-write mirrors`);
|
|
115
|
+
},
|
|
116
|
+
};
|
package/engine/kb-sweep.js
CHANGED
|
@@ -288,8 +288,10 @@ function _writeSweepState(state) {
|
|
|
288
288
|
// can distinguish "still running" from "runner crashed". When this module is
|
|
289
289
|
// imported by the detached runner, process.pid is the runner's pid — which
|
|
290
290
|
// is exactly what we want.
|
|
291
|
+
// Phase 9.3: route through shared.mutateKbSweepState (file-locked RMW) so
|
|
292
|
+
// the dashboard process + the detached runner can't race each other.
|
|
291
293
|
const augmented = { pid: process.pid, ...state };
|
|
292
|
-
try {
|
|
294
|
+
try { shared.mutateKbSweepState(() => augmented); } catch { /* ignore */ }
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/**
|
|
@@ -416,10 +418,10 @@ function reconcileSweepStateOnBoot(opts = {}) {
|
|
|
416
418
|
error: `sweep abandoned: ${reason}`,
|
|
417
419
|
reconciliationReason: reason,
|
|
418
420
|
};
|
|
419
|
-
// Direct
|
|
420
|
-
// for forensics — _writeSweepState would overwrite it with this
|
|
421
|
-
// pid, destroying the diagnostic value of the record.
|
|
422
|
-
try {
|
|
421
|
+
// Direct mutateKbSweepState (NOT _writeSweepState) so the original pid is
|
|
422
|
+
// preserved for forensics — _writeSweepState would overwrite it with this
|
|
423
|
+
// process's pid, destroying the diagnostic value of the record.
|
|
424
|
+
try { shared.mutateKbSweepState(() => failedState); } catch { /* ignore */ }
|
|
423
425
|
return {
|
|
424
426
|
scanned: 1, released: 1, reason,
|
|
425
427
|
prevStatus: state.status, prevPid: pid,
|
|
@@ -551,7 +553,7 @@ async function _runKbSweepImpl(opts = {}) {
|
|
|
551
553
|
summary.summary = `${summary.hashDuplicatesArchived} hash-dup, ${summary.llmDuplicatesArchived} llm-dup, ${summary.staleRemoved} stale, ${summary.reclassified} reclassified, ${summary.rewritten} rewritten (${(summary.bytesBefore - summary.bytesAfter).toLocaleString()} bytes saved)`;
|
|
552
554
|
|
|
553
555
|
if (!opts.dryRun) {
|
|
554
|
-
try {
|
|
556
|
+
try { shared.mutateKbSwept(() => ({ timestamp: ts(), summary: summary.summary, detail: summary })); } catch { /* ignore */ }
|
|
555
557
|
try { queries.invalidateKnowledgeBaseCache(); } catch { /* ignore */ }
|
|
556
558
|
}
|
|
557
559
|
return summary;
|
|
@@ -587,7 +589,7 @@ function spawnSweepRunnerDetached(opts = {}) {
|
|
|
587
589
|
const sweepToken = `${startedAt}-${Math.random().toString(36).slice(2, 8)}`;
|
|
588
590
|
|
|
589
591
|
try {
|
|
590
|
-
|
|
592
|
+
shared.mutateKbSweepState(() => ({
|
|
591
593
|
status: 'starting', startedAt, startedAtIso: new Date().toISOString(),
|
|
592
594
|
sweepToken, pid: null,
|
|
593
595
|
}));
|
|
@@ -637,14 +639,18 @@ function spawnSweepRunnerDetached(opts = {}) {
|
|
|
637
639
|
}
|
|
638
640
|
if (logFdNum != null) try { fsLocal.closeSync(logFdNum); } catch { /* ignore */ }
|
|
639
641
|
|
|
642
|
+
// Phase 9.3: atomic starting→in-flight transition under the file lock
|
|
643
|
+
// (was previously a safeJson→safeWrite race).
|
|
640
644
|
try {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
645
|
+
shared.mutateKbSweepState((current) => {
|
|
646
|
+
if (current && current.status === 'starting' && current.sweepToken === sweepToken) {
|
|
647
|
+
return {
|
|
648
|
+
status: 'in-flight', startedAt, startedAtIso: new Date().toISOString(),
|
|
649
|
+
sweepToken, pid: proc.pid,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return undefined; // unchanged → skipWriteIfUnchanged suppresses the write
|
|
653
|
+
});
|
|
648
654
|
} catch { /* best-effort */ }
|
|
649
655
|
|
|
650
656
|
proc.unref();
|
package/engine/shared.js
CHANGED
|
@@ -65,6 +65,16 @@ const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
|
65
65
|
// process-lifetime (state/pid/ownerToken). See ENGINE_DEFAULTS.abandonedReconciliationVersion
|
|
66
66
|
// for the first consumer.
|
|
67
67
|
const ENGINE_STATE_PATH = path.join(ENGINE_DIR, 'state.json');
|
|
68
|
+
// Phase 9.3: wrap raw safeWrite callers for kb-checkpoint, kb-swept,
|
|
69
|
+
// kb-sweep-state, and test-results so they go through mutateJsonFileLocked
|
|
70
|
+
// (the same path mutateControl / mutateEngineState / mutateCooldowns use).
|
|
71
|
+
// This sets them up for SQL routing via _SMALL_STATE_MUTATE_ROUTES later, and
|
|
72
|
+
// in the case of kb-sweep-state.json closes a real cross-process race (the
|
|
73
|
+
// dashboard process and the detached kb-sweep-runner child both write it).
|
|
74
|
+
const KB_CHECKPOINT_PATH = path.join(ENGINE_DIR, 'kb-checkpoint.json');
|
|
75
|
+
const KB_SWEPT_PATH = path.join(ENGINE_DIR, 'kb-swept.json');
|
|
76
|
+
const KB_SWEEP_STATE_PATH = path.join(ENGINE_DIR, 'kb-sweep-state.json');
|
|
77
|
+
const TEST_RESULTS_PATH = path.join(ENGINE_DIR, 'test-results.json');
|
|
68
78
|
const PR_LINKS_PATH = path.join(MINIONS_DIR, 'engine', 'pr-links.json');
|
|
69
79
|
const PINNED_ITEMS_PATH = path.join(MINIONS_DIR, 'engine', 'kb-pins.json');
|
|
70
80
|
const LOG_PATH = path.join(MINIONS_DIR, 'engine', 'log.json');
|
|
@@ -532,6 +542,26 @@ function _routeJsonReadToSql(p) {
|
|
|
532
542
|
const store = require('./small-state-store');
|
|
533
543
|
return { value: store.readQaSessions() };
|
|
534
544
|
}
|
|
545
|
+
if (norm.endsWith('/engine/pr-links.json')) {
|
|
546
|
+
const store = require('./small-state-store');
|
|
547
|
+
return { value: store.readPrLinks() };
|
|
548
|
+
}
|
|
549
|
+
if (norm.endsWith('/engine/cooldowns.json')) {
|
|
550
|
+
const store = require('./small-state-store');
|
|
551
|
+
return { value: store.readCooldowns() };
|
|
552
|
+
}
|
|
553
|
+
if (norm.endsWith('/engine/pending-rebases.json')) {
|
|
554
|
+
const store = require('./small-state-store');
|
|
555
|
+
return { value: store.readPendingRebases() };
|
|
556
|
+
}
|
|
557
|
+
if (norm.endsWith('/engine/cc-sessions.json')) {
|
|
558
|
+
const store = require('./small-state-store');
|
|
559
|
+
return { value: store.readCcSessions() };
|
|
560
|
+
}
|
|
561
|
+
if (norm.endsWith('/engine/doc-sessions.json')) {
|
|
562
|
+
const store = require('./small-state-store');
|
|
563
|
+
return { value: store.readDocSessions() };
|
|
564
|
+
}
|
|
535
565
|
// Per-project work-items.json — match `/projects/<name>/work-items.json`.
|
|
536
566
|
// When SQL has no rows for the scope AND the JSON file is absent on
|
|
537
567
|
// disk, preserve the legacy "file missing → null" semantic. This guards
|
|
@@ -582,9 +612,9 @@ function _routeJsonReadToSql(p) {
|
|
|
582
612
|
*
|
|
583
613
|
* **SQL routing (Phase 9):** when the path matches a migrated state file
|
|
584
614
|
* (work-items, pull-requests, dispatch, metrics, watches, schedule-runs,
|
|
585
|
-
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions
|
|
586
|
-
* the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
587
|
-
* on disk is no longer authoritative.
|
|
615
|
+
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions,
|
|
616
|
+
* pr-links), the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
617
|
+
* JSON file on disk is no longer authoritative.
|
|
588
618
|
*
|
|
589
619
|
* Counterpart: `safeJsonNoRestore` for terminal artifacts and "missing == gone"
|
|
590
620
|
* reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
|
|
@@ -1319,13 +1349,60 @@ function withFileLock(lockPath, fn, {
|
|
|
1319
1349
|
throw lastErr;
|
|
1320
1350
|
}
|
|
1321
1351
|
|
|
1352
|
+
// Route table for small-state files migrated to SQL stores. Each entry
|
|
1353
|
+
// declares the SQL store function to invoke and the expected default JSON
|
|
1354
|
+
// shape (for the "ensure file exists" fallback below).
|
|
1355
|
+
const _SMALL_STATE_MUTATE_ROUTES = {
|
|
1356
|
+
'cooldowns.json': { fn: 'applyCooldownsMutation', mirror: '_mirrorCooldownsJson', defaultShape: 'object' },
|
|
1357
|
+
'pending-rebases.json': { fn: 'applyPendingRebasesMutation', mirror: '_mirrorPendingRebasesJson', defaultShape: 'array' },
|
|
1358
|
+
'cc-sessions.json': { fn: 'applyCcSessionsMutation', mirror: '_mirrorCcSessionsJson', defaultShape: 'array' },
|
|
1359
|
+
'doc-sessions.json': { fn: 'applyDocSessionsMutation', mirror: '_mirrorDocSessionsJson', defaultShape: 'object' },
|
|
1360
|
+
'pr-links.json': { fn: 'applyPrLinksMutation', mirror: '_mirrorPrLinksJson', defaultShape: 'object' },
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1322
1363
|
function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
|
|
1323
1364
|
const baseName = path.basename(filePath);
|
|
1324
|
-
|
|
1365
|
+
const smallRoute = _SMALL_STATE_MUTATE_ROUTES[baseName];
|
|
1366
|
+
if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !smallRoute) return null;
|
|
1325
1367
|
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
1326
1368
|
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
1327
1369
|
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/' + baseName;
|
|
1328
1370
|
if (!insideMinionsDir) return null;
|
|
1371
|
+
|
|
1372
|
+
// Small-state files live exclusively under <MINIONS_DIR>/engine/<baseName>.
|
|
1373
|
+
// Don't hijack writes to ad-hoc paths that happen to share the basename.
|
|
1374
|
+
if (smallRoute) {
|
|
1375
|
+
if (!fpNorm.endsWith('/engine/' + baseName)) return null;
|
|
1376
|
+
let result;
|
|
1377
|
+
try {
|
|
1378
|
+
const store = require('./small-state-store');
|
|
1379
|
+
const out = store[smallRoute.fn]((data) => {
|
|
1380
|
+
const next = mutateFn(data);
|
|
1381
|
+
if (onWrote) onWrote();
|
|
1382
|
+
return next;
|
|
1383
|
+
});
|
|
1384
|
+
result = out && Object.prototype.hasOwnProperty.call(out, 'result') ? out.result : undefined;
|
|
1385
|
+
try { store[smallRoute.mirror](filePath); } catch { /* mirror best-effort */ }
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
// SQLite unavailable / table missing — fall through to JSON path.
|
|
1388
|
+
const msg = e && e.message ? String(e.message) : '';
|
|
1389
|
+
if (/SQLite unavailable|no such table|not a database|node:sqlite/i.test(msg)) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
throw e;
|
|
1393
|
+
}
|
|
1394
|
+
if (!fs.existsSync(filePath)) {
|
|
1395
|
+
try {
|
|
1396
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1397
|
+
const fallback = smallRoute.defaultShape === 'array'
|
|
1398
|
+
? (Array.isArray(result) ? result : [])
|
|
1399
|
+
: (result && typeof result === 'object' && !Array.isArray(result) ? result : {});
|
|
1400
|
+
safeWrite(filePath, fallback);
|
|
1401
|
+
} catch { /* best-effort */ }
|
|
1402
|
+
}
|
|
1403
|
+
return { routed: result };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1329
1406
|
let result;
|
|
1330
1407
|
if (baseName === 'work-items.json') {
|
|
1331
1408
|
result = mutateWorkItems(filePath, (arr) => {
|
|
@@ -1447,6 +1524,45 @@ function mutateCooldowns(mutator) {
|
|
|
1447
1524
|
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1448
1525
|
}
|
|
1449
1526
|
|
|
1527
|
+
// Phase 9.3 (W-mp48wxqw / SQL-canonicalization prep): lock-safe RMW wrappers
|
|
1528
|
+
// for the small JSON state files that were still going through raw safeWrite.
|
|
1529
|
+
// Pattern mirrors mutateControl / mutateEngineState / mutateCooldowns — so a
|
|
1530
|
+
// future SQL migration is a mechanical _SMALL_STATE_MUTATE_ROUTES addition.
|
|
1531
|
+
|
|
1532
|
+
// KB classification checkpoint counter (consolidation.js post-classify).
|
|
1533
|
+
function mutateKbCheckpoint(mutator) {
|
|
1534
|
+
return mutateJsonFileLocked(KB_CHECKPOINT_PATH, (data) => {
|
|
1535
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1536
|
+
return mutator(data) || data;
|
|
1537
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// KB last-sweep summary (kb-sweep.js post-sweep, dashboard reader).
|
|
1541
|
+
function mutateKbSwept(mutator) {
|
|
1542
|
+
return mutateJsonFileLocked(KB_SWEPT_PATH, (data) => {
|
|
1543
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1544
|
+
return mutator(data) || data;
|
|
1545
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// KB sweep state machine. Written by BOTH the dashboard process AND the
|
|
1549
|
+
// detached kb-sweep-runner child — wrapping under mutateJsonFileLocked makes
|
|
1550
|
+
// cross-process writes file-locked (previously a real race via raw safeWrite).
|
|
1551
|
+
function mutateKbSweepState(mutator) {
|
|
1552
|
+
return mutateJsonFileLocked(KB_SWEEP_STATE_PATH, (data) => {
|
|
1553
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1554
|
+
return mutator(data) || data;
|
|
1555
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Test results history (capped at TEST_RESULTS_CAP by cleanup.js).
|
|
1559
|
+
function mutateTestResults(mutator) {
|
|
1560
|
+
return mutateJsonFileLocked(TEST_RESULTS_PATH, (data) => {
|
|
1561
|
+
if (!Array.isArray(data)) data = [];
|
|
1562
|
+
return mutator(data) || data;
|
|
1563
|
+
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1450
1566
|
let _uidCounter = 0;
|
|
1451
1567
|
|
|
1452
1568
|
/**
|
|
@@ -4988,9 +5104,19 @@ function getPrLinks() {
|
|
|
4988
5104
|
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
4989
5105
|
}
|
|
4990
5106
|
} catch { /* SQL unavailable — skip primary source */ }
|
|
4991
|
-
// Fallback: static pr-links.json
|
|
5107
|
+
// Fallback: pr_links SQL store (was static pr-links.json file). Phase 9.1
|
|
5108
|
+
// routes both reads and writes through small-state-store; the JSON file is
|
|
5109
|
+
// kept as a dual-write mirror. SQL failures fall through to the JSON file.
|
|
5110
|
+
let static_ = null;
|
|
4992
5111
|
try {
|
|
4993
|
-
const
|
|
5112
|
+
const store = require('./small-state-store');
|
|
5113
|
+
static_ = store.readPrLinks();
|
|
5114
|
+
} catch { static_ = null; }
|
|
5115
|
+
if (!static_ || Object.keys(static_).length === 0) {
|
|
5116
|
+
try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
|
|
5117
|
+
catch { static_ = null; }
|
|
5118
|
+
}
|
|
5119
|
+
if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
|
|
4994
5120
|
for (const [k, v] of Object.entries(static_)) {
|
|
4995
5121
|
const canonical = parseCanonicalPrId(k);
|
|
4996
5122
|
let normalizedKey = canonical ? `${canonical.scope}#${canonical.prNumber}` : k;
|
|
@@ -5004,7 +5130,7 @@ function getPrLinks() {
|
|
|
5004
5130
|
}
|
|
5005
5131
|
if (!links[normalizedKey]) mergePrLinkItems(links, normalizedKey, v);
|
|
5006
5132
|
}
|
|
5007
|
-
}
|
|
5133
|
+
}
|
|
5008
5134
|
return links;
|
|
5009
5135
|
}
|
|
5010
5136
|
|
|
@@ -5030,7 +5156,7 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5030
5156
|
const effectivePrId = canonicalPrId || prId;
|
|
5031
5157
|
if (!effectivePrId || !itemId) return;
|
|
5032
5158
|
const legacyPrId = String(prId || '');
|
|
5033
|
-
|
|
5159
|
+
const mutator = (links) => {
|
|
5034
5160
|
if (!links || Array.isArray(links) || typeof links !== 'object') links = {};
|
|
5035
5161
|
const mergedCurrent = new Set(normalizePrLinkItems(links[effectivePrId]));
|
|
5036
5162
|
if (legacyPrId && legacyPrId !== effectivePrId && links[legacyPrId]) {
|
|
@@ -5040,7 +5166,24 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5040
5166
|
if (!mergedCurrent.has(itemId)) mergedCurrent.add(itemId);
|
|
5041
5167
|
links[effectivePrId] = [...mergedCurrent];
|
|
5042
5168
|
return links;
|
|
5043
|
-
}
|
|
5169
|
+
};
|
|
5170
|
+
// Phase 9.1: pr-links is SQL-canonical via small-state-store; the JSON file
|
|
5171
|
+
// is a dual-write mirror. SQLite failures fall through to the legacy JSON
|
|
5172
|
+
// path so older installs without --experimental-sqlite still work.
|
|
5173
|
+
let routedViaSql = false;
|
|
5174
|
+
try {
|
|
5175
|
+
const store = require('./small-state-store');
|
|
5176
|
+
store.applyPrLinksMutation(mutator);
|
|
5177
|
+
try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
|
|
5178
|
+
routedViaSql = true;
|
|
5179
|
+
} catch (e) {
|
|
5180
|
+
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5181
|
+
throw e;
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
if (!routedViaSql) {
|
|
5185
|
+
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
5186
|
+
}
|
|
5044
5187
|
|
|
5045
5188
|
if (!project) return;
|
|
5046
5189
|
const prPath = projectPrPath(project);
|
|
@@ -5944,6 +6087,10 @@ module.exports = {
|
|
|
5944
6087
|
mutateEngineState, // W-mp60tw0u000j3931
|
|
5945
6088
|
readEngineState, // W-mp60tw0u000j3931
|
|
5946
6089
|
mutateCooldowns,
|
|
6090
|
+
mutateKbCheckpoint, // Phase 9.3
|
|
6091
|
+
mutateKbSwept, // Phase 9.3
|
|
6092
|
+
mutateKbSweepState, // Phase 9.3
|
|
6093
|
+
mutateTestResults, // Phase 9.3
|
|
5947
6094
|
mutateWorkItems,
|
|
5948
6095
|
reopenWorkItem,
|
|
5949
6096
|
mutatePullRequests,
|
|
@@ -829,6 +829,550 @@ function _mirrorQaSessionsJson(filePath) {
|
|
|
829
829
|
} catch { /* mirror best-effort */ }
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
// ─── pr_links ───────────────────────────────────────────────────────────────
|
|
833
|
+
// Shape: { [prId]: <value> } where <value> is typically array of item IDs
|
|
834
|
+
// but may also be a bare string (legacy). Readers normalize.
|
|
835
|
+
|
|
836
|
+
let _prLinksHash = null;
|
|
837
|
+
|
|
838
|
+
function _hydratePrLinks(db) {
|
|
839
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
840
|
+
const raw = _readJson(fp) || {};
|
|
841
|
+
db.prepare('DELETE FROM pr_links').run();
|
|
842
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
const ins = db.prepare('INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(pr_id) DO NOTHING');
|
|
845
|
+
for (const [prId, value] of Object.entries(raw)) {
|
|
846
|
+
ins.run(String(prId), JSON.stringify(value), now);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function _resyncPrLinksIfDiverged(db) {
|
|
851
|
+
const fp = _resolveFilePath('pr-links.json');
|
|
852
|
+
const currentHash = _fileContentHash(fp);
|
|
853
|
+
if (currentHash == null) return;
|
|
854
|
+
if (_prLinksHash != null && currentHash === _prLinksHash) return;
|
|
855
|
+
if (_prLinksHash == null) {
|
|
856
|
+
const sqlHas = db.prepare('SELECT 1 FROM pr_links LIMIT 1').get();
|
|
857
|
+
if (sqlHas) {
|
|
858
|
+
_prLinksHash = currentHash;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
_hydratePrLinks(db);
|
|
863
|
+
_prLinksHash = currentHash;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function _readPrLinksFromSql(db) {
|
|
867
|
+
const rows = db.prepare('SELECT pr_id, data FROM pr_links').all();
|
|
868
|
+
const out = {};
|
|
869
|
+
for (const row of rows) {
|
|
870
|
+
try { out[row.pr_id] = JSON.parse(row.data); }
|
|
871
|
+
catch { /* skip malformed */ }
|
|
872
|
+
}
|
|
873
|
+
return out;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readPrLinks() {
|
|
877
|
+
const { getDb } = require('./db');
|
|
878
|
+
let db;
|
|
879
|
+
try { db = getDb(); }
|
|
880
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
881
|
+
try { _resyncPrLinksIfDiverged(db); }
|
|
882
|
+
catch { /* table may be missing on a stale install — fall back to JSON */ }
|
|
883
|
+
let out;
|
|
884
|
+
try { out = _readPrLinksFromSql(db); }
|
|
885
|
+
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
886
|
+
if (Object.keys(out).length === 0) {
|
|
887
|
+
const fallback = _readJson(_resolveFilePath('pr-links.json'));
|
|
888
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function applyPrLinksMutation(mutator) {
|
|
895
|
+
const { getDb, withTransaction } = require('./db');
|
|
896
|
+
let db;
|
|
897
|
+
try { db = getDb(); }
|
|
898
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
899
|
+
|
|
900
|
+
return withTransaction(db, () => {
|
|
901
|
+
_resyncPrLinksIfDiverged(db);
|
|
902
|
+
const before = _readPrLinksFromSql(db);
|
|
903
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
904
|
+
const next = mutator(before);
|
|
905
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
906
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
907
|
+
return { wrote: false, result: beforeSnap };
|
|
908
|
+
}
|
|
909
|
+
const afterIds = new Set(Object.keys(after));
|
|
910
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
911
|
+
let wrote = false;
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const upsert = db.prepare(`
|
|
914
|
+
INSERT INTO pr_links (pr_id, data, updated_at)
|
|
915
|
+
VALUES (?, ?, ?)
|
|
916
|
+
ON CONFLICT(pr_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
917
|
+
`);
|
|
918
|
+
const del = db.prepare('DELETE FROM pr_links WHERE pr_id = ?');
|
|
919
|
+
for (const id of afterIds) {
|
|
920
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
921
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
922
|
+
wrote = true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (const id of beforeIds) {
|
|
926
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
927
|
+
}
|
|
928
|
+
return { wrote, result: after };
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function _mirrorPrLinksJson(filePath) {
|
|
933
|
+
try {
|
|
934
|
+
const shared = require('./shared');
|
|
935
|
+
const { getDb } = require('./db');
|
|
936
|
+
const obj = _readPrLinksFromSql(getDb());
|
|
937
|
+
const target = filePath || _resolveFilePath('pr-links.json');
|
|
938
|
+
shared.safeWrite(target, obj);
|
|
939
|
+
const h = _fileContentHash(target);
|
|
940
|
+
if (h != null) _prLinksHash = h;
|
|
941
|
+
} catch { /* mirror best-effort */ }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ─── cooldowns ─────────────────────────────────────────────────────────────
|
|
945
|
+
// Shape: { [key]: { timestamp, failures, ... } }
|
|
946
|
+
// SQL: row per key.
|
|
947
|
+
|
|
948
|
+
let _cooldownsHash = null;
|
|
949
|
+
|
|
950
|
+
function _hydrateCooldowns(db) {
|
|
951
|
+
const fp = _resolveFilePath('cooldowns.json');
|
|
952
|
+
const raw = _readJson(fp) || {};
|
|
953
|
+
db.prepare('DELETE FROM cooldowns').run();
|
|
954
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
955
|
+
const now = Date.now();
|
|
956
|
+
const ins = db.prepare('INSERT INTO cooldowns (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
|
|
957
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
958
|
+
ins.run(String(key), JSON.stringify(value), now);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function _resyncCooldownsIfDiverged(db) {
|
|
963
|
+
const fp = _resolveFilePath('cooldowns.json');
|
|
964
|
+
const currentHash = _fileContentHash(fp);
|
|
965
|
+
if (currentHash == null) return;
|
|
966
|
+
if (_cooldownsHash != null && currentHash === _cooldownsHash) return;
|
|
967
|
+
if (_cooldownsHash == null) {
|
|
968
|
+
const sqlHas = db.prepare('SELECT 1 FROM cooldowns LIMIT 1').get();
|
|
969
|
+
if (sqlHas) { _cooldownsHash = currentHash; return; }
|
|
970
|
+
}
|
|
971
|
+
_hydrateCooldowns(db);
|
|
972
|
+
_cooldownsHash = currentHash;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function _readCooldownsFromSql(db) {
|
|
976
|
+
const rows = db.prepare('SELECT key, data FROM cooldowns').all();
|
|
977
|
+
const out = {};
|
|
978
|
+
for (const row of rows) {
|
|
979
|
+
try { out[row.key] = JSON.parse(row.data); }
|
|
980
|
+
catch { /* skip malformed */ }
|
|
981
|
+
}
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function readCooldowns() {
|
|
986
|
+
const { getDb } = require('./db');
|
|
987
|
+
let db;
|
|
988
|
+
try { db = getDb(); }
|
|
989
|
+
catch { return _readJson(_resolveFilePath('cooldowns.json')) || {}; }
|
|
990
|
+
try { _resyncCooldownsIfDiverged(db); }
|
|
991
|
+
catch { /* table may be missing on stale install */ }
|
|
992
|
+
let out;
|
|
993
|
+
try { out = _readCooldownsFromSql(db); }
|
|
994
|
+
catch { return _readJson(_resolveFilePath('cooldowns.json')) || {}; }
|
|
995
|
+
if (Object.keys(out).length === 0) {
|
|
996
|
+
const fallback = _readJson(_resolveFilePath('cooldowns.json'));
|
|
997
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
998
|
+
return {};
|
|
999
|
+
}
|
|
1000
|
+
return out;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function applyCooldownsMutation(mutator) {
|
|
1004
|
+
const { getDb, withTransaction } = require('./db');
|
|
1005
|
+
let db;
|
|
1006
|
+
try { db = getDb(); }
|
|
1007
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1008
|
+
|
|
1009
|
+
return withTransaction(db, () => {
|
|
1010
|
+
_resyncCooldownsIfDiverged(db);
|
|
1011
|
+
const before = _readCooldownsFromSql(db);
|
|
1012
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1013
|
+
const next = mutator(before);
|
|
1014
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1015
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
1016
|
+
return { wrote: false, result: beforeSnap };
|
|
1017
|
+
}
|
|
1018
|
+
const afterIds = new Set(Object.keys(after));
|
|
1019
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
1020
|
+
let wrote = false;
|
|
1021
|
+
const now = Date.now();
|
|
1022
|
+
const upsert = db.prepare(`
|
|
1023
|
+
INSERT INTO cooldowns (key, data, updated_at)
|
|
1024
|
+
VALUES (?, ?, ?)
|
|
1025
|
+
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1026
|
+
`);
|
|
1027
|
+
const del = db.prepare('DELETE FROM cooldowns WHERE key = ?');
|
|
1028
|
+
for (const id of afterIds) {
|
|
1029
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
1030
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
1031
|
+
wrote = true;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
for (const id of beforeIds) {
|
|
1035
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
1036
|
+
}
|
|
1037
|
+
return { wrote, result: after };
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function _mirrorCooldownsJson(filePath) {
|
|
1042
|
+
try {
|
|
1043
|
+
const shared = require('./shared');
|
|
1044
|
+
const { getDb } = require('./db');
|
|
1045
|
+
const obj = _readCooldownsFromSql(getDb());
|
|
1046
|
+
const target = filePath || _resolveFilePath('cooldowns.json');
|
|
1047
|
+
shared.safeWrite(target, obj);
|
|
1048
|
+
const h = _fileContentHash(target);
|
|
1049
|
+
if (h != null) _cooldownsHash = h;
|
|
1050
|
+
} catch { /* mirror best-effort */ }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ─── pending_rebases ───────────────────────────────────────────────────────
|
|
1054
|
+
// Shape: [ { prId, branch, projectName, mergedItemId, queuedAt, attempts, ... }, ... ]
|
|
1055
|
+
// SQL: row per array entry (rowid auto-increment preserves insertion order).
|
|
1056
|
+
// No stable natural key, so mutations are full clear+insert.
|
|
1057
|
+
|
|
1058
|
+
let _pendingRebasesHash = null;
|
|
1059
|
+
|
|
1060
|
+
function _hydratePendingRebases(db) {
|
|
1061
|
+
const fp = _resolveFilePath('pending-rebases.json');
|
|
1062
|
+
const raw = _readJson(fp) || [];
|
|
1063
|
+
db.prepare('DELETE FROM pending_rebases').run();
|
|
1064
|
+
if (!Array.isArray(raw)) return;
|
|
1065
|
+
const now = Date.now();
|
|
1066
|
+
const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
|
|
1067
|
+
for (const entry of raw) {
|
|
1068
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1069
|
+
ins.run(JSON.stringify(entry), now);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function _resyncPendingRebasesIfDiverged(db) {
|
|
1074
|
+
const fp = _resolveFilePath('pending-rebases.json');
|
|
1075
|
+
const currentHash = _fileContentHash(fp);
|
|
1076
|
+
if (currentHash == null) return;
|
|
1077
|
+
if (_pendingRebasesHash != null && currentHash === _pendingRebasesHash) return;
|
|
1078
|
+
if (_pendingRebasesHash == null) {
|
|
1079
|
+
const sqlHas = db.prepare('SELECT 1 FROM pending_rebases LIMIT 1').get();
|
|
1080
|
+
if (sqlHas) { _pendingRebasesHash = currentHash; return; }
|
|
1081
|
+
}
|
|
1082
|
+
_hydratePendingRebases(db);
|
|
1083
|
+
_pendingRebasesHash = currentHash;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function _readPendingRebasesFromSql(db) {
|
|
1087
|
+
const rows = db.prepare('SELECT data FROM pending_rebases ORDER BY seq').all();
|
|
1088
|
+
const out = [];
|
|
1089
|
+
for (const row of rows) {
|
|
1090
|
+
try { out.push(JSON.parse(row.data)); }
|
|
1091
|
+
catch { /* skip malformed */ }
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function readPendingRebases() {
|
|
1097
|
+
const { getDb } = require('./db');
|
|
1098
|
+
let db;
|
|
1099
|
+
try { db = getDb(); }
|
|
1100
|
+
catch { return _readJson(_resolveFilePath('pending-rebases.json')) || []; }
|
|
1101
|
+
try { _resyncPendingRebasesIfDiverged(db); }
|
|
1102
|
+
catch { /* table missing */ }
|
|
1103
|
+
let out;
|
|
1104
|
+
try { out = _readPendingRebasesFromSql(db); }
|
|
1105
|
+
catch { return _readJson(_resolveFilePath('pending-rebases.json')) || []; }
|
|
1106
|
+
if (out.length === 0) {
|
|
1107
|
+
const fallback = _readJson(_resolveFilePath('pending-rebases.json'));
|
|
1108
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function applyPendingRebasesMutation(mutator) {
|
|
1115
|
+
const { getDb, withTransaction } = require('./db');
|
|
1116
|
+
let db;
|
|
1117
|
+
try { db = getDb(); }
|
|
1118
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1119
|
+
|
|
1120
|
+
return withTransaction(db, () => {
|
|
1121
|
+
_resyncPendingRebasesIfDiverged(db);
|
|
1122
|
+
const before = _readPendingRebasesFromSql(db);
|
|
1123
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1124
|
+
const next = mutator(before);
|
|
1125
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1126
|
+
if (!Array.isArray(after)) {
|
|
1127
|
+
return { wrote: false, result: beforeSnap };
|
|
1128
|
+
}
|
|
1129
|
+
let wrote = false;
|
|
1130
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
1131
|
+
const now = Date.now();
|
|
1132
|
+
db.prepare('DELETE FROM pending_rebases').run();
|
|
1133
|
+
const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
|
|
1134
|
+
for (const entry of after) {
|
|
1135
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1136
|
+
ins.run(JSON.stringify(entry), now);
|
|
1137
|
+
}
|
|
1138
|
+
wrote = true;
|
|
1139
|
+
}
|
|
1140
|
+
return { wrote, result: after };
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function _mirrorPendingRebasesJson(filePath) {
|
|
1145
|
+
try {
|
|
1146
|
+
const shared = require('./shared');
|
|
1147
|
+
const { getDb } = require('./db');
|
|
1148
|
+
const arr = _readPendingRebasesFromSql(getDb());
|
|
1149
|
+
const target = filePath || _resolveFilePath('pending-rebases.json');
|
|
1150
|
+
shared.safeWrite(target, arr);
|
|
1151
|
+
const h = _fileContentHash(target);
|
|
1152
|
+
if (h != null) _pendingRebasesHash = h;
|
|
1153
|
+
} catch { /* mirror best-effort */ }
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ─── cc_sessions ───────────────────────────────────────────────────────────
|
|
1157
|
+
// Shape: [ { id, sessionId, title, _promptHash, lastActiveAt, ... }, ... ]
|
|
1158
|
+
// SQL: row per id (each entry has a unique `id` tab key).
|
|
1159
|
+
|
|
1160
|
+
let _ccSessionsHash = null;
|
|
1161
|
+
|
|
1162
|
+
function _hydrateCcSessions(db) {
|
|
1163
|
+
const fp = _resolveFilePath('cc-sessions.json');
|
|
1164
|
+
const raw = _readJson(fp) || [];
|
|
1165
|
+
db.prepare('DELETE FROM cc_sessions').run();
|
|
1166
|
+
if (!Array.isArray(raw)) return;
|
|
1167
|
+
const now = Date.now();
|
|
1168
|
+
const ins = db.prepare('INSERT INTO cc_sessions (id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
|
1169
|
+
for (const entry of raw) {
|
|
1170
|
+
if (!entry || typeof entry !== 'object' || !entry.id) continue;
|
|
1171
|
+
ins.run(String(entry.id), JSON.stringify(entry), now);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function _resyncCcSessionsIfDiverged(db) {
|
|
1176
|
+
const fp = _resolveFilePath('cc-sessions.json');
|
|
1177
|
+
const currentHash = _fileContentHash(fp);
|
|
1178
|
+
if (currentHash == null) return;
|
|
1179
|
+
if (_ccSessionsHash != null && currentHash === _ccSessionsHash) return;
|
|
1180
|
+
if (_ccSessionsHash == null) {
|
|
1181
|
+
const sqlHas = db.prepare('SELECT 1 FROM cc_sessions LIMIT 1').get();
|
|
1182
|
+
if (sqlHas) { _ccSessionsHash = currentHash; return; }
|
|
1183
|
+
}
|
|
1184
|
+
_hydrateCcSessions(db);
|
|
1185
|
+
_ccSessionsHash = currentHash;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function _readCcSessionsFromSql(db) {
|
|
1189
|
+
const rows = db.prepare('SELECT data FROM cc_sessions ORDER BY updated_at, id').all();
|
|
1190
|
+
const out = [];
|
|
1191
|
+
for (const row of rows) {
|
|
1192
|
+
try { out.push(JSON.parse(row.data)); }
|
|
1193
|
+
catch { /* skip malformed */ }
|
|
1194
|
+
}
|
|
1195
|
+
return out;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function readCcSessions() {
|
|
1199
|
+
const { getDb } = require('./db');
|
|
1200
|
+
let db;
|
|
1201
|
+
try { db = getDb(); }
|
|
1202
|
+
catch { return _readJson(_resolveFilePath('cc-sessions.json')) || []; }
|
|
1203
|
+
try { _resyncCcSessionsIfDiverged(db); }
|
|
1204
|
+
catch { /* table missing */ }
|
|
1205
|
+
let out;
|
|
1206
|
+
try { out = _readCcSessionsFromSql(db); }
|
|
1207
|
+
catch { return _readJson(_resolveFilePath('cc-sessions.json')) || []; }
|
|
1208
|
+
if (out.length === 0) {
|
|
1209
|
+
const fallback = _readJson(_resolveFilePath('cc-sessions.json'));
|
|
1210
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
1211
|
+
return [];
|
|
1212
|
+
}
|
|
1213
|
+
return out;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function applyCcSessionsMutation(mutator) {
|
|
1217
|
+
const { getDb, withTransaction } = require('./db');
|
|
1218
|
+
let db;
|
|
1219
|
+
try { db = getDb(); }
|
|
1220
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1221
|
+
|
|
1222
|
+
return withTransaction(db, () => {
|
|
1223
|
+
_resyncCcSessionsIfDiverged(db);
|
|
1224
|
+
const before = _readCcSessionsFromSql(db);
|
|
1225
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1226
|
+
const next = mutator(before);
|
|
1227
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1228
|
+
if (!Array.isArray(after)) {
|
|
1229
|
+
return { wrote: false, result: beforeSnap };
|
|
1230
|
+
}
|
|
1231
|
+
const beforeById = new Map(beforeSnap.filter(e => e && e.id).map(e => [String(e.id), e]));
|
|
1232
|
+
const afterById = new Map(after.filter(e => e && e.id).map(e => [String(e.id), e]));
|
|
1233
|
+
let wrote = false;
|
|
1234
|
+
const now = Date.now();
|
|
1235
|
+
const upsert = db.prepare(`
|
|
1236
|
+
INSERT INTO cc_sessions (id, data, updated_at)
|
|
1237
|
+
VALUES (?, ?, ?)
|
|
1238
|
+
ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1239
|
+
`);
|
|
1240
|
+
const del = db.prepare('DELETE FROM cc_sessions WHERE id = ?');
|
|
1241
|
+
for (const [id, entry] of afterById) {
|
|
1242
|
+
const prior = beforeById.get(id);
|
|
1243
|
+
if (!prior || JSON.stringify(prior) !== JSON.stringify(entry)) {
|
|
1244
|
+
upsert.run(id, JSON.stringify(entry), now);
|
|
1245
|
+
wrote = true;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
for (const id of beforeById.keys()) {
|
|
1249
|
+
if (!afterById.has(id)) { del.run(id); wrote = true; }
|
|
1250
|
+
}
|
|
1251
|
+
return { wrote, result: after };
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function _mirrorCcSessionsJson(filePath) {
|
|
1256
|
+
try {
|
|
1257
|
+
const shared = require('./shared');
|
|
1258
|
+
const { getDb } = require('./db');
|
|
1259
|
+
const arr = _readCcSessionsFromSql(getDb());
|
|
1260
|
+
const target = filePath || _resolveFilePath('cc-sessions.json');
|
|
1261
|
+
shared.safeWrite(target, arr);
|
|
1262
|
+
const h = _fileContentHash(target);
|
|
1263
|
+
if (h != null) _ccSessionsHash = h;
|
|
1264
|
+
} catch { /* mirror best-effort */ }
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// ─── doc_sessions ──────────────────────────────────────────────────────────
|
|
1268
|
+
// Shape: { [filePath]: { sessionId, lastActiveAt, turnCount, ... } }
|
|
1269
|
+
// SQL: row per filePath key.
|
|
1270
|
+
|
|
1271
|
+
let _docSessionsHash = null;
|
|
1272
|
+
|
|
1273
|
+
function _hydrateDocSessions(db) {
|
|
1274
|
+
const fp = _resolveFilePath('doc-sessions.json');
|
|
1275
|
+
const raw = _readJson(fp) || {};
|
|
1276
|
+
db.prepare('DELETE FROM doc_sessions').run();
|
|
1277
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
1278
|
+
const now = Date.now();
|
|
1279
|
+
const ins = db.prepare('INSERT INTO doc_sessions (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
|
|
1280
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1281
|
+
ins.run(String(key), JSON.stringify(value), now);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function _resyncDocSessionsIfDiverged(db) {
|
|
1286
|
+
const fp = _resolveFilePath('doc-sessions.json');
|
|
1287
|
+
const currentHash = _fileContentHash(fp);
|
|
1288
|
+
if (currentHash == null) return;
|
|
1289
|
+
if (_docSessionsHash != null && currentHash === _docSessionsHash) return;
|
|
1290
|
+
if (_docSessionsHash == null) {
|
|
1291
|
+
const sqlHas = db.prepare('SELECT 1 FROM doc_sessions LIMIT 1').get();
|
|
1292
|
+
if (sqlHas) { _docSessionsHash = currentHash; return; }
|
|
1293
|
+
}
|
|
1294
|
+
_hydrateDocSessions(db);
|
|
1295
|
+
_docSessionsHash = currentHash;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function _readDocSessionsFromSql(db) {
|
|
1299
|
+
const rows = db.prepare('SELECT key, data FROM doc_sessions').all();
|
|
1300
|
+
const out = {};
|
|
1301
|
+
for (const row of rows) {
|
|
1302
|
+
try { out[row.key] = JSON.parse(row.data); }
|
|
1303
|
+
catch { /* skip malformed */ }
|
|
1304
|
+
}
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function readDocSessions() {
|
|
1309
|
+
const { getDb } = require('./db');
|
|
1310
|
+
let db;
|
|
1311
|
+
try { db = getDb(); }
|
|
1312
|
+
catch { return _readJson(_resolveFilePath('doc-sessions.json')) || {}; }
|
|
1313
|
+
try { _resyncDocSessionsIfDiverged(db); }
|
|
1314
|
+
catch { /* table missing */ }
|
|
1315
|
+
let out;
|
|
1316
|
+
try { out = _readDocSessionsFromSql(db); }
|
|
1317
|
+
catch { return _readJson(_resolveFilePath('doc-sessions.json')) || {}; }
|
|
1318
|
+
if (Object.keys(out).length === 0) {
|
|
1319
|
+
const fallback = _readJson(_resolveFilePath('doc-sessions.json'));
|
|
1320
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
1321
|
+
return {};
|
|
1322
|
+
}
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function applyDocSessionsMutation(mutator) {
|
|
1327
|
+
const { getDb, withTransaction } = require('./db');
|
|
1328
|
+
let db;
|
|
1329
|
+
try { db = getDb(); }
|
|
1330
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1331
|
+
|
|
1332
|
+
return withTransaction(db, () => {
|
|
1333
|
+
_resyncDocSessionsIfDiverged(db);
|
|
1334
|
+
const before = _readDocSessionsFromSql(db);
|
|
1335
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1336
|
+
const next = mutator(before);
|
|
1337
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1338
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
1339
|
+
return { wrote: false, result: beforeSnap };
|
|
1340
|
+
}
|
|
1341
|
+
const afterIds = new Set(Object.keys(after));
|
|
1342
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
1343
|
+
let wrote = false;
|
|
1344
|
+
const now = Date.now();
|
|
1345
|
+
const upsert = db.prepare(`
|
|
1346
|
+
INSERT INTO doc_sessions (key, data, updated_at)
|
|
1347
|
+
VALUES (?, ?, ?)
|
|
1348
|
+
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1349
|
+
`);
|
|
1350
|
+
const del = db.prepare('DELETE FROM doc_sessions WHERE key = ?');
|
|
1351
|
+
for (const id of afterIds) {
|
|
1352
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
1353
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
1354
|
+
wrote = true;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
for (const id of beforeIds) {
|
|
1358
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
1359
|
+
}
|
|
1360
|
+
return { wrote, result: after };
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function _mirrorDocSessionsJson(filePath) {
|
|
1365
|
+
try {
|
|
1366
|
+
const shared = require('./shared');
|
|
1367
|
+
const { getDb } = require('./db');
|
|
1368
|
+
const obj = _readDocSessionsFromSql(getDb());
|
|
1369
|
+
const target = filePath || _resolveFilePath('doc-sessions.json');
|
|
1370
|
+
shared.safeWrite(target, obj);
|
|
1371
|
+
const h = _fileContentHash(target);
|
|
1372
|
+
if (h != null) _docSessionsHash = h;
|
|
1373
|
+
} catch { /* mirror best-effort */ }
|
|
1374
|
+
}
|
|
1375
|
+
|
|
832
1376
|
// ─── Test seam ─────────────────────────────────────────────────────────────
|
|
833
1377
|
|
|
834
1378
|
function _resetAllForTest() {
|
|
@@ -841,6 +1385,11 @@ function _resetAllForTest() {
|
|
|
841
1385
|
db.exec('DELETE FROM worktree_pool');
|
|
842
1386
|
try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
|
|
843
1387
|
try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
|
|
1388
|
+
try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
|
|
1389
|
+
try { db.exec('DELETE FROM cooldowns'); } catch { /* migration not applied */ }
|
|
1390
|
+
try { db.exec('DELETE FROM pending_rebases'); } catch { /* migration not applied */ }
|
|
1391
|
+
try { db.exec('DELETE FROM cc_sessions'); } catch { /* migration not applied */ }
|
|
1392
|
+
try { db.exec('DELETE FROM doc_sessions'); } catch { /* migration not applied */ }
|
|
844
1393
|
} catch { /* not initialized */ }
|
|
845
1394
|
_scheduleRunsHash = null;
|
|
846
1395
|
_pipelineRunsHash = null;
|
|
@@ -848,6 +1397,11 @@ function _resetAllForTest() {
|
|
|
848
1397
|
_worktreePoolHash = null;
|
|
849
1398
|
_qaRunsHash = null;
|
|
850
1399
|
_qaSessionsHash = null;
|
|
1400
|
+
_prLinksHash = null;
|
|
1401
|
+
_cooldownsHash = null;
|
|
1402
|
+
_pendingRebasesHash = null;
|
|
1403
|
+
_ccSessionsHash = null;
|
|
1404
|
+
_docSessionsHash = null;
|
|
851
1405
|
}
|
|
852
1406
|
|
|
853
1407
|
module.exports = {
|
|
@@ -875,6 +1429,26 @@ module.exports = {
|
|
|
875
1429
|
readQaSessions,
|
|
876
1430
|
applyQaSessionsMutation,
|
|
877
1431
|
_mirrorQaSessionsJson,
|
|
1432
|
+
// pr_links
|
|
1433
|
+
readPrLinks,
|
|
1434
|
+
applyPrLinksMutation,
|
|
1435
|
+
_mirrorPrLinksJson,
|
|
1436
|
+
// cooldowns
|
|
1437
|
+
readCooldowns,
|
|
1438
|
+
applyCooldownsMutation,
|
|
1439
|
+
_mirrorCooldownsJson,
|
|
1440
|
+
// pending_rebases
|
|
1441
|
+
readPendingRebases,
|
|
1442
|
+
applyPendingRebasesMutation,
|
|
1443
|
+
_mirrorPendingRebasesJson,
|
|
1444
|
+
// cc_sessions
|
|
1445
|
+
readCcSessions,
|
|
1446
|
+
applyCcSessionsMutation,
|
|
1447
|
+
_mirrorCcSessionsJson,
|
|
1448
|
+
// doc_sessions
|
|
1449
|
+
readDocSessions,
|
|
1450
|
+
applyDocSessionsMutation,
|
|
1451
|
+
_mirrorDocSessionsJson,
|
|
878
1452
|
// test seam
|
|
879
1453
|
_resetAllForTest,
|
|
880
1454
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2116",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|