@adhdev/daemon-core 0.9.82-rc.507 → 0.9.82-rc.509
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/config/mesh-config.d.ts +12 -0
- package/dist/index.js +285 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +285 -83
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-node-slots.d.ts +22 -0
- package/dist/mesh/mesh-runtime-store.d.ts +2 -2
- package/dist/mesh/mesh-scheduling-runtime.d.ts +5 -1
- package/dist/mesh/mesh-work-queue.d.ts +3 -3
- package/dist/mesh/model-provider-compat.d.ts +44 -0
- package/dist/providers/cli-provider-instance-types.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +54 -0
- package/dist/repo-mesh-types.d.ts +30 -29
- package/package.json +3 -3
- package/src/commands/med-family/mesh-crud.ts +24 -14
- package/src/config/mesh-config.ts +71 -24
- package/src/mesh/coordinator-prompt.ts +26 -16
- package/src/mesh/mesh-node-slots.ts +52 -0
- package/src/mesh/mesh-queue-assignment.ts +43 -24
- package/src/mesh/mesh-runtime-store.ts +3 -3
- package/src/mesh/mesh-scheduling-runtime.ts +24 -9
- package/src/mesh/mesh-work-queue.ts +3 -3
- package/src/mesh/model-provider-compat.ts +73 -0
- package/src/providers/cli-provider-instance-types.ts +15 -0
- package/src/providers/cli-provider-instance.ts +174 -2
- package/src/repo-mesh-types.ts +39 -38
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
COMPLETED_FINALIZATION_RETRY_MS,
|
|
57
57
|
COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
58
58
|
NATIVE_HISTORY_MESH_IDLE_SETTLE_MS,
|
|
59
|
+
PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS,
|
|
59
60
|
BACKGROUND_TASK_HOLD_MAX_MS,
|
|
60
61
|
USER_INPUT_ACK_DEDUP_WINDOW_MS,
|
|
61
62
|
STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS,
|
|
@@ -194,6 +195,40 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
194
195
|
*/
|
|
195
196
|
private static readonly AUTO_APPROVE_MASK_STALL_MS = 10500;
|
|
196
197
|
|
|
198
|
+
/**
|
|
199
|
+
* AUTOAPPROVE-FLAP-INBOX-MISSING: sticky-approval overlay window. Same time-tick
|
|
200
|
+
* hold idea as the FALSE-IDLE completion gate — an approval signal that was
|
|
201
|
+
* DOMINANT within this recent window is re-presented across a momentary busy blip
|
|
202
|
+
* instead of collapsing.
|
|
203
|
+
*
|
|
204
|
+
* RCA (live 2026-07-13): a claude-cli worker sitting at a Bash approval modal
|
|
205
|
+
* ("Do you want to proceed? ❯1.Yes") flaps waiting_approval↔busy on a ~2-3s period.
|
|
206
|
+
* The spec `approval→busy` transition fires whenever the footer/modal approval
|
|
207
|
+
* markers momentarily drop out of their parsed sections while the PRIOR command's
|
|
208
|
+
* residual spinner text ("✳ Checking vendor drift…") still matches the busy regex.
|
|
209
|
+
* On the busy frame the adapter reports status='generating', activeModal=null. That
|
|
210
|
+
* corrupts THREE consumers at once: (1) mesh_active_work samples 'generating' →
|
|
211
|
+
* collectPendingApprovals never sees 'awaiting_approval' → mesh_list_pending_approvals
|
|
212
|
+
* count:0 (inbox miss); (2) the auto-approve settle gate is torn down each busy phase
|
|
213
|
+
* so the 600ms settle never accrues → auto-approve never fires; (3) a mesh_approve
|
|
214
|
+
* landing on a busy frame hits "Not in approval state". The existing FLAP machinery
|
|
215
|
+
* (AUTO_APPROVE_FLAP_CONTINUITY_MS) only keeps the settle gate warm while status is
|
|
216
|
+
* STILL waiting_approval (buttons scrolled out) — it does nothing once the FSM fully
|
|
217
|
+
* commits to 'busy', and it never stabilises the status the inbox samples.
|
|
218
|
+
*
|
|
219
|
+
* Fix: when the raw adapter status flaps to generating/busy/idle but a
|
|
220
|
+
* waiting_approval WITH a concrete modal was observed within this window, overlay
|
|
221
|
+
* the cached modal and report status='waiting_approval'. This stabilized status
|
|
222
|
+
* feeds getState (→ inbox), detectStatusTransition (→ event emission), and
|
|
223
|
+
* maybeAutoApproveStatus (→ settle gate) uniformly, so the approval both registers
|
|
224
|
+
* in the inbox and settles for auto-approve across the flap. Bounded (a genuine
|
|
225
|
+
* resume that never returns to approval unmasks after this window) and scoped at the
|
|
226
|
+
* call site to autonomous mesh sessions. 4000ms bridges the observed ~2-3s flap with
|
|
227
|
+
* margin while staying well under AUTO_APPROVE_MASK_STALL_MS (a truly stalled/absent
|
|
228
|
+
* approval still surfaces).
|
|
229
|
+
*/
|
|
230
|
+
private static readonly APPROVAL_STICKY_FLAP_MS = 4000;
|
|
231
|
+
|
|
197
232
|
/**
|
|
198
233
|
* FALSE-IDLE (inter-approval quiet valley): grace window after an auto-approve
|
|
199
234
|
* (or mesh_approve) RESOLVES a modal during which a subsequent generating→idle
|
|
@@ -298,6 +333,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
298
333
|
// signature) while a genuinely closed modal — buttons empty continuously past
|
|
299
334
|
// the continuity window — is still recognised and resets the gate.
|
|
300
335
|
private autoApproveLastModalSeenAt = 0;
|
|
336
|
+
// AUTOAPPROVE-FLAP-INBOX-MISSING sticky-approval overlay (see APPROVAL_STICKY_FLAP_MS).
|
|
337
|
+
// The wall-clock of the last frame where the RAW adapter reported waiting_approval with
|
|
338
|
+
// a CONCRETE modal (buttons present), the cached modal to re-present across a busy blip,
|
|
339
|
+
// and the approvalEntrySeq at that frame (so a stabilized frame carries the right seq to
|
|
340
|
+
// the emission-dedup fingerprint). All zero/null when no recent concrete approval.
|
|
341
|
+
private approvalStickyLastConcreteAt = 0;
|
|
342
|
+
private approvalStickyModal: { message?: string; buttons?: unknown[]; kind?: string | null } | null = null;
|
|
343
|
+
private approvalStickyEntrySeq = 0;
|
|
301
344
|
// STATUS-MISMATCH: wall-clock when the CURRENT auto-approve episode (waiting_approval
|
|
302
345
|
// + shouldAutoApprove) first began wanting to mask. Unlike pendingAutoApprovalSince it
|
|
303
346
|
// is NOT reset when the modal signature changes (a still-streaming/flapping prompt) and
|
|
@@ -533,7 +576,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
533
576
|
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
534
577
|
// parseSession), with provider-loader.ts updated to store script source strings
|
|
535
578
|
// alongside the loaded function references for extended-legacy providers.
|
|
536
|
-
|
|
579
|
+
// AUTOAPPROVE-FLAP-INBOX-MISSING: apply the same sticky-approval overlay the
|
|
580
|
+
// FSM path uses so the status this getState() surfaces to the mesh probe (and
|
|
581
|
+
// thus mesh_active_work → the pending-approval inbox) stays waiting_approval
|
|
582
|
+
// across a busy flap frame, instead of momentarily reading generating (count:0).
|
|
583
|
+
const adapterStatus = this.stabilizeFlappingApprovalStatus(this.adapter.getStatus());
|
|
537
584
|
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
538
585
|
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
539
586
|
}
|
|
@@ -1753,7 +1800,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1753
1800
|
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1754
1801
|
return null;
|
|
1755
1802
|
}
|
|
1803
|
+
// (FALSE-IDLE-MIDTURN antigravity) A native-history session whose
|
|
1804
|
+
// external-native transcript has NO in-turn final assistant bubble yet
|
|
1805
|
+
// (present=false AND the probe's tail is not an assistant reply) is NOT
|
|
1806
|
+
// proven done. antigravity previously returned null here — an IMMEDIATE
|
|
1807
|
+
// clean emit with zero transcript evidence — so a momentary PTY-parser
|
|
1808
|
+
// idle blip MID-TURN (the parser reads idle while the turn is still in
|
|
1809
|
+
// flight and the transcript's answer has not landed) fired an early
|
|
1810
|
+
// agent:generating_completed the coordinator could never correct. Instead
|
|
1811
|
+
// HOLD for the transcript: re-probe each retry (readExternalCompletionMessages
|
|
1812
|
+
// runs forceRefresh every call) and clear the block only once the assistant
|
|
1813
|
+
// turn actually lands (the probe branch above → genuine emit). Non-terminal
|
|
1814
|
+
// and bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS (30s) so a turn that
|
|
1815
|
+
// genuinely produced no assistant bubble (tool-only) still force-emits a weak
|
|
1816
|
+
// completion rather than wedging — preserving the a0fb6b05 "antigravity always
|
|
1817
|
+
// eventually emits" fix while filtering the mid-turn false-idle. Scoped to
|
|
1818
|
+
// autonomous mesh sessions (allowMissingAssistantTimeout) so an interactive
|
|
1819
|
+
// antigravity session, which has no coordinator to misfire at, is untouched.
|
|
1756
1820
|
if (this.type === 'antigravity-cli') {
|
|
1821
|
+
if (allowMissingAssistantTimeout) {
|
|
1822
|
+
return { reason: 'missing_final_assistant', terminal: false, holdForTranscript: true };
|
|
1823
|
+
}
|
|
1757
1824
|
return null;
|
|
1758
1825
|
}
|
|
1759
1826
|
// (SETTLE-VALLEY) The inter-approval idle valley: a native-history mesh worker
|
|
@@ -1809,6 +1876,40 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1809
1876
|
}
|
|
1810
1877
|
} catch { /* defensive: screen text read is best-effort */ }
|
|
1811
1878
|
|
|
1879
|
+
// (FALSE-IDLE-MIDTURN codex/PTY) The turn-complete quiet-dwell gate. We only reach
|
|
1880
|
+
// here with finalAssistantEvidence.present === true. For a PTY-PARSED provider
|
|
1881
|
+
// (codex: !adapterOwnsMessagesElsewhere), that "present" verdict is derived from the
|
|
1882
|
+
// on-screen assistant text, which can be a PARTIAL sentence fragment captured mid-stream
|
|
1883
|
+
// when the FSM momentarily read idle. completionHasFinalAssistantMessage accepts the
|
|
1884
|
+
// fragment and hasAdapterPendingResponse can transiently read clean between chunks, so
|
|
1885
|
+
// present flips true mid-turn and this path would clean-emit an early completion. The
|
|
1886
|
+
// flush's lastOutputAt continuity guard only cancels when NEW output ARRIVES during the
|
|
1887
|
+
// settle — it cannot catch a turn that fell quiet just before the arm. Require instead a
|
|
1888
|
+
// minimum QUIET DWELL since the last raw PTY output: a genuinely finished turn's screen
|
|
1889
|
+
// has been stable well past this bound, whereas a mid-stream fragment either just received
|
|
1890
|
+
// output or is about to. Non-terminal HOLD (bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS),
|
|
1891
|
+
// so a real completion re-passes the gate one retry later once the dwell is met; and it
|
|
1892
|
+
// force-emits at the 30s cap rather than wedging. Scoped to autonomous mesh sessions
|
|
1893
|
+
// (allowMissingAssistantTimeout) and PTY-parsed sources only — native-history providers
|
|
1894
|
+
// (antigravity/claude) resolve evidence from the authoritative transcript above, not the
|
|
1895
|
+
// screen, so this dwell does not apply to them and interactive sessions are untouched.
|
|
1896
|
+
if (allowMissingAssistantTimeout
|
|
1897
|
+
&& !adapterOwnsMessagesElsewhere
|
|
1898
|
+
&& finalAssistantEvidence.source === 'parsed') {
|
|
1899
|
+
try {
|
|
1900
|
+
const outStatus = this.adapter.getStatus({ allowParse: false }) as any;
|
|
1901
|
+
const lastOutputAt = typeof outStatus?.lastOutputAt === 'number' && Number.isFinite(outStatus.lastOutputAt)
|
|
1902
|
+
? outStatus.lastOutputAt as number
|
|
1903
|
+
: undefined;
|
|
1904
|
+
if (typeof lastOutputAt === 'number') {
|
|
1905
|
+
const quietMs = Date.now() - lastOutputAt;
|
|
1906
|
+
if (quietMs < PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS) {
|
|
1907
|
+
return { reason: 'parsed_final_assistant_quiet_dwell', terminal: false };
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
} catch { /* defensive: dwell read is best-effort — fall through to emit */ }
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1812
1913
|
return null;
|
|
1813
1914
|
}
|
|
1814
1915
|
|
|
@@ -2342,6 +2443,72 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2342
2443
|
});
|
|
2343
2444
|
}
|
|
2344
2445
|
|
|
2446
|
+
/**
|
|
2447
|
+
* AUTOAPPROVE-FLAP-INBOX-MISSING sticky-approval overlay. Returns the adapterStatus a
|
|
2448
|
+
* flap-prone claude-cli approval SHOULD present this frame — either the raw status
|
|
2449
|
+
* unchanged, or, when the raw status has momentarily flapped OFF a recently-dominant
|
|
2450
|
+
* concrete approval, a synthetic `waiting_approval` re-presenting the cached modal.
|
|
2451
|
+
*
|
|
2452
|
+
* Records the concrete approval whenever the raw status is waiting_approval WITH
|
|
2453
|
+
* buttons. On a subsequent non-approval frame (the spec `approval→busy` flap), if that
|
|
2454
|
+
* concrete approval was seen within APPROVAL_STICKY_FLAP_MS AND the engine has NOT
|
|
2455
|
+
* resolved a modal since (lastApprovalResolvedAt not advanced past the sticky start),
|
|
2456
|
+
* overlay the cached modal + waiting_approval so the inbox / auto-approve / mesh_approve
|
|
2457
|
+
* all see the stable approval. A genuine resolution (auto-approve or mesh_approve fires
|
|
2458
|
+
* resolveModal → lastApprovalResolvedAt advances) clears the sticky immediately, so a
|
|
2459
|
+
* legitimate post-approval resume is NEVER masked as a lingering approval. Bounded by the
|
|
2460
|
+
* window, and scoped to autonomous mesh sessions (a foreground/attended or non-mesh
|
|
2461
|
+
* session, where a human answers the prompt, is returned untouched).
|
|
2462
|
+
*/
|
|
2463
|
+
private stabilizeFlappingApprovalStatus(adapterStatus: any, now = Date.now()): any {
|
|
2464
|
+
// Only autonomous auto-approving mesh sessions are subject to the delegated flap;
|
|
2465
|
+
// never overlay for attended/foreground/non-mesh sessions.
|
|
2466
|
+
if (!this.isAutonomousMeshSession() || !this.shouldAutoApprove()) return adapterStatus;
|
|
2467
|
+
|
|
2468
|
+
const rawStatus = adapterStatus?.status;
|
|
2469
|
+
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
2470
|
+
? (this.adapter as any).lastApprovalResolvedAt as number
|
|
2471
|
+
: 0;
|
|
2472
|
+
|
|
2473
|
+
if (rawStatus === 'waiting_approval') {
|
|
2474
|
+
// A concrete modal this frame refreshes the sticky anchor; an approval frame
|
|
2475
|
+
// with buttons momentarily scrolled out is left to the existing settle-gate
|
|
2476
|
+
// hysteresis (we do not touch it — status is already waiting_approval).
|
|
2477
|
+
if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal)) {
|
|
2478
|
+
this.approvalStickyLastConcreteAt = now;
|
|
2479
|
+
this.approvalStickyModal = adapterStatus.activeModal;
|
|
2480
|
+
this.approvalStickyEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
2481
|
+
? adapterStatus.approvalEntrySeq
|
|
2482
|
+
: this.approvalStickyEntrySeq;
|
|
2483
|
+
}
|
|
2484
|
+
return adapterStatus;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// Non-approval frame. Overlay only if a concrete approval was dominant within the
|
|
2488
|
+
// window AND no resolution has happened since the sticky anchor (a resolveModal
|
|
2489
|
+
// advances lastApprovalResolvedAt to at/after the anchor → the flap is really a
|
|
2490
|
+
// genuine resume, so drop the sticky and report the raw status).
|
|
2491
|
+
if (this.approvalStickyLastConcreteAt > 0 && this.approvalStickyModal) {
|
|
2492
|
+
const withinWindow = (now - this.approvalStickyLastConcreteAt) < CliProviderInstance.APPROVAL_STICKY_FLAP_MS;
|
|
2493
|
+
const resolvedSinceAnchor = resolvedAt >= this.approvalStickyLastConcreteAt;
|
|
2494
|
+
if (withinWindow && !resolvedSinceAnchor) {
|
|
2495
|
+
return {
|
|
2496
|
+
...adapterStatus,
|
|
2497
|
+
status: 'waiting_approval',
|
|
2498
|
+
activeModal: this.approvalStickyModal,
|
|
2499
|
+
...(this.approvalStickyEntrySeq ? { approvalEntrySeq: this.approvalStickyEntrySeq } : {}),
|
|
2500
|
+
approvalStickyOverlay: true,
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
// Window lapsed or a resolution landed — clear the sticky so a later approval
|
|
2504
|
+
// re-anchors from scratch and a genuine resume surfaces immediately.
|
|
2505
|
+
this.approvalStickyLastConcreteAt = 0;
|
|
2506
|
+
this.approvalStickyModal = null;
|
|
2507
|
+
this.approvalStickyEntrySeq = 0;
|
|
2508
|
+
}
|
|
2509
|
+
return adapterStatus;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2345
2512
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
2346
2513
|
// Manual-attendance suppression (provider-common): when a human is
|
|
2347
2514
|
// Manual-attendance suppression (provider-common): when a human is
|
|
@@ -2777,7 +2944,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2777
2944
|
// Status-change handling is a hot path: PTY output can fire it many times
|
|
2778
2945
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
2779
2946
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
2780
|
-
|
|
2947
|
+
// AUTOAPPROVE-FLAP-INBOX-MISSING: stabilize a flap-prone approval BEFORE it feeds
|
|
2948
|
+
// maybeAutoApproveStatus / newStatus / the waiting_approval emission branch below,
|
|
2949
|
+
// so a momentary busy blip during the flap re-presents the cached modal + status
|
|
2950
|
+
// rather than emitting generating (which would corrupt the inbox and tear down the
|
|
2951
|
+
// settle gate). No-op for non-mesh/foreground/non-approval frames.
|
|
2952
|
+
const adapterStatus = this.stabilizeFlappingApprovalStatus(this.adapter.getStatus({ allowParse: false }), now);
|
|
2781
2953
|
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
2782
2954
|
this.provider,
|
|
2783
2955
|
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -388,16 +388,15 @@ export interface RepoMeshRelatedRepo {
|
|
|
388
388
|
* must satisfy both. Omitting `maxParallel` means this provider is bounded only
|
|
389
389
|
* by the global/taskMode caps (full backward compatibility).
|
|
390
390
|
*
|
|
391
|
-
* Routing is governed exclusively by required_tags (see nodeSatisfiesRequiredTags)
|
|
392
|
-
*
|
|
393
|
-
*
|
|
391
|
+
* Routing is governed exclusively by required_tags (see nodeSatisfiesRequiredTags).
|
|
392
|
+
* To route work to a specific node, advertise an ordinary capability tag on the
|
|
393
|
+
* node and require it on the task.
|
|
394
|
+
*
|
|
395
|
+
* NOTE: the per-(node, provider) parallelism cap now lives on `slots[].maxParallel`
|
|
396
|
+
* (see NodeCapabilitySlot). The former `providerRoles` field has been removed; a
|
|
397
|
+
* persisted meshes.json that still carries it is migrated to `slots` on load
|
|
398
|
+
* (see migrateLoadedMeshConfig).
|
|
394
399
|
*/
|
|
395
|
-
export interface RepoMeshProviderRole {
|
|
396
|
-
/** Provider type this entry governs (e.g. 'claude-cli', 'codex-cli'). */
|
|
397
|
-
providerType: string;
|
|
398
|
-
/** Max concurrent active tasks for this (node, provider). Omit = no per-provider cap. */
|
|
399
|
-
maxParallel?: number;
|
|
400
|
-
}
|
|
401
400
|
|
|
402
401
|
export interface RepoMeshNodePolicy {
|
|
403
402
|
readOnly?: boolean;
|
|
@@ -413,26 +412,14 @@ export interface RepoMeshNodePolicy {
|
|
|
413
412
|
schedulingPriority?: number;
|
|
414
413
|
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
415
414
|
providerPriority?: string[];
|
|
416
|
-
/**
|
|
417
|
-
* Per-(node, provider) parallelism declarations. Each entry binds a
|
|
418
|
-
* providerType on THIS node to an optional maxParallel cap. maxParallel is
|
|
419
|
-
* enforced as an additional, stricter-wins constraint on top of the global
|
|
420
|
-
* maxParallelTasks/taskMode caps. Missing/empty: the node behaves exactly as
|
|
421
|
-
* before (global caps only). Routing is governed solely by required_tags.
|
|
422
|
-
*
|
|
423
|
-
* SUPERSEDED by `slots` (ORCHESTRATION_NODE_SLOTS.md). Kept for back-compat:
|
|
424
|
-
* when `slots` is absent, providerRoles + providerPriority + the machine-global
|
|
425
|
-
* difficultyBrains are auto-derived into slots via deriveSlotsFromLegacy.
|
|
426
|
-
*/
|
|
427
|
-
providerRoles?: RepoMeshProviderRole[];
|
|
428
415
|
/**
|
|
429
416
|
* Node capability slots (ORCHESTRATION_NODE_SLOTS.md) — the ordered "Preferred
|
|
430
417
|
* AI tools" profile that is the single source of truth for task routing, MAGI
|
|
431
418
|
* fan-out, and orchestrator-proposed edits. Each slot bundles provider + model
|
|
432
419
|
* + thinkingLevel + difficulty range + capability tags + per-slot maxParallel.
|
|
433
420
|
* Order = preference. When absent, the scheduler derives slots from the legacy
|
|
434
|
-
* providerPriority/
|
|
435
|
-
*
|
|
421
|
+
* providerPriority/difficultyBrains (deriveSlotsFromLegacy) so existing nodes
|
|
422
|
+
* keep working without reconfiguration.
|
|
436
423
|
*/
|
|
437
424
|
slots?: NodeCapabilitySlot[];
|
|
438
425
|
/**
|
|
@@ -696,29 +683,38 @@ export function resolveDelegatedWorkerAutoApprove(
|
|
|
696
683
|
}
|
|
697
684
|
|
|
698
685
|
/**
|
|
699
|
-
* Resolve the enforced per-(node, provider) maxParallel cap
|
|
700
|
-
*
|
|
701
|
-
* claim path as a stricter-wins constraint layered on top of the global
|
|
702
|
-
* Case-insensitive, trimmed match on
|
|
703
|
-
*
|
|
686
|
+
* Resolve the enforced per-(node, provider) maxParallel cap from a node's resolved
|
|
687
|
+
* capability slots, or undefined when no matching slot declares a finite cap. Used
|
|
688
|
+
* by the queue claim path as a stricter-wins constraint layered on top of the global
|
|
689
|
+
* caps. Case-insensitive, trimmed match on the slot's provider.
|
|
690
|
+
*
|
|
691
|
+
* When a node declares multiple slots for the same provider (e.g. distinct
|
|
692
|
+
* difficulty ranges), their caps SUM into a single per-(node, provider) pool — the
|
|
693
|
+
* provider can run up to the total across all its slots. Legacy-derived slots (via
|
|
694
|
+
* deriveSlotsFromLegacy) produce one slot per provider, so the sum equals that
|
|
695
|
+
* single slot's cap and behavior is preserved exactly.
|
|
696
|
+
*
|
|
697
|
+
* Callers pass the already-resolved slots (explicit policy.slots, else legacy-
|
|
698
|
+
* derived) — this keeps the resolver free of the difficultyBrains dependency and
|
|
699
|
+
* usable from any layer.
|
|
704
700
|
*/
|
|
705
701
|
export function resolveProviderMaxParallel(
|
|
706
|
-
|
|
702
|
+
slots: NodeCapabilitySlot[] | null | undefined,
|
|
707
703
|
providerType: string | null | undefined,
|
|
708
704
|
): number | undefined {
|
|
709
705
|
const wanted = typeof providerType === 'string' ? providerType.trim().toLowerCase() : '';
|
|
710
706
|
if (!wanted) return undefined;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
for (const
|
|
714
|
-
if (!
|
|
715
|
-
const type = typeof
|
|
707
|
+
if (!Array.isArray(slots)) return undefined;
|
|
708
|
+
let total: number | undefined;
|
|
709
|
+
for (const slot of slots) {
|
|
710
|
+
if (!slot || typeof slot !== 'object') continue;
|
|
711
|
+
const type = typeof slot.provider === 'string' ? slot.provider.trim().toLowerCase() : '';
|
|
716
712
|
if (!type || type !== wanted) continue;
|
|
717
|
-
const raw = Number(
|
|
718
|
-
if (!Number.isFinite(raw) || raw < 0)
|
|
719
|
-
|
|
713
|
+
const raw = Number(slot.maxParallel);
|
|
714
|
+
if (!Number.isFinite(raw) || raw < 0) continue;
|
|
715
|
+
total = (total ?? 0) + Math.floor(raw);
|
|
720
716
|
}
|
|
721
|
-
return
|
|
717
|
+
return total;
|
|
722
718
|
}
|
|
723
719
|
|
|
724
720
|
// ─── Capabilities ───────────────────────────────
|
|
@@ -987,6 +983,11 @@ export interface RepoMeshNodeSchedulingStatus {
|
|
|
987
983
|
load: number;
|
|
988
984
|
schedulingPriority?: number;
|
|
989
985
|
maxConcurrentSessions?: number;
|
|
986
|
+
/**
|
|
987
|
+
* Per-(node, provider) caps + consumption. Field name kept for dashboard
|
|
988
|
+
* back-compat; the cap source is now slots[].maxParallel (the removed
|
|
989
|
+
* policy.providerRoles no longer exists).
|
|
990
|
+
*/
|
|
990
991
|
providerRoles?: RepoMeshNodeProviderSchedulingStatus[];
|
|
991
992
|
capReached: boolean;
|
|
992
993
|
capReasons?: string[];
|