@adhdev/daemon-core 0.9.82-rc.380 → 0.9.82-rc.382
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/index.d.ts +4 -2
- package/dist/index.js +302 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +295 -61
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-forwarding.d.ts +1 -0
- package/dist/mesh/mesh-runtime-store.d.ts +12 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/providers/cli-provider-instance.d.ts +18 -0
- package/dist/repo-mesh-types.d.ts +81 -0
- package/package.json +2 -2
- package/src/commands/high-family/mesh-status.ts +36 -0
- package/src/config/mesh-config.ts +6 -58
- package/src/index.ts +13 -0
- package/src/mesh/coordinator-prompt.ts +3 -3
- package/src/mesh/mesh-event-forwarding.ts +63 -2
- package/src/mesh/mesh-runtime-store.ts +23 -0
- package/src/mesh/mesh-scheduling-runtime.ts +199 -0
- package/src/providers/cli-provider-instance.ts +27 -0
- package/src/repo-mesh-types.ts +164 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Mesh scheduling-runtime view — a read-only, derived projection of how the queue
|
|
2
|
+
// scheduler currently sees the mesh: which tie-break strategy is live, the global
|
|
3
|
+
// parallel caps and how much of them is consumed, and per-node load / priority /
|
|
4
|
+
// per-(node, provider) caps plus a structured "why this node can't take more write
|
|
5
|
+
// work right now" reason set.
|
|
6
|
+
//
|
|
7
|
+
// This is OBSERVABILITY ONLY. It re-derives the same numbers the live claim path
|
|
8
|
+
// (maybeAutoLaunchOneQueueSession → orderEligibleNodes → claimNextQueueTask) acts
|
|
9
|
+
// on, but never mutates anything and never drives a scheduling decision. It is built
|
|
10
|
+
// from the in-memory mesh config + the queue snapshot, so callers (mesh_status) get a
|
|
11
|
+
// consistent picture without touching the SQLite claim transaction. Keeping the
|
|
12
|
+
// derivation here — next to the constants it mirrors — means the exposed numbers and
|
|
13
|
+
// the enforced numbers stay defined in one module family.
|
|
14
|
+
|
|
15
|
+
import type { RepoMeshNodePolicy, RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
16
|
+
import {
|
|
17
|
+
normalizeMeshSchedulingStrategy,
|
|
18
|
+
resolveMaxParallelTasks,
|
|
19
|
+
resolveNodeSchedulingPriority,
|
|
20
|
+
resolveProviderMaxParallel,
|
|
21
|
+
} from '../repo-mesh-types.js';
|
|
22
|
+
import { normalizeMeshNodeId } from '@adhdev/mesh-shared';
|
|
23
|
+
import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
|
|
24
|
+
|
|
25
|
+
/** Per-(node, provider) cap and its current consumption. */
|
|
26
|
+
export interface MeshNodeProviderSchedulingRuntime {
|
|
27
|
+
providerType: string;
|
|
28
|
+
/** Declared maxParallel cap for this (node, provider). Omitted when uncapped. */
|
|
29
|
+
maxParallel?: number;
|
|
30
|
+
/** Active (status='assigned') tasks on this node claimed by this provider. */
|
|
31
|
+
activeAssigned: number;
|
|
32
|
+
/** True when activeAssigned has reached maxParallel (a further claim is refused). */
|
|
33
|
+
capReached: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Per-node scheduling-runtime projection. */
|
|
37
|
+
export interface MeshNodeSchedulingRuntime {
|
|
38
|
+
nodeId: string;
|
|
39
|
+
/** Active (status='assigned') task count on this node — the least-loaded rank key. */
|
|
40
|
+
load: number;
|
|
41
|
+
/** Soft scheduling priority (PRIORITY rank key; higher = preferred). */
|
|
42
|
+
schedulingPriority: number;
|
|
43
|
+
/** Per-node concurrent-session cap, when configured. */
|
|
44
|
+
maxConcurrentSessions?: number;
|
|
45
|
+
/** Per-(node, provider) caps + consumption, when providerRoles declares any. */
|
|
46
|
+
providerRoles?: MeshNodeProviderSchedulingRuntime[];
|
|
47
|
+
/**
|
|
48
|
+
* True when the node currently cannot claim a NEW write (non-readonly) task —
|
|
49
|
+
* either the global write cap is exhausted or a node-local gate (active write
|
|
50
|
+
* assignment / a fully-consumed provider cap / session cap) blocks it.
|
|
51
|
+
*/
|
|
52
|
+
capReached: boolean;
|
|
53
|
+
/** Structured reasons backing capReached (empty when the node can take work). */
|
|
54
|
+
capReasons: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Mesh-level scheduling-runtime projection. */
|
|
58
|
+
export interface MeshSchedulingRuntime {
|
|
59
|
+
/** Resolved tie-break strategy (defaults to 'first_eligible'). */
|
|
60
|
+
strategy: RepoMeshSchedulingStrategy;
|
|
61
|
+
/** Effective global write-task parallel cap (clamped). */
|
|
62
|
+
maxParallelTasks: number;
|
|
63
|
+
/** Derived read-only diagnosis cap (max(2, 2× write cap)) — mirrors the claim path. */
|
|
64
|
+
maxReadonlyParallelTasks: number;
|
|
65
|
+
/** Current global write (non-readonly) assigned-task load. */
|
|
66
|
+
activeWriteAssigned: number;
|
|
67
|
+
/** Current global read-only assigned-task load. */
|
|
68
|
+
activeReadonlyAssigned: number;
|
|
69
|
+
/** True when the global write cap is exhausted (no new write task can launch). */
|
|
70
|
+
globalWriteCapReached: boolean;
|
|
71
|
+
/** True when the read-only diagnosis cap is exhausted. */
|
|
72
|
+
globalReadonlyCapReached: boolean;
|
|
73
|
+
/** Per-node projections, in mesh config order. */
|
|
74
|
+
nodes: MeshNodeSchedulingRuntime[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface MeshLike {
|
|
78
|
+
policy?: { schedulingStrategy?: unknown; maxParallelTasks?: unknown } | null;
|
|
79
|
+
nodes?: Array<{ id?: string; nodeId?: string; node_id?: string; policy?: RepoMeshNodePolicy | null; isLocalWorktree?: boolean }> | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isReadonly(task: MeshWorkQueueEntry): boolean {
|
|
83
|
+
return task.taskMode === 'live_debug_readonly';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isAssigned(task: MeshWorkQueueEntry): boolean {
|
|
87
|
+
return task.status === 'assigned';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the scheduling-runtime projection for a mesh from its config + a snapshot of
|
|
92
|
+
* its work queue. Pure and side-effect free — safe to call on any read path.
|
|
93
|
+
*
|
|
94
|
+
* The derivation deliberately mirrors maybeAutoLaunchOneQueueSession's gates so the
|
|
95
|
+
* exposed "capReached/capReasons" match why a real claim would be refused:
|
|
96
|
+
* • global write cap → activeWriteAssigned >= maxParallelTasks
|
|
97
|
+
* • node write conflict→ a node already holding an assigned write task (worktree isolation)
|
|
98
|
+
* • provider cap → a (node, provider) at its declared maxParallel
|
|
99
|
+
* It does NOT re-evaluate eligibility/required-tags (those are task-specific); it only
|
|
100
|
+
* reports the capacity picture, which is what an operator needs to read load balance.
|
|
101
|
+
*/
|
|
102
|
+
export function buildMeshSchedulingRuntime(
|
|
103
|
+
mesh: MeshLike | null | undefined,
|
|
104
|
+
queue: MeshWorkQueueEntry[],
|
|
105
|
+
): MeshSchedulingRuntime {
|
|
106
|
+
const strategy = normalizeMeshSchedulingStrategy(mesh?.policy?.schedulingStrategy);
|
|
107
|
+
const maxParallelTasks = resolveMaxParallelTasks(mesh?.policy?.maxParallelTasks);
|
|
108
|
+
// Read-only diagnoses are exempt from the write cap and get their own higher
|
|
109
|
+
// safety cap (2× the write cap, floor 2) — identical to the claim path.
|
|
110
|
+
const maxReadonlyParallelTasks = Math.max(2, maxParallelTasks * 2);
|
|
111
|
+
|
|
112
|
+
const assignedTasks = (Array.isArray(queue) ? queue : []).filter(isAssigned);
|
|
113
|
+
const activeWriteAssigned = assignedTasks.filter(t => !isReadonly(t)).length;
|
|
114
|
+
const activeReadonlyAssigned = assignedTasks.filter(isReadonly).length;
|
|
115
|
+
const globalWriteCapReached = activeWriteAssigned >= maxParallelTasks;
|
|
116
|
+
const globalReadonlyCapReached = activeReadonlyAssigned >= maxReadonlyParallelTasks;
|
|
117
|
+
|
|
118
|
+
// Pre-bucket assigned tasks by node so per-node load and per-provider counts are
|
|
119
|
+
// a single pass rather than O(nodes × queue).
|
|
120
|
+
const writeAssignedByNode = new Map<string, number>();
|
|
121
|
+
const assignedByNode = new Map<string, number>();
|
|
122
|
+
const providerCountByNode = new Map<string, Map<string, number>>();
|
|
123
|
+
for (const task of assignedTasks) {
|
|
124
|
+
const nodeId = typeof task.assignedNodeId === 'string' ? task.assignedNodeId.trim() : '';
|
|
125
|
+
if (!nodeId) continue;
|
|
126
|
+
assignedByNode.set(nodeId, (assignedByNode.get(nodeId) ?? 0) + 1);
|
|
127
|
+
if (!isReadonly(task)) {
|
|
128
|
+
writeAssignedByNode.set(nodeId, (writeAssignedByNode.get(nodeId) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
const provider = typeof task.assignedProviderType === 'string' ? task.assignedProviderType : '';
|
|
131
|
+
if (provider) {
|
|
132
|
+
let byProvider = providerCountByNode.get(nodeId);
|
|
133
|
+
if (!byProvider) { byProvider = new Map(); providerCountByNode.set(nodeId, byProvider); }
|
|
134
|
+
byProvider.set(provider, (byProvider.get(provider) ?? 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nodes: MeshNodeSchedulingRuntime[] = [];
|
|
139
|
+
for (const rawNode of (Array.isArray(mesh?.nodes) ? mesh!.nodes! : [])) {
|
|
140
|
+
const nodeId = normalizeMeshNodeId(rawNode);
|
|
141
|
+
if (!nodeId) continue;
|
|
142
|
+
const policy = (rawNode?.policy || undefined) as RepoMeshNodePolicy | undefined;
|
|
143
|
+
const load = assignedByNode.get(nodeId) ?? 0;
|
|
144
|
+
const schedulingPriority = resolveNodeSchedulingPriority(policy);
|
|
145
|
+
|
|
146
|
+
const capReasons: string[] = [];
|
|
147
|
+
if (globalWriteCapReached) capReasons.push('global_max_parallel_tasks_reached');
|
|
148
|
+
// Write isolation: a node already holding an assigned write task can't take another.
|
|
149
|
+
if ((writeAssignedByNode.get(nodeId) ?? 0) > 0) capReasons.push('node_has_active_assignment');
|
|
150
|
+
|
|
151
|
+
// Per-(node, provider) caps, with live consumption.
|
|
152
|
+
let providerRoles: MeshNodeProviderSchedulingRuntime[] | undefined;
|
|
153
|
+
const declaredRoles = Array.isArray(policy?.providerRoles) ? policy!.providerRoles! : [];
|
|
154
|
+
if (declaredRoles.length) {
|
|
155
|
+
const byProvider = providerCountByNode.get(nodeId);
|
|
156
|
+
providerRoles = [];
|
|
157
|
+
for (const role of declaredRoles) {
|
|
158
|
+
if (!role || typeof role !== 'object') continue;
|
|
159
|
+
const providerType = typeof role.providerType === 'string' ? role.providerType.trim() : '';
|
|
160
|
+
if (!providerType) continue;
|
|
161
|
+
const maxParallel = resolveProviderMaxParallel(policy, providerType);
|
|
162
|
+
const activeAssigned = byProvider?.get(providerType) ?? 0;
|
|
163
|
+
const capReached = maxParallel !== undefined && activeAssigned >= maxParallel;
|
|
164
|
+
providerRoles.push({
|
|
165
|
+
providerType,
|
|
166
|
+
...(maxParallel !== undefined ? { maxParallel } : {}),
|
|
167
|
+
activeAssigned,
|
|
168
|
+
capReached,
|
|
169
|
+
});
|
|
170
|
+
if (capReached) capReasons.push(`max_provider_parallel_reached:${providerType}`);
|
|
171
|
+
}
|
|
172
|
+
if (!providerRoles.length) providerRoles = undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const maxConcurrentSessions = Number(policy?.maxConcurrentSessions);
|
|
176
|
+
const hasSessionCap = Number.isFinite(maxConcurrentSessions) && maxConcurrentSessions >= 0;
|
|
177
|
+
|
|
178
|
+
nodes.push({
|
|
179
|
+
nodeId,
|
|
180
|
+
load,
|
|
181
|
+
schedulingPriority,
|
|
182
|
+
...(hasSessionCap ? { maxConcurrentSessions: Math.floor(maxConcurrentSessions) } : {}),
|
|
183
|
+
...(providerRoles ? { providerRoles } : {}),
|
|
184
|
+
capReached: capReasons.length > 0,
|
|
185
|
+
capReasons,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
strategy,
|
|
191
|
+
maxParallelTasks,
|
|
192
|
+
maxReadonlyParallelTasks,
|
|
193
|
+
activeWriteAssigned,
|
|
194
|
+
activeReadonlyAssigned,
|
|
195
|
+
globalWriteCapReached,
|
|
196
|
+
globalReadonlyCapReached,
|
|
197
|
+
nodes,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -972,9 +972,36 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
972
972
|
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
973
973
|
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
974
974
|
* coordinator as if they were task completions.
|
|
975
|
+
*
|
|
976
|
+
* MESHID-DROP-ON-DETACH (Fix C): a coordinator-LAUNCHED worker session
|
|
977
|
+
* (launchedByCoordinator) holds its mesh membership (meshNodeFor / meshNodeId /
|
|
978
|
+
* meshCoordinatorDaemonId) at the SESSION level — set once at launch
|
|
979
|
+
* (mesh_launch_session / queue auto-launch), independent of any single task.
|
|
980
|
+
* The original detach wiped meshNodeFor + meshNodeId together with the
|
|
981
|
+
* task-level meshActiveTaskId, so the FIRST task completion stripped the
|
|
982
|
+
* membership and EVERY subsequent completion forwarded with meshId absent —
|
|
983
|
+
* resolveWorkerDelegateRouting fell to mesh_unresolved and the coordinator
|
|
984
|
+
* rejected the forward "meshId required". For a launched member we therefore
|
|
985
|
+
* clear ONLY the task-level marker (meshActiveTaskId) and preserve the
|
|
986
|
+
* session-level membership so its next task's completion still resolves.
|
|
987
|
+
* A task-less ad-hoc turn on a preserved-membership session is NOT misrouted:
|
|
988
|
+
* its completion carries no taskId and the session holds no active assignment,
|
|
989
|
+
* so the forwarder's WARMUPGAP guard skips the dispatch-row flip (it only
|
|
990
|
+
* injects a benign task-less notification). A NON-launched session (a plain CLI
|
|
991
|
+
* session adopted by mesh_send_task --direct, launchedByCoordinator falsy)
|
|
992
|
+
* keeps the original full clear so an ad-hoc session is never left pinned.
|
|
975
993
|
*/
|
|
976
994
|
detachMeshAssignment(): void {
|
|
977
995
|
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
996
|
+
// Session-level member: keep membership, drop only the task-level marker.
|
|
997
|
+
if (this.settings.launchedByCoordinator === true) {
|
|
998
|
+
if (!this.settings.meshActiveTaskId) return;
|
|
999
|
+
const { meshActiveTaskId, ...rest } = this.settings;
|
|
1000
|
+
void meshActiveTaskId;
|
|
1001
|
+
this.settings = rest;
|
|
1002
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
978
1005
|
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
979
1006
|
void meshNodeFor; void meshActiveTaskId;
|
|
980
1007
|
// WTCLAIM (A): clear the active binding but PRESERVE the last bound node id
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -352,6 +352,114 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
|
352
352
|
maxTaskRetries: 1,
|
|
353
353
|
};
|
|
354
354
|
|
|
355
|
+
// ─── Policy normalization (single source of truth) ──────────────────────────
|
|
356
|
+
//
|
|
357
|
+
// Every mesh policy passes through mergeAndNormalizePolicy exactly once on write
|
|
358
|
+
// (createMesh/updateMesh) and again whenever a policy is materialized for display
|
|
359
|
+
// or scheduling. Co-locating the default constant, the per-field normalizers, and
|
|
360
|
+
// the merge here keeps the three former layers (DEFAULT_MESH_POLICY, the merge in
|
|
361
|
+
// mesh-config, and the scattered field clamps) from drifting apart. The function
|
|
362
|
+
// is idempotent: feeding it an already-normalized policy yields the same object.
|
|
363
|
+
|
|
364
|
+
const SESSION_CLEANUP_MODES = new Set<RepoMeshSessionCleanupMode>([
|
|
365
|
+
'preserve', 'stop', 'delete_stopped', 'stop_and_delete',
|
|
366
|
+
]);
|
|
367
|
+
const SPAWNED_SESSION_VISIBILITY_MODES = new Set<RepoMeshSpawnedSessionVisibility>([
|
|
368
|
+
'visible', 'hidden',
|
|
369
|
+
]);
|
|
370
|
+
const DIRTY_WORKSPACE_BEHAVIORS = new Set<RepoMeshPolicy['dirtyWorkspaceBehavior']>([
|
|
371
|
+
'block', 'warn', 'checkpoint_then_continue',
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
/** Min/max bounds for the global write-task parallel cap. */
|
|
375
|
+
export const MESH_MAX_PARALLEL_TASKS_MIN = 1;
|
|
376
|
+
export const MESH_MAX_PARALLEL_TASKS_MAX = 8;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Resolve the effective global write-task parallel cap from a raw policy value,
|
|
380
|
+
* clamped to [MESH_MAX_PARALLEL_TASKS_MIN, MESH_MAX_PARALLEL_TASKS_MAX] and
|
|
381
|
+
* defaulting to DEFAULT_MESH_POLICY.maxParallelTasks for a missing/NaN value.
|
|
382
|
+
* Both the config write path and the runtime scheduler read the cap through this
|
|
383
|
+
* helper so they can never disagree on what "max parallel" means.
|
|
384
|
+
*/
|
|
385
|
+
export function resolveMaxParallelTasks(value: unknown): number {
|
|
386
|
+
const n = Number(value);
|
|
387
|
+
if (!Number.isFinite(n)) return DEFAULT_MESH_POLICY.maxParallelTasks;
|
|
388
|
+
return Math.max(MESH_MAX_PARALLEL_TASKS_MIN, Math.min(MESH_MAX_PARALLEL_TASKS_MAX, Math.floor(n)));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Normalize an autoFastForward sub-policy, filling defaults and dropping an
|
|
393
|
+
* invalid maxBehind. Mirrors the (previously mesh-config-local) shape so the merge
|
|
394
|
+
* always emits a fully-populated, valid autoFastForward object.
|
|
395
|
+
*/
|
|
396
|
+
export function normalizeAutoFastForwardPolicy(value: unknown): NonNullable<RepoMeshPolicy['autoFastForward']> {
|
|
397
|
+
const record = value && typeof value === 'object' && !Array.isArray(value)
|
|
398
|
+
? value as Record<string, unknown>
|
|
399
|
+
: {};
|
|
400
|
+
const maxBehind = Number(record.maxBehind);
|
|
401
|
+
return {
|
|
402
|
+
enabled: record.enabled !== false,
|
|
403
|
+
...(Number.isFinite(maxBehind) && maxBehind >= 0 ? { maxBehind: Math.floor(maxBehind) } : {}),
|
|
404
|
+
requireCleanSubmodules: record.requireCleanSubmodules !== false,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Canonical merge+normalize for a RepoMeshPolicy. Layers (lowest→highest):
|
|
410
|
+
* DEFAULT_MESH_POLICY → base (existing persisted policy) → patch (incoming change),
|
|
411
|
+
* then applies every per-field normalizer so the result is always valid regardless
|
|
412
|
+
* of what a hand-edited meshes.json or a partial patch contained.
|
|
413
|
+
*
|
|
414
|
+
* Persistence economy is preserved: schedulingStrategy is dropped when it
|
|
415
|
+
* normalizes to the 'first_eligible' default, and autoConvergeCodeChange is dropped
|
|
416
|
+
* unless explicitly true — so an untouched meshes.json stays byte-for-byte the same.
|
|
417
|
+
*/
|
|
418
|
+
export function mergeAndNormalizePolicy(
|
|
419
|
+
base: RepoMeshPolicy | undefined,
|
|
420
|
+
patch: Partial<RepoMeshPolicy> | undefined,
|
|
421
|
+
): RepoMeshPolicy {
|
|
422
|
+
const autoFastForward = normalizeAutoFastForwardPolicy({
|
|
423
|
+
...DEFAULT_MESH_POLICY.autoFastForward,
|
|
424
|
+
...((base?.autoFastForward && typeof base.autoFastForward === 'object') ? base.autoFastForward : {}),
|
|
425
|
+
...((patch?.autoFastForward && typeof patch.autoFastForward === 'object') ? patch.autoFastForward : {}),
|
|
426
|
+
});
|
|
427
|
+
const policy: RepoMeshPolicy = {
|
|
428
|
+
...DEFAULT_MESH_POLICY,
|
|
429
|
+
...(base || {}),
|
|
430
|
+
...(patch || {}),
|
|
431
|
+
autoFastForward,
|
|
432
|
+
};
|
|
433
|
+
if (!DIRTY_WORKSPACE_BEHAVIORS.has(policy.dirtyWorkspaceBehavior)) {
|
|
434
|
+
policy.dirtyWorkspaceBehavior = 'warn';
|
|
435
|
+
}
|
|
436
|
+
policy.maxParallelTasks = resolveMaxParallelTasks(policy.maxParallelTasks);
|
|
437
|
+
policy.allowAutoPublishSubmoduleMainCommits = policy.allowAutoPublishSubmoduleMainCommits === true;
|
|
438
|
+
if (!SESSION_CLEANUP_MODES.has(policy.sessionCleanupOnNodeRemove as RepoMeshSessionCleanupMode)) {
|
|
439
|
+
policy.sessionCleanupOnNodeRemove = 'preserve';
|
|
440
|
+
}
|
|
441
|
+
if (!SPAWNED_SESSION_VISIBILITY_MODES.has(policy.spawnedSessionVisibility as RepoMeshSpawnedSessionVisibility)) {
|
|
442
|
+
policy.spawnedSessionVisibility = DEFAULT_MESH_POLICY.spawnedSessionVisibility;
|
|
443
|
+
}
|
|
444
|
+
// Load-balancing: normalize the scheduling strategy so an invalid/blank value
|
|
445
|
+
// falls back to 'first_eligible' (strict no-change). Only persist the field when
|
|
446
|
+
// it is explicitly a non-default value to keep existing meshes.json untouched.
|
|
447
|
+
const normalizedStrategy = normalizeMeshSchedulingStrategy(policy.schedulingStrategy);
|
|
448
|
+
if (normalizedStrategy === 'first_eligible') {
|
|
449
|
+
delete policy.schedulingStrategy;
|
|
450
|
+
} else {
|
|
451
|
+
policy.schedulingStrategy = normalizedStrategy;
|
|
452
|
+
}
|
|
453
|
+
// Convergence routing: strict opt-in (default false). Only persist when explicitly
|
|
454
|
+
// enabled so existing meshes.json stays byte-for-byte untouched.
|
|
455
|
+
if (policy.autoConvergeCodeChange === true) {
|
|
456
|
+
policy.autoConvergeCodeChange = true;
|
|
457
|
+
} else {
|
|
458
|
+
delete policy.autoConvergeCodeChange;
|
|
459
|
+
}
|
|
460
|
+
return policy;
|
|
461
|
+
}
|
|
462
|
+
|
|
355
463
|
/**
|
|
356
464
|
* Resolve whether a delegated worker session launched onto `nodePolicy` (within a mesh
|
|
357
465
|
* governed by `meshPolicy`) should auto-approve. Precedence: node override → mesh policy
|
|
@@ -585,6 +693,44 @@ export interface LocalMeshNodeEntry {
|
|
|
585
693
|
|
|
586
694
|
// ─── Mesh Status (runtime, not persisted) ───────
|
|
587
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Per-(node, provider) cap + consumption, as surfaced on a node's scheduling
|
|
698
|
+
* status. Wire-shape mirror of MeshNodeProviderSchedulingRuntime.
|
|
699
|
+
*/
|
|
700
|
+
export interface RepoMeshNodeProviderSchedulingStatus {
|
|
701
|
+
providerType: string;
|
|
702
|
+
maxParallel?: number;
|
|
703
|
+
activeAssigned: number;
|
|
704
|
+
capReached: boolean;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Per-node scheduling runtime exposed on RepoMeshNodeStatus.scheduling. Carried in
|
|
709
|
+
* full by verbose mesh_status; compact mesh_status sends only {load, capReached}.
|
|
710
|
+
*/
|
|
711
|
+
export interface RepoMeshNodeSchedulingStatus {
|
|
712
|
+
load: number;
|
|
713
|
+
schedulingPriority?: number;
|
|
714
|
+
maxConcurrentSessions?: number;
|
|
715
|
+
providerRoles?: RepoMeshNodeProviderSchedulingStatus[];
|
|
716
|
+
capReached: boolean;
|
|
717
|
+
capReasons?: string[];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Mesh-level scheduling rollup exposed on RepoMeshStatus.scheduling: which tie-break
|
|
722
|
+
* strategy is live and how much of the global parallel caps is consumed.
|
|
723
|
+
*/
|
|
724
|
+
export interface RepoMeshSchedulingStatus {
|
|
725
|
+
strategy: RepoMeshSchedulingStrategy;
|
|
726
|
+
maxParallelTasks: number;
|
|
727
|
+
maxReadonlyParallelTasks: number;
|
|
728
|
+
activeWriteAssigned: number;
|
|
729
|
+
activeReadonlyAssigned: number;
|
|
730
|
+
globalWriteCapReached: boolean;
|
|
731
|
+
globalReadonlyCapReached: boolean;
|
|
732
|
+
}
|
|
733
|
+
|
|
588
734
|
export interface RepoMeshStatus {
|
|
589
735
|
meshId: string;
|
|
590
736
|
meshName: string;
|
|
@@ -595,6 +741,11 @@ export interface RepoMeshStatus {
|
|
|
595
741
|
nodes: RepoMeshNodeStatus[];
|
|
596
742
|
queue?: RepoMeshQueueStatus;
|
|
597
743
|
ledger?: RepoMeshLedgerStatus;
|
|
744
|
+
/**
|
|
745
|
+
* Mesh-level scheduling rollup (strategy + global cap consumption). Omitted by
|
|
746
|
+
* daemons predating the scheduling-runtime exposure — treat as optional.
|
|
747
|
+
*/
|
|
748
|
+
scheduling?: RepoMeshSchedulingStatus;
|
|
598
749
|
/**
|
|
599
750
|
* Mission summaries for the dashboard overview. Active/paused missions plus a
|
|
600
751
|
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
@@ -674,6 +825,19 @@ export interface RepoMeshNodeStatus {
|
|
|
674
825
|
lastSeenAt?: string;
|
|
675
826
|
updatedAt?: string;
|
|
676
827
|
connection?: RepoMeshPeerConnectionStatus;
|
|
828
|
+
/**
|
|
829
|
+
* Per-node scheduling runtime (load / priority / provider caps / claim-block
|
|
830
|
+
* reasons). Verbose mesh_status carries the full shape; compact carries only
|
|
831
|
+
* {load, capReached}. Omitted by daemons predating the exposure.
|
|
832
|
+
*/
|
|
833
|
+
scheduling?: RepoMeshNodeSchedulingStatus;
|
|
834
|
+
/**
|
|
835
|
+
* Stale-daemon-build marker: the live daemon's build commit is a strict ancestor
|
|
836
|
+
* of this node's workspace HEAD (merged code not yet live). Best-effort, set by
|
|
837
|
+
* mesh_status when the git probe reports daemonBuildBehind; shape is daemon-defined
|
|
838
|
+
* (scope/isDaemonAffecting flags). Omitted when the build is current.
|
|
839
|
+
*/
|
|
840
|
+
staleDaemonBuild?: Record<string, unknown>;
|
|
677
841
|
error?: string;
|
|
678
842
|
}
|
|
679
843
|
|