@adhdev/daemon-core 0.9.82-rc.469 → 0.9.82-rc.470
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/index.d.ts +2 -2
- package/dist/index.js +4166 -4020
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4166 -4021
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +29 -0
- package/dist/mesh/mesh-ledger.d.ts +6 -0
- package/dist/mesh/mesh-missions.d.ts +37 -0
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/package.json +3 -3
- package/src/index.ts +2 -2
- package/src/mesh/coordinator-prompt.ts +3 -0
- package/src/mesh/mesh-active-work.ts +50 -0
- package/src/mesh/mesh-event-classify.ts +5 -0
- package/src/mesh/mesh-ledger.ts +10 -0
- package/src/mesh/mesh-missions.ts +119 -0
- package/src/mesh/mesh-runtime-store.ts +36 -4
- package/src/mesh/mesh-work-queue.ts +79 -19
|
@@ -881,7 +881,7 @@ export function enqueueTask(
|
|
|
881
881
|
const maxRetries = typeof opts?.maxRetries === 'number' && Number.isFinite(opts.maxRetries) && opts.maxRetries >= 0
|
|
882
882
|
? Math.floor(opts.maxRetries)
|
|
883
883
|
: undefined;
|
|
884
|
-
|
|
884
|
+
const result = withQueueLock(meshId, () => {
|
|
885
885
|
if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, id)) {
|
|
886
886
|
throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
|
|
887
887
|
}
|
|
@@ -927,6 +927,10 @@ export function enqueueTask(
|
|
|
927
927
|
MeshRuntimeStore.getInstance().insertQueueEntry(entry);
|
|
928
928
|
return entry;
|
|
929
929
|
});
|
|
930
|
+
// A fresh pending task returns its mission to a non-terminal state — reset any
|
|
931
|
+
// stale close-candidate marker so a later re-completion can nudge again.
|
|
932
|
+
scheduleMissionCloseCandidateCheck(meshId, [result]);
|
|
933
|
+
return result;
|
|
930
934
|
}
|
|
931
935
|
|
|
932
936
|
/**
|
|
@@ -1070,9 +1074,17 @@ function resolveDependencyFailurePolicy(meshId: string): DependencyFailurePolicy
|
|
|
1070
1074
|
*
|
|
1071
1075
|
* Must be called inside the queue lock of the triggering transition.
|
|
1072
1076
|
*/
|
|
1073
|
-
|
|
1077
|
+
/**
|
|
1078
|
+
* Cascade a dependency failure. Returns the dependents whose status was flipped to
|
|
1079
|
+
* `cancelled` (the 'cancel' policy) so the caller can trigger mission_close_candidate
|
|
1080
|
+
* detection for their missions too — a cascade can be the very transition that leaves
|
|
1081
|
+
* a *different* mission all-terminal. Under the 'block' policy nothing goes terminal
|
|
1082
|
+
* (dependents are only marked blocked), so the returned list is empty.
|
|
1083
|
+
*/
|
|
1084
|
+
function propagateDependencyFailure(meshId: string, failedTaskId: string): MeshWorkQueueEntry[] {
|
|
1074
1085
|
const policy = resolveDependencyFailurePolicy(meshId);
|
|
1075
1086
|
const store = MeshRuntimeStore.getInstance();
|
|
1087
|
+
const cancelled: MeshWorkQueueEntry[] = [];
|
|
1076
1088
|
const frontier = [failedTaskId];
|
|
1077
1089
|
const seen = new Set<string>(frontier);
|
|
1078
1090
|
while (frontier.length > 0) {
|
|
@@ -1087,6 +1099,7 @@ function propagateDependencyFailure(meshId: string, failedTaskId: string): void
|
|
|
1087
1099
|
dependent.cancelledAt = new Date().toISOString();
|
|
1088
1100
|
dependent.cancelReason = `dependency_failed:${currentId}`;
|
|
1089
1101
|
store.updateQueueEntry(dependent);
|
|
1102
|
+
cancelled.push(dependent);
|
|
1090
1103
|
frontier.push(dependent.id); // cascade to transitive dependents
|
|
1091
1104
|
} else {
|
|
1092
1105
|
dependent.blockedReason = `dependency_failed:${currentId}`;
|
|
@@ -1094,10 +1107,39 @@ function propagateDependencyFailure(meshId: string, failedTaskId: string): void
|
|
|
1094
1107
|
}
|
|
1095
1108
|
}
|
|
1096
1109
|
}
|
|
1110
|
+
return cancelled;
|
|
1097
1111
|
}
|
|
1098
1112
|
|
|
1099
1113
|
const DEPENDENCY_FAILURE_TERMINALS = new Set<MeshTaskStatus>(['failed', 'cancelled']);
|
|
1100
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* G3 (step ①) — fire-and-forget mission_close_candidate detection for the missions of
|
|
1117
|
+
* the given task ids. Called after any task-status mutation (completion / failure /
|
|
1118
|
+
* cancel / dependency-cascade / new-task enqueue) so a mission whose tasks all just
|
|
1119
|
+
* became terminal gets a one-shot "consider closing" nudge, and a mission that just
|
|
1120
|
+
* gained a non-terminal task has its idempotency marker reset.
|
|
1121
|
+
*
|
|
1122
|
+
* Loaded via a lazy dynamic import to break the static queue↔missions import cycle
|
|
1123
|
+
* (mesh-missions statically imports getQueue from here): the resolve happens off the
|
|
1124
|
+
* mutation's critical path, and any failure is swallowed — this is a best-effort hint,
|
|
1125
|
+
* never allowed to affect the task write that triggered it.
|
|
1126
|
+
*/
|
|
1127
|
+
function scheduleMissionCloseCandidateCheck(meshId: string, entries: Array<MeshWorkQueueEntry | null | undefined>): void {
|
|
1128
|
+
const missionIds = new Set<string>();
|
|
1129
|
+
for (const entry of entries) {
|
|
1130
|
+
const missionId = entry?.missionId;
|
|
1131
|
+
if (typeof missionId === 'string' && missionId.trim()) missionIds.add(missionId.trim());
|
|
1132
|
+
}
|
|
1133
|
+
if (missionIds.size === 0) return;
|
|
1134
|
+
void import('./mesh-missions.js')
|
|
1135
|
+
.then(({ maybeEmitMissionCloseCandidate }) => {
|
|
1136
|
+
for (const missionId of missionIds) {
|
|
1137
|
+
try { maybeEmitMissionCloseCandidate(meshId, missionId); } catch { /* best-effort per mission */ }
|
|
1138
|
+
}
|
|
1139
|
+
})
|
|
1140
|
+
.catch(() => { /* best-effort: never break a task mutation on the hint path */ });
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1101
1143
|
/**
|
|
1102
1144
|
* Update the status of a specific task.
|
|
1103
1145
|
* Used when a session completes, fails, or stalls.
|
|
@@ -1109,7 +1151,7 @@ export function updateTaskStatus(
|
|
|
1109
1151
|
opts?: MeshQueueMutationOptions,
|
|
1110
1152
|
): MeshWorkQueueEntry | null {
|
|
1111
1153
|
requireMeshHostQueueOwner(opts);
|
|
1112
|
-
|
|
1154
|
+
const result = withQueueLock(meshId, () => {
|
|
1113
1155
|
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
1114
1156
|
if (!entry) return null;
|
|
1115
1157
|
entry.status = status;
|
|
@@ -1117,9 +1159,11 @@ export function updateTaskStatus(
|
|
|
1117
1159
|
// Any transition OFF `assigned` ends the single-flight dispatch window (terminal
|
|
1118
1160
|
// completion/failure, or the dispatch-failure requeue to `pending`).
|
|
1119
1161
|
if (status !== 'assigned') endTaskDispatchInFlight(meshId, taskId);
|
|
1120
|
-
|
|
1121
|
-
return entry;
|
|
1162
|
+
const cascaded = DEPENDENCY_FAILURE_TERMINALS.has(status) ? propagateDependencyFailure(meshId, taskId) : [];
|
|
1163
|
+
return { entry, cascaded };
|
|
1122
1164
|
});
|
|
1165
|
+
if (result) scheduleMissionCloseCandidateCheck(meshId, [result.entry, ...result.cascaded]);
|
|
1166
|
+
return result ? result.entry : null;
|
|
1123
1167
|
}
|
|
1124
1168
|
|
|
1125
1169
|
export function recordTaskAutoLaunch(
|
|
@@ -1146,7 +1190,7 @@ export function cancelTask(
|
|
|
1146
1190
|
opts?: { reason?: string } & MeshQueueMutationOptions,
|
|
1147
1191
|
): MeshWorkQueueEntry | null {
|
|
1148
1192
|
requireMeshHostQueueOwner(opts);
|
|
1149
|
-
|
|
1193
|
+
const result = withQueueLock(meshId, () => {
|
|
1150
1194
|
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
1151
1195
|
if (!entry) return null;
|
|
1152
1196
|
const now = new Date().toISOString();
|
|
@@ -1155,9 +1199,11 @@ export function cancelTask(
|
|
|
1155
1199
|
if (opts?.reason) entry.cancelReason = opts.reason;
|
|
1156
1200
|
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
1157
1201
|
endTaskDispatchInFlight(meshId, taskId);
|
|
1158
|
-
propagateDependencyFailure(meshId, taskId);
|
|
1159
|
-
return entry;
|
|
1202
|
+
const cascaded = propagateDependencyFailure(meshId, taskId);
|
|
1203
|
+
return { entry, cascaded };
|
|
1160
1204
|
});
|
|
1205
|
+
if (result) scheduleMissionCloseCandidateCheck(meshId, [result.entry, ...result.cascaded]);
|
|
1206
|
+
return result ? result.entry : null;
|
|
1161
1207
|
}
|
|
1162
1208
|
|
|
1163
1209
|
/**
|
|
@@ -1188,7 +1234,7 @@ export function requeueTask(
|
|
|
1188
1234
|
} & MeshQueueMutationOptions,
|
|
1189
1235
|
): MeshWorkQueueEntry | null {
|
|
1190
1236
|
requireMeshHostQueueOwner(opts);
|
|
1191
|
-
|
|
1237
|
+
const result = withQueueLock(meshId, () => {
|
|
1192
1238
|
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
1193
1239
|
if (!entry) return null;
|
|
1194
1240
|
// CANON-IDENTITY single-flight: refuse (no-op) to reopen a task whose dispatch
|
|
@@ -1203,7 +1249,8 @@ export function requeueTask(
|
|
|
1203
1249
|
// from this single-flight guard; the exemption hook (group-id check) belongs here.
|
|
1204
1250
|
if (!opts?.force && isTaskDispatchInFlight(meshId, taskId)) {
|
|
1205
1251
|
LOG.warn('MeshQueue', `Refusing to requeue task ${taskId} on mesh ${meshId}: it is actively dispatched/generating (single-flight in-flight). Requeueing now would open a duplicate second dispatch into another session. Pass force to override.`);
|
|
1206
|
-
|
|
1252
|
+
// No status change → no mission aggregate change; nothing to re-check.
|
|
1253
|
+
return { entry, cascaded: [] as MeshWorkQueueEntry[], missionAffected: false };
|
|
1207
1254
|
}
|
|
1208
1255
|
// Proceeding to requeue (or force-override): the prior dispatch is being abandoned,
|
|
1209
1256
|
// so end the single-flight window for this task id.
|
|
@@ -1216,8 +1263,9 @@ export function requeueTask(
|
|
|
1216
1263
|
entry.cancelReason = `max_retries_exceeded: requeued ${currentCount} time(s), limit is ${maxRetries}`;
|
|
1217
1264
|
entry.updatedAt = new Date().toISOString();
|
|
1218
1265
|
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
1219
|
-
propagateDependencyFailure(meshId, taskId);
|
|
1220
|
-
|
|
1266
|
+
const cascaded = propagateDependencyFailure(meshId, taskId);
|
|
1267
|
+
// Terminal (failed) → mission may now be all-terminal.
|
|
1268
|
+
return { entry, cascaded, missionAffected: true };
|
|
1221
1269
|
}
|
|
1222
1270
|
entry.status = 'pending';
|
|
1223
1271
|
// Operator requeue clears a dependency-failure block — the operator is
|
|
@@ -1235,8 +1283,13 @@ export function requeueTask(
|
|
|
1235
1283
|
entry.requeueCount = currentCount + 1;
|
|
1236
1284
|
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
1237
1285
|
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
1238
|
-
|
|
1286
|
+
// Non-terminal (back to pending) → mission left the all-terminal state; the
|
|
1287
|
+
// close-candidate check resets any stale idempotency marker so a later
|
|
1288
|
+
// re-completion can nudge again.
|
|
1289
|
+
return { entry, cascaded: [] as MeshWorkQueueEntry[], missionAffected: true };
|
|
1239
1290
|
});
|
|
1291
|
+
if (result?.missionAffected) scheduleMissionCloseCandidateCheck(meshId, [result.entry, ...result.cascaded]);
|
|
1292
|
+
return result ? result.entry : null;
|
|
1240
1293
|
}
|
|
1241
1294
|
|
|
1242
1295
|
/**
|
|
@@ -1269,7 +1322,7 @@ export function reclaimStrandedAssignedTask(
|
|
|
1269
1322
|
opts?: { reason?: string; ageMs?: number } & MeshQueueMutationOptions,
|
|
1270
1323
|
): MeshWorkQueueEntry | null {
|
|
1271
1324
|
requireMeshHostQueueOwner(opts);
|
|
1272
|
-
|
|
1325
|
+
const result = withQueueLock(meshId, () => {
|
|
1273
1326
|
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
1274
1327
|
if (!entry) return null;
|
|
1275
1328
|
// Only a still-assigned row is stranded. If a completion/cancel already moved it
|
|
@@ -1291,12 +1344,13 @@ export function reclaimStrandedAssignedTask(
|
|
|
1291
1344
|
// The stranded assignment is being torn down (→ pending or failed); end its
|
|
1292
1345
|
// single-flight window so a re-claim/requeue is not blocked.
|
|
1293
1346
|
endTaskDispatchInFlight(meshId, taskId);
|
|
1347
|
+
let cascaded: MeshWorkQueueEntry[] = [];
|
|
1294
1348
|
if (reclaims > MAX_STRANDED_RECLAIMS) {
|
|
1295
1349
|
// Repeatedly undeliverable — stop cycling and fail it so dependents unblock.
|
|
1296
1350
|
entry.status = 'failed';
|
|
1297
1351
|
entry.cancelReason = `stranded_dispatch_unrecovered: reclaimed ${reclaims - 1} time(s) without a confirmed dispatch`;
|
|
1298
1352
|
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
1299
|
-
propagateDependencyFailure(meshId, taskId);
|
|
1353
|
+
cascaded = propagateDependencyFailure(meshId, taskId);
|
|
1300
1354
|
} else {
|
|
1301
1355
|
entry.status = 'pending';
|
|
1302
1356
|
entry.requeuedAt = now;
|
|
@@ -1317,8 +1371,12 @@ export function reclaimStrandedAssignedTask(
|
|
|
1317
1371
|
},
|
|
1318
1372
|
});
|
|
1319
1373
|
} catch { /* ledger write is best-effort */ }
|
|
1320
|
-
return entry;
|
|
1374
|
+
return { entry, cascaded };
|
|
1321
1375
|
});
|
|
1376
|
+
// Reclaim toggles the mission aggregate either way: → failed may make it all-terminal;
|
|
1377
|
+
// → pending resets any stale close-candidate marker. Re-check in both outcomes.
|
|
1378
|
+
if (result) scheduleMissionCloseCandidateCheck(meshId, [result.entry, ...result.cascaded]);
|
|
1379
|
+
return result ? result.entry : null;
|
|
1322
1380
|
}
|
|
1323
1381
|
|
|
1324
1382
|
/**
|
|
@@ -1330,7 +1388,7 @@ export function updateSessionTaskStatus(
|
|
|
1330
1388
|
status: MeshTaskStatus,
|
|
1331
1389
|
opts?: { occurredAt?: string; taskId?: string },
|
|
1332
1390
|
): MeshWorkQueueEntry | null {
|
|
1333
|
-
|
|
1391
|
+
const result = withQueueLock(meshId, () => {
|
|
1334
1392
|
const store = MeshRuntimeStore.getInstance();
|
|
1335
1393
|
const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
|
|
1336
1394
|
const entry = store.findAssignedBySession(meshId, sessionId, occurredAtIso, opts?.taskId);
|
|
@@ -1352,9 +1410,11 @@ export function updateSessionTaskStatus(
|
|
|
1352
1410
|
// The worker reported a terminal/non-assigned outcome — the dispatch is over;
|
|
1353
1411
|
// release the single-flight mark so the task id can be re-dispatched later.
|
|
1354
1412
|
if (status !== 'assigned') endTaskDispatchInFlight(meshId, entry.id);
|
|
1355
|
-
|
|
1356
|
-
return entry;
|
|
1413
|
+
const cascaded = DEPENDENCY_FAILURE_TERMINALS.has(status) ? propagateDependencyFailure(meshId, entry.id) : [];
|
|
1414
|
+
return { entry, cascaded };
|
|
1357
1415
|
});
|
|
1416
|
+
if (result) scheduleMissionCloseCandidateCheck(meshId, [result.entry, ...result.cascaded]);
|
|
1417
|
+
return result ? result.entry : null;
|
|
1358
1418
|
}
|
|
1359
1419
|
|
|
1360
1420
|
/**
|