@adhdev/daemon-core 0.9.82-rc.537 → 0.9.82-rc.539

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.
@@ -9,7 +9,7 @@ import { appendLedgerEntry } from './mesh-ledger.js';
9
9
  import type { MeshLedgerKind } from './mesh-ledger.js';
10
10
  import { createSessionDelivery } from './mesh-delivery-policy.js';
11
11
  import { isTaskDispatchInFlight, endTaskDispatchInFlight } from './mesh-task-inflight.js';
12
- import { sessionIdsEquivalent, isMeshTaskDifficulty, type MeshTaskDifficulty } from '@adhdev/mesh-shared';
12
+ import { sessionIdsEquivalent, isMeshTaskDifficulty, normalizeNodeCapabilitySlots, type MeshTaskDifficulty } from '@adhdev/mesh-shared';
13
13
 
14
14
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
15
15
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
@@ -692,6 +692,37 @@ function firstProviderPriority(policy: unknown): string | undefined {
692
692
  return raw.find(type => typeof type === 'string' && type.trim())?.trim();
693
693
  }
694
694
 
695
+ /**
696
+ * Ordered, de-duplicated provider types a node can launch, resolved from
697
+ * `policy.slots` (the single source of truth — ORCHESTRATION_NODE_SLOTS.md) with a
698
+ * fallback to the legacy `policy.providerPriority`. Used to advertise a
699
+ * `provider=<type>` capability tag for EVERY provider the node supports, not just
700
+ * providerPriority[0], so required_tags: ["provider=cursor-cli"] is satisfiable on a
701
+ * node whose slots include cursor-cli even when it is not the first priority entry.
702
+ *
703
+ * Only provider NAMES are needed here, so slots are read via the dependency-light
704
+ * normalizeNodeCapabilitySlots rather than resolveNodeCapabilitySlots (which pulls in
705
+ * difficultyBrains) — keeping tag derivation free of scheduling-config imports.
706
+ */
707
+ function readNodeProviderTypes(policy: unknown): string[] {
708
+ const record = policy && typeof policy === 'object' && !Array.isArray(policy)
709
+ ? policy as Record<string, unknown>
710
+ : {};
711
+ const seen = new Set<string>();
712
+ const out: string[] = [];
713
+ const push = (type: unknown) => {
714
+ const trimmed = typeof type === 'string' ? type.trim() : '';
715
+ if (!trimmed || seen.has(trimmed)) return;
716
+ seen.add(trimmed);
717
+ out.push(trimmed);
718
+ };
719
+ for (const slot of normalizeNodeCapabilitySlots(record.slots)) push(slot.provider);
720
+ if (Array.isArray(record.providerPriority)) {
721
+ for (const type of record.providerPriority) push(type);
722
+ }
723
+ return out;
724
+ }
725
+
695
726
  function readNodeOverride(node: { userOverrides?: unknown } | undefined, key: 'platform' | 'arch'): string | null {
696
727
  const overrides = node?.userOverrides;
697
728
  if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) return null;
@@ -715,9 +746,20 @@ export function buildMeshNodeCapabilityTags(
715
746
  node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown; userOverrides?: unknown; reportedPlatform?: unknown; reportedArch?: unknown } | undefined,
716
747
  providerType?: string,
717
748
  ): string[] {
718
- const provider = typeof providerType === 'string' && providerType.trim()
749
+ // When an explicit providerType is pinned (per-provider tag set used by the
750
+ // queue slot matcher), advertise ONLY that provider's tag — so
751
+ // provider=codex-cli matches only when codex-cli is the launched provider.
752
+ // When no provider is pinned (the representative tag set consulted by
753
+ // nodeSatisfiesRequiredTags), advertise a provider= tag for EVERY provider the
754
+ // node can launch (all policy.slots, else providerPriority), so
755
+ // required_tags: ["provider=cursor-cli"] is satisfiable on a node whose slots
756
+ // include cursor-cli even when it is not the first priority entry.
757
+ const pinnedProvider = typeof providerType === 'string' && providerType.trim()
719
758
  ? providerType.trim()
720
- : firstProviderPriority(node?.policy);
759
+ : undefined;
760
+ const providerTags = pinnedProvider
761
+ ? [pinnedProvider]
762
+ : readNodeProviderTypes(node?.policy);
721
763
  const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
722
764
  ? node.worktreeBranch.trim()
723
765
  : null;
@@ -744,7 +786,7 @@ export function buildMeshNodeCapabilityTags(
744
786
  ...(Array.isArray(node?.capabilities) ? node.capabilities : []),
745
787
  `os=${os}`,
746
788
  `arch=${arch}`,
747
- ...(provider ? [`provider=${provider}`] : []),
789
+ ...providerTags.map(p => `provider=${p}`),
748
790
  // Worktree nodes automatically expose a "worktree=<branch>" tag so that
749
791
  // mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
750
792
  // only to the matching worktree node.
@@ -1358,6 +1400,34 @@ export function requeueTask(
1358
1400
  */
1359
1401
  const MAX_STRANDED_RECLAIMS = 3;
1360
1402
 
1403
+ /**
1404
+ * TASK-PROMPT-REDRIVE-AFTER-COMPLETE: the reclaim reasons the assigned-stranded watchdog
1405
+ * uses when it RE-DRIVES a delivered-but-not-terminal task (returns it to 'pending' so the
1406
+ * SAME prompt is re-dispatched). These are distinct from `assigned_stranded_dispatch_unconfirmed`
1407
+ * (a dispatch that was NEVER handed off — nothing ran, so a late completion is impossible).
1408
+ *
1409
+ * A re-drive assumes the worker never finished. But for an autoLaunch/worktree worker the
1410
+ * turn-lifecycle events (agent:generating_started/completed) do NOT reliably reach the
1411
+ * coordinator ledger, so the deadline can elapse and re-drive fire while the worker's genuine
1412
+ * completion is merely LATE (observed live: it lands 0.9s–98s AFTER the reclaim). The late
1413
+ * completion must then SUPERSEDE the re-drive rather than be dropped — the completion handler's
1414
+ * flip-miss safety net checks a row reclaimed for one of these reasons within
1415
+ * {@link REDRIVE_SUPERSEDE_WINDOW_MS} of its `requeuedAt`.
1416
+ */
1417
+ export const REDRIVE_RECLAIM_REASONS: ReadonlySet<string> = new Set([
1418
+ 'delivered_no_turn_deadline',
1419
+ 'reclaim_after_unknown_grace',
1420
+ 'delivered_not_consumed_redrive',
1421
+ ]);
1422
+
1423
+ /**
1424
+ * How long after a re-drive reclaim's `requeuedAt` a late completion still supersedes the
1425
+ * re-dispatch. Comfortably covers the observed 0.9s–98s completion-vs-reclaim race with margin,
1426
+ * while staying far short of the time it would take a genuinely fresh re-dispatched turn to
1427
+ * produce its OWN completion — so a real second turn is never mistaken for the superseded one.
1428
+ */
1429
+ export const REDRIVE_SUPERSEDE_WINDOW_MS = 5 * 60_000;
1430
+
1361
1431
  /**
1362
1432
  * Bug B: reclaim a task stuck in 'assigned' because its dispatch was never confirmed.
1363
1433
  *
@@ -83,7 +83,39 @@ function findQuestionLineIndex(
83
83
  lines: string[],
84
84
  ): { index: number; matchedSource: 'primary' | string } | null {
85
85
  const primary = compile(spec.questionPattern, spec.questionFlags ?? 'i');
86
- // Search bottom-up to prefer the most recent modal.
86
+ // A question keyword frequently ALSO appears inside a button label:
87
+ // cursor-agent's Workspace-Trust modal renders `▶ [a] Trust this workspace`
88
+ // and its questionPattern matches the bare word `Trust`; the git-command
89
+ // prompt offers an `Approve`/`Allow`/`Run` button while the pattern lists
90
+ // `approve|Approve|Allow`. A bottom-up scan therefore lands on the BUTTON
91
+ // line (lower on screen) instead of the real prose question above it, and
92
+ // extractButtons (which starts at question.index + 1) then scopes the
93
+ // affirmative button OUT — leaving fewer than minButtons → parseApproval
94
+ // returns null → the approval never surfaces and the session wedges in
95
+ // `starting`/`generating`. Skip lines that are themselves button lines so
96
+ // the search resolves to the prose question, not a button label that merely
97
+ // shares a keyword. This is the general form of the kimi defect-C fix.
98
+ const buttonFlags = spec.buttonFlags && spec.buttonFlags.includes('m')
99
+ ? spec.buttonFlags
100
+ : `${spec.buttonFlags ?? ''}m`;
101
+ const buttonRe = compile(spec.buttonPattern, buttonFlags);
102
+ const isButtonLine = (line: string): boolean => {
103
+ buttonRe.lastIndex = 0;
104
+ return buttonRe.test(line);
105
+ };
106
+ // First pass: prefer a question line that is NOT itself a button line.
107
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
108
+ if (primary.test(lines[i]) && !isButtonLine(lines[i])) return { index: i, matchedSource: 'primary' };
109
+ }
110
+ for (const variant of spec.questionVariants ?? []) {
111
+ const re = compile(variant.regex, variant.flags ?? 'i');
112
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
113
+ if (re.test(lines[i]) && !isButtonLine(lines[i])) return { index: i, matchedSource: variant.label ?? 'variant' };
114
+ }
115
+ }
116
+ // Fallback: no non-button question line found. Accept a button-line match so
117
+ // providers whose question genuinely renders on the button row (rare) still
118
+ // work — behaviour identical to the pre-fix scan.
87
119
  for (let i = lines.length - 1; i >= 0; i -= 1) {
88
120
  if (primary.test(lines[i])) return { index: i, matchedSource: 'primary' };
89
121
  }