@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
|
@@ -108,28 +108,6 @@ export declare function nodeSatisfiesRequiredTags(requiredTags: unknown, capabil
|
|
|
108
108
|
export declare function resolveConvergeRequiredTags(meshId: string, taskMode: MeshTaskMode | undefined, explicitRequiredTags: string[], opts?: {
|
|
109
109
|
targetNodeId?: string;
|
|
110
110
|
}): string[];
|
|
111
|
-
/**
|
|
112
|
-
* Declarative task_mode → role affinity resolution (precedent: resolveConvergeRequiredTags).
|
|
113
|
-
*
|
|
114
|
-
* At enqueue time, auto-inject a `role=<role>` required tag derived from the task's
|
|
115
|
-
* taskMode via the mesh's policy.taskAffinity (falling back to DEFAULT_TASKMODE_ROLE_MAP),
|
|
116
|
-
* so the task hard-filters onto nodes advertising that role through the ordinary
|
|
117
|
-
* required-tags path (nodeSatisfiesRequiredTags) — no claim/scheduler change needed.
|
|
118
|
-
*
|
|
119
|
-
* Strict precedence / backward compatibility — the injection is skipped (returns the
|
|
120
|
-
* explicit tags unchanged) when ANY of:
|
|
121
|
-
* - the caller pinned an explicit targetNodeId (operator chose the node), or
|
|
122
|
-
* - the caller supplied their own non-empty required_tags (caller's routing wins), or
|
|
123
|
-
* - affinity resolves to no role (policy disabled, blank override, or unknown mode), or
|
|
124
|
-
* - SOFT fallback: no node in the mesh advertises the resolved role=<role> — injecting
|
|
125
|
-
* would block the task with zero eligible nodes, so we skip it and let least_loaded
|
|
126
|
-
* eligibility take over (a warning is logged so the misconfiguration is visible).
|
|
127
|
-
* Idempotent: normalizeMeshCapabilityTags dedupes.
|
|
128
|
-
*/
|
|
129
|
-
export declare function resolveTaskAffinityRequiredTags(meshId: string, taskMode: MeshTaskMode | undefined, explicitRequiredTags: string[], opts?: {
|
|
130
|
-
targetNodeId?: string;
|
|
131
|
-
callerSpecifiedRequiredTags?: boolean;
|
|
132
|
-
}): string[];
|
|
133
111
|
/**
|
|
134
112
|
* M1: detect dependency cycles before enqueue. Walks the dependency graph of
|
|
135
113
|
* existing queue entries plus the new task's edges. Fail-closed: a cycle
|
|
@@ -119,62 +119,6 @@ export declare function resolveNodeSchedulingPriority(nodePolicy: Pick<RepoMeshN
|
|
|
119
119
|
*/
|
|
120
120
|
export declare const MESH_CONVERGE_REFINE_TAG = "converge=refine";
|
|
121
121
|
export declare const MESH_CONVERGE_FAST_FORWARD_TAG = "converge=fast_forward";
|
|
122
|
-
/**
|
|
123
|
-
* Standard mesh resource-pool roles. These are the dashboard-provided defaults for
|
|
124
|
-
* the role=<x> routing tag (advertised by node policy.providerRoles and matched
|
|
125
|
-
* through nodeSatisfiesRequiredTags). A mesh is free to declare additional custom
|
|
126
|
-
* role strings in policy.taskAffinity; the dashboard role dropdown exposes the union
|
|
127
|
-
* of these four standards plus any config-declared roles. Roles are lowercased
|
|
128
|
-
* everywhere (the role=<x> tag is lowercased in roleCapabilityTags).
|
|
129
|
-
*/
|
|
130
|
-
export declare const STANDARD_MESH_ROLES: readonly ["investigator", "coder", "validator", "converger"];
|
|
131
|
-
export type StandardMeshRole = typeof STANDARD_MESH_ROLES[number];
|
|
132
|
-
/**
|
|
133
|
-
* Default taskMode → role mapping (all five task modes). A task enqueued with a
|
|
134
|
-
* given taskMode auto-routes to a node advertising the mapped role=<x> tag, unless
|
|
135
|
-
* the mesh overrides the mapping in policy.taskAffinity.byTaskMode. Keyed by the
|
|
136
|
-
* MeshTaskMode string literals (the canonical union lives in mesh-work-queue.ts;
|
|
137
|
-
* this map is keyed by string to avoid a types ↔ queue import cycle). Kept in sync
|
|
138
|
-
* with MeshTaskMode by the exhaustive lookup in resolveTaskAffinityRole.
|
|
139
|
-
*/
|
|
140
|
-
export declare const DEFAULT_TASKMODE_ROLE_MAP: Readonly<Record<string, StandardMeshRole>>;
|
|
141
|
-
/**
|
|
142
|
-
* Declarative task_mode → role affinity policy. When set, enqueueTask auto-injects a
|
|
143
|
-
* `role=<role>` required tag (see resolveTaskAffinityRequiredTags) so the task
|
|
144
|
-
* hard-filters onto nodes advertising that role — but only when the caller did NOT
|
|
145
|
-
* already pin the task with an explicit target_node_id or its own required_tags.
|
|
146
|
-
*
|
|
147
|
-
* - `enabled`: master switch. Defaults to true so the built-in DEFAULT_TASKMODE_ROLE_MAP
|
|
148
|
-
* takes effect even when a mesh declares no taskAffinity block at all. Set false to
|
|
149
|
-
* fully disable affinity routing (today's pre-affinity behavior).
|
|
150
|
-
* - `byTaskMode`: per-taskMode override of the default mapping. A role string here may
|
|
151
|
-
* be any of STANDARD_MESH_ROLES or a custom role declared by the operator. An empty
|
|
152
|
-
* string disables affinity for that one task mode.
|
|
153
|
-
* - `customRoles`: extra (non-standard) role strings to surface in the dashboard role
|
|
154
|
-
* dropdown. Routing itself does not require a role to be pre-declared here — any role
|
|
155
|
-
* referenced in byTaskMode is injected regardless; customRoles only widens the UI list.
|
|
156
|
-
*/
|
|
157
|
-
export interface RepoMeshTaskAffinityPolicy {
|
|
158
|
-
enabled?: boolean;
|
|
159
|
-
byTaskMode?: Record<string, string>;
|
|
160
|
-
customRoles?: string[];
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Resolve the affinity role for a task mode against a mesh's taskAffinity policy.
|
|
164
|
-
* Returns the lowercased role string to inject as `role=<role>`, or null when no
|
|
165
|
-
* affinity applies (affinity disabled, an explicit empty override, or an unknown mode).
|
|
166
|
-
*
|
|
167
|
-
* Precedence: policy.byTaskMode[mode] (operator override) → DEFAULT_TASKMODE_ROLE_MAP[mode].
|
|
168
|
-
* A blank override string explicitly opts that mode out of affinity routing.
|
|
169
|
-
*/
|
|
170
|
-
export declare function resolveTaskAffinityRole(taskMode: string | undefined, policy: RepoMeshTaskAffinityPolicy | null | undefined): string | null;
|
|
171
|
-
/**
|
|
172
|
-
* Union of the standard roles plus any operator-declared roles (byTaskMode values +
|
|
173
|
-
* customRoles), lowercased and deduped, preserving standard-first ordering. Used by
|
|
174
|
-
* the dashboard role dropdown so operators can pick a standard or a config-declared
|
|
175
|
-
* custom role. Pure/UI-facing — does not gate routing.
|
|
176
|
-
*/
|
|
177
|
-
export declare function resolveMeshRoleOptions(policy: RepoMeshTaskAffinityPolicy | null | undefined): string[];
|
|
178
122
|
/**
|
|
179
123
|
* Resolve whether the load-balancing scheduler should auto-inject a
|
|
180
124
|
* `converge=refine` required tag onto code_change tasks so they hard-filter onto
|
|
@@ -224,18 +168,6 @@ export interface RepoMeshPolicy {
|
|
|
224
168
|
* Defaults to false: code_change routing is unchanged unless opted in.
|
|
225
169
|
*/
|
|
226
170
|
autoConvergeCodeChange?: boolean;
|
|
227
|
-
/**
|
|
228
|
-
* Declarative task_mode → role affinity routing. When present (and not disabled),
|
|
229
|
-
* enqueueTask auto-injects a `role=<role>` required tag for the task's taskMode so
|
|
230
|
-
* the work hard-filters onto nodes advertising that role. Explicit target_node_id
|
|
231
|
-
* routing and any caller-supplied required_tags are preserved (auto-injection only
|
|
232
|
-
* applies when the caller pinned neither). When the mapped role has no advertising
|
|
233
|
-
* node in the mesh, the task is NOT blocked — the role tag is skipped so the work
|
|
234
|
-
* falls back to ordinary least_loaded eligibility (soft affinity). Defaults to the
|
|
235
|
-
* built-in DEFAULT_TASKMODE_ROLE_MAP even when this block is omitted; set
|
|
236
|
-
* `{ enabled: false }` to fully restore pre-affinity routing.
|
|
237
|
-
*/
|
|
238
|
-
taskAffinity?: RepoMeshTaskAffinityPolicy;
|
|
239
171
|
/**
|
|
240
172
|
* Whether sessions spawned by mesh/coordinator policy should auto-open as visible
|
|
241
173
|
* dashboard tabs or start hidden. Defaults to 'visible' to preserve existing
|
|
@@ -278,21 +210,7 @@ export interface RepoMeshRelatedRepo {
|
|
|
278
210
|
workspace: string;
|
|
279
211
|
}
|
|
280
212
|
/**
|
|
281
|
-
* Per-(node, provider)
|
|
282
|
-
*
|
|
283
|
-
* `role` is a free-form resource-pool label (recommended values:
|
|
284
|
-
* 'investigation' | 'coding' | 'orchestration') describing what this
|
|
285
|
-
* (node, provider) combination is *for*. As of the load-balancing scheduler it
|
|
286
|
-
* is ALSO routable: each declared role is advertised as a synthetic `role=<x>`
|
|
287
|
-
* capability tag (see buildMeshNodeCapabilityTags), so a task enqueued with
|
|
288
|
-
* requiredTags: ["role=validation"] is hard-filtered to nodes/providers that
|
|
289
|
-
* declare that role — through the same nodeSatisfiesRequiredTags path as any
|
|
290
|
-
* other tag. There is intentionally no separate "advertisedRoles" field: the
|
|
291
|
-
* label and the routing tag are one mechanism. A task that does not require a
|
|
292
|
-
* `role=` tag ignores roles entirely (opt-in, fully backward compatible).
|
|
293
|
-
*
|
|
294
|
-
* role is intentionally orthogonal to taskMode: taskMode classifies the *work*
|
|
295
|
-
* (code_change vs live_debug_readonly), role classifies the *resource pool*.
|
|
213
|
+
* Per-(node, provider) parallelism declaration.
|
|
296
214
|
*
|
|
297
215
|
* `maxParallel` is the only enforced field: the queue will not assign a task
|
|
298
216
|
* to this (node, provider) once it already has `maxParallel` active
|
|
@@ -300,12 +218,14 @@ export interface RepoMeshRelatedRepo {
|
|
|
300
218
|
* provider) cap disagree, the stricter (lower effective) limit wins — a claim
|
|
301
219
|
* must satisfy both. Omitting `maxParallel` means this provider is bounded only
|
|
302
220
|
* by the global/taskMode caps (full backward compatibility).
|
|
221
|
+
*
|
|
222
|
+
* Routing is governed exclusively by required_tags (see nodeSatisfiesRequiredTags);
|
|
223
|
+
* this entry carries no routing role. To route work to a specific node, advertise an
|
|
224
|
+
* ordinary capability tag on the node and require it on the task.
|
|
303
225
|
*/
|
|
304
226
|
export interface RepoMeshProviderRole {
|
|
305
227
|
/** Provider type this entry governs (e.g. 'claude-cli', 'codex-cli'). */
|
|
306
228
|
providerType: string;
|
|
307
|
-
/** Free-form role label; recommended: 'investigation' | 'coding' | 'orchestration'. */
|
|
308
|
-
role?: string;
|
|
309
229
|
/** Max concurrent active tasks for this (node, provider). Omit = no per-provider cap. */
|
|
310
230
|
maxParallel?: number;
|
|
311
231
|
}
|
|
@@ -324,13 +244,11 @@ export interface RepoMeshNodePolicy {
|
|
|
324
244
|
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
325
245
|
providerPriority?: string[];
|
|
326
246
|
/**
|
|
327
|
-
* Per-(node, provider)
|
|
328
|
-
* providerType on THIS node to an optional
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* caps)
|
|
332
|
-
* can hard-filter by required role. Missing/empty: the node behaves exactly
|
|
333
|
-
* as before (global caps only, no role tags).
|
|
247
|
+
* Per-(node, provider) parallelism declarations. Each entry binds a
|
|
248
|
+
* providerType on THIS node to an optional maxParallel cap. maxParallel is
|
|
249
|
+
* enforced as an additional, stricter-wins constraint on top of the global
|
|
250
|
+
* maxParallelTasks/taskMode caps. Missing/empty: the node behaves exactly as
|
|
251
|
+
* before (global caps only). Routing is governed solely by required_tags.
|
|
334
252
|
*/
|
|
335
253
|
providerRoles?: RepoMeshProviderRole[];
|
|
336
254
|
/**
|
|
@@ -369,18 +287,12 @@ export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
|
|
|
369
287
|
* launch path merges the envelope as a settingsOverride on top of the provider defaults.
|
|
370
288
|
*/
|
|
371
289
|
export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove'> | null): boolean;
|
|
372
|
-
/**
|
|
373
|
-
* Resolve the per-(node, provider) role declaration from a node policy.
|
|
374
|
-
* Case-insensitive, trimmed match on providerType. Returns undefined when the
|
|
375
|
-
* node has no providerRoles entry for this provider (caller then applies only
|
|
376
|
-
* the global caps). Defensive against malformed config — non-object entries and
|
|
377
|
-
* blank providerTypes are skipped rather than throwing.
|
|
378
|
-
*/
|
|
379
|
-
export declare function resolveProviderRole(nodePolicy: Pick<RepoMeshNodePolicy, 'providerRoles'> | null | undefined, providerType: string | null | undefined): RepoMeshProviderRole | undefined;
|
|
380
290
|
/**
|
|
381
291
|
* Resolve the enforced per-(node, provider) maxParallel cap, or undefined when
|
|
382
292
|
* no finite, non-negative cap is declared for this provider. Used by the queue
|
|
383
293
|
* claim path as a stricter-wins constraint layered on top of the global caps.
|
|
294
|
+
* Case-insensitive, trimmed match on providerType. Defensive against malformed
|
|
295
|
+
* config — non-object entries and blank providerTypes are skipped rather than throwing.
|
|
384
296
|
*/
|
|
385
297
|
export declare function resolveProviderMaxParallel(nodePolicy: Pick<RepoMeshNodePolicy, 'providerRoles'> | null | undefined, providerType: string | null | undefined): number | undefined;
|
|
386
298
|
export interface RepoMeshNodeCapabilities {
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -302,6 +302,15 @@ export interface SessionEntry {
|
|
|
302
302
|
seenCompletionMarker?: string;
|
|
303
303
|
surfaceHidden?: boolean;
|
|
304
304
|
settings?: Record<string, any>;
|
|
305
|
+
/**
|
|
306
|
+
* True owning-daemon id for a session a coordinator synthesises into its own
|
|
307
|
+
* status snapshot (mesh delegated sessions). The dashboard attributes the session
|
|
308
|
+
* to this daemon instead of the snapshot daemon so the worker node — not the
|
|
309
|
+
* coordinator — is shown as its machine.
|
|
310
|
+
*/
|
|
311
|
+
ownerDaemonId?: string;
|
|
312
|
+
/** True owning-machine display name fallback when the owning daemon is not aggregated. */
|
|
313
|
+
ownerMachineName?: string;
|
|
305
314
|
/** Set when this session is acting as a mesh coordinator for the given mesh. */
|
|
306
315
|
coordinator?: {
|
|
307
316
|
meshId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.322",
|
|
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,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.322",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -85,6 +85,7 @@ import { basename as pathBasename, join as pathJoin, resolve as pathResolve } fr
|
|
|
85
85
|
import * as fs from 'fs';
|
|
86
86
|
import { execFileSync } from 'node:child_process';
|
|
87
87
|
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
88
|
+
import { workingDirBasename } from '../providers/working-dir.js';
|
|
88
89
|
|
|
89
90
|
type ReleaseChannel = 'stable' | 'preview';
|
|
90
91
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -128,22 +129,21 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Normalize a providerRoles array (RepoMeshNodePolicy.providerRoles) from raw
|
|
131
|
-
* tool args. Each entry binds a providerType to an optional
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
132
|
+
* tool args. Each entry binds a providerType to an optional `maxParallel` cap.
|
|
133
|
+
* Entries without a usable providerType are dropped; the last entry wins on
|
|
134
|
+
* duplicate providerType. Returns [] when no valid entries — callers then omit
|
|
135
|
+
* the field entirely (full backward compat). Routing is governed by required_tags;
|
|
136
|
+
* any legacy `role` field on the input is ignored.
|
|
135
137
|
*/
|
|
136
|
-
function normalizeProviderRoles(value: unknown): Array<{ providerType: string;
|
|
138
|
+
function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
|
|
137
139
|
if (!Array.isArray(value)) return [];
|
|
138
|
-
const byType = new Map<string, { providerType: string;
|
|
140
|
+
const byType = new Map<string, { providerType: string; maxParallel?: number }>();
|
|
139
141
|
for (const raw of value) {
|
|
140
142
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
141
143
|
const rec = raw as Record<string, unknown>;
|
|
142
144
|
const providerType = typeof rec.providerType === 'string' ? rec.providerType.trim() : '';
|
|
143
145
|
if (!providerType) continue;
|
|
144
|
-
const entry: { providerType: string;
|
|
145
|
-
const role = typeof rec.role === 'string' ? rec.role.trim() : '';
|
|
146
|
-
if (role) entry.role = role;
|
|
146
|
+
const entry: { providerType: string; maxParallel?: number } = { providerType };
|
|
147
147
|
const maxParallel = Number(rec.maxParallel);
|
|
148
148
|
if (Number.isFinite(maxParallel) && maxParallel >= 0) entry.maxParallel = Math.floor(maxParallel);
|
|
149
149
|
byType.set(providerType.toLowerCase(), entry);
|
|
@@ -224,11 +224,14 @@ function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>):
|
|
|
224
224
|
// joinRepoPath + readGitSubmodules moved to @adhdev/mesh-shared (readGitSubmodules)
|
|
225
225
|
// — used via sharedNormalizeGitStatus / sharedPickBestTransitGitStatus below.
|
|
226
226
|
|
|
227
|
-
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
227
|
+
export function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
228
228
|
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
229
229
|
if (explicit) return explicit;
|
|
230
230
|
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
231
|
-
|
|
231
|
+
// Use the OS-agnostic basename: a workspace reported by a Windows node
|
|
232
|
+
// (`D:\gh\adhdev-cloud`) must still collapse to its trailing segment even when
|
|
233
|
+
// this coordinator's own `path.basename` is POSIX-only and would not split `\`.
|
|
234
|
+
const workspaceName = workspace ? workingDirBasename(workspace) : undefined;
|
|
232
235
|
const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
233
236
|
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
234
237
|
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
@@ -874,6 +877,22 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
|
874
877
|
return sessionId ? [sessionId] : [];
|
|
875
878
|
}
|
|
876
879
|
|
|
880
|
+
/**
|
|
881
|
+
* Resolve the owning-node attribution for a mesh node record so a coordinator can
|
|
882
|
+
* stamp the TRUE owner onto a synthetic session entry instead of letting the
|
|
883
|
+
* dashboard fall back to the coordinator's own daemonId. Returns whichever of the
|
|
884
|
+
* owning node's `daemonId` / display machine name could be read from the node's
|
|
885
|
+
* (possibly multi-serialization-path) shape; both may be undefined for a node that
|
|
886
|
+
* never carried machine identity.
|
|
887
|
+
*/
|
|
888
|
+
export function resolveMeshNodeAttribution(node: unknown): { daemonId?: string; machineName?: string } {
|
|
889
|
+
const record = readObjectRecord(node);
|
|
890
|
+
return {
|
|
891
|
+
daemonId: readMeshNodeDaemonId(record),
|
|
892
|
+
machineName: readMeshNodeDisplayMachineName(record),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
877
896
|
export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
878
897
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
879
898
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -20,9 +20,8 @@ import type {
|
|
|
20
20
|
RepoMeshCoordinatorConfig,
|
|
21
21
|
RepoMeshHostMetadata,
|
|
22
22
|
RepoMeshDaemonRole,
|
|
23
|
-
RepoMeshTaskAffinityPolicy,
|
|
24
23
|
} from '../repo-mesh-types.js';
|
|
25
|
-
import { DEFAULT_MESH_POLICY, normalizeMeshSchedulingStrategy
|
|
24
|
+
import { DEFAULT_MESH_POLICY, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
26
25
|
import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
|
|
27
26
|
|
|
28
27
|
// ─── Persistence ────────────────────────────────
|
|
@@ -37,12 +36,71 @@ function loadMeshConfig(): LocalMeshConfig {
|
|
|
37
36
|
try {
|
|
38
37
|
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
39
38
|
if (!raw || !Array.isArray(raw.meshes)) return { meshes: [] };
|
|
40
|
-
|
|
39
|
+
const config = raw as LocalMeshConfig;
|
|
40
|
+
const migrated = migrateLoadedMeshConfig(config);
|
|
41
|
+
// Persist eagerly when the on-load migration changed anything, so the
|
|
42
|
+
// dead field is gone from disk even on a pure-read path (mesh_status /
|
|
43
|
+
// mesh_list_nodes) that never otherwise mutates the config. Best-effort:
|
|
44
|
+
// a write failure (e.g. read-only fs) must not break reads, so swallow.
|
|
45
|
+
if (migrated) {
|
|
46
|
+
try {
|
|
47
|
+
saveMeshConfig(config);
|
|
48
|
+
} catch {
|
|
49
|
+
// keep the in-memory strip; disk converges on the next mutating op
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
41
53
|
} catch {
|
|
42
54
|
return { meshes: [] };
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
/**
|
|
59
|
+
* In-place migration applied to every loaded meshes.json. Strips data that
|
|
60
|
+
* outlived the feature that wrote it so the persisted config converges on the
|
|
61
|
+
* current schema the next time it is saved.
|
|
62
|
+
*
|
|
63
|
+
* Currently: drops the dead `role` field from each node policy's providerRoles
|
|
64
|
+
* entries. providerRoles is retained for its `maxParallel` per-(node, provider)
|
|
65
|
+
* cap, but the routing `role` was removed (routing is governed solely by
|
|
66
|
+
* required_tags). A meshes.json written before the removal still carries
|
|
67
|
+
* `role: "validator"` etc.; this drops it on load so mesh_status / mesh_list_nodes
|
|
68
|
+
* never surface the dead field and the next saveMeshConfig() persists it gone.
|
|
69
|
+
*
|
|
70
|
+
* Returns true when the config was mutated (caller may persist eagerly).
|
|
71
|
+
*/
|
|
72
|
+
function migrateLoadedMeshConfig(config: LocalMeshConfig): boolean {
|
|
73
|
+
let changed = false;
|
|
74
|
+
for (const mesh of config.meshes) {
|
|
75
|
+
if (!mesh || !Array.isArray(mesh.nodes)) continue;
|
|
76
|
+
for (const node of mesh.nodes) {
|
|
77
|
+
if (stripDeadRoleFromProviderRoles(node?.policy)) changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return changed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Drop the legacy `role` field from each providerRoles entry of a node policy,
|
|
85
|
+
* in place. Keeps providerType + maxParallel (the still-meaningful per-(node,
|
|
86
|
+
* provider) cap). Defensive against malformed entries — non-object items are
|
|
87
|
+
* left untouched. Returns true when at least one `role` field was removed.
|
|
88
|
+
*/
|
|
89
|
+
function stripDeadRoleFromProviderRoles(policy: unknown): boolean {
|
|
90
|
+
if (!policy || typeof policy !== 'object') return false;
|
|
91
|
+
const roles = (policy as { providerRoles?: unknown }).providerRoles;
|
|
92
|
+
if (!Array.isArray(roles)) return false;
|
|
93
|
+
let changed = false;
|
|
94
|
+
for (const entry of roles) {
|
|
95
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
96
|
+
&& Object.prototype.hasOwnProperty.call(entry, 'role')) {
|
|
97
|
+
delete (entry as Record<string, unknown>).role;
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return changed;
|
|
102
|
+
}
|
|
103
|
+
|
|
46
104
|
function normalizeCapabilityTags(value: unknown): string[] | undefined {
|
|
47
105
|
if (!Array.isArray(value)) return undefined;
|
|
48
106
|
const seen = new Set<string>();
|
|
@@ -135,79 +193,9 @@ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMe
|
|
|
135
193
|
} else {
|
|
136
194
|
delete policy.autoConvergeCodeChange;
|
|
137
195
|
}
|
|
138
|
-
// Task-mode → role affinity: format-only validation (drop blanks). Unknown/custom
|
|
139
|
-
// roles are NOT blocked — only a warning is logged. Only persist when the block
|
|
140
|
-
// carries meaningful content so existing meshes.json stays untouched.
|
|
141
|
-
const normalizedAffinity = normalizeTaskAffinityPolicy(policy.taskAffinity);
|
|
142
|
-
if (normalizedAffinity) {
|
|
143
|
-
policy.taskAffinity = normalizedAffinity;
|
|
144
|
-
} else {
|
|
145
|
-
delete policy.taskAffinity;
|
|
146
|
-
}
|
|
147
196
|
return policy;
|
|
148
197
|
}
|
|
149
198
|
|
|
150
|
-
/**
|
|
151
|
-
* Format-only normalization of a task-affinity policy. Trims/lowercases role strings,
|
|
152
|
-
* drops blank custom roles and blank byTaskMode keys, and warns (never blocks) when a
|
|
153
|
-
* declared role is not one of the standard four. Returns undefined when the result is
|
|
154
|
-
* empty/no-op so the field stays absent from persisted config.
|
|
155
|
-
*/
|
|
156
|
-
function normalizeTaskAffinityPolicy(
|
|
157
|
-
value: unknown,
|
|
158
|
-
): RepoMeshTaskAffinityPolicy | undefined {
|
|
159
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
160
|
-
const record = value as Record<string, unknown>;
|
|
161
|
-
const out: RepoMeshTaskAffinityPolicy = {};
|
|
162
|
-
let hasContent = false;
|
|
163
|
-
|
|
164
|
-
if (typeof record.enabled === 'boolean') {
|
|
165
|
-
out.enabled = record.enabled;
|
|
166
|
-
// `enabled:true` is the default — only persist the explicit `false`.
|
|
167
|
-
if (record.enabled === false) hasContent = true;
|
|
168
|
-
else delete out.enabled;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (record.byTaskMode && typeof record.byTaskMode === 'object' && !Array.isArray(record.byTaskMode)) {
|
|
172
|
-
const byTaskMode: Record<string, string> = {};
|
|
173
|
-
for (const [mode, raw] of Object.entries(record.byTaskMode as Record<string, unknown>)) {
|
|
174
|
-
const modeKey = mode.trim();
|
|
175
|
-
if (!modeKey) continue;
|
|
176
|
-
if (typeof raw !== 'string') continue;
|
|
177
|
-
// A blank value is a deliberate opt-out for that mode — preserve it as ''.
|
|
178
|
-
const role = raw.trim().toLowerCase();
|
|
179
|
-
byTaskMode[modeKey] = role;
|
|
180
|
-
if (role && !STANDARD_MESH_ROLES.includes(role as any)) {
|
|
181
|
-
try {
|
|
182
|
-
console.warn(`[mesh] task_affinity.byTaskMode.${modeKey}: custom role "${role}" is not a standard mesh role (${STANDARD_MESH_ROLES.join('/')}); routing still applies`);
|
|
183
|
-
} catch { /* best-effort */ }
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (Object.keys(byTaskMode).length > 0) {
|
|
187
|
-
out.byTaskMode = byTaskMode;
|
|
188
|
-
hasContent = true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (Array.isArray(record.customRoles)) {
|
|
193
|
-
const seen = new Set<string>();
|
|
194
|
-
const customRoles: string[] = [];
|
|
195
|
-
for (const raw of record.customRoles) {
|
|
196
|
-
if (typeof raw !== 'string') continue;
|
|
197
|
-
const role = raw.trim().toLowerCase();
|
|
198
|
-
if (!role || seen.has(role)) continue;
|
|
199
|
-
seen.add(role);
|
|
200
|
-
customRoles.push(role);
|
|
201
|
-
}
|
|
202
|
-
if (customRoles.length > 0) {
|
|
203
|
-
out.customRoles = customRoles;
|
|
204
|
-
hasContent = true;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return hasContent ? out : undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
199
|
function normalizeAutoFastForwardPolicy(value: unknown): NonNullable<RepoMeshPolicy['autoFastForward']> {
|
|
212
200
|
const record = value && typeof value === 'object' && !Array.isArray(value)
|
|
213
201
|
? value as Record<string, unknown>
|
package/src/index.ts
CHANGED
|
@@ -135,8 +135,6 @@ export type {
|
|
|
135
135
|
RepoMeshLedgerStatus,
|
|
136
136
|
MeshAsyncJobLifecycle,
|
|
137
137
|
RepoMeshSchedulingStrategy,
|
|
138
|
-
RepoMeshTaskAffinityPolicy,
|
|
139
|
-
StandardMeshRole,
|
|
140
138
|
} from './repo-mesh-types.js';
|
|
141
139
|
export {
|
|
142
140
|
DEFAULT_MESH_POLICY,
|
|
@@ -148,10 +146,6 @@ export {
|
|
|
148
146
|
MESH_CONVERGE_REFINE_TAG,
|
|
149
147
|
MESH_CONVERGE_FAST_FORWARD_TAG,
|
|
150
148
|
resolveAutoConvergeCodeChange,
|
|
151
|
-
STANDARD_MESH_ROLES,
|
|
152
|
-
DEFAULT_TASKMODE_ROLE_MAP,
|
|
153
|
-
resolveTaskAffinityRole,
|
|
154
|
-
resolveMeshRoleOptions,
|
|
155
149
|
} from './repo-mesh-types.js';
|
|
156
150
|
|
|
157
151
|
// ── Git Surface ──
|
|
@@ -237,7 +231,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
237
231
|
export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
238
232
|
|
|
239
233
|
// ── Mesh Work Queue (GUPP) ──
|
|
240
|
-
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags,
|
|
234
|
+
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
241
235
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
|
|
242
236
|
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
|
|
243
237
|
export type { StaleDirectPruneClassification } from './mesh/mesh-active-work.js';
|
|
@@ -306,7 +300,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
|
|
|
306
300
|
// ── Commands ──
|
|
307
301
|
export { DaemonCommandHandler } from './commands/handler.js';
|
|
308
302
|
export type { CommandResult, CommandContext } from './commands/handler.js';
|
|
309
|
-
export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails } from './commands/router.js';
|
|
303
|
+
export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails, resolveMeshNodeAttribution } from './commands/router.js';
|
|
310
304
|
export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
|
|
311
305
|
export {
|
|
312
306
|
maybeRunDaemonUpgradeHelperFromEnv,
|
|
@@ -269,20 +269,19 @@ function buildNodeConfigSection(mesh: LocalMeshEntry): string {
|
|
|
269
269
|
const explicitMachineLabel = typeof (n as any).machineLabel === 'string' ? (n as any).machineLabel : '';
|
|
270
270
|
const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
|
|
271
271
|
const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
|
|
272
|
-
// Per-(node, provider)
|
|
273
|
-
//
|
|
272
|
+
// Per-(node, provider) maxParallel cap. Only maxParallel is enforced by the
|
|
273
|
+
// queue; routing is governed by required_tags, not provider roles.
|
|
274
274
|
const providerRoles = Array.isArray(n.policy?.providerRoles)
|
|
275
|
-
? (n.policy!.providerRoles as Array<{ providerType?: unknown;
|
|
275
|
+
? (n.policy!.providerRoles as Array<{ providerType?: unknown; maxParallel?: unknown }>)
|
|
276
276
|
.map(r => {
|
|
277
277
|
const type = typeof r?.providerType === 'string' ? r.providerType.trim() : '';
|
|
278
278
|
if (!type) return '';
|
|
279
|
-
const role = typeof r?.role === 'string' && r.role.trim() ? `=${r.role.trim()}` : '';
|
|
280
279
|
const cap = Number.isFinite(Number(r?.maxParallel)) ? ` (max ${Math.floor(Number(r.maxParallel))})` : '';
|
|
281
|
-
return `${type}${
|
|
280
|
+
return `${type}${cap}`;
|
|
282
281
|
})
|
|
283
282
|
.filter(Boolean)
|
|
284
283
|
: [];
|
|
285
|
-
const providerRolesSuffix = providerRoles.length ? ` |
|
|
284
|
+
const providerRolesSuffix = providerRoles.length ? ` | caps: ${providerRoles.join(', ')}` : '';
|
|
286
285
|
lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${providerRolesSuffix}${suffix}`);
|
|
287
286
|
const nodePrompt = typeof (n as any).systemPrompt === 'string' ? (n as any).systemPrompt.trim() : '';
|
|
288
287
|
if (nodePrompt) {
|