@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.61
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/dist/boot/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/router.d.ts +24 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +3522 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3496 -587
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +48 -0
- package/dist/mesh/mesh-events.d.ts +17 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +23 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +160 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/commands/router.ts +2178 -419
- package/src/config/mesh-config.ts +244 -1
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +39 -5
- package/src/mesh/coordinator-prompt.ts +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +210 -38
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +137 -0
- package/src/mesh/mesh-work-queue.ts +202 -122
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +66 -1
- package/src/repo-mesh-types.ts +174 -0
|
@@ -1,20 +1,70 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
5
|
+
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
6
|
+
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
5
7
|
|
|
6
8
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
9
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
8
10
|
export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
|
|
11
|
+
export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
|
|
9
12
|
|
|
10
13
|
export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
|
|
11
14
|
export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
|
|
15
|
+
export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
|
|
16
|
+
|
|
17
|
+
export interface MeshTaskModeValidationResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
taskMode?: MeshTaskMode;
|
|
20
|
+
violations: string[];
|
|
21
|
+
allowedOperations?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
|
|
25
|
+
{ label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
|
|
26
|
+
{ label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv)|push\b)/i },
|
|
27
|
+
{ label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
|
|
28
|
+
{ label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release)\b/i },
|
|
29
|
+
{ label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i)\b/i },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
|
|
33
|
+
if (typeof value !== 'string') return undefined;
|
|
34
|
+
const normalized = value.trim() as MeshTaskMode;
|
|
35
|
+
return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
|
|
39
|
+
const taskMode = normalizeMeshTaskMode(mode);
|
|
40
|
+
if (!taskMode) {
|
|
41
|
+
return { valid: true, violations: [] };
|
|
42
|
+
}
|
|
43
|
+
if (taskMode !== 'live_debug_readonly') {
|
|
44
|
+
return { valid: true, taskMode, violations: [] };
|
|
45
|
+
}
|
|
46
|
+
const violations = LIVE_DEBUG_READONLY_FORBIDDEN
|
|
47
|
+
.filter(rule => rule.pattern.test(message || ''))
|
|
48
|
+
.map(rule => rule.label);
|
|
49
|
+
return {
|
|
50
|
+
valid: violations.length === 0,
|
|
51
|
+
taskMode,
|
|
52
|
+
violations,
|
|
53
|
+
allowedOperations: [
|
|
54
|
+
'process/log/window/port/session inspection',
|
|
55
|
+
'read-only filesystem listing/reading',
|
|
56
|
+
'status probes and keep-running handle reporting',
|
|
57
|
+
'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
12
61
|
|
|
13
62
|
export interface MeshWorkQueueEntry {
|
|
14
63
|
id: string;
|
|
15
64
|
meshId: string;
|
|
16
65
|
message: string;
|
|
17
66
|
status: MeshTaskStatus;
|
|
67
|
+
taskMode?: MeshTaskMode;
|
|
18
68
|
/** If specified, only this node can claim the task (used by legacy mesh_send_task) */
|
|
19
69
|
targetNodeId?: string;
|
|
20
70
|
/** If specified, only this runtime session can claim the task */
|
|
@@ -45,11 +95,40 @@ export interface MeshWorkQueueEntry {
|
|
|
45
95
|
updatedAt: string;
|
|
46
96
|
}
|
|
47
97
|
|
|
98
|
+
export interface MeshQueueMutationOptions {
|
|
99
|
+
ownerRole?: RepoMeshDaemonRole;
|
|
100
|
+
}
|
|
101
|
+
|
|
48
102
|
function getQueuePath(meshId: string): string {
|
|
49
103
|
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
50
104
|
return join(getLedgerDir(), `${safe}.queue.json`);
|
|
51
105
|
}
|
|
52
106
|
|
|
107
|
+
function getLockPath(meshId: string): string {
|
|
108
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
109
|
+
return join(getLedgerDir(), `${safe}.queue.lock`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Simple advisory file lock using O_EXCL (atomic create) for queue mutations.
|
|
114
|
+
* Retries up to 10 times at 30 ms intervals; proceeds without lock on timeout
|
|
115
|
+
* to prevent deadlock (best-effort — far better than no locking at all).
|
|
116
|
+
*/
|
|
117
|
+
function withQueueLock<T>(meshId: string, fn: () => T): T {
|
|
118
|
+
const lockPath = getLockPath(meshId);
|
|
119
|
+
let fd = -1;
|
|
120
|
+
for (let i = 0; i < 10; i++) {
|
|
121
|
+
try { fd = openSync(lockPath, 'wx'); break; } catch {
|
|
122
|
+
const deadline = Date.now() + 30;
|
|
123
|
+
while (Date.now() < deadline) { /* spin */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try { return fn(); } finally {
|
|
127
|
+
if (fd !== -1) try { closeSync(fd); } catch { /* noop */ }
|
|
128
|
+
try { unlinkSync(lockPath); } catch { /* already removed */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
53
132
|
function readQueue(meshId: string): MeshWorkQueueEntry[] {
|
|
54
133
|
const path = getQueuePath(meshId);
|
|
55
134
|
if (!existsSync(path)) return [];
|
|
@@ -72,22 +151,30 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
72
151
|
export function enqueueTask(
|
|
73
152
|
meshId: string,
|
|
74
153
|
message: string,
|
|
75
|
-
opts?: { targetNodeId?: string; targetSessionId?: string }
|
|
154
|
+
opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string } & MeshQueueMutationOptions,
|
|
76
155
|
): MeshWorkQueueEntry {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
156
|
+
requireMeshHostQueueOwner(opts);
|
|
157
|
+
const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
|
|
158
|
+
if (!modeValidation.valid) {
|
|
159
|
+
throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
|
|
160
|
+
}
|
|
161
|
+
return withQueueLock(meshId, () => {
|
|
162
|
+
const queue = readQueue(meshId);
|
|
163
|
+
const entry: MeshWorkQueueEntry = {
|
|
164
|
+
id: randomUUID(),
|
|
165
|
+
meshId,
|
|
166
|
+
message,
|
|
167
|
+
status: 'pending',
|
|
168
|
+
taskMode: modeValidation.taskMode,
|
|
169
|
+
targetNodeId: opts?.targetNodeId,
|
|
170
|
+
targetSessionId: opts?.targetSessionId,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
updatedAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
queue.push(entry);
|
|
175
|
+
writeQueue(meshId, queue);
|
|
176
|
+
return entry;
|
|
177
|
+
});
|
|
91
178
|
}
|
|
92
179
|
|
|
93
180
|
/**
|
|
@@ -106,39 +193,29 @@ export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }):
|
|
|
106
193
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
107
194
|
*/
|
|
108
195
|
export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
q.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const entry = queue[targetIdx];
|
|
134
|
-
entry.status = 'assigned';
|
|
135
|
-
entry.assignedNodeId = nodeId;
|
|
136
|
-
entry.assignedSessionId = sessionId;
|
|
137
|
-
entry.dispatchTimestamp = new Date().toISOString();
|
|
138
|
-
entry.updatedAt = new Date().toISOString();
|
|
139
|
-
|
|
140
|
-
writeQueue(meshId, queue);
|
|
141
|
-
return entry;
|
|
196
|
+
return withQueueLock(meshId, () => {
|
|
197
|
+
const queue = readQueue(meshId);
|
|
198
|
+
const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
|
|
199
|
+
q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
|
|
200
|
+
));
|
|
201
|
+
if (hasActiveAssignment) return null;
|
|
202
|
+
let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
|
|
203
|
+
if (targetIdx === -1) {
|
|
204
|
+
targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
|
|
205
|
+
}
|
|
206
|
+
if (targetIdx === -1) {
|
|
207
|
+
targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
|
|
208
|
+
}
|
|
209
|
+
if (targetIdx === -1) return null;
|
|
210
|
+
const entry = queue[targetIdx];
|
|
211
|
+
entry.status = 'assigned';
|
|
212
|
+
entry.assignedNodeId = nodeId;
|
|
213
|
+
entry.assignedSessionId = sessionId;
|
|
214
|
+
entry.dispatchTimestamp = new Date().toISOString();
|
|
215
|
+
entry.updatedAt = new Date().toISOString();
|
|
216
|
+
writeQueue(meshId, queue);
|
|
217
|
+
return entry;
|
|
218
|
+
});
|
|
142
219
|
}
|
|
143
220
|
|
|
144
221
|
/**
|
|
@@ -149,15 +226,18 @@ export function updateTaskStatus(
|
|
|
149
226
|
meshId: string,
|
|
150
227
|
taskId: string,
|
|
151
228
|
status: MeshTaskStatus,
|
|
229
|
+
opts?: MeshQueueMutationOptions,
|
|
152
230
|
): MeshWorkQueueEntry | null {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
requireMeshHostQueueOwner(opts);
|
|
232
|
+
return withQueueLock(meshId, () => {
|
|
233
|
+
const queue = readQueue(meshId);
|
|
234
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
235
|
+
if (idx === -1) return null;
|
|
236
|
+
queue[idx].status = status;
|
|
237
|
+
queue[idx].updatedAt = new Date().toISOString();
|
|
238
|
+
writeQueue(meshId, queue);
|
|
239
|
+
return queue[idx];
|
|
240
|
+
});
|
|
161
241
|
}
|
|
162
242
|
|
|
163
243
|
export function recordTaskAutoLaunch(
|
|
@@ -165,17 +245,16 @@ export function recordTaskAutoLaunch(
|
|
|
165
245
|
taskId: string,
|
|
166
246
|
autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
|
|
167
247
|
): MeshWorkQueueEntry | null {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
...autoLaunch,
|
|
174
|
-
updatedAt
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return queue[idx];
|
|
248
|
+
return withQueueLock(meshId, () => {
|
|
249
|
+
const queue = readQueue(meshId);
|
|
250
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
251
|
+
if (idx === -1) return null;
|
|
252
|
+
const now = new Date().toISOString();
|
|
253
|
+
queue[idx].autoLaunch = { ...autoLaunch, updatedAt: now };
|
|
254
|
+
queue[idx].updatedAt = now;
|
|
255
|
+
writeQueue(meshId, queue);
|
|
256
|
+
return queue[idx];
|
|
257
|
+
});
|
|
179
258
|
}
|
|
180
259
|
|
|
181
260
|
/**
|
|
@@ -184,19 +263,21 @@ export function recordTaskAutoLaunch(
|
|
|
184
263
|
export function cancelTask(
|
|
185
264
|
meshId: string,
|
|
186
265
|
taskId: string,
|
|
187
|
-
opts?: { reason?: string },
|
|
266
|
+
opts?: { reason?: string } & MeshQueueMutationOptions,
|
|
188
267
|
): MeshWorkQueueEntry | null {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
268
|
+
requireMeshHostQueueOwner(opts);
|
|
269
|
+
return withQueueLock(meshId, () => {
|
|
270
|
+
const queue = readQueue(meshId);
|
|
271
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
272
|
+
if (idx === -1) return null;
|
|
273
|
+
const now = new Date().toISOString();
|
|
274
|
+
queue[idx].status = 'cancelled';
|
|
275
|
+
queue[idx].updatedAt = now;
|
|
276
|
+
queue[idx].cancelledAt = now;
|
|
277
|
+
if (opts?.reason) queue[idx].cancelReason = opts.reason;
|
|
278
|
+
writeQueue(meshId, queue);
|
|
279
|
+
return queue[idx];
|
|
280
|
+
});
|
|
200
281
|
}
|
|
201
282
|
|
|
202
283
|
/**
|
|
@@ -212,29 +293,31 @@ export function requeueTask(
|
|
|
212
293
|
targetSessionId?: string;
|
|
213
294
|
clearTargetNode?: boolean;
|
|
214
295
|
clearTargetSession?: boolean;
|
|
215
|
-
},
|
|
296
|
+
} & MeshQueueMutationOptions,
|
|
216
297
|
): MeshWorkQueueEntry | null {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
298
|
+
requireMeshHostQueueOwner(opts);
|
|
299
|
+
return withQueueLock(meshId, () => {
|
|
300
|
+
const queue = readQueue(meshId);
|
|
301
|
+
const idx = queue.findIndex(q => q.id === taskId);
|
|
302
|
+
if (idx === -1) return null;
|
|
303
|
+
const entry = queue[idx];
|
|
304
|
+
const now = new Date().toISOString();
|
|
305
|
+
entry.status = 'pending';
|
|
306
|
+
delete entry.assignedNodeId;
|
|
307
|
+
delete entry.assignedSessionId;
|
|
308
|
+
delete entry.cancelledAt;
|
|
309
|
+
delete entry.cancelReason;
|
|
310
|
+
if (opts?.clearTargetNode) delete entry.targetNodeId;
|
|
311
|
+
if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
|
|
312
|
+
if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
|
|
313
|
+
if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
|
|
314
|
+
entry.updatedAt = now;
|
|
315
|
+
entry.requeuedAt = now;
|
|
316
|
+
entry.requeueCount = (entry.requeueCount || 0) + 1;
|
|
317
|
+
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
318
|
+
writeQueue(meshId, queue);
|
|
319
|
+
return entry;
|
|
320
|
+
});
|
|
238
321
|
}
|
|
239
322
|
|
|
240
323
|
/**
|
|
@@ -244,29 +327,26 @@ export function updateSessionTaskStatus(
|
|
|
244
327
|
meshId: string,
|
|
245
328
|
sessionId: string,
|
|
246
329
|
status: MeshTaskStatus,
|
|
330
|
+
opts?: { occurredAt?: string },
|
|
247
331
|
): MeshWorkQueueEntry | null {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
|
|
332
|
+
return withQueueLock(meshId, () => {
|
|
333
|
+
const queue = readQueue(meshId);
|
|
334
|
+
const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
|
|
335
|
+
const hasOccurredAt = Number.isFinite(occurredAtTime);
|
|
336
|
+
let bestIdx = -1;
|
|
337
|
+
let bestTime = 0;
|
|
338
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
339
|
+
if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
|
|
257
340
|
const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
|
|
258
|
-
if (time >
|
|
259
|
-
|
|
260
|
-
bestIdx = i;
|
|
261
|
-
}
|
|
341
|
+
if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
|
|
342
|
+
if (time > bestTime) { bestTime = time; bestIdx = i; }
|
|
262
343
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return queue[bestIdx];
|
|
344
|
+
if (bestIdx === -1) return null;
|
|
345
|
+
queue[bestIdx].status = status;
|
|
346
|
+
queue[bestIdx].updatedAt = new Date().toISOString();
|
|
347
|
+
writeQueue(meshId, queue);
|
|
348
|
+
return queue[bestIdx];
|
|
349
|
+
});
|
|
270
350
|
}
|
|
271
351
|
|
|
272
352
|
export interface MeshWorkQueueStats {
|