@yemi33/minions 0.1.2066 → 0.1.2068
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/README.md +3 -3
- package/docs/runtime-adapters.md +20 -18
- package/engine/db/index.js +55 -8
- package/engine/db/migrations/002-dispatches.js +113 -0
- package/engine/db/migrations/003-work-items.js +128 -0
- package/engine/dispatch-store.js +316 -0
- package/engine/dispatch.js +50 -3
- package/engine/llm.js +4 -3
- package/engine/queries.js +47 -19
- package/engine/restart-health.js +8 -2
- package/engine/runtimes/codex.js +862 -0
- package/engine/runtimes/index.js +1 -0
- package/engine/shared.js +37 -8
- package/engine/work-items-store.js +351 -0
- package/package.json +1 -1
package/engine/runtimes/index.js
CHANGED
package/engine/shared.js
CHANGED
|
@@ -4572,24 +4572,53 @@ function listProcessReachable(rootPids, allProcesses = null) {
|
|
|
4572
4572
|
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
4573
4573
|
*/
|
|
4574
4574
|
function mutateWorkItems(filePath, mutator) {
|
|
4575
|
+
// Phase 2 SQL path. Route through work-items-store so SQL is the source
|
|
4576
|
+
// of truth; mirror back to the JSON file for legacy direct-readers.
|
|
4577
|
+
//
|
|
4578
|
+
// The SQL store identifies records by `scope` (central or project name)
|
|
4579
|
+
// derived from the file path's last two segments. Ad-hoc file paths
|
|
4580
|
+
// outside the MINIONS_DIR layout (e.g. tests using createTmpDir()) can't
|
|
4581
|
+
// be mapped to a stable scope, so we short-circuit to the legacy
|
|
4582
|
+
// JSON path for those. Production callers always use
|
|
4583
|
+
// shared.projectWorkItemsPath(p) / MINIONS_DIR/work-items.json.
|
|
4584
|
+
//
|
|
4585
|
+
// SQLite failures fall through to the legacy JSON path below — keeps a
|
|
4586
|
+
// node:sqlite-broken install fully functional.
|
|
4587
|
+
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
4588
|
+
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
4589
|
+
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/work-items.json';
|
|
4590
|
+
if (insideMinionsDir) {
|
|
4591
|
+
try {
|
|
4592
|
+
const store = require('./work-items-store');
|
|
4593
|
+
const scope = store.scopeForFilePath(filePath);
|
|
4594
|
+
const { wrote, result } = store.applyWorkItemsMutation(scope, (items) => {
|
|
4595
|
+
if (!Array.isArray(items)) items = [];
|
|
4596
|
+
return mutator(items) || items;
|
|
4597
|
+
});
|
|
4598
|
+
if (wrote) {
|
|
4599
|
+
try { store._mirrorJsonFromSql(scope, filePath); } catch { /* mirror is best-effort */ }
|
|
4600
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
4601
|
+
}
|
|
4602
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
4603
|
+
return result;
|
|
4604
|
+
} catch (e) {
|
|
4605
|
+
if (!e || !/SQLite unavailable|no such table|node:sqlite/.test(String(e.message))) {
|
|
4606
|
+
throw e;
|
|
4607
|
+
}
|
|
4608
|
+
// Fall through to the legacy JSON path on SQLite errors only.
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4575
4612
|
const result = mutateJsonFileLocked(filePath, (data) => {
|
|
4576
4613
|
if (!Array.isArray(data)) data = [];
|
|
4577
4614
|
return mutator(data) || data;
|
|
4578
4615
|
}, {
|
|
4579
4616
|
defaultValue: [],
|
|
4580
4617
|
skipWriteIfUnchanged: true,
|
|
4581
|
-
// Emit only when an actual write happened. The dedup path through
|
|
4582
|
-
// createWorkItemWithDedup returns the array unchanged → skipWriteIfUnchanged
|
|
4583
|
-
// suppresses the write, and we want to suppress the event too (otherwise
|
|
4584
|
-
// duplicate POSTs bump the cache version for no observable change).
|
|
4585
4618
|
onWrote: () => {
|
|
4586
4619
|
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
4587
4620
|
},
|
|
4588
4621
|
});
|
|
4589
|
-
// Invalidate the read cache so the next getWorkItems() sees fresh data
|
|
4590
|
-
// (W-mpodww9h000b460a). Mirrors dispatch.js's invalidateDispatchCache
|
|
4591
|
-
// call after mutateDispatch. Lazy-required to avoid the queries→shared
|
|
4592
|
-
// require cycle.
|
|
4593
4622
|
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
4594
4623
|
return result;
|
|
4595
4624
|
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// engine/work-items-store.js — SQL-backed implementation of the
|
|
2
|
+
// per-file work-items array. Same external contract as the legacy
|
|
3
|
+
// JSON-file reader:
|
|
4
|
+
//
|
|
5
|
+
// readWorkItemsForScope(scope) -> [wi, wi, ...]
|
|
6
|
+
// applyWorkItemsMutation(scope, fn) -> runs fn against the array,
|
|
7
|
+
// diffs by id, applies in one
|
|
8
|
+
// transaction, returns
|
|
9
|
+
// { wrote, result }.
|
|
10
|
+
//
|
|
11
|
+
// `scope` is 'central' for the top-level work-items.json or a project
|
|
12
|
+
// name for projects/<name>/work-items.json. Mirrors the file convention
|
|
13
|
+
// established by migration 003.
|
|
14
|
+
//
|
|
15
|
+
// All callers go through shared.mutateWorkItems and
|
|
16
|
+
// queries.getWorkItems — those are thin wrappers over the helpers here.
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
function _toMs(v) {
|
|
22
|
+
if (v == null) return null;
|
|
23
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
24
|
+
const parsed = Date.parse(v);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Map an absolute filePath → scope key. The store doesn't know about the
|
|
29
|
+
// caller's file layout; we derive scope from the path's last two segments
|
|
30
|
+
// because every work-items file in this codebase sits at either
|
|
31
|
+
// <root>/work-items.json (central) or <root>/projects/<name>/work-items.json.
|
|
32
|
+
function scopeForFilePath(filePath) {
|
|
33
|
+
const norm = String(filePath).replace(/\\/g, '/');
|
|
34
|
+
const m = norm.match(/projects\/([^/]+)\/work-items\.json$/);
|
|
35
|
+
if (m) return m[1];
|
|
36
|
+
// Anything else — including the central work-items.json under MINIONS_DIR
|
|
37
|
+
// and any one-off test fixture file — lands in 'central'. Tests that
|
|
38
|
+
// need to keep multiple fixture files distinct should put them under a
|
|
39
|
+
// synthetic projects/<name>/ subtree.
|
|
40
|
+
return 'central';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// JSON fallback path resolution. Used both for the empty-SQL fallback
|
|
44
|
+
// (test seeding pattern from Phase 1) and for the post-write JSON mirror.
|
|
45
|
+
function _filePathForScope(scope) {
|
|
46
|
+
const shared = require('./shared');
|
|
47
|
+
if (scope === 'central') {
|
|
48
|
+
return path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
49
|
+
}
|
|
50
|
+
return path.join(shared.MINIONS_DIR, 'projects', scope, 'work-items.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _parseRow(row) {
|
|
54
|
+
if (!row || !row.data) return null;
|
|
55
|
+
try { return JSON.parse(row.data); }
|
|
56
|
+
catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _readJsonArrayFallback(scope) {
|
|
60
|
+
const fp = _filePathForScope(scope);
|
|
61
|
+
let raw;
|
|
62
|
+
try { raw = fs.readFileSync(fp, 'utf8'); }
|
|
63
|
+
catch { return []; }
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
67
|
+
} catch (e) {
|
|
68
|
+
try {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.warn(`[work-items-store] corrupt JSON in ${fp}: ${e.message}`);
|
|
71
|
+
} catch { /* console may be wrapped in tests */ }
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readWorkItemsForScope(scope) {
|
|
77
|
+
const { getDb } = require('./db');
|
|
78
|
+
let db;
|
|
79
|
+
try { db = getDb(); }
|
|
80
|
+
catch { return _readJsonArrayFallback(scope); }
|
|
81
|
+
|
|
82
|
+
_resyncScopeIfJsonDiverged(db, scope);
|
|
83
|
+
|
|
84
|
+
const rows = db.prepare(`
|
|
85
|
+
SELECT data FROM work_items
|
|
86
|
+
WHERE scope = ?
|
|
87
|
+
ORDER BY rowid
|
|
88
|
+
`).all(scope);
|
|
89
|
+
|
|
90
|
+
if (rows.length === 0) {
|
|
91
|
+
// Same fallback shape as dispatch-store: SQL empty AND JSON has
|
|
92
|
+
// content means a test seeded via fs.writeFileSync (legacy helper)
|
|
93
|
+
// or a fresh install pre-migration. Returning the JSON keeps those
|
|
94
|
+
// call sites working without touching every helper.
|
|
95
|
+
const fallback = _readJsonArrayFallback(scope);
|
|
96
|
+
if (fallback.length > 0) return fallback;
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const wi = _parseRow(row);
|
|
103
|
+
if (wi) out.push(wi);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If the JSON mirror's mtime advanced since our last mirror write, the
|
|
109
|
+
// file was rewritten outside the store (test cleanup + safeWrite, manual
|
|
110
|
+
// operator edit, external tool). Treat the JSON as canonical and rebuild
|
|
111
|
+
// SQL for that scope from it. Called from BOTH read and write paths so
|
|
112
|
+
// readers don't see stale SQL state and writers don't compute diffs
|
|
113
|
+
// against a SQL snapshot that no longer matches what callers expect.
|
|
114
|
+
//
|
|
115
|
+
// Safe to call inside `withTransaction` — DELETE + INSERT against a
|
|
116
|
+
// scope is bounded by the JSON's row count.
|
|
117
|
+
function _resyncScopeIfJsonDiverged(db, scope) {
|
|
118
|
+
const jsonPath = _filePathForScope(scope);
|
|
119
|
+
const currentMtime = _statMtimeMs(jsonPath);
|
|
120
|
+
const lastMirrorMtime = _lastMirrorMtimeByScope.get(scope);
|
|
121
|
+
if (currentMtime == null) return; // JSON absent → nothing to resync
|
|
122
|
+
if (lastMirrorMtime != null && Math.abs(currentMtime - lastMirrorMtime) <= 1) return; // in sync
|
|
123
|
+
// No mirror record but JSON exists: first-time hydrate iff SQL is empty
|
|
124
|
+
// for this scope (avoid trampling a freshly-backfilled migration state
|
|
125
|
+
// on the very first read).
|
|
126
|
+
if (lastMirrorMtime == null) {
|
|
127
|
+
const sqlHas = db.prepare('SELECT 1 FROM work_items WHERE scope = ? LIMIT 1').get(scope);
|
|
128
|
+
if (sqlHas) {
|
|
129
|
+
// Record current mtime as the baseline so the next divergence is
|
|
130
|
+
// detected, but don't rebuild — migration already populated SQL.
|
|
131
|
+
_lastMirrorMtimeByScope.set(scope, currentMtime);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
_hydrateScopeFromJson(db, scope);
|
|
136
|
+
_lastMirrorMtimeByScope.set(scope, currentMtime);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Read all rows across all scopes — used by queries.getWorkItems which
|
|
140
|
+
// needs to surface central + every project's items in a single shot,
|
|
141
|
+
// tagged with their source scope.
|
|
142
|
+
function readAllWorkItems() {
|
|
143
|
+
const { getDb } = require('./db');
|
|
144
|
+
let db;
|
|
145
|
+
try { db = getDb(); }
|
|
146
|
+
catch { return null; } // signal: SQL unavailable, caller must fall back
|
|
147
|
+
|
|
148
|
+
// Pick up any external JSON edits for every scope SQL knows about.
|
|
149
|
+
// Also resync 'central' explicitly so first-time reads on a JSON-only
|
|
150
|
+
// install hydrate the central scope. Per-project scopes get checked
|
|
151
|
+
// lazily on next read or mutation of that scope.
|
|
152
|
+
try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
|
|
153
|
+
const knownScopes = db.prepare('SELECT DISTINCT scope FROM work_items').all().map(r => r.scope);
|
|
154
|
+
for (const s of knownScopes) {
|
|
155
|
+
if (s === 'central') continue;
|
|
156
|
+
try { _resyncScopeIfJsonDiverged(db, s); } catch {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const rows = db.prepare('SELECT data, scope FROM work_items ORDER BY rowid').all();
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
const wi = _parseRow(row);
|
|
163
|
+
if (!wi) continue;
|
|
164
|
+
// Preserve the legacy `_source` convention from queries.getWorkItems.
|
|
165
|
+
wi._source = row.scope;
|
|
166
|
+
out.push(wi);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _indexById(arr) {
|
|
172
|
+
const out = new Map();
|
|
173
|
+
for (const wi of arr) {
|
|
174
|
+
if (!wi || !wi.id) continue;
|
|
175
|
+
out.set(String(wi.id), wi);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _computeWorkItemsDiff(scope, before, after) {
|
|
181
|
+
const beforeMap = _indexById(before);
|
|
182
|
+
const afterMap = _indexById(after);
|
|
183
|
+
const toUpsert = [];
|
|
184
|
+
const toDelete = [];
|
|
185
|
+
for (const [id, wi] of afterMap) {
|
|
186
|
+
const prev = beforeMap.get(id);
|
|
187
|
+
if (!prev) { toUpsert.push(wi); continue; }
|
|
188
|
+
if (JSON.stringify(prev) !== JSON.stringify(wi)) toUpsert.push(wi);
|
|
189
|
+
}
|
|
190
|
+
for (const [id] of beforeMap) {
|
|
191
|
+
if (!afterMap.has(id)) toDelete.push(id);
|
|
192
|
+
}
|
|
193
|
+
return { scope, toUpsert, toDelete };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _applyWorkItemsDiff(db, diff) {
|
|
197
|
+
if (diff.toUpsert.length === 0 && diff.toDelete.length === 0) return false;
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const upsertStmt = db.prepare(`
|
|
200
|
+
INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at)
|
|
201
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
202
|
+
ON CONFLICT(scope, id) DO UPDATE SET
|
|
203
|
+
status = excluded.status,
|
|
204
|
+
type = excluded.type,
|
|
205
|
+
agent = excluded.agent,
|
|
206
|
+
parent_id = excluded.parent_id,
|
|
207
|
+
created_at = excluded.created_at,
|
|
208
|
+
completed_at = excluded.completed_at,
|
|
209
|
+
data = excluded.data,
|
|
210
|
+
updated_at = excluded.updated_at
|
|
211
|
+
`);
|
|
212
|
+
const deleteStmt = db.prepare('DELETE FROM work_items WHERE scope = ? AND id = ?');
|
|
213
|
+
|
|
214
|
+
for (const wi of diff.toUpsert) {
|
|
215
|
+
upsertStmt.run(
|
|
216
|
+
String(wi.id),
|
|
217
|
+
diff.scope,
|
|
218
|
+
String(wi.status || 'pending'),
|
|
219
|
+
wi.type || null,
|
|
220
|
+
wi.dispatched_to || wi.agent || null,
|
|
221
|
+
wi.parent_id || null,
|
|
222
|
+
_toMs(wi.created_at),
|
|
223
|
+
_toMs(wi.completed_at || wi.completedAt),
|
|
224
|
+
JSON.stringify(wi),
|
|
225
|
+
now,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
for (const id of diff.toDelete) {
|
|
229
|
+
deleteStmt.run(diff.scope, id);
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Apply a mutation to the work-items array for a given scope. Wrapped in
|
|
235
|
+
// a single transaction so the diff and apply can't race against a
|
|
236
|
+
// concurrent reader/writer.
|
|
237
|
+
//
|
|
238
|
+
// Returns { wrote, result }:
|
|
239
|
+
// wrote — true iff at least one INSERT/UPDATE/DELETE landed
|
|
240
|
+
// result — the post-mutation array (legacy return shape of
|
|
241
|
+
// mutateJsonFileLocked)
|
|
242
|
+
// In-process record of the JSON mirror mtime we last wrote, keyed by
|
|
243
|
+
// scope. Used to detect external JSON edits between mutations — both
|
|
244
|
+
// (a) tests that clean up + re-seed via fs.writeFileSync, and
|
|
245
|
+
// (b) operators that hand-edit work-items.json in production. When the
|
|
246
|
+
// current JSON mtime differs from what we recorded, the JSON has been
|
|
247
|
+
// rewritten outside the store; we treat the JSON as canonical and
|
|
248
|
+
// re-hydrate SQL from it before computing the diff.
|
|
249
|
+
const _lastMirrorMtimeByScope = new Map();
|
|
250
|
+
|
|
251
|
+
function _statMtimeMs(filePath) {
|
|
252
|
+
try { return fs.statSync(filePath).mtimeMs; }
|
|
253
|
+
catch { return null; }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function _hydrateScopeFromJson(db, scope) {
|
|
257
|
+
const jsonItems = _readJsonArrayFallback(scope);
|
|
258
|
+
// DELETE before re-insert: callers that wrote a smaller JSON file
|
|
259
|
+
// (test cleanup() removing items) must end up with a smaller SQL state.
|
|
260
|
+
db.prepare('DELETE FROM work_items WHERE scope = ?').run(scope);
|
|
261
|
+
if (jsonItems.length === 0) return;
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
const ins = db.prepare(`
|
|
264
|
+
INSERT INTO work_items (id, scope, status, type, agent, parent_id, created_at, completed_at, data, updated_at)
|
|
265
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
266
|
+
ON CONFLICT(scope, id) DO NOTHING
|
|
267
|
+
`);
|
|
268
|
+
for (const wi of jsonItems) {
|
|
269
|
+
if (!wi || !wi.id) continue;
|
|
270
|
+
ins.run(
|
|
271
|
+
String(wi.id),
|
|
272
|
+
scope,
|
|
273
|
+
String(wi.status || 'pending'),
|
|
274
|
+
wi.type || null,
|
|
275
|
+
wi.dispatched_to || wi.agent || null,
|
|
276
|
+
wi.parent_id || null,
|
|
277
|
+
_toMs(wi.created_at),
|
|
278
|
+
_toMs(wi.completed_at || wi.completedAt),
|
|
279
|
+
JSON.stringify(wi),
|
|
280
|
+
now,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function applyWorkItemsMutation(scope, mutator) {
|
|
286
|
+
const { getDb, withTransaction } = require('./db');
|
|
287
|
+
let db;
|
|
288
|
+
try { db = getDb(); }
|
|
289
|
+
catch (e) {
|
|
290
|
+
throw new Error(`engine/work-items-store: SQLite unavailable (${e.message}); cannot mutate work_items`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return withTransaction(db, () => {
|
|
294
|
+
// Re-hydrate SQL from JSON if the file was touched outside the
|
|
295
|
+
// store (external edit, test re-seed, first-time hydrate after a
|
|
296
|
+
// fresh install). Without this, the diff would compare against a
|
|
297
|
+
// stale SQL snapshot and the mirror write would overwrite the JSON
|
|
298
|
+
// with that stale state.
|
|
299
|
+
_resyncScopeIfJsonDiverged(db, scope);
|
|
300
|
+
|
|
301
|
+
const before = readWorkItemsForScope(scope);
|
|
302
|
+
// Strip the _source decoration before handing to the mutator — it's
|
|
303
|
+
// added by readAllWorkItems only and would round-trip as garbage into
|
|
304
|
+
// the data column otherwise.
|
|
305
|
+
for (const wi of before) { if (wi && wi._source) delete wi._source; }
|
|
306
|
+
const beforeSnapshot = JSON.parse(JSON.stringify(before));
|
|
307
|
+
const next = mutator(before);
|
|
308
|
+
const after = (next === undefined || next === null)
|
|
309
|
+
? before
|
|
310
|
+
: (Array.isArray(next) ? next : before);
|
|
311
|
+
const diff = _computeWorkItemsDiff(scope, beforeSnapshot, after);
|
|
312
|
+
const wrote = _applyWorkItemsDiff(db, diff);
|
|
313
|
+
return { wrote, result: after };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Mirror SQL → JSON file. Phase 2 keeps the file as a read-only derivative
|
|
318
|
+
// so legacy direct-readers keep working; SQL is the source of truth.
|
|
319
|
+
function _mirrorJsonFromSql(scope, filePath) {
|
|
320
|
+
try {
|
|
321
|
+
const shared = require('./shared');
|
|
322
|
+
const items = readWorkItemsForScope(scope);
|
|
323
|
+
// Strip _source if present — only added by readAllWorkItems, but
|
|
324
|
+
// belt-and-suspenders since the mirror is observable.
|
|
325
|
+
for (const wi of items) { if (wi && wi._source) delete wi._source; }
|
|
326
|
+
const target = filePath || _filePathForScope(scope);
|
|
327
|
+
shared.safeWrite(target, items);
|
|
328
|
+
// Record the mtime we just wrote so the next mutation can detect
|
|
329
|
+
// an external edit (mtime advanced while we weren't looking).
|
|
330
|
+
const m = _statMtimeMs(target);
|
|
331
|
+
if (m != null) _lastMirrorMtimeByScope.set(scope, m);
|
|
332
|
+
} catch {
|
|
333
|
+
// Mirror failures are non-fatal: SQL has already committed.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Test seam.
|
|
338
|
+
function _resetWorkItemsTableForTest() {
|
|
339
|
+
const { getDb } = require('./db');
|
|
340
|
+
try { getDb().exec('DELETE FROM work_items'); } catch { /* not initialized */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
scopeForFilePath,
|
|
345
|
+
readWorkItemsForScope,
|
|
346
|
+
readAllWorkItems,
|
|
347
|
+
applyWorkItemsMutation,
|
|
348
|
+
_filePathForScope,
|
|
349
|
+
_mirrorJsonFromSql,
|
|
350
|
+
_resetWorkItemsTableForTest,
|
|
351
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2068",
|
|
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"
|