@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
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');
|
|
@@ -536,6 +546,22 @@ function _routeJsonReadToSql(p) {
|
|
|
536
546
|
const store = require('./small-state-store');
|
|
537
547
|
return { value: store.readPrLinks() };
|
|
538
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
|
+
}
|
|
539
565
|
// Per-project work-items.json — match `/projects/<name>/work-items.json`.
|
|
540
566
|
// When SQL has no rows for the scope AND the JSON file is absent on
|
|
541
567
|
// disk, preserve the legacy "file missing → null" semantic. This guards
|
|
@@ -565,9 +591,11 @@ function _routeJsonReadToSql(p) {
|
|
|
565
591
|
const store = require('./pull-requests-store');
|
|
566
592
|
return { value: store.readPullRequestsForScope('central') };
|
|
567
593
|
}
|
|
568
|
-
} catch {
|
|
569
|
-
//
|
|
570
|
-
|
|
594
|
+
} catch (e) {
|
|
595
|
+
// Phase 9.4: store/load failures (not SQLite-unavailable — the CLI shim
|
|
596
|
+
// in bin/minions.js guarantees node:sqlite is loadable) propagate up so
|
|
597
|
+
// the caller can decide whether to retry or surface.
|
|
598
|
+
throw e;
|
|
571
599
|
}
|
|
572
600
|
return null;
|
|
573
601
|
}
|
|
@@ -1323,13 +1351,64 @@ function withFileLock(lockPath, fn, {
|
|
|
1323
1351
|
throw lastErr;
|
|
1324
1352
|
}
|
|
1325
1353
|
|
|
1354
|
+
// Route table for small-state files migrated to SQL stores. Each entry
|
|
1355
|
+
// declares the SQL store function to invoke and the expected default JSON
|
|
1356
|
+
// shape (for the "ensure file exists" fallback below).
|
|
1357
|
+
const _SMALL_STATE_MUTATE_ROUTES = {
|
|
1358
|
+
'cooldowns.json': { fn: 'applyCooldownsMutation', mirror: '_mirrorCooldownsJson', defaultShape: 'object' },
|
|
1359
|
+
'pending-rebases.json': { fn: 'applyPendingRebasesMutation', mirror: '_mirrorPendingRebasesJson', defaultShape: 'array' },
|
|
1360
|
+
'cc-sessions.json': { fn: 'applyCcSessionsMutation', mirror: '_mirrorCcSessionsJson', defaultShape: 'array' },
|
|
1361
|
+
'doc-sessions.json': { fn: 'applyDocSessionsMutation', mirror: '_mirrorDocSessionsJson', defaultShape: 'object' },
|
|
1362
|
+
'pr-links.json': { fn: 'applyPrLinksMutation', mirror: '_mirrorPrLinksJson', defaultShape: 'object' },
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1326
1365
|
function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
|
|
1327
1366
|
const baseName = path.basename(filePath);
|
|
1328
|
-
|
|
1367
|
+
const smallRoute = _SMALL_STATE_MUTATE_ROUTES[baseName];
|
|
1368
|
+
if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !smallRoute) return null;
|
|
1329
1369
|
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
1330
1370
|
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
1331
1371
|
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/' + baseName;
|
|
1332
1372
|
if (!insideMinionsDir) return null;
|
|
1373
|
+
|
|
1374
|
+
// Small-state files live exclusively under <MINIONS_DIR>/engine/<baseName>.
|
|
1375
|
+
// Don't hijack writes to ad-hoc paths that happen to share the basename.
|
|
1376
|
+
if (smallRoute) {
|
|
1377
|
+
if (!fpNorm.endsWith('/engine/' + baseName)) return null;
|
|
1378
|
+
const store = require('./small-state-store');
|
|
1379
|
+
// Hold the JSON file lock across the SQL transaction AND the mirror
|
|
1380
|
+
// write. The SQL transaction itself is cross-process serialized via
|
|
1381
|
+
// BEGIN IMMEDIATE inside the SQL store, but the JSON mirror is a
|
|
1382
|
+
// separate read-from-SQL → atomic-rename that can race across
|
|
1383
|
+
// processes: an earlier mirror's stale snapshot can land AFTER a
|
|
1384
|
+
// later mirror's complete snapshot and lose committed rows from the
|
|
1385
|
+
// on-disk JSON file. Pre-Phase-9.4 this was masked by the JSON
|
|
1386
|
+
// fallback (concurrent writers serialized through the same lock at
|
|
1387
|
+
// the bottom of mutateJsonFileLocked); after the fallback removal,
|
|
1388
|
+
// the bare mirror races. Locking around the SQL+mirror block puts
|
|
1389
|
+
// those two operations back into one cross-process critical section.
|
|
1390
|
+
const lockPath = `${filePath}.lock`;
|
|
1391
|
+
return withFileLock(lockPath, () => {
|
|
1392
|
+
const out = store[smallRoute.fn]((data) => {
|
|
1393
|
+
const next = mutateFn(data);
|
|
1394
|
+
if (onWrote) onWrote();
|
|
1395
|
+
return next;
|
|
1396
|
+
});
|
|
1397
|
+
const result = out && Object.prototype.hasOwnProperty.call(out, 'result') ? out.result : undefined;
|
|
1398
|
+
try { store[smallRoute.mirror](filePath); } catch { /* mirror best-effort */ }
|
|
1399
|
+
if (!fs.existsSync(filePath)) {
|
|
1400
|
+
try {
|
|
1401
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1402
|
+
const fallback = smallRoute.defaultShape === 'array'
|
|
1403
|
+
? (Array.isArray(result) ? result : [])
|
|
1404
|
+
: (result && typeof result === 'object' && !Array.isArray(result) ? result : {});
|
|
1405
|
+
safeWrite(filePath, fallback);
|
|
1406
|
+
} catch { /* best-effort */ }
|
|
1407
|
+
}
|
|
1408
|
+
return { routed: result };
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1333
1412
|
let result;
|
|
1334
1413
|
if (baseName === 'work-items.json') {
|
|
1335
1414
|
result = mutateWorkItems(filePath, (arr) => {
|
|
@@ -1451,6 +1530,45 @@ function mutateCooldowns(mutator) {
|
|
|
1451
1530
|
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1452
1531
|
}
|
|
1453
1532
|
|
|
1533
|
+
// Phase 9.3 (W-mp48wxqw / SQL-canonicalization prep): lock-safe RMW wrappers
|
|
1534
|
+
// for the small JSON state files that were still going through raw safeWrite.
|
|
1535
|
+
// Pattern mirrors mutateControl / mutateEngineState / mutateCooldowns — so a
|
|
1536
|
+
// future SQL migration is a mechanical _SMALL_STATE_MUTATE_ROUTES addition.
|
|
1537
|
+
|
|
1538
|
+
// KB classification checkpoint counter (consolidation.js post-classify).
|
|
1539
|
+
function mutateKbCheckpoint(mutator) {
|
|
1540
|
+
return mutateJsonFileLocked(KB_CHECKPOINT_PATH, (data) => {
|
|
1541
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1542
|
+
return mutator(data) || data;
|
|
1543
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// KB last-sweep summary (kb-sweep.js post-sweep, dashboard reader).
|
|
1547
|
+
function mutateKbSwept(mutator) {
|
|
1548
|
+
return mutateJsonFileLocked(KB_SWEPT_PATH, (data) => {
|
|
1549
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1550
|
+
return mutator(data) || data;
|
|
1551
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// KB sweep state machine. Written by BOTH the dashboard process AND the
|
|
1555
|
+
// detached kb-sweep-runner child — wrapping under mutateJsonFileLocked makes
|
|
1556
|
+
// cross-process writes file-locked (previously a real race via raw safeWrite).
|
|
1557
|
+
function mutateKbSweepState(mutator) {
|
|
1558
|
+
return mutateJsonFileLocked(KB_SWEEP_STATE_PATH, (data) => {
|
|
1559
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
1560
|
+
return mutator(data) || data;
|
|
1561
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Test results history (capped at TEST_RESULTS_CAP by cleanup.js).
|
|
1565
|
+
function mutateTestResults(mutator) {
|
|
1566
|
+
return mutateJsonFileLocked(TEST_RESULTS_PATH, (data) => {
|
|
1567
|
+
if (!Array.isArray(data)) data = [];
|
|
1568
|
+
return mutator(data) || data;
|
|
1569
|
+
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1454
1572
|
let _uidCounter = 0;
|
|
1455
1573
|
|
|
1456
1574
|
/**
|
|
@@ -3094,32 +3212,20 @@ const WATCH_ACTION_TYPE = {
|
|
|
3094
3212
|
|
|
3095
3213
|
/**
|
|
3096
3214
|
* Phase 7 — small state file mutators. Each routes through the
|
|
3097
|
-
* small-state-store
|
|
3098
|
-
* event on real writes.
|
|
3099
|
-
*
|
|
3215
|
+
* small-state-store and mirrors back to its JSON file, then emits a topic
|
|
3216
|
+
* event on real writes. SQL-canonical (Phase 9.4): SQLite failures
|
|
3217
|
+
* propagate; the CLI shim in bin/minions.js guarantees `node:sqlite` is
|
|
3218
|
+
* loadable on every supported Node version.
|
|
3100
3219
|
*/
|
|
3101
|
-
function _smallStateMutator({ filePath, applyMutation, mirror, topic
|
|
3220
|
+
function _smallStateMutator({ filePath, applyMutation, mirror, topic }) {
|
|
3102
3221
|
return (mutator) => {
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
if (
|
|
3107
|
-
|
|
3108
|
-
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3109
|
-
}
|
|
3110
|
-
return result;
|
|
3111
|
-
} catch (e) {
|
|
3112
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) throw e;
|
|
3222
|
+
const store = require('./small-state-store');
|
|
3223
|
+
const { wrote, result } = store[applyMutation]((obj) => mutator(obj) || obj);
|
|
3224
|
+
if (wrote) {
|
|
3225
|
+
try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
|
|
3226
|
+
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3113
3227
|
}
|
|
3114
|
-
return
|
|
3115
|
-
if (data == null) data = defaultValue();
|
|
3116
|
-
return mutator(data) || data;
|
|
3117
|
-
}, {
|
|
3118
|
-
defaultValue: defaultValue(),
|
|
3119
|
-
onWrote: () => {
|
|
3120
|
-
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3121
|
-
},
|
|
3122
|
-
});
|
|
3228
|
+
return result;
|
|
3123
3229
|
};
|
|
3124
3230
|
}
|
|
3125
3231
|
|
|
@@ -3175,31 +3281,18 @@ function _qaDualWriteEnabled() {
|
|
|
3175
3281
|
function _qaMutator({ filePath, applyMutation, mirror, topic }) {
|
|
3176
3282
|
return (mutator) => {
|
|
3177
3283
|
return withFileLock(filePath + '.lock', () => {
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
if (
|
|
3185
|
-
if (
|
|
3186
|
-
try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
|
|
3187
|
-
}
|
|
3188
|
-
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3284
|
+
const store = require('./small-state-store');
|
|
3285
|
+
const { wrote, result } = store[applyMutation]((arr) => {
|
|
3286
|
+
if (!Array.isArray(arr)) arr = [];
|
|
3287
|
+
return mutator(arr) || arr;
|
|
3288
|
+
});
|
|
3289
|
+
if (wrote) {
|
|
3290
|
+
if (_qaDualWriteEnabled()) {
|
|
3291
|
+
try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
|
|
3189
3292
|
}
|
|
3190
|
-
|
|
3191
|
-
} catch (e) {
|
|
3192
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) throw e;
|
|
3193
|
-
return mutateJsonFileLocked(filePath, (data) => {
|
|
3194
|
-
if (!Array.isArray(data)) data = [];
|
|
3195
|
-
return mutator(data) || data;
|
|
3196
|
-
}, {
|
|
3197
|
-
defaultValue: [],
|
|
3198
|
-
onWrote: () => {
|
|
3199
|
-
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3200
|
-
},
|
|
3201
|
-
});
|
|
3293
|
+
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
3202
3294
|
}
|
|
3295
|
+
return result;
|
|
3203
3296
|
}, { timeoutMs: 5000, retries: 3 });
|
|
3204
3297
|
};
|
|
3205
3298
|
}
|
|
@@ -3222,36 +3315,20 @@ const mutateQaSessions = _qaMutator({
|
|
|
3222
3315
|
* Route a watches mutation through the SQL store. Same shape as
|
|
3223
3316
|
* mutateWorkItems / mutatePullRequests: mutator receives the watches
|
|
3224
3317
|
* array, mutates in place or returns a replacement, and the store
|
|
3225
|
-
* diffs by id.
|
|
3226
|
-
* SQLite failure.
|
|
3318
|
+
* diffs by id. SQL-canonical (Phase 9.4); SQLite failures propagate.
|
|
3227
3319
|
*/
|
|
3228
3320
|
function mutateWatches(mutator) {
|
|
3229
3321
|
const watchesPath = path.join(MINIONS_DIR, 'engine', 'watches.json');
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
return mutator(arr) || arr;
|
|
3235
|
-
});
|
|
3236
|
-
if (wrote) {
|
|
3237
|
-
try { store._mirrorJsonFromSql(watchesPath); } catch { /* mirror best-effort */ }
|
|
3238
|
-
try { require('./db-events').emitStateEvent('watches'); } catch { /* optional */ }
|
|
3239
|
-
}
|
|
3240
|
-
return result;
|
|
3241
|
-
} catch (e) {
|
|
3242
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
3243
|
-
throw e;
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
return mutateJsonFileLocked(watchesPath, (data) => {
|
|
3247
|
-
if (!Array.isArray(data)) data = [];
|
|
3248
|
-
return mutator(data) || data;
|
|
3249
|
-
}, {
|
|
3250
|
-
defaultValue: [],
|
|
3251
|
-
onWrote: () => {
|
|
3252
|
-
try { require('./db-events').emitStateEvent('watches'); } catch { /* optional */ }
|
|
3253
|
-
},
|
|
3322
|
+
const store = require('./watches-store');
|
|
3323
|
+
const { wrote, result } = store.applyWatchesMutation((arr) => {
|
|
3324
|
+
if (!Array.isArray(arr)) arr = [];
|
|
3325
|
+
return mutator(arr) || arr;
|
|
3254
3326
|
});
|
|
3327
|
+
if (wrote) {
|
|
3328
|
+
try { store._mirrorJsonFromSql(watchesPath); } catch { /* mirror best-effort */ }
|
|
3329
|
+
try { require('./db-events').emitStateEvent('watches'); } catch { /* optional */ }
|
|
3330
|
+
}
|
|
3331
|
+
return result;
|
|
3255
3332
|
}
|
|
3256
3333
|
|
|
3257
3334
|
/**
|
|
@@ -3259,37 +3336,20 @@ function mutateWatches(mutator) {
|
|
|
3259
3336
|
* mirror. Same shape as mutateWorkItems / mutatePullRequests: mutator
|
|
3260
3337
|
* receives the full legacy-shape metrics object, mutates in place or
|
|
3261
3338
|
* returns a replacement, and the store diffs by (kind, key) row.
|
|
3262
|
-
*
|
|
3263
|
-
* Falls back to the legacy mutateJsonFileLocked path on SQLite failure
|
|
3264
|
-
* so a node:sqlite-broken install keeps recording metrics.
|
|
3339
|
+
* SQL-canonical (Phase 9.4); SQLite failures propagate.
|
|
3265
3340
|
*/
|
|
3266
3341
|
function mutateMetrics(mutator) {
|
|
3267
3342
|
const metricsPath = path.join(MINIONS_DIR, 'engine', 'metrics.json');
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
return mutator(m) || m;
|
|
3273
|
-
});
|
|
3274
|
-
if (wrote) {
|
|
3275
|
-
try { store._mirrorJsonFromSql(metricsPath); } catch { /* mirror best-effort */ }
|
|
3276
|
-
try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
|
|
3277
|
-
}
|
|
3278
|
-
return result;
|
|
3279
|
-
} catch (e) {
|
|
3280
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
3281
|
-
throw e;
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
return mutateJsonFileLocked(metricsPath, (metrics) => {
|
|
3285
|
-
if (!metrics || typeof metrics !== 'object') metrics = {};
|
|
3286
|
-
return mutator(metrics) || metrics;
|
|
3287
|
-
}, {
|
|
3288
|
-
defaultValue: {},
|
|
3289
|
-
onWrote: () => {
|
|
3290
|
-
try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
|
|
3291
|
-
},
|
|
3343
|
+
const store = require('./metrics-store');
|
|
3344
|
+
const { wrote, result } = store.applyMetricsMutation((m) => {
|
|
3345
|
+
if (!m || typeof m !== 'object') m = {};
|
|
3346
|
+
return mutator(m) || m;
|
|
3292
3347
|
});
|
|
3348
|
+
if (wrote) {
|
|
3349
|
+
try { store._mirrorJsonFromSql(metricsPath); } catch { /* mirror best-effort */ }
|
|
3350
|
+
try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
|
|
3351
|
+
}
|
|
3352
|
+
return result;
|
|
3293
3353
|
}
|
|
3294
3354
|
|
|
3295
3355
|
/** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
|
|
@@ -5055,23 +5115,11 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
5055
5115
|
links[effectivePrId] = [...mergedCurrent];
|
|
5056
5116
|
return links;
|
|
5057
5117
|
};
|
|
5058
|
-
// Phase 9.
|
|
5059
|
-
// is a
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
try {
|
|
5063
|
-
const store = require('./small-state-store');
|
|
5064
|
-
store.applyPrLinksMutation(mutator);
|
|
5065
|
-
try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
|
|
5066
|
-
routedViaSql = true;
|
|
5067
|
-
} catch (e) {
|
|
5068
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5069
|
-
throw e;
|
|
5070
|
-
}
|
|
5071
|
-
}
|
|
5072
|
-
if (!routedViaSql) {
|
|
5073
|
-
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
5074
|
-
}
|
|
5118
|
+
// Phase 9.4: pr-links is SQL-only via small-state-store; the JSON file
|
|
5119
|
+
// is a write-only mirror artifact for legacy direct-disk readers.
|
|
5120
|
+
const store = require('./small-state-store');
|
|
5121
|
+
store.applyPrLinksMutation(mutator);
|
|
5122
|
+
try { store._mirrorPrLinksJson(); } catch { /* mirror best-effort */ }
|
|
5075
5123
|
|
|
5076
5124
|
if (!project) return;
|
|
5077
5125
|
const prPath = projectPrPath(project);
|
|
@@ -5463,42 +5511,27 @@ function listProcessReachable(rootPids, allProcesses = null) {
|
|
|
5463
5511
|
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
5464
5512
|
*/
|
|
5465
5513
|
function mutateWorkItems(filePath, mutator) {
|
|
5466
|
-
// Phase
|
|
5467
|
-
//
|
|
5468
|
-
//
|
|
5469
|
-
//
|
|
5470
|
-
//
|
|
5471
|
-
// derived from the file path's last two segments. Ad-hoc file paths
|
|
5472
|
-
// outside the MINIONS_DIR layout (e.g. tests using createTmpDir()) can't
|
|
5473
|
-
// be mapped to a stable scope, so we short-circuit to the legacy
|
|
5474
|
-
// JSON path for those. Production callers always use
|
|
5475
|
-
// shared.projectWorkItemsPath(p) / MINIONS_DIR/work-items.json.
|
|
5476
|
-
//
|
|
5477
|
-
// SQLite failures fall through to the legacy JSON path below — keeps a
|
|
5478
|
-
// node:sqlite-broken install fully functional.
|
|
5514
|
+
// Phase 9.4 SQL-only. Route through work-items-store; SQL is the canonical
|
|
5515
|
+
// (and only) store. The legacy JSON path below remains ONLY for ad-hoc
|
|
5516
|
+
// file paths outside the MINIONS_DIR layout (e.g. tests using
|
|
5517
|
+
// createTmpDir()) that can't be mapped to a stable scope. Production
|
|
5518
|
+
// callers always use shared.projectWorkItemsPath(p) / MINIONS_DIR/work-items.json.
|
|
5479
5519
|
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
5480
5520
|
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
5481
5521
|
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/work-items.json';
|
|
5482
5522
|
if (insideMinionsDir) {
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
5493
|
-
}
|
|
5494
|
-
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
5495
|
-
return result;
|
|
5496
|
-
} catch (e) {
|
|
5497
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5498
|
-
throw e;
|
|
5499
|
-
}
|
|
5500
|
-
// Fall through to the legacy JSON path on SQLite errors only.
|
|
5523
|
+
const store = require('./work-items-store');
|
|
5524
|
+
const scope = store.scopeForFilePath(filePath);
|
|
5525
|
+
const { wrote, result } = store.applyWorkItemsMutation(scope, (items) => {
|
|
5526
|
+
if (!Array.isArray(items)) items = [];
|
|
5527
|
+
return mutator(items) || items;
|
|
5528
|
+
});
|
|
5529
|
+
if (wrote) {
|
|
5530
|
+
try { store._mirrorJsonFromSql(scope, filePath); } catch { /* mirror best-effort */ }
|
|
5531
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
5501
5532
|
}
|
|
5533
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
5534
|
+
return result;
|
|
5502
5535
|
}
|
|
5503
5536
|
|
|
5504
5537
|
const result = mutateJsonFileLocked(filePath, (data) => {
|
|
@@ -5536,31 +5569,24 @@ function reopenWorkItem(wi) {
|
|
|
5536
5569
|
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
5537
5570
|
*/
|
|
5538
5571
|
function mutatePullRequests(filePath, mutator) {
|
|
5539
|
-
// Phase
|
|
5540
|
-
//
|
|
5541
|
-
//
|
|
5572
|
+
// Phase 9.4 SQL-only. Route through pull-requests-store when filePath sits
|
|
5573
|
+
// under MINIONS_DIR. Ad-hoc tmp paths (legacy tests using createTmpDir)
|
|
5574
|
+
// still fall through to the JSON path.
|
|
5542
5575
|
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
5543
5576
|
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
5544
5577
|
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/pull-requests.json';
|
|
5545
5578
|
if (insideMinionsDir) {
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
5556
|
-
}
|
|
5557
|
-
return result;
|
|
5558
|
-
} catch (e) {
|
|
5559
|
-
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
5560
|
-
throw e;
|
|
5561
|
-
}
|
|
5562
|
-
// Fall through to legacy JSON path on SQLite errors only.
|
|
5579
|
+
const store = require('./pull-requests-store');
|
|
5580
|
+
const scope = store.scopeForFilePath(filePath);
|
|
5581
|
+
const { wrote, result } = store.applyPullRequestsMutation(scope, (prs) => {
|
|
5582
|
+
if (!Array.isArray(prs)) prs = [];
|
|
5583
|
+
return mutator(prs) || prs;
|
|
5584
|
+
});
|
|
5585
|
+
if (wrote) {
|
|
5586
|
+
try { store._mirrorJsonFromSql(scope, filePath); } catch { /* mirror best-effort */ }
|
|
5587
|
+
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
5563
5588
|
}
|
|
5589
|
+
return result;
|
|
5564
5590
|
}
|
|
5565
5591
|
|
|
5566
5592
|
return mutateJsonFileLocked(filePath, (data) => {
|
|
@@ -5975,6 +6001,10 @@ module.exports = {
|
|
|
5975
6001
|
mutateEngineState, // W-mp60tw0u000j3931
|
|
5976
6002
|
readEngineState, // W-mp60tw0u000j3931
|
|
5977
6003
|
mutateCooldowns,
|
|
6004
|
+
mutateKbCheckpoint, // Phase 9.3
|
|
6005
|
+
mutateKbSwept, // Phase 9.3
|
|
6006
|
+
mutateKbSweepState, // Phase 9.3
|
|
6007
|
+
mutateTestResults, // Phase 9.3
|
|
5978
6008
|
mutateWorkItems,
|
|
5979
6009
|
reopenWorkItem,
|
|
5980
6010
|
mutatePullRequests,
|