@adhdev/daemon-core 0.9.82-rc.453 → 0.9.82-rc.454

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.
@@ -324,10 +324,28 @@ export declare function updateSessionTaskStatus(meshId: string, sessionId: strin
324
324
  * Used by the completion event path to decide whether to wake the queue.
325
325
  */
326
326
  export declare function hasPendingDependents(meshId: string, taskId: string): boolean;
327
+ /**
328
+ * M1: THE single dependency-gate predicate. A task is claimable from a
329
+ * dependency standpoint iff it carries no system block (`blockedReason`) AND
330
+ * every id in `dependsOn` has reached 'completed'.
331
+ *
332
+ * DEPENDSON-GATE-SYMMETRY: every scheduler surface that decides whether a
333
+ * pending task may run MUST route through this one predicate — the queue claim
334
+ * (claimNextQueueTask), the auto-launch candidate filter
335
+ * (maybeAutoLaunchOneQueueSession), and the cloud eager P2P push
336
+ * (enqueue-and-push). If any surface computes dependency readiness on its own,
337
+ * the gate goes asymmetric and a task blocked from the pull path can still be
338
+ * eager-pushed straight to an idle session, silently bypassing its
339
+ * prerequisites. The semantics here (all deps completed && !blocked) are the
340
+ * invariant — do not fork them.
341
+ */
342
+ export declare function taskDependenciesSatisfied(entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>, statusById: Map<string, MeshTaskStatus | string>): boolean;
327
343
  /**
328
344
  * M1-4: view-time dependency state for a task — unmet dependency ids and
329
345
  * whether the task is currently claimable from a dependency standpoint.
330
- * Not stored (truth stays in task statuses).
346
+ * Not stored (truth stays in task statuses). The `dependenciesSatisfied` field
347
+ * is derived from {@link taskDependenciesSatisfied} so the view and the
348
+ * scheduler gates can never disagree.
331
349
  */
332
350
  export declare function describeTaskDependencyState(entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>, statusById: Map<string, MeshTaskStatus | string>): {
333
351
  waitingOn: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.453",
3
+ "version": "0.9.82-rc.454",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,8 +46,8 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.453",
50
- "@adhdev/session-host-core": "0.9.82-rc.453",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.454",
50
+ "@adhdev/session-host-core": "0.9.82-rc.454",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
53
53
  "ajv-formats": "^3.0.1",
package/src/index.ts CHANGED
@@ -270,7 +270,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
270
270
  export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
271
271
 
272
272
  // ── Mesh Work Queue (GUPP) ──
273
- export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, isTaskReadonly, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
273
+ export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, isTaskReadonly, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState, taskDependenciesSatisfied } from './mesh/mesh-work-queue.js';
274
274
  export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
275
275
  export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, pruneStaleDirectDispatches, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
276
276
  export type { StaleDirectPruneClassification, StaleDirectPruneResult, PruneStaleDirectDispatchesOptions } from './mesh/mesh-active-work.js';
@@ -6,7 +6,7 @@ import { getMesh } from '../config/mesh-config.js';
6
6
  import { detectCLI } from '../detection/cli-detector.js';
7
7
  import { LOG } from '../logging/logger.js';
8
8
  import { appendLedgerEntry } from './mesh-ledger.js';
9
- import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, getActiveDirectDispatches, isTaskReadonly } from './mesh-work-queue.js';
9
+ import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, getActiveDirectDispatches, isTaskReadonly, taskDependenciesSatisfied } from './mesh-work-queue.js';
10
10
  import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
11
11
  import { fastForwardMeshNode } from './mesh-fast-forward.js';
12
12
  import { createSessionDelivery, updateSessionDeliveryStatus } from './mesh-delivery-policy.js';
@@ -1287,6 +1287,10 @@ function readMeshNodeId(node: any): string {
1287
1287
 
1288
1288
  async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, meshId: string, mesh: any): Promise<boolean> {
1289
1289
  const queue = getQueue(meshId);
1290
+ // DEPENDSON-GATE-SYMMETRY: status index over the FULL queue (incl. completed)
1291
+ // so the dependency gate below sees the terminal state of every referenced
1292
+ // dependency, not just the still-active rows.
1293
+ const statusById = new Map(queue.map(task => [task.id, task.status] as const));
1290
1294
  const pending = queue.filter(task => task.status === 'pending');
1291
1295
  if (!pending.length) return false;
1292
1296
 
@@ -1300,6 +1304,16 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1300
1304
  // higher safety cap (default 2× the write cap).
1301
1305
  const maxReadonlyParallelTasks = resolveMaxReadonlyParallelTasks(maxParallelTasks);
1302
1306
  for (const task of pending) {
1307
+ // DEPENDSON-GATE-SYMMETRY: never spawn a session for a task whose
1308
+ // dependsOn set is not all-completed (or that carries a system block). The
1309
+ // launched session would idle→claim and be refused by the SAME predicate in
1310
+ // claimNextQueueTask, producing orphan-session / re-launch churn. Skip it so
1311
+ // a later tick — after the dependency completes — launches it. Tasks with no
1312
+ // dependsOn pass through unchanged (predicate is true).
1313
+ if (!taskDependenciesSatisfied(task, statusById)) {
1314
+ markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'dependencies_unsatisfied' });
1315
+ continue;
1316
+ }
1303
1317
  const isReadonly = isTaskReadonly(task);
1304
1318
  if (isReadonly) {
1305
1319
  if (activeReadonlyAssignedCount(meshId) >= maxReadonlyParallelTasks) {
@@ -3,7 +3,7 @@ import { dirname, join } from 'path';
3
3
  import { LOG } from '../logging/logger.js';
4
4
  import { loadBetterSqlite3 } from '../system/load-better-sqlite3.js';
5
5
  import { getLedgerDir } from './mesh-ledger.js';
6
- import { nodeSatisfiesRequiredTags, isTaskReadonly } from './mesh-work-queue.js';
6
+ import { nodeSatisfiesRequiredTags, isTaskReadonly, taskDependenciesSatisfied } from './mesh-work-queue.js';
7
7
  import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, sessionIdsEquivalent } from '@adhdev/mesh-shared';
8
8
  import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
9
9
  import type BetterSqlite3 from 'better-sqlite3';
@@ -756,11 +756,11 @@ export class MeshRuntimeStore {
756
756
  ).all(meshId, ...depIds) as Array<{ id: string; status: string }>;
757
757
  for (const r of depRows) depStatus.set(r.id, r.status);
758
758
  }
759
- const dependenciesSatisfied = (candidate: MeshWorkQueueEntry): boolean => {
760
- if (candidate.blockedReason) return false;
761
- const deps = Array.isArray(candidate.dependsOn) ? candidate.dependsOn : [];
762
- return deps.every(depId => depStatus.get(depId) === 'completed');
763
- };
759
+ // DEPENDSON-GATE-SYMMETRY: the claim gate shares the single
760
+ // taskDependenciesSatisfied predicate with the auto-launch filter and
761
+ // the cloud eager P2P push, so a task blocked here is blocked there too.
762
+ const dependenciesSatisfied = (candidate: MeshWorkQueueEntry): boolean =>
763
+ taskDependenciesSatisfied(candidate, depStatus);
764
764
 
765
765
  // Per-candidate node-conflict gate: write tasks require an idle node; read-only
766
766
  // tasks bypass the node-busy check so N read-only diagnoses can run on one node
@@ -1276,10 +1276,36 @@ export function hasPendingDependents(meshId: string, taskId: string): boolean {
1276
1276
  .some(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(taskId));
1277
1277
  }
1278
1278
 
1279
+ /**
1280
+ * M1: THE single dependency-gate predicate. A task is claimable from a
1281
+ * dependency standpoint iff it carries no system block (`blockedReason`) AND
1282
+ * every id in `dependsOn` has reached 'completed'.
1283
+ *
1284
+ * DEPENDSON-GATE-SYMMETRY: every scheduler surface that decides whether a
1285
+ * pending task may run MUST route through this one predicate — the queue claim
1286
+ * (claimNextQueueTask), the auto-launch candidate filter
1287
+ * (maybeAutoLaunchOneQueueSession), and the cloud eager P2P push
1288
+ * (enqueue-and-push). If any surface computes dependency readiness on its own,
1289
+ * the gate goes asymmetric and a task blocked from the pull path can still be
1290
+ * eager-pushed straight to an idle session, silently bypassing its
1291
+ * prerequisites. The semantics here (all deps completed && !blocked) are the
1292
+ * invariant — do not fork them.
1293
+ */
1294
+ export function taskDependenciesSatisfied(
1295
+ entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
1296
+ statusById: Map<string, MeshTaskStatus | string>,
1297
+ ): boolean {
1298
+ if (entry.blockedReason) return false;
1299
+ const deps = Array.isArray(entry.dependsOn) ? entry.dependsOn : [];
1300
+ return deps.every(depId => statusById.get(depId) === 'completed');
1301
+ }
1302
+
1279
1303
  /**
1280
1304
  * M1-4: view-time dependency state for a task — unmet dependency ids and
1281
1305
  * whether the task is currently claimable from a dependency standpoint.
1282
- * Not stored (truth stays in task statuses).
1306
+ * Not stored (truth stays in task statuses). The `dependenciesSatisfied` field
1307
+ * is derived from {@link taskDependenciesSatisfied} so the view and the
1308
+ * scheduler gates can never disagree.
1283
1309
  */
1284
1310
  export function describeTaskDependencyState(
1285
1311
  entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
@@ -1289,7 +1315,7 @@ export function describeTaskDependencyState(
1289
1315
  const waitingOn = deps.filter(depId => statusById.get(depId) !== 'completed');
1290
1316
  return {
1291
1317
  waitingOn,
1292
- dependenciesSatisfied: waitingOn.length === 0 && !entry.blockedReason,
1318
+ dependenciesSatisfied: taskDependenciesSatisfied(entry, statusById),
1293
1319
  };
1294
1320
  }
1295
1321