@yemi33/minions 0.1.2115 → 0.1.2117
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/index.js +39 -10
- package/engine/db/migrate.js +11 -4
- package/engine/db/migrations/011-remaining-state.js +116 -0
- package/engine/dispatch-store.js +1 -3
- package/engine/dispatch.js +24 -59
- package/engine/kb-sweep.js +20 -14
- package/engine/pull-requests-store.js +3 -25
- package/engine/shared.js +201 -171
- package/engine/small-state-store.js +455 -21
- package/engine/work-items-store.js +6 -33
- package/package.json +1 -1
|
@@ -94,9 +94,7 @@ function _readScheduleRunsFromSql(db) {
|
|
|
94
94
|
|
|
95
95
|
function readScheduleRuns() {
|
|
96
96
|
const { getDb } = require('./db');
|
|
97
|
-
|
|
98
|
-
try { db = getDb(); }
|
|
99
|
-
catch { return _readJson(_resolveFilePath('schedule-runs.json')) || {}; }
|
|
97
|
+
const db = getDb();
|
|
100
98
|
_resyncScheduleRunsIfDiverged(db);
|
|
101
99
|
const out = _readScheduleRunsFromSql(db);
|
|
102
100
|
if (Object.keys(out).length === 0) {
|
|
@@ -207,9 +205,7 @@ function _readPipelineRunsFromSql(db) {
|
|
|
207
205
|
|
|
208
206
|
function readPipelineRuns() {
|
|
209
207
|
const { getDb } = require('./db');
|
|
210
|
-
|
|
211
|
-
try { db = getDb(); }
|
|
212
|
-
catch { return _readJson(_resolveFilePath('pipeline-runs.json')) || {}; }
|
|
208
|
+
const db = getDb();
|
|
213
209
|
_resyncPipelineRunsIfDiverged(db);
|
|
214
210
|
const out = _readPipelineRunsFromSql(db);
|
|
215
211
|
if (Object.keys(out).length === 0) {
|
|
@@ -329,9 +325,7 @@ function _readManagedProcessesFromSql(db) {
|
|
|
329
325
|
|
|
330
326
|
function readManagedProcesses() {
|
|
331
327
|
const { getDb } = require('./db');
|
|
332
|
-
|
|
333
|
-
try { db = getDb(); }
|
|
334
|
-
catch { return _readJson(_resolveFilePath('managed-processes.json')) || { specs: [] }; }
|
|
328
|
+
const db = getDb();
|
|
335
329
|
_resyncManagedProcessesIfDiverged(db);
|
|
336
330
|
const out = _readManagedProcessesFromSql(db);
|
|
337
331
|
if (out.specs.length === 0) {
|
|
@@ -449,9 +443,7 @@ function _readWorktreePoolFromSql(db) {
|
|
|
449
443
|
|
|
450
444
|
function readWorktreePool() {
|
|
451
445
|
const { getDb } = require('./db');
|
|
452
|
-
|
|
453
|
-
try { db = getDb(); }
|
|
454
|
-
catch { return _readJson(_resolveFilePath('worktree-pool.json')) || { entries: [] }; }
|
|
446
|
+
const db = getDb();
|
|
455
447
|
_resyncWorktreePoolIfDiverged(db);
|
|
456
448
|
const out = _readWorktreePoolFromSql(db);
|
|
457
449
|
if (out.entries.length === 0) {
|
|
@@ -581,9 +573,7 @@ function _readQaRunsFromSqlOnly(db) {
|
|
|
581
573
|
|
|
582
574
|
function readQaRuns() {
|
|
583
575
|
const { getDb } = require('./db');
|
|
584
|
-
|
|
585
|
-
try { db = getDb(); }
|
|
586
|
-
catch { return _readJson(_resolveFilePath('qa-runs.json')) || []; }
|
|
576
|
+
const db = getDb();
|
|
587
577
|
_resyncQaRunsIfDiverged(db);
|
|
588
578
|
const out = _readQaRunsFromSqlOnly(db);
|
|
589
579
|
if (out.length === 0) {
|
|
@@ -741,9 +731,7 @@ function _readQaSessionsFromSqlOnly(db) {
|
|
|
741
731
|
|
|
742
732
|
function readQaSessions() {
|
|
743
733
|
const { getDb } = require('./db');
|
|
744
|
-
|
|
745
|
-
try { db = getDb(); }
|
|
746
|
-
catch { return _readJson(_resolveFilePath('qa-sessions.json')) || []; }
|
|
734
|
+
const db = getDb();
|
|
747
735
|
_resyncQaSessionsIfDiverged(db);
|
|
748
736
|
const out = _readQaSessionsFromSqlOnly(db);
|
|
749
737
|
if (out.length === 0) {
|
|
@@ -875,9 +863,7 @@ function _readPrLinksFromSql(db) {
|
|
|
875
863
|
|
|
876
864
|
function readPrLinks() {
|
|
877
865
|
const { getDb } = require('./db');
|
|
878
|
-
|
|
879
|
-
try { db = getDb(); }
|
|
880
|
-
catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
|
|
866
|
+
const db = getDb();
|
|
881
867
|
try { _resyncPrLinksIfDiverged(db); }
|
|
882
868
|
catch { /* table may be missing on a stale install — fall back to JSON */ }
|
|
883
869
|
let out;
|
|
@@ -941,6 +927,430 @@ function _mirrorPrLinksJson(filePath) {
|
|
|
941
927
|
} catch { /* mirror best-effort */ }
|
|
942
928
|
}
|
|
943
929
|
|
|
930
|
+
// ─── cooldowns ─────────────────────────────────────────────────────────────
|
|
931
|
+
// Shape: { [key]: { timestamp, failures, ... } }
|
|
932
|
+
// SQL: row per key.
|
|
933
|
+
|
|
934
|
+
let _cooldownsHash = null;
|
|
935
|
+
|
|
936
|
+
function _hydrateCooldowns(db) {
|
|
937
|
+
const fp = _resolveFilePath('cooldowns.json');
|
|
938
|
+
const raw = _readJson(fp) || {};
|
|
939
|
+
db.prepare('DELETE FROM cooldowns').run();
|
|
940
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
const ins = db.prepare('INSERT INTO cooldowns (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
|
|
943
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
944
|
+
ins.run(String(key), JSON.stringify(value), now);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function _resyncCooldownsIfDiverged(db) {
|
|
949
|
+
const fp = _resolveFilePath('cooldowns.json');
|
|
950
|
+
const currentHash = _fileContentHash(fp);
|
|
951
|
+
if (currentHash == null) return;
|
|
952
|
+
if (_cooldownsHash != null && currentHash === _cooldownsHash) return;
|
|
953
|
+
if (_cooldownsHash == null) {
|
|
954
|
+
const sqlHas = db.prepare('SELECT 1 FROM cooldowns LIMIT 1').get();
|
|
955
|
+
if (sqlHas) { _cooldownsHash = currentHash; return; }
|
|
956
|
+
}
|
|
957
|
+
_hydrateCooldowns(db);
|
|
958
|
+
_cooldownsHash = currentHash;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function _readCooldownsFromSql(db) {
|
|
962
|
+
const rows = db.prepare('SELECT key, data FROM cooldowns').all();
|
|
963
|
+
const out = {};
|
|
964
|
+
for (const row of rows) {
|
|
965
|
+
try { out[row.key] = JSON.parse(row.data); }
|
|
966
|
+
catch { /* skip malformed */ }
|
|
967
|
+
}
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function readCooldowns() {
|
|
972
|
+
const { getDb } = require('./db');
|
|
973
|
+
const db = getDb();
|
|
974
|
+
try { _resyncCooldownsIfDiverged(db); }
|
|
975
|
+
catch { /* table may be missing on stale install */ }
|
|
976
|
+
let out;
|
|
977
|
+
try { out = _readCooldownsFromSql(db); }
|
|
978
|
+
catch { return _readJson(_resolveFilePath('cooldowns.json')) || {}; }
|
|
979
|
+
if (Object.keys(out).length === 0) {
|
|
980
|
+
const fallback = _readJson(_resolveFilePath('cooldowns.json'));
|
|
981
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
982
|
+
return {};
|
|
983
|
+
}
|
|
984
|
+
return out;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function applyCooldownsMutation(mutator) {
|
|
988
|
+
const { getDb, withTransaction } = require('./db');
|
|
989
|
+
let db;
|
|
990
|
+
try { db = getDb(); }
|
|
991
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
992
|
+
|
|
993
|
+
return withTransaction(db, () => {
|
|
994
|
+
_resyncCooldownsIfDiverged(db);
|
|
995
|
+
const before = _readCooldownsFromSql(db);
|
|
996
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
997
|
+
const next = mutator(before);
|
|
998
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
999
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
1000
|
+
return { wrote: false, result: beforeSnap };
|
|
1001
|
+
}
|
|
1002
|
+
const afterIds = new Set(Object.keys(after));
|
|
1003
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
1004
|
+
let wrote = false;
|
|
1005
|
+
const now = Date.now();
|
|
1006
|
+
const upsert = db.prepare(`
|
|
1007
|
+
INSERT INTO cooldowns (key, data, updated_at)
|
|
1008
|
+
VALUES (?, ?, ?)
|
|
1009
|
+
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1010
|
+
`);
|
|
1011
|
+
const del = db.prepare('DELETE FROM cooldowns WHERE key = ?');
|
|
1012
|
+
for (const id of afterIds) {
|
|
1013
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
1014
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
1015
|
+
wrote = true;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
for (const id of beforeIds) {
|
|
1019
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
1020
|
+
}
|
|
1021
|
+
return { wrote, result: after };
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function _mirrorCooldownsJson(filePath) {
|
|
1026
|
+
try {
|
|
1027
|
+
const shared = require('./shared');
|
|
1028
|
+
const { getDb } = require('./db');
|
|
1029
|
+
const obj = _readCooldownsFromSql(getDb());
|
|
1030
|
+
const target = filePath || _resolveFilePath('cooldowns.json');
|
|
1031
|
+
shared.safeWrite(target, obj);
|
|
1032
|
+
const h = _fileContentHash(target);
|
|
1033
|
+
if (h != null) _cooldownsHash = h;
|
|
1034
|
+
} catch { /* mirror best-effort */ }
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ─── pending_rebases ───────────────────────────────────────────────────────
|
|
1038
|
+
// Shape: [ { prId, branch, projectName, mergedItemId, queuedAt, attempts, ... }, ... ]
|
|
1039
|
+
// SQL: row per array entry (rowid auto-increment preserves insertion order).
|
|
1040
|
+
// No stable natural key, so mutations are full clear+insert.
|
|
1041
|
+
|
|
1042
|
+
let _pendingRebasesHash = null;
|
|
1043
|
+
|
|
1044
|
+
function _hydratePendingRebases(db) {
|
|
1045
|
+
const fp = _resolveFilePath('pending-rebases.json');
|
|
1046
|
+
const raw = _readJson(fp) || [];
|
|
1047
|
+
db.prepare('DELETE FROM pending_rebases').run();
|
|
1048
|
+
if (!Array.isArray(raw)) return;
|
|
1049
|
+
const now = Date.now();
|
|
1050
|
+
const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
|
|
1051
|
+
for (const entry of raw) {
|
|
1052
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1053
|
+
ins.run(JSON.stringify(entry), now);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function _resyncPendingRebasesIfDiverged(db) {
|
|
1058
|
+
const fp = _resolveFilePath('pending-rebases.json');
|
|
1059
|
+
const currentHash = _fileContentHash(fp);
|
|
1060
|
+
if (currentHash == null) return;
|
|
1061
|
+
if (_pendingRebasesHash != null && currentHash === _pendingRebasesHash) return;
|
|
1062
|
+
if (_pendingRebasesHash == null) {
|
|
1063
|
+
const sqlHas = db.prepare('SELECT 1 FROM pending_rebases LIMIT 1').get();
|
|
1064
|
+
if (sqlHas) { _pendingRebasesHash = currentHash; return; }
|
|
1065
|
+
}
|
|
1066
|
+
_hydratePendingRebases(db);
|
|
1067
|
+
_pendingRebasesHash = currentHash;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function _readPendingRebasesFromSql(db) {
|
|
1071
|
+
const rows = db.prepare('SELECT data FROM pending_rebases ORDER BY seq').all();
|
|
1072
|
+
const out = [];
|
|
1073
|
+
for (const row of rows) {
|
|
1074
|
+
try { out.push(JSON.parse(row.data)); }
|
|
1075
|
+
catch { /* skip malformed */ }
|
|
1076
|
+
}
|
|
1077
|
+
return out;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function readPendingRebases() {
|
|
1081
|
+
const { getDb } = require('./db');
|
|
1082
|
+
const db = getDb();
|
|
1083
|
+
try { _resyncPendingRebasesIfDiverged(db); }
|
|
1084
|
+
catch { /* table missing */ }
|
|
1085
|
+
let out;
|
|
1086
|
+
try { out = _readPendingRebasesFromSql(db); }
|
|
1087
|
+
catch { return _readJson(_resolveFilePath('pending-rebases.json')) || []; }
|
|
1088
|
+
if (out.length === 0) {
|
|
1089
|
+
const fallback = _readJson(_resolveFilePath('pending-rebases.json'));
|
|
1090
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function applyPendingRebasesMutation(mutator) {
|
|
1097
|
+
const { getDb, withTransaction } = require('./db');
|
|
1098
|
+
let db;
|
|
1099
|
+
try { db = getDb(); }
|
|
1100
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1101
|
+
|
|
1102
|
+
return withTransaction(db, () => {
|
|
1103
|
+
_resyncPendingRebasesIfDiverged(db);
|
|
1104
|
+
const before = _readPendingRebasesFromSql(db);
|
|
1105
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1106
|
+
const next = mutator(before);
|
|
1107
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1108
|
+
if (!Array.isArray(after)) {
|
|
1109
|
+
return { wrote: false, result: beforeSnap };
|
|
1110
|
+
}
|
|
1111
|
+
let wrote = false;
|
|
1112
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
1113
|
+
const now = Date.now();
|
|
1114
|
+
db.prepare('DELETE FROM pending_rebases').run();
|
|
1115
|
+
const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
|
|
1116
|
+
for (const entry of after) {
|
|
1117
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1118
|
+
ins.run(JSON.stringify(entry), now);
|
|
1119
|
+
}
|
|
1120
|
+
wrote = true;
|
|
1121
|
+
}
|
|
1122
|
+
return { wrote, result: after };
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function _mirrorPendingRebasesJson(filePath) {
|
|
1127
|
+
try {
|
|
1128
|
+
const shared = require('./shared');
|
|
1129
|
+
const { getDb } = require('./db');
|
|
1130
|
+
const arr = _readPendingRebasesFromSql(getDb());
|
|
1131
|
+
const target = filePath || _resolveFilePath('pending-rebases.json');
|
|
1132
|
+
shared.safeWrite(target, arr);
|
|
1133
|
+
const h = _fileContentHash(target);
|
|
1134
|
+
if (h != null) _pendingRebasesHash = h;
|
|
1135
|
+
} catch { /* mirror best-effort */ }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ─── cc_sessions ───────────────────────────────────────────────────────────
|
|
1139
|
+
// Shape: [ { id, sessionId, title, _promptHash, lastActiveAt, ... }, ... ]
|
|
1140
|
+
// SQL: row per id (each entry has a unique `id` tab key).
|
|
1141
|
+
|
|
1142
|
+
let _ccSessionsHash = null;
|
|
1143
|
+
|
|
1144
|
+
function _hydrateCcSessions(db) {
|
|
1145
|
+
const fp = _resolveFilePath('cc-sessions.json');
|
|
1146
|
+
const raw = _readJson(fp) || [];
|
|
1147
|
+
db.prepare('DELETE FROM cc_sessions').run();
|
|
1148
|
+
if (!Array.isArray(raw)) return;
|
|
1149
|
+
const now = Date.now();
|
|
1150
|
+
const ins = db.prepare('INSERT INTO cc_sessions (id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
|
1151
|
+
for (const entry of raw) {
|
|
1152
|
+
if (!entry || typeof entry !== 'object' || !entry.id) continue;
|
|
1153
|
+
ins.run(String(entry.id), JSON.stringify(entry), now);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function _resyncCcSessionsIfDiverged(db) {
|
|
1158
|
+
const fp = _resolveFilePath('cc-sessions.json');
|
|
1159
|
+
const currentHash = _fileContentHash(fp);
|
|
1160
|
+
if (currentHash == null) return;
|
|
1161
|
+
if (_ccSessionsHash != null && currentHash === _ccSessionsHash) return;
|
|
1162
|
+
if (_ccSessionsHash == null) {
|
|
1163
|
+
const sqlHas = db.prepare('SELECT 1 FROM cc_sessions LIMIT 1').get();
|
|
1164
|
+
if (sqlHas) { _ccSessionsHash = currentHash; return; }
|
|
1165
|
+
}
|
|
1166
|
+
_hydrateCcSessions(db);
|
|
1167
|
+
_ccSessionsHash = currentHash;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function _readCcSessionsFromSql(db) {
|
|
1171
|
+
const rows = db.prepare('SELECT data FROM cc_sessions ORDER BY updated_at, id').all();
|
|
1172
|
+
const out = [];
|
|
1173
|
+
for (const row of rows) {
|
|
1174
|
+
try { out.push(JSON.parse(row.data)); }
|
|
1175
|
+
catch { /* skip malformed */ }
|
|
1176
|
+
}
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function readCcSessions() {
|
|
1181
|
+
const { getDb } = require('./db');
|
|
1182
|
+
const db = getDb();
|
|
1183
|
+
try { _resyncCcSessionsIfDiverged(db); }
|
|
1184
|
+
catch { /* table missing */ }
|
|
1185
|
+
let out;
|
|
1186
|
+
try { out = _readCcSessionsFromSql(db); }
|
|
1187
|
+
catch { return _readJson(_resolveFilePath('cc-sessions.json')) || []; }
|
|
1188
|
+
if (out.length === 0) {
|
|
1189
|
+
const fallback = _readJson(_resolveFilePath('cc-sessions.json'));
|
|
1190
|
+
if (Array.isArray(fallback) && fallback.length > 0) return fallback;
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
return out;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function applyCcSessionsMutation(mutator) {
|
|
1197
|
+
const { getDb, withTransaction } = require('./db');
|
|
1198
|
+
let db;
|
|
1199
|
+
try { db = getDb(); }
|
|
1200
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1201
|
+
|
|
1202
|
+
return withTransaction(db, () => {
|
|
1203
|
+
_resyncCcSessionsIfDiverged(db);
|
|
1204
|
+
const before = _readCcSessionsFromSql(db);
|
|
1205
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1206
|
+
const next = mutator(before);
|
|
1207
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1208
|
+
if (!Array.isArray(after)) {
|
|
1209
|
+
return { wrote: false, result: beforeSnap };
|
|
1210
|
+
}
|
|
1211
|
+
const beforeById = new Map(beforeSnap.filter(e => e && e.id).map(e => [String(e.id), e]));
|
|
1212
|
+
const afterById = new Map(after.filter(e => e && e.id).map(e => [String(e.id), e]));
|
|
1213
|
+
let wrote = false;
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
const upsert = db.prepare(`
|
|
1216
|
+
INSERT INTO cc_sessions (id, data, updated_at)
|
|
1217
|
+
VALUES (?, ?, ?)
|
|
1218
|
+
ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1219
|
+
`);
|
|
1220
|
+
const del = db.prepare('DELETE FROM cc_sessions WHERE id = ?');
|
|
1221
|
+
for (const [id, entry] of afterById) {
|
|
1222
|
+
const prior = beforeById.get(id);
|
|
1223
|
+
if (!prior || JSON.stringify(prior) !== JSON.stringify(entry)) {
|
|
1224
|
+
upsert.run(id, JSON.stringify(entry), now);
|
|
1225
|
+
wrote = true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const id of beforeById.keys()) {
|
|
1229
|
+
if (!afterById.has(id)) { del.run(id); wrote = true; }
|
|
1230
|
+
}
|
|
1231
|
+
return { wrote, result: after };
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function _mirrorCcSessionsJson(filePath) {
|
|
1236
|
+
try {
|
|
1237
|
+
const shared = require('./shared');
|
|
1238
|
+
const { getDb } = require('./db');
|
|
1239
|
+
const arr = _readCcSessionsFromSql(getDb());
|
|
1240
|
+
const target = filePath || _resolveFilePath('cc-sessions.json');
|
|
1241
|
+
shared.safeWrite(target, arr);
|
|
1242
|
+
const h = _fileContentHash(target);
|
|
1243
|
+
if (h != null) _ccSessionsHash = h;
|
|
1244
|
+
} catch { /* mirror best-effort */ }
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ─── doc_sessions ──────────────────────────────────────────────────────────
|
|
1248
|
+
// Shape: { [filePath]: { sessionId, lastActiveAt, turnCount, ... } }
|
|
1249
|
+
// SQL: row per filePath key.
|
|
1250
|
+
|
|
1251
|
+
let _docSessionsHash = null;
|
|
1252
|
+
|
|
1253
|
+
function _hydrateDocSessions(db) {
|
|
1254
|
+
const fp = _resolveFilePath('doc-sessions.json');
|
|
1255
|
+
const raw = _readJson(fp) || {};
|
|
1256
|
+
db.prepare('DELETE FROM doc_sessions').run();
|
|
1257
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
1258
|
+
const now = Date.now();
|
|
1259
|
+
const ins = db.prepare('INSERT INTO doc_sessions (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
|
|
1260
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1261
|
+
ins.run(String(key), JSON.stringify(value), now);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function _resyncDocSessionsIfDiverged(db) {
|
|
1266
|
+
const fp = _resolveFilePath('doc-sessions.json');
|
|
1267
|
+
const currentHash = _fileContentHash(fp);
|
|
1268
|
+
if (currentHash == null) return;
|
|
1269
|
+
if (_docSessionsHash != null && currentHash === _docSessionsHash) return;
|
|
1270
|
+
if (_docSessionsHash == null) {
|
|
1271
|
+
const sqlHas = db.prepare('SELECT 1 FROM doc_sessions LIMIT 1').get();
|
|
1272
|
+
if (sqlHas) { _docSessionsHash = currentHash; return; }
|
|
1273
|
+
}
|
|
1274
|
+
_hydrateDocSessions(db);
|
|
1275
|
+
_docSessionsHash = currentHash;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function _readDocSessionsFromSql(db) {
|
|
1279
|
+
const rows = db.prepare('SELECT key, data FROM doc_sessions').all();
|
|
1280
|
+
const out = {};
|
|
1281
|
+
for (const row of rows) {
|
|
1282
|
+
try { out[row.key] = JSON.parse(row.data); }
|
|
1283
|
+
catch { /* skip malformed */ }
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function readDocSessions() {
|
|
1289
|
+
const { getDb } = require('./db');
|
|
1290
|
+
const db = getDb();
|
|
1291
|
+
try { _resyncDocSessionsIfDiverged(db); }
|
|
1292
|
+
catch { /* table missing */ }
|
|
1293
|
+
let out;
|
|
1294
|
+
try { out = _readDocSessionsFromSql(db); }
|
|
1295
|
+
catch { return _readJson(_resolveFilePath('doc-sessions.json')) || {}; }
|
|
1296
|
+
if (Object.keys(out).length === 0) {
|
|
1297
|
+
const fallback = _readJson(_resolveFilePath('doc-sessions.json'));
|
|
1298
|
+
if (fallback && Object.keys(fallback).length > 0) return fallback;
|
|
1299
|
+
return {};
|
|
1300
|
+
}
|
|
1301
|
+
return out;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function applyDocSessionsMutation(mutator) {
|
|
1305
|
+
const { getDb, withTransaction } = require('./db');
|
|
1306
|
+
let db;
|
|
1307
|
+
try { db = getDb(); }
|
|
1308
|
+
catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }
|
|
1309
|
+
|
|
1310
|
+
return withTransaction(db, () => {
|
|
1311
|
+
_resyncDocSessionsIfDiverged(db);
|
|
1312
|
+
const before = _readDocSessionsFromSql(db);
|
|
1313
|
+
const beforeSnap = JSON.parse(JSON.stringify(before));
|
|
1314
|
+
const next = mutator(before);
|
|
1315
|
+
const after = (next === undefined || next === null) ? before : next;
|
|
1316
|
+
if (!after || typeof after !== 'object' || Array.isArray(after)) {
|
|
1317
|
+
return { wrote: false, result: beforeSnap };
|
|
1318
|
+
}
|
|
1319
|
+
const afterIds = new Set(Object.keys(after));
|
|
1320
|
+
const beforeIds = new Set(Object.keys(beforeSnap));
|
|
1321
|
+
let wrote = false;
|
|
1322
|
+
const now = Date.now();
|
|
1323
|
+
const upsert = db.prepare(`
|
|
1324
|
+
INSERT INTO doc_sessions (key, data, updated_at)
|
|
1325
|
+
VALUES (?, ?, ?)
|
|
1326
|
+
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
1327
|
+
`);
|
|
1328
|
+
const del = db.prepare('DELETE FROM doc_sessions WHERE key = ?');
|
|
1329
|
+
for (const id of afterIds) {
|
|
1330
|
+
if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
|
|
1331
|
+
upsert.run(id, JSON.stringify(after[id]), now);
|
|
1332
|
+
wrote = true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
for (const id of beforeIds) {
|
|
1336
|
+
if (!afterIds.has(id)) { del.run(id); wrote = true; }
|
|
1337
|
+
}
|
|
1338
|
+
return { wrote, result: after };
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function _mirrorDocSessionsJson(filePath) {
|
|
1343
|
+
try {
|
|
1344
|
+
const shared = require('./shared');
|
|
1345
|
+
const { getDb } = require('./db');
|
|
1346
|
+
const obj = _readDocSessionsFromSql(getDb());
|
|
1347
|
+
const target = filePath || _resolveFilePath('doc-sessions.json');
|
|
1348
|
+
shared.safeWrite(target, obj);
|
|
1349
|
+
const h = _fileContentHash(target);
|
|
1350
|
+
if (h != null) _docSessionsHash = h;
|
|
1351
|
+
} catch { /* mirror best-effort */ }
|
|
1352
|
+
}
|
|
1353
|
+
|
|
944
1354
|
// ─── Test seam ─────────────────────────────────────────────────────────────
|
|
945
1355
|
|
|
946
1356
|
function _resetAllForTest() {
|
|
@@ -954,6 +1364,10 @@ function _resetAllForTest() {
|
|
|
954
1364
|
try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
|
|
955
1365
|
try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
|
|
956
1366
|
try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
|
|
1367
|
+
try { db.exec('DELETE FROM cooldowns'); } catch { /* migration not applied */ }
|
|
1368
|
+
try { db.exec('DELETE FROM pending_rebases'); } catch { /* migration not applied */ }
|
|
1369
|
+
try { db.exec('DELETE FROM cc_sessions'); } catch { /* migration not applied */ }
|
|
1370
|
+
try { db.exec('DELETE FROM doc_sessions'); } catch { /* migration not applied */ }
|
|
957
1371
|
} catch { /* not initialized */ }
|
|
958
1372
|
_scheduleRunsHash = null;
|
|
959
1373
|
_pipelineRunsHash = null;
|
|
@@ -962,6 +1376,10 @@ function _resetAllForTest() {
|
|
|
962
1376
|
_qaRunsHash = null;
|
|
963
1377
|
_qaSessionsHash = null;
|
|
964
1378
|
_prLinksHash = null;
|
|
1379
|
+
_cooldownsHash = null;
|
|
1380
|
+
_pendingRebasesHash = null;
|
|
1381
|
+
_ccSessionsHash = null;
|
|
1382
|
+
_docSessionsHash = null;
|
|
965
1383
|
}
|
|
966
1384
|
|
|
967
1385
|
module.exports = {
|
|
@@ -993,6 +1411,22 @@ module.exports = {
|
|
|
993
1411
|
readPrLinks,
|
|
994
1412
|
applyPrLinksMutation,
|
|
995
1413
|
_mirrorPrLinksJson,
|
|
1414
|
+
// cooldowns
|
|
1415
|
+
readCooldowns,
|
|
1416
|
+
applyCooldownsMutation,
|
|
1417
|
+
_mirrorCooldownsJson,
|
|
1418
|
+
// pending_rebases
|
|
1419
|
+
readPendingRebases,
|
|
1420
|
+
applyPendingRebasesMutation,
|
|
1421
|
+
_mirrorPendingRebasesJson,
|
|
1422
|
+
// cc_sessions
|
|
1423
|
+
readCcSessions,
|
|
1424
|
+
applyCcSessionsMutation,
|
|
1425
|
+
_mirrorCcSessionsJson,
|
|
1426
|
+
// doc_sessions
|
|
1427
|
+
readDocSessions,
|
|
1428
|
+
applyDocSessionsMutation,
|
|
1429
|
+
_mirrorDocSessionsJson,
|
|
996
1430
|
// test seam
|
|
997
1431
|
_resetAllForTest,
|
|
998
1432
|
};
|
|
@@ -75,9 +75,7 @@ function _readJsonArrayFallback(scope) {
|
|
|
75
75
|
|
|
76
76
|
function readWorkItemsForScope(scope) {
|
|
77
77
|
const { getDb } = require('./db');
|
|
78
|
-
|
|
79
|
-
try { db = getDb(); }
|
|
80
|
-
catch { return _readJsonArrayFallback(scope); }
|
|
78
|
+
const db = getDb();
|
|
81
79
|
|
|
82
80
|
_resyncScopeIfJsonDiverged(db, scope);
|
|
83
81
|
|
|
@@ -88,10 +86,9 @@ function readWorkItemsForScope(scope) {
|
|
|
88
86
|
`).all(scope);
|
|
89
87
|
|
|
90
88
|
if (rows.length === 0) {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// call sites working without touching every helper.
|
|
89
|
+
// SQL empty AND JSON has content means a test seeded via fs.writeFileSync
|
|
90
|
+
// (legacy helper) or a fresh install pre-migration. Returning the JSON
|
|
91
|
+
// keeps those call sites working without touching every helper.
|
|
95
92
|
const fallback = _readJsonArrayFallback(scope);
|
|
96
93
|
if (fallback.length > 0) return fallback;
|
|
97
94
|
return [];
|
|
@@ -161,29 +158,9 @@ function _enumerateJsonScopes() {
|
|
|
161
158
|
// Read all rows across all scopes — used by queries.getWorkItems which
|
|
162
159
|
// needs to surface central + every project's items in a single shot,
|
|
163
160
|
// tagged with their source scope.
|
|
164
|
-
//
|
|
165
|
-
// Issue #3035: SQL-unavailable installs (Node 22.x without
|
|
166
|
-
// --experimental-sqlite, prior to v0.1.2113) returned [] from this path
|
|
167
|
-
// because `getDb()` throws and the legacy `return null` signal was
|
|
168
|
-
// swallowed by `queries.getWorkItems`'s `|| []`. Mirror the per-scope
|
|
169
|
-
// reader's JSON-fallback shape so the aggregate API stays useful on
|
|
170
|
-
// Node versions that don't have node:sqlite enabled.
|
|
171
161
|
function readAllWorkItems() {
|
|
172
162
|
const { getDb } = require('./db');
|
|
173
|
-
|
|
174
|
-
try { db = getDb(); }
|
|
175
|
-
catch {
|
|
176
|
-
// SQLite unavailable — read every JSON scope on disk directly.
|
|
177
|
-
const out = [];
|
|
178
|
-
for (const scope of _enumerateJsonScopes()) {
|
|
179
|
-
for (const wi of _readJsonArrayFallback(scope)) {
|
|
180
|
-
if (!wi || typeof wi !== 'object') continue;
|
|
181
|
-
wi._source = scope;
|
|
182
|
-
out.push(wi);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
163
|
+
const db = getDb();
|
|
187
164
|
|
|
188
165
|
// Pick up any external JSON edits for every scope SQL knows about.
|
|
189
166
|
// Also resync 'central' explicitly so first-time reads on a JSON-only
|
|
@@ -326,11 +303,7 @@ function _hydrateScopeFromJson(db, scope) {
|
|
|
326
303
|
|
|
327
304
|
function applyWorkItemsMutation(scope, mutator) {
|
|
328
305
|
const { getDb, withTransaction } = require('./db');
|
|
329
|
-
|
|
330
|
-
try { db = getDb(); }
|
|
331
|
-
catch (e) {
|
|
332
|
-
throw new Error(`engine/work-items-store: SQLite unavailable (${e.message}); cannot mutate work_items`);
|
|
333
|
-
}
|
|
306
|
+
const db = getDb();
|
|
334
307
|
|
|
335
308
|
return withTransaction(db, () => {
|
|
336
309
|
// Re-hydrate SQL from JSON if the file was touched outside the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2117",
|
|
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"
|