@adhdev/daemon-core 0.9.82-rc.320 → 0.9.82-rc.322
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/commands/router.d.ts +13 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +56 -177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +55 -172
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +0 -22
- package/dist/repo-mesh-types.d.ts +12 -100
- package/dist/shared-types.d.ts +9 -0
- package/package.json +2 -2
- package/src/commands/router.ts +30 -11
- package/src/config/mesh-config.ts +61 -73
- package/src/index.ts +2 -8
- package/src/mesh/coordinator-prompt.ts +5 -6
- package/src/mesh/mesh-work-queue.ts +5 -125
- package/src/repo-mesh-types.ts +21 -159
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
3
|
-
import type { RepoMeshDaemonRole
|
|
4
|
-
import { MESH_CONVERGE_REFINE_TAG, resolveAutoConvergeCodeChange
|
|
3
|
+
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
4
|
+
import { MESH_CONVERGE_REFINE_TAG, resolveAutoConvergeCodeChange } from '../repo-mesh-types.js';
|
|
5
5
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
6
6
|
import { getMesh } from '../config/mesh-config.js';
|
|
7
7
|
|
|
@@ -204,45 +204,6 @@ function firstProviderPriority(policy: unknown): string | undefined {
|
|
|
204
204
|
return raw.find(type => typeof type === 'string' && type.trim())?.trim();
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
/**
|
|
208
|
-
* Synthetic `role=<x>` capability tags advertised by a node's policy.providerRoles.
|
|
209
|
-
*
|
|
210
|
-
* When a specific `providerType` is being evaluated, only that provider's declared
|
|
211
|
-
* role is emitted — so role-based routing (requiredTags: ["role=validation"]) gates
|
|
212
|
-
* the *selected* provider through the ordinary capability-tag filter. When no
|
|
213
|
-
* provider is selected (node-level eligibility scan), every declared role is emitted
|
|
214
|
-
* so the node passes the filter if ANY of its providers could satisfy the role; the
|
|
215
|
-
* per-provider tag set then narrows it during provider selection.
|
|
216
|
-
*
|
|
217
|
-
* Roles are lowercased and deduped. Missing/empty providerRoles emits nothing, so a
|
|
218
|
-
* node that never declares roles advertises no `role=` tags and is therefore only
|
|
219
|
-
* matched by role-unconstrained tasks (full backward compatibility).
|
|
220
|
-
*/
|
|
221
|
-
function roleCapabilityTags(policy: unknown, providerType: string | undefined): string[] {
|
|
222
|
-
const roles = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
223
|
-
? (policy as Record<string, unknown>).providerRoles
|
|
224
|
-
: undefined;
|
|
225
|
-
if (!Array.isArray(roles)) return [];
|
|
226
|
-
const wantedProvider = typeof providerType === 'string' && providerType.trim()
|
|
227
|
-
? providerType.trim().toLowerCase()
|
|
228
|
-
: '';
|
|
229
|
-
const out: string[] = [];
|
|
230
|
-
for (const entry of roles) {
|
|
231
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
232
|
-
const type = typeof (entry as any).providerType === 'string'
|
|
233
|
-
? (entry as any).providerType.trim().toLowerCase()
|
|
234
|
-
: '';
|
|
235
|
-
const role = typeof (entry as any).role === 'string'
|
|
236
|
-
? (entry as any).role.trim().toLowerCase()
|
|
237
|
-
: '';
|
|
238
|
-
if (!role) continue;
|
|
239
|
-
// When narrowing to a selected provider, only emit that provider's role.
|
|
240
|
-
if (wantedProvider && type && type !== wantedProvider) continue;
|
|
241
|
-
out.push(`role=${role}`);
|
|
242
|
-
}
|
|
243
|
-
return out;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
207
|
export function buildMeshNodeCapabilityTags(
|
|
247
208
|
node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown } | undefined,
|
|
248
209
|
providerType?: string,
|
|
@@ -273,13 +234,6 @@ export function buildMeshNodeCapabilityTags(
|
|
|
273
234
|
// the load-balancing scheduler auto-injects converge=refine for code_change
|
|
274
235
|
// so such work is hard-filtered onto refine-capable nodes.
|
|
275
236
|
...(node?.isLocalWorktree === true ? ['converge=refine'] : ['converge=fast_forward']),
|
|
276
|
-
// Role-based routing: advertise role=<x> for each (node, provider) role
|
|
277
|
-
// declared in policy.providerRoles. Narrowed to the selected provider when
|
|
278
|
-
// one is given so the chosen provider must match a task's required role;
|
|
279
|
-
// when no provider is selected, all declared roles are advertised for the
|
|
280
|
-
// node-level eligibility scan. Reuses the ordinary required-tags filter —
|
|
281
|
-
// no separate role field/gate.
|
|
282
|
-
...roleCapabilityTags(node?.policy, providerType),
|
|
283
237
|
]);
|
|
284
238
|
}
|
|
285
239
|
|
|
@@ -328,67 +282,6 @@ export function resolveConvergeRequiredTags(
|
|
|
328
282
|
return normalizeMeshCapabilityTags([...explicitRequiredTags, MESH_CONVERGE_REFINE_TAG]);
|
|
329
283
|
}
|
|
330
284
|
|
|
331
|
-
/**
|
|
332
|
-
* Whether any node in the mesh advertises the given `role=<role>` capability tag.
|
|
333
|
-
* Reuses buildMeshNodeCapabilityTags (node-level scan: every declared providerRole is
|
|
334
|
-
* advertised) so the membership test matches the exact tag the claim/eligibility filter
|
|
335
|
-
* would see. Used to keep task-affinity SOFT — if no node can satisfy the role, the
|
|
336
|
-
* injection is skipped so the task falls back to ordinary least_loaded eligibility
|
|
337
|
-
* rather than being blocked.
|
|
338
|
-
*/
|
|
339
|
-
function meshHasNodeAdvertisingRole(nodes: LocalMeshNodeEntry[] | undefined, role: string): boolean {
|
|
340
|
-
if (!Array.isArray(nodes) || nodes.length === 0) return false;
|
|
341
|
-
const wanted = `role=${role}`;
|
|
342
|
-
return nodes.some(node => buildMeshNodeCapabilityTags(node as any).includes(wanted));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Declarative task_mode → role affinity resolution (precedent: resolveConvergeRequiredTags).
|
|
347
|
-
*
|
|
348
|
-
* At enqueue time, auto-inject a `role=<role>` required tag derived from the task's
|
|
349
|
-
* taskMode via the mesh's policy.taskAffinity (falling back to DEFAULT_TASKMODE_ROLE_MAP),
|
|
350
|
-
* so the task hard-filters onto nodes advertising that role through the ordinary
|
|
351
|
-
* required-tags path (nodeSatisfiesRequiredTags) — no claim/scheduler change needed.
|
|
352
|
-
*
|
|
353
|
-
* Strict precedence / backward compatibility — the injection is skipped (returns the
|
|
354
|
-
* explicit tags unchanged) when ANY of:
|
|
355
|
-
* - the caller pinned an explicit targetNodeId (operator chose the node), or
|
|
356
|
-
* - the caller supplied their own non-empty required_tags (caller's routing wins), or
|
|
357
|
-
* - affinity resolves to no role (policy disabled, blank override, or unknown mode), or
|
|
358
|
-
* - SOFT fallback: no node in the mesh advertises the resolved role=<role> — injecting
|
|
359
|
-
* would block the task with zero eligible nodes, so we skip it and let least_loaded
|
|
360
|
-
* eligibility take over (a warning is logged so the misconfiguration is visible).
|
|
361
|
-
* Idempotent: normalizeMeshCapabilityTags dedupes.
|
|
362
|
-
*/
|
|
363
|
-
export function resolveTaskAffinityRequiredTags(
|
|
364
|
-
meshId: string,
|
|
365
|
-
taskMode: MeshTaskMode | undefined,
|
|
366
|
-
explicitRequiredTags: string[],
|
|
367
|
-
opts?: { targetNodeId?: string; callerSpecifiedRequiredTags?: boolean },
|
|
368
|
-
): string[] {
|
|
369
|
-
// Caller pinned the node, or brought their own required_tags → respect it verbatim.
|
|
370
|
-
if (typeof opts?.targetNodeId === 'string' && opts.targetNodeId.trim()) return explicitRequiredTags;
|
|
371
|
-
if (opts?.callerSpecifiedRequiredTags === true) return explicitRequiredTags;
|
|
372
|
-
|
|
373
|
-
let mesh;
|
|
374
|
-
try {
|
|
375
|
-
mesh = getMesh(meshId);
|
|
376
|
-
} catch {
|
|
377
|
-
return explicitRequiredTags;
|
|
378
|
-
}
|
|
379
|
-
const role = resolveTaskAffinityRole(taskMode, mesh?.policy?.taskAffinity);
|
|
380
|
-
if (!role) return explicitRequiredTags;
|
|
381
|
-
|
|
382
|
-
// SOFT affinity: only hard-filter when the role actually exists in the mesh.
|
|
383
|
-
if (!meshHasNodeAdvertisingRole(mesh?.nodes, role)) {
|
|
384
|
-
try {
|
|
385
|
-
console.warn(`[mesh] task_affinity: no node advertises role=${role} for taskMode=${taskMode} in mesh ${meshId}; skipping injection (least_loaded fallback)`);
|
|
386
|
-
} catch { /* logging is best-effort */ }
|
|
387
|
-
return explicitRequiredTags;
|
|
388
|
-
}
|
|
389
|
-
return normalizeMeshCapabilityTags([...explicitRequiredTags, `role=${role}`]);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
285
|
function withQueueLock<T>(_meshId: string, fn: () => T): T {
|
|
393
286
|
return MeshRuntimeStore.getInstance().transaction(fn);
|
|
394
287
|
}
|
|
@@ -474,31 +367,18 @@ export function enqueueTask(
|
|
|
474
367
|
throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
|
|
475
368
|
}
|
|
476
369
|
assertNoDependencyCycle(meshId, id, dependsOn);
|
|
477
|
-
// Did the caller bring their own routing tags? Affinity auto-injection is
|
|
478
|
-
// suppressed when so (caller's required_tags win), matching the spec — measured
|
|
479
|
-
// against the ORIGINAL caller tags, before convergence augmentation below.
|
|
480
370
|
const callerTags = normalizeMeshCapabilityTags(opts?.requiredTags);
|
|
481
|
-
const callerSpecifiedRequiredTags = callerTags.length > 0;
|
|
482
371
|
// Convergence routing (opt-in): auto-inject converge=refine for code_change
|
|
483
372
|
// tasks so they hard-filter onto refine-capable worktree nodes. No-op unless
|
|
484
373
|
// the mesh opts in; explicit target_node_id / required_tags are preserved.
|
|
485
|
-
|
|
374
|
+
// Routing is otherwise governed solely by the caller's required_tags (hard
|
|
375
|
+
// filter through nodeSatisfiesRequiredTags) — no role/taskMode auto-routing.
|
|
376
|
+
const resolvedRequiredTags = resolveConvergeRequiredTags(
|
|
486
377
|
meshId,
|
|
487
378
|
modeValidation.taskMode,
|
|
488
379
|
callerTags,
|
|
489
380
|
{ targetNodeId: opts?.targetNodeId },
|
|
490
381
|
);
|
|
491
|
-
// Declarative task_mode → role affinity (precedent: convergence routing above):
|
|
492
|
-
// inject role=<role> for the task's taskMode unless the caller pinned a node or
|
|
493
|
-
// supplied their own required_tags. SOFT — skipped when no node advertises the
|
|
494
|
-
// role (least_loaded fallback). Built-in default mapping applies even without a
|
|
495
|
-
// taskAffinity config block; { enabled:false } disables it.
|
|
496
|
-
resolvedRequiredTags = resolveTaskAffinityRequiredTags(
|
|
497
|
-
meshId,
|
|
498
|
-
modeValidation.taskMode,
|
|
499
|
-
resolvedRequiredTags,
|
|
500
|
-
{ targetNodeId: opts?.targetNodeId, callerSpecifiedRequiredTags },
|
|
501
|
-
);
|
|
502
382
|
const entry: MeshWorkQueueEntry = {
|
|
503
383
|
id,
|
|
504
384
|
meshId,
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -165,106 +165,6 @@ export function resolveNodeSchedulingPriority(
|
|
|
165
165
|
export const MESH_CONVERGE_REFINE_TAG = 'converge=refine';
|
|
166
166
|
export const MESH_CONVERGE_FAST_FORWARD_TAG = 'converge=fast_forward';
|
|
167
167
|
|
|
168
|
-
/**
|
|
169
|
-
* Standard mesh resource-pool roles. These are the dashboard-provided defaults for
|
|
170
|
-
* the role=<x> routing tag (advertised by node policy.providerRoles and matched
|
|
171
|
-
* through nodeSatisfiesRequiredTags). A mesh is free to declare additional custom
|
|
172
|
-
* role strings in policy.taskAffinity; the dashboard role dropdown exposes the union
|
|
173
|
-
* of these four standards plus any config-declared roles. Roles are lowercased
|
|
174
|
-
* everywhere (the role=<x> tag is lowercased in roleCapabilityTags).
|
|
175
|
-
*/
|
|
176
|
-
export const STANDARD_MESH_ROLES = ['investigator', 'coder', 'validator', 'converger'] as const;
|
|
177
|
-
export type StandardMeshRole = typeof STANDARD_MESH_ROLES[number];
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Default taskMode → role mapping (all five task modes). A task enqueued with a
|
|
181
|
-
* given taskMode auto-routes to a node advertising the mapped role=<x> tag, unless
|
|
182
|
-
* the mesh overrides the mapping in policy.taskAffinity.byTaskMode. Keyed by the
|
|
183
|
-
* MeshTaskMode string literals (the canonical union lives in mesh-work-queue.ts;
|
|
184
|
-
* this map is keyed by string to avoid a types ↔ queue import cycle). Kept in sync
|
|
185
|
-
* with MeshTaskMode by the exhaustive lookup in resolveTaskAffinityRole.
|
|
186
|
-
*/
|
|
187
|
-
export const DEFAULT_TASKMODE_ROLE_MAP: Readonly<Record<string, StandardMeshRole>> = {
|
|
188
|
-
live_debug_readonly: 'investigator',
|
|
189
|
-
code_change: 'coder',
|
|
190
|
-
launch_app: 'coder',
|
|
191
|
-
validation: 'validator',
|
|
192
|
-
convergence: 'converger',
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Declarative task_mode → role affinity policy. When set, enqueueTask auto-injects a
|
|
197
|
-
* `role=<role>` required tag (see resolveTaskAffinityRequiredTags) so the task
|
|
198
|
-
* hard-filters onto nodes advertising that role — but only when the caller did NOT
|
|
199
|
-
* already pin the task with an explicit target_node_id or its own required_tags.
|
|
200
|
-
*
|
|
201
|
-
* - `enabled`: master switch. Defaults to true so the built-in DEFAULT_TASKMODE_ROLE_MAP
|
|
202
|
-
* takes effect even when a mesh declares no taskAffinity block at all. Set false to
|
|
203
|
-
* fully disable affinity routing (today's pre-affinity behavior).
|
|
204
|
-
* - `byTaskMode`: per-taskMode override of the default mapping. A role string here may
|
|
205
|
-
* be any of STANDARD_MESH_ROLES or a custom role declared by the operator. An empty
|
|
206
|
-
* string disables affinity for that one task mode.
|
|
207
|
-
* - `customRoles`: extra (non-standard) role strings to surface in the dashboard role
|
|
208
|
-
* dropdown. Routing itself does not require a role to be pre-declared here — any role
|
|
209
|
-
* referenced in byTaskMode is injected regardless; customRoles only widens the UI list.
|
|
210
|
-
*/
|
|
211
|
-
export interface RepoMeshTaskAffinityPolicy {
|
|
212
|
-
enabled?: boolean;
|
|
213
|
-
byTaskMode?: Record<string, string>;
|
|
214
|
-
customRoles?: string[];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Resolve the affinity role for a task mode against a mesh's taskAffinity policy.
|
|
219
|
-
* Returns the lowercased role string to inject as `role=<role>`, or null when no
|
|
220
|
-
* affinity applies (affinity disabled, an explicit empty override, or an unknown mode).
|
|
221
|
-
*
|
|
222
|
-
* Precedence: policy.byTaskMode[mode] (operator override) → DEFAULT_TASKMODE_ROLE_MAP[mode].
|
|
223
|
-
* A blank override string explicitly opts that mode out of affinity routing.
|
|
224
|
-
*/
|
|
225
|
-
export function resolveTaskAffinityRole(
|
|
226
|
-
taskMode: string | undefined,
|
|
227
|
-
policy: RepoMeshTaskAffinityPolicy | null | undefined,
|
|
228
|
-
): string | null {
|
|
229
|
-
if (!taskMode) return null;
|
|
230
|
-
if (policy?.enabled === false) return null;
|
|
231
|
-
const override = policy?.byTaskMode && Object.prototype.hasOwnProperty.call(policy.byTaskMode, taskMode)
|
|
232
|
-
? policy.byTaskMode[taskMode]
|
|
233
|
-
: undefined;
|
|
234
|
-
if (typeof override === 'string') {
|
|
235
|
-
const trimmed = override.trim().toLowerCase();
|
|
236
|
-
// An explicit blank override opts this task mode out of affinity routing.
|
|
237
|
-
return trimmed ? trimmed : null;
|
|
238
|
-
}
|
|
239
|
-
const fallback = DEFAULT_TASKMODE_ROLE_MAP[taskMode];
|
|
240
|
-
return fallback ?? null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Union of the standard roles plus any operator-declared roles (byTaskMode values +
|
|
245
|
-
* customRoles), lowercased and deduped, preserving standard-first ordering. Used by
|
|
246
|
-
* the dashboard role dropdown so operators can pick a standard or a config-declared
|
|
247
|
-
* custom role. Pure/UI-facing — does not gate routing.
|
|
248
|
-
*/
|
|
249
|
-
export function resolveMeshRoleOptions(policy: RepoMeshTaskAffinityPolicy | null | undefined): string[] {
|
|
250
|
-
const out: string[] = [...STANDARD_MESH_ROLES];
|
|
251
|
-
const seen = new Set<string>(out);
|
|
252
|
-
const add = (raw: unknown) => {
|
|
253
|
-
if (typeof raw !== 'string') return;
|
|
254
|
-
const role = raw.trim().toLowerCase();
|
|
255
|
-
if (!role || seen.has(role)) return;
|
|
256
|
-
seen.add(role);
|
|
257
|
-
out.push(role);
|
|
258
|
-
};
|
|
259
|
-
if (policy?.byTaskMode) {
|
|
260
|
-
for (const value of Object.values(policy.byTaskMode)) add(value);
|
|
261
|
-
}
|
|
262
|
-
if (Array.isArray(policy?.customRoles)) {
|
|
263
|
-
for (const value of policy.customRoles) add(value);
|
|
264
|
-
}
|
|
265
|
-
return out;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
168
|
/**
|
|
269
169
|
* Resolve whether the load-balancing scheduler should auto-inject a
|
|
270
170
|
* `converge=refine` required tag onto code_change tasks so they hard-filter onto
|
|
@@ -320,18 +220,6 @@ export interface RepoMeshPolicy {
|
|
|
320
220
|
* Defaults to false: code_change routing is unchanged unless opted in.
|
|
321
221
|
*/
|
|
322
222
|
autoConvergeCodeChange?: boolean;
|
|
323
|
-
/**
|
|
324
|
-
* Declarative task_mode → role affinity routing. When present (and not disabled),
|
|
325
|
-
* enqueueTask auto-injects a `role=<role>` required tag for the task's taskMode so
|
|
326
|
-
* the work hard-filters onto nodes advertising that role. Explicit target_node_id
|
|
327
|
-
* routing and any caller-supplied required_tags are preserved (auto-injection only
|
|
328
|
-
* applies when the caller pinned neither). When the mapped role has no advertising
|
|
329
|
-
* node in the mesh, the task is NOT blocked — the role tag is skipped so the work
|
|
330
|
-
* falls back to ordinary least_loaded eligibility (soft affinity). Defaults to the
|
|
331
|
-
* built-in DEFAULT_TASKMODE_ROLE_MAP even when this block is omitted; set
|
|
332
|
-
* `{ enabled: false }` to fully restore pre-affinity routing.
|
|
333
|
-
*/
|
|
334
|
-
taskAffinity?: RepoMeshTaskAffinityPolicy;
|
|
335
223
|
/**
|
|
336
224
|
* Whether sessions spawned by mesh/coordinator policy should auto-open as visible
|
|
337
225
|
* dashboard tabs or start hidden. Defaults to 'visible' to preserve existing
|
|
@@ -376,21 +264,7 @@ export interface RepoMeshRelatedRepo {
|
|
|
376
264
|
}
|
|
377
265
|
|
|
378
266
|
/**
|
|
379
|
-
* Per-(node, provider)
|
|
380
|
-
*
|
|
381
|
-
* `role` is a free-form resource-pool label (recommended values:
|
|
382
|
-
* 'investigation' | 'coding' | 'orchestration') describing what this
|
|
383
|
-
* (node, provider) combination is *for*. As of the load-balancing scheduler it
|
|
384
|
-
* is ALSO routable: each declared role is advertised as a synthetic `role=<x>`
|
|
385
|
-
* capability tag (see buildMeshNodeCapabilityTags), so a task enqueued with
|
|
386
|
-
* requiredTags: ["role=validation"] is hard-filtered to nodes/providers that
|
|
387
|
-
* declare that role — through the same nodeSatisfiesRequiredTags path as any
|
|
388
|
-
* other tag. There is intentionally no separate "advertisedRoles" field: the
|
|
389
|
-
* label and the routing tag are one mechanism. A task that does not require a
|
|
390
|
-
* `role=` tag ignores roles entirely (opt-in, fully backward compatible).
|
|
391
|
-
*
|
|
392
|
-
* role is intentionally orthogonal to taskMode: taskMode classifies the *work*
|
|
393
|
-
* (code_change vs live_debug_readonly), role classifies the *resource pool*.
|
|
267
|
+
* Per-(node, provider) parallelism declaration.
|
|
394
268
|
*
|
|
395
269
|
* `maxParallel` is the only enforced field: the queue will not assign a task
|
|
396
270
|
* to this (node, provider) once it already has `maxParallel` active
|
|
@@ -398,12 +272,14 @@ export interface RepoMeshRelatedRepo {
|
|
|
398
272
|
* provider) cap disagree, the stricter (lower effective) limit wins — a claim
|
|
399
273
|
* must satisfy both. Omitting `maxParallel` means this provider is bounded only
|
|
400
274
|
* by the global/taskMode caps (full backward compatibility).
|
|
275
|
+
*
|
|
276
|
+
* Routing is governed exclusively by required_tags (see nodeSatisfiesRequiredTags);
|
|
277
|
+
* this entry carries no routing role. To route work to a specific node, advertise an
|
|
278
|
+
* ordinary capability tag on the node and require it on the task.
|
|
401
279
|
*/
|
|
402
280
|
export interface RepoMeshProviderRole {
|
|
403
281
|
/** Provider type this entry governs (e.g. 'claude-cli', 'codex-cli'). */
|
|
404
282
|
providerType: string;
|
|
405
|
-
/** Free-form role label; recommended: 'investigation' | 'coding' | 'orchestration'. */
|
|
406
|
-
role?: string;
|
|
407
283
|
/** Max concurrent active tasks for this (node, provider). Omit = no per-provider cap. */
|
|
408
284
|
maxParallel?: number;
|
|
409
285
|
}
|
|
@@ -423,13 +299,11 @@ export interface RepoMeshNodePolicy {
|
|
|
423
299
|
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
424
300
|
providerPriority?: string[];
|
|
425
301
|
/**
|
|
426
|
-
* Per-(node, provider)
|
|
427
|
-
* providerType on THIS node to an optional
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* caps)
|
|
431
|
-
* can hard-filter by required role. Missing/empty: the node behaves exactly
|
|
432
|
-
* as before (global caps only, no role tags).
|
|
302
|
+
* Per-(node, provider) parallelism declarations. Each entry binds a
|
|
303
|
+
* providerType on THIS node to an optional maxParallel cap. maxParallel is
|
|
304
|
+
* enforced as an additional, stricter-wins constraint on top of the global
|
|
305
|
+
* maxParallelTasks/taskMode caps. Missing/empty: the node behaves exactly as
|
|
306
|
+
* before (global caps only). Routing is governed solely by required_tags.
|
|
433
307
|
*/
|
|
434
308
|
providerRoles?: RepoMeshProviderRole[];
|
|
435
309
|
/**
|
|
@@ -496,16 +370,16 @@ export function resolveDelegatedWorkerAutoApprove(
|
|
|
496
370
|
}
|
|
497
371
|
|
|
498
372
|
/**
|
|
499
|
-
* Resolve the per-(node, provider)
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* blank providerTypes are skipped rather than throwing.
|
|
373
|
+
* Resolve the enforced per-(node, provider) maxParallel cap, or undefined when
|
|
374
|
+
* no finite, non-negative cap is declared for this provider. Used by the queue
|
|
375
|
+
* claim path as a stricter-wins constraint layered on top of the global caps.
|
|
376
|
+
* Case-insensitive, trimmed match on providerType. Defensive against malformed
|
|
377
|
+
* config — non-object entries and blank providerTypes are skipped rather than throwing.
|
|
504
378
|
*/
|
|
505
|
-
export function
|
|
379
|
+
export function resolveProviderMaxParallel(
|
|
506
380
|
nodePolicy: Pick<RepoMeshNodePolicy, 'providerRoles'> | null | undefined,
|
|
507
381
|
providerType: string | null | undefined,
|
|
508
|
-
):
|
|
382
|
+
): number | undefined {
|
|
509
383
|
const wanted = typeof providerType === 'string' ? providerType.trim().toLowerCase() : '';
|
|
510
384
|
if (!wanted) return undefined;
|
|
511
385
|
const roles = nodePolicy?.providerRoles;
|
|
@@ -513,26 +387,14 @@ export function resolveProviderRole(
|
|
|
513
387
|
for (const entry of roles) {
|
|
514
388
|
if (!entry || typeof entry !== 'object') continue;
|
|
515
389
|
const type = typeof entry.providerType === 'string' ? entry.providerType.trim().toLowerCase() : '';
|
|
516
|
-
if (type
|
|
390
|
+
if (!type || type !== wanted) continue;
|
|
391
|
+
const raw = Number(entry.maxParallel);
|
|
392
|
+
if (!Number.isFinite(raw) || raw < 0) return undefined;
|
|
393
|
+
return Math.floor(raw);
|
|
517
394
|
}
|
|
518
395
|
return undefined;
|
|
519
396
|
}
|
|
520
397
|
|
|
521
|
-
/**
|
|
522
|
-
* Resolve the enforced per-(node, provider) maxParallel cap, or undefined when
|
|
523
|
-
* no finite, non-negative cap is declared for this provider. Used by the queue
|
|
524
|
-
* claim path as a stricter-wins constraint layered on top of the global caps.
|
|
525
|
-
*/
|
|
526
|
-
export function resolveProviderMaxParallel(
|
|
527
|
-
nodePolicy: Pick<RepoMeshNodePolicy, 'providerRoles'> | null | undefined,
|
|
528
|
-
providerType: string | null | undefined,
|
|
529
|
-
): number | undefined {
|
|
530
|
-
const role = resolveProviderRole(nodePolicy, providerType);
|
|
531
|
-
const raw = Number(role?.maxParallel);
|
|
532
|
-
if (!Number.isFinite(raw) || raw < 0) return undefined;
|
|
533
|
-
return Math.floor(raw);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
398
|
// ─── Capabilities ───────────────────────────────
|
|
537
399
|
|
|
538
400
|
export interface RepoMeshNodeCapabilities {
|
package/src/shared-types.d.ts
CHANGED
|
@@ -206,6 +206,14 @@ export interface SessionEntry {
|
|
|
206
206
|
completionMarker?: string;
|
|
207
207
|
seenCompletionMarker?: string;
|
|
208
208
|
surfaceHidden?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* True owning-daemon id for a session a coordinator synthesises into its own
|
|
211
|
+
* status snapshot (mesh delegated sessions), so the dashboard attributes it to the
|
|
212
|
+
* worker node instead of the coordinator daemon hosting the snapshot.
|
|
213
|
+
*/
|
|
214
|
+
ownerDaemonId?: string;
|
|
215
|
+
/** True owning-machine display name fallback when the owning daemon is not aggregated. */
|
|
216
|
+
ownerMachineName?: string;
|
|
209
217
|
}
|
|
210
218
|
/**
|
|
211
219
|
* Compact session metadata stored in UserSessionDO and reused by server-side
|
package/src/shared-types.ts
CHANGED
|
@@ -399,6 +399,15 @@ export interface SessionEntry {
|
|
|
399
399
|
seenCompletionMarker?: string;
|
|
400
400
|
surfaceHidden?: boolean;
|
|
401
401
|
settings?: Record<string, any>;
|
|
402
|
+
/**
|
|
403
|
+
* True owning-daemon id for a session a coordinator synthesises into its own
|
|
404
|
+
* status snapshot (mesh delegated sessions). The dashboard attributes the session
|
|
405
|
+
* to this daemon instead of the snapshot daemon so the worker node — not the
|
|
406
|
+
* coordinator — is shown as its machine.
|
|
407
|
+
*/
|
|
408
|
+
ownerDaemonId?: string;
|
|
409
|
+
/** True owning-machine display name fallback when the owning daemon is not aggregated. */
|
|
410
|
+
ownerMachineName?: string;
|
|
402
411
|
/** Set when this session is acting as a mesh coordinator for the given mesh. */
|
|
403
412
|
coordinator?: { meshId: string; role: 'coordinator' };
|
|
404
413
|
meshQueueStats?: {
|