@adhdev/daemon-core 0.9.82-rc.536 → 0.9.82-rc.538
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/chat/source-resolver.d.ts +21 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +13 -0
- package/dist/index.js +344 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -17
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-completion-synthesis.d.ts +14 -0
- package/dist/mesh/mesh-work-queue.d.ts +22 -0
- package/package.json +3 -3
- package/src/chat/source-resolver.ts +0 -0
- package/src/cli-adapters/cli-state-engine.ts +51 -1
- package/src/cli-adapters/provider-cli-adapter.ts +7 -0
- package/src/commands/chat-commands-read.ts +103 -7
- package/src/mesh/mesh-completion-synthesis.ts +85 -0
- package/src/mesh/mesh-event-forwarding.ts +96 -1
- package/src/mesh/mesh-queue-assignment.ts +7 -3
- package/src/mesh/mesh-reconcile-loop.ts +53 -2
- package/src/mesh/mesh-work-queue.ts +74 -4
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +108 -1
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +33 -1
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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
|
-
...(
|
|
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
|
*
|
|
@@ -227,6 +227,108 @@ function modalMatches(spec: ModalSpec, input: CliStatusInput): boolean {
|
|
|
227
227
|
return false;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Index (line number) of the last line in `screenText` that carries a modal
|
|
232
|
+
* cue — question line, question variant, or a button-block label line — or -1
|
|
233
|
+
* if none. Used to detect a *stale* modal box: some CLIs (e.g. cursor-agent's
|
|
234
|
+
* "Workspace Trust Required" prompt) never clear their box rows after the user
|
|
235
|
+
* answers. The redraw that replaces the modal with the idle composer is shorter
|
|
236
|
+
* than the box, so the top modal rows linger in the terminal grid. Without a
|
|
237
|
+
* spatial check, the unscoped whole-screen `modalMatches` keeps firing
|
|
238
|
+
* `waiting_approval` forever and the session wedges in `starting` — the
|
|
239
|
+
* startup gate never releases because `detectStatus` never returns `idle`.
|
|
240
|
+
*/
|
|
241
|
+
function lastModalCueLine(spec: ModalSpec, screenText: string): number {
|
|
242
|
+
if (!screenText) return -1;
|
|
243
|
+
const lines = screenText.split('\n');
|
|
244
|
+
const question = compile(spec.questionPattern, spec.questionFlags ?? 'i');
|
|
245
|
+
const variants = (spec.questionVariants ?? []).map((v) => compile(v.regex, v.flags ?? 'i'));
|
|
246
|
+
const buttonFlags = spec.buttonFlags && spec.buttonFlags.includes('m')
|
|
247
|
+
? spec.buttonFlags
|
|
248
|
+
: `${spec.buttonFlags ?? ''}m`;
|
|
249
|
+
const buttonRe = compile(spec.buttonPattern, buttonFlags);
|
|
250
|
+
let last = -1;
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
const line = lines[i];
|
|
253
|
+
question.lastIndex = 0;
|
|
254
|
+
if (question.test(line)) { last = i; continue; }
|
|
255
|
+
if (variants.some((re) => { re.lastIndex = 0; return re.test(line); })) { last = i; continue; }
|
|
256
|
+
buttonRe.lastIndex = 0;
|
|
257
|
+
if (buttonRe.test(line)) { last = i; continue; }
|
|
258
|
+
}
|
|
259
|
+
return last;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* True when the modal cue is *stale* — a leftover box the CLI failed to clear —
|
|
264
|
+
* because the live idle composer has repainted BELOW it.
|
|
265
|
+
*
|
|
266
|
+
* The discriminator is spatial, so a live modal (whose own selection cursor /
|
|
267
|
+
* button block can incidentally match a settled-prompt regex) is NOT mistaken
|
|
268
|
+
* for stale:
|
|
269
|
+
*
|
|
270
|
+
* - A settled-prompt cue must match strictly BELOW the last modal cue line.
|
|
271
|
+
* - AND at least one non-blank line between them is neither a modal cue nor
|
|
272
|
+
* part of the settled-prompt match itself (the separator prose).
|
|
273
|
+
*
|
|
274
|
+
* A live modal renders its question + button block FLUSH against its own
|
|
275
|
+
* composer/selection cursor (no intervening prose). A stale box, by contrast,
|
|
276
|
+
* has the CLI's welcome banner / follow-up hint / mode footer repainted between
|
|
277
|
+
* the leftover box rows and the live composer. So the discriminator is: a
|
|
278
|
+
* settled-prompt cue matches strictly BELOW the last modal cue line AND at least
|
|
279
|
+
* one non-blank, non-modal line separates them. That separator is exactly the
|
|
280
|
+
* content a live modal never has between its buttons and its cursor, and it is
|
|
281
|
+
* robust to the terminal-snapshot append that can shuffle the tail window.
|
|
282
|
+
*/
|
|
283
|
+
function modalSupersededBySettledPrompt(
|
|
284
|
+
modalSpec: ModalSpec,
|
|
285
|
+
settledSpec: SettledPromptSpec | undefined,
|
|
286
|
+
settled: ReturnType<typeof compileSettledPromptMatchers> | null,
|
|
287
|
+
input: CliStatusInput,
|
|
288
|
+
): boolean {
|
|
289
|
+
if (!settled || !settledSpec) return false;
|
|
290
|
+
if (settledSpec.scope === 'whole-screen') return false;
|
|
291
|
+
const screenText = input.screenText ?? '';
|
|
292
|
+
if (!screenText) return false;
|
|
293
|
+
const modalLine = lastModalCueLine(modalSpec, screenText);
|
|
294
|
+
if (modalLine < 0) return false;
|
|
295
|
+
const lines = screenText.split('\n');
|
|
296
|
+
const below = lines.slice(modalLine + 1);
|
|
297
|
+
if (below.length === 0) return false;
|
|
298
|
+
// A settled prompt (composer) must render somewhere below the modal box.
|
|
299
|
+
const belowText = below.join('\n');
|
|
300
|
+
if (!settled.prompt.test(belowText)) return false;
|
|
301
|
+
if (settled.footers.length > 0 && !settled.footers.every((f) => f.test(belowText))) return false;
|
|
302
|
+
// Require a real separator between the leftover box and the composer: a
|
|
303
|
+
// non-blank line that is neither a modal cue NOR part of the settled-prompt
|
|
304
|
+
// match itself. That separator is the CLI's welcome banner / follow-up hint /
|
|
305
|
+
// mode footer a stale box shows above the repainted composer. A live modal's
|
|
306
|
+
// selection cursor sits flush against its buttons with no such prose between
|
|
307
|
+
// them (and the cursor line, even if it matches the settled regex, is not a
|
|
308
|
+
// separator), so an active modal is never misread as stale.
|
|
309
|
+
const question = compile(modalSpec.questionPattern, modalSpec.questionFlags ?? 'i');
|
|
310
|
+
const variants = (modalSpec.questionVariants ?? []).map((v) => compile(v.regex, v.flags ?? 'i'));
|
|
311
|
+
const buttonFlags = modalSpec.buttonFlags && modalSpec.buttonFlags.includes('m')
|
|
312
|
+
? modalSpec.buttonFlags
|
|
313
|
+
: `${modalSpec.buttonFlags ?? ''}m`;
|
|
314
|
+
const buttonRe = compile(modalSpec.buttonPattern, buttonFlags);
|
|
315
|
+
const isModalCueLine = (line: string): boolean => {
|
|
316
|
+
question.lastIndex = 0;
|
|
317
|
+
if (question.test(line)) return true;
|
|
318
|
+
if (variants.some((re) => { re.lastIndex = 0; return re.test(line); })) return true;
|
|
319
|
+
buttonRe.lastIndex = 0;
|
|
320
|
+
return buttonRe.test(line);
|
|
321
|
+
};
|
|
322
|
+
// A single-line settled regex would let a lone match count as its own line;
|
|
323
|
+
// test each below-line against the prompt regex on that line alone.
|
|
324
|
+
const settledPromptLineRe = compile(settledSpec.regex, (settledSpec.flags ?? 'm').includes('m') ? (settledSpec.flags ?? 'm') : `${settledSpec.flags ?? ''}m`);
|
|
325
|
+
const isSettledLine = (line: string): boolean => {
|
|
326
|
+
settledPromptLineRe.lastIndex = 0;
|
|
327
|
+
return settledPromptLineRe.test(line);
|
|
328
|
+
};
|
|
329
|
+
return below.some((line) => line.trim() !== '' && !isModalCueLine(line) && !isSettledLine(line));
|
|
330
|
+
}
|
|
331
|
+
|
|
230
332
|
// ─── Public builder ────────────────────────────────────────────────────
|
|
231
333
|
|
|
232
334
|
const DEFAULT_ORDER: DispatchGroup[] = ['spinner', 'modal', 'settled-prompt'];
|
|
@@ -248,7 +350,12 @@ function evaluateGroup(
|
|
|
248
350
|
}
|
|
249
351
|
case 'modal': {
|
|
250
352
|
if (!spec.modal) return null;
|
|
251
|
-
|
|
353
|
+
if (!modalMatches(spec.modal, input)) return null;
|
|
354
|
+
// A modal cue with the live composer repainted below it (in the settled
|
|
355
|
+
// prompt's own tail scope) is a stale box the CLI failed to clear — yield
|
|
356
|
+
// so settled-prompt/idle can win.
|
|
357
|
+
if (modalSupersededBySettledPrompt(spec.modal, spec.settledPrompt, compiled.settled, input)) return null;
|
|
358
|
+
return 'waiting_approval';
|
|
252
359
|
}
|
|
253
360
|
case 'settled-prompt': {
|
|
254
361
|
if (!spec.settledPrompt || !compiled.settled) return null;
|
|
@@ -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
|
-
//
|
|
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
|
}
|