@adhdev/daemon-core 0.9.82-rc.482 → 0.9.82-rc.483
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 +5 -0
- package/dist/index.js +98 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -7
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/contracts.d.ts +27 -4
- package/dist/repo-mesh-types.d.ts +1 -1
- package/package.json +3 -3
- package/src/commands/low-family/coordinator-prompt.ts +53 -0
- package/src/commands/med-family/mesh-crud.ts +11 -0
- package/src/config/mesh-config.ts +12 -0
- package/src/mesh/contracts.ts +42 -7
- package/src/mesh/coordinator-prompt.ts +19 -0
- package/src/mesh/mesh-events-pending.ts +20 -0
- package/src/repo-mesh-types.ts +1 -1
package/dist/mesh/contracts.d.ts
CHANGED
|
@@ -161,7 +161,12 @@ export declare function assertPendingMeshCoordinatorEventV2(raw: unknown, path?:
|
|
|
161
161
|
* Decide whether a v2 pending event should be delivered to the given drainer.
|
|
162
162
|
* Centralised so every drain implementation uses the same rule.
|
|
163
163
|
*
|
|
164
|
-
* - 'broadcast': always delivered.
|
|
164
|
+
* - 'broadcast': always delivered. NOTE: a terminal task event that reached the
|
|
165
|
+
* queue as broadcast is an ownership leak (it belongs to its dispatching
|
|
166
|
+
* coordinator). This pure helper does not have the drain-window
|
|
167
|
+
* daemon-form/session matching semantics, so the terminal+broadcast
|
|
168
|
+
* dispatchedBy filter is applied one layer up in the drainer (see
|
|
169
|
+
* mesh-events-pending routeV2EventsForDrainer) where those semantics live.
|
|
165
170
|
* - 'system': never delivered to coordinators (system handler only).
|
|
166
171
|
* - 'unicast': delivered iff intendedFor matches drainer identity.
|
|
167
172
|
*
|
|
@@ -170,6 +175,15 @@ export declare function assertPendingMeshCoordinatorEventV2(raw: unknown, path?:
|
|
|
170
175
|
* are quarantined to a dedicated drain endpoint instead.
|
|
171
176
|
*/
|
|
172
177
|
export declare function shouldDeliverPendingEventToCoordinator(event: PendingMeshCoordinatorEventV2, drainer: CoordinatorIdentity): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* True for a terminal task event (completion / stop / refine outcome). A terminal
|
|
180
|
+
* event belongs to exactly the coordinator that dispatched the task, so it must
|
|
181
|
+
* never fan out to sibling coordinators that did not dispatch it. Used by the
|
|
182
|
+
* emit-side stamp (to avoid downgrading an unaddressed terminal event to full
|
|
183
|
+
* broadcast) and by the drain-side filter (defense-in-depth for any terminal
|
|
184
|
+
* event that already reached the queue as broadcast).
|
|
185
|
+
*/
|
|
186
|
+
export declare function isTerminalTaskEvent(eventName: string): boolean;
|
|
173
187
|
/**
|
|
174
188
|
* Default the v2 scope for an event by its producer event name (design decision
|
|
175
189
|
* §3). Terminal task events and coordinator-addressed alerts → unicast (routed
|
|
@@ -219,9 +233,18 @@ export declare function coordinatorIdentityFromEmitFields(fields: {
|
|
|
219
233
|
* returns undefined: the event stays a v1 (unstamped) event and is broadcast-
|
|
220
234
|
* treated during rollout, exactly as before — no regression, no fabricated
|
|
221
235
|
* identity. When the resolved scope is 'unicast' but no `intendedFor` is
|
|
222
|
-
* available, the
|
|
223
|
-
*
|
|
224
|
-
*
|
|
236
|
+
* available, the fallback depends on the event class:
|
|
237
|
+
*
|
|
238
|
+
* - Terminal task events (completion / stop / refine outcome) MUST NOT be
|
|
239
|
+
* broadcast to every coordinator — a completion belongs to the coordinator
|
|
240
|
+
* that dispatched the task, and broadcasting it makes non-owner coordinators
|
|
241
|
+
* (e.g. sibling MAGI coordinators that never dispatched this replica's task)
|
|
242
|
+
* act on a completion that is not theirs (MAGI-REPLICA-COMPLETION-EVENT-LEAK).
|
|
243
|
+
* For these we address the event to `dispatchedBy` (the dispatching
|
|
244
|
+
* coordinator) and KEEP it unicast, so the stamp stays contract-valid and the
|
|
245
|
+
* event reaches only its originating coordinator.
|
|
246
|
+
* - Any other unicast event with no addressable target falls back to broadcast
|
|
247
|
+
* (contract-valid, still delivered, never dropped) — unchanged.
|
|
225
248
|
*/
|
|
226
249
|
export declare function buildPendingEventEmitStamp(opts: {
|
|
227
250
|
eventName: string;
|
|
@@ -372,7 +372,7 @@ export declare function magiAutoLaunchedSessionCleanupDecision(args: {
|
|
|
372
372
|
};
|
|
373
373
|
/** Min/max bounds for the global write-task parallel cap. */
|
|
374
374
|
export declare const MESH_MAX_PARALLEL_TASKS_MIN = 1;
|
|
375
|
-
export declare const MESH_MAX_PARALLEL_TASKS_MAX =
|
|
375
|
+
export declare const MESH_MAX_PARALLEL_TASKS_MAX = 64;
|
|
376
376
|
/**
|
|
377
377
|
* Default multiplier applied to the write cap to derive the read-only diagnosis
|
|
378
378
|
* cap. Read-only (live_debug_readonly) tasks carry no isolation/merge cost so they
|
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.483",
|
|
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",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.483",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.483",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -8,6 +8,59 @@
|
|
|
8
8
|
import type { LowFamilyContext, LowFamilyHandler } from './types.js';
|
|
9
9
|
|
|
10
10
|
export const coordinatorPromptHandlers: Record<string, LowFamilyHandler> = {
|
|
11
|
+
/**
|
|
12
|
+
* Render the coordinator system prompt for a mesh + CLI type, so the
|
|
13
|
+
* dashboard can show the operator exactly what a coordinator session
|
|
14
|
+
* receives by default. This resolves the mesh, applies its repo-mesh
|
|
15
|
+
* config, and runs the SAME buildCoordinatorSystemPrompt the launch path
|
|
16
|
+
* uses — minus the runtime-only best-effort sections (mission / recent
|
|
17
|
+
* activity / operating notes), which are launch-scope and not part of the
|
|
18
|
+
* static "default base" an operator is trying to preview here.
|
|
19
|
+
*
|
|
20
|
+
* It respects mesh-level and user-file override/append layering, so the
|
|
21
|
+
* preview reflects the effective prompt: with no overrides configured it
|
|
22
|
+
* shows the pure daemon default; with an override set it shows that.
|
|
23
|
+
*/
|
|
24
|
+
coordinator_prompt_preview: async (ctx: LowFamilyContext, args: any) => {
|
|
25
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
26
|
+
const cliType = typeof args?.cliType === 'string' && args.cliType.trim() ? args.cliType.trim() : 'claude-cli';
|
|
27
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
28
|
+
try {
|
|
29
|
+
// Prefer the router-bound resolver (inline cache aware); fall back to
|
|
30
|
+
// local config when a bare context is used (e.g. unit tests).
|
|
31
|
+
let mesh: any = null;
|
|
32
|
+
if (ctx.getMeshForCommand) {
|
|
33
|
+
const resolved = await ctx.getMeshForCommand(meshId);
|
|
34
|
+
mesh = resolved?.mesh ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (!mesh) {
|
|
37
|
+
const { getMesh } = await import('../../config/mesh-config.js');
|
|
38
|
+
mesh = getMesh(meshId);
|
|
39
|
+
}
|
|
40
|
+
if (!mesh) return { success: false, error: `mesh not found: ${meshId}` };
|
|
41
|
+
|
|
42
|
+
// Apply the on-disk repo-mesh config overlay exactly as launch does,
|
|
43
|
+
// so policy/nodes reflect the effective mesh.
|
|
44
|
+
let effectiveMesh = mesh;
|
|
45
|
+
try {
|
|
46
|
+
const { loadRepoMeshJsonConfig, applyRepoMeshConfig } = await import('../../config/mesh-json-config.js');
|
|
47
|
+
const workspace = typeof mesh?.workspace === 'string' ? mesh.workspace : undefined;
|
|
48
|
+
if (workspace) {
|
|
49
|
+
const loaded = loadRepoMeshJsonConfig(workspace);
|
|
50
|
+
if (loaded?.sourceType !== 'invalid') {
|
|
51
|
+
effectiveMesh = applyRepoMeshConfig(mesh, loaded?.config);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch { /* overlay is best-effort — fall back to the raw mesh */ }
|
|
55
|
+
|
|
56
|
+
const { buildCoordinatorSystemPrompt } = await import('../../mesh/coordinator-prompt.js');
|
|
57
|
+
const prompt = buildCoordinatorSystemPrompt({ mesh: effectiveMesh, coordinatorCliType: cliType });
|
|
58
|
+
return { success: true, prompt, cliType, meshId, bytes: Buffer.byteLength(prompt, 'utf8') };
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
return { success: false, error: error?.message || String(error) };
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
11
64
|
list_coordinator_prompts: async (_ctx: LowFamilyContext, _args: any) => {
|
|
12
65
|
const fs = await import('node:fs');
|
|
13
66
|
const path = await import('node:path');
|
|
@@ -480,6 +480,9 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
480
480
|
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
481
481
|
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
482
482
|
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
483
|
+
const capabilities = Array.isArray(args?.capabilities)
|
|
484
|
+
? args.capabilities.map((t: any) => typeof t === 'string' ? t.trim() : '').filter(Boolean)
|
|
485
|
+
: undefined;
|
|
483
486
|
const node = addNode(meshId, {
|
|
484
487
|
workspace,
|
|
485
488
|
...(repoRoot ? { repoRoot } : {}),
|
|
@@ -487,6 +490,7 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
487
490
|
...(machineId ? { machineId } : {}),
|
|
488
491
|
...(policy ? { policy } : {}),
|
|
489
492
|
...(role ? { role } : {}),
|
|
493
|
+
...(capabilities && capabilities.length ? { capabilities } : {}),
|
|
490
494
|
});
|
|
491
495
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
492
496
|
// mesh_status hands back a coordinator-memory aggregate
|
|
@@ -543,6 +547,13 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
543
547
|
} else if (args?.systemPrompt === null) {
|
|
544
548
|
patch.systemPrompt = undefined;
|
|
545
549
|
}
|
|
550
|
+
// Operator custom capability tags. An explicit (possibly empty) array
|
|
551
|
+
// replaces them; omitting the arg leaves existing tags untouched.
|
|
552
|
+
if (Array.isArray(args?.capabilities)) {
|
|
553
|
+
patch.capabilities = args.capabilities
|
|
554
|
+
.map((t: any) => typeof t === 'string' ? t.trim() : '')
|
|
555
|
+
.filter(Boolean);
|
|
556
|
+
}
|
|
546
557
|
const node = updateNode(meshId, nodeId, patch as any);
|
|
547
558
|
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
548
559
|
// Provider priority / systemPrompt changes don't touch
|
|
@@ -570,6 +570,11 @@ export function updateNode(
|
|
|
570
570
|
opts: {
|
|
571
571
|
userOverrides?: Partial<RepoMeshNodeCapabilities>;
|
|
572
572
|
policy?: RepoMeshNodePolicy;
|
|
573
|
+
/** Operator-defined custom capability tags used by mesh queue matching.
|
|
574
|
+
* Passing an array replaces the node's custom tags (empty/whitespace
|
|
575
|
+
* entries dropped, deduped); an empty result clears them. Omit to leave
|
|
576
|
+
* the existing tags untouched. */
|
|
577
|
+
capabilities?: string[];
|
|
573
578
|
worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
|
|
574
579
|
/** Per-node instruction surfaced in the coordinator prompt. Pass an
|
|
575
580
|
* empty string or undefined to clear it. */
|
|
@@ -610,6 +615,13 @@ export function updateNode(
|
|
|
610
615
|
node.reportedDaemonBuildVersion = opts.reportedDaemonBuildVersion.trim();
|
|
611
616
|
}
|
|
612
617
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
618
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'capabilities')) {
|
|
619
|
+
// Explicit replace: normalize (trim/dedup/drop-empties); an empty result
|
|
620
|
+
// clears the tags entirely so the field never persists as [].
|
|
621
|
+
const tags = normalizeCapabilityTags(opts.capabilities);
|
|
622
|
+
if (tags && tags.length) node.capabilities = tags;
|
|
623
|
+
else delete node.capabilities;
|
|
624
|
+
}
|
|
613
625
|
if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
|
|
614
626
|
if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
|
|
615
627
|
// Honor explicit clears: { systemPrompt: undefined } drops the field.
|
package/src/mesh/contracts.ts
CHANGED
|
@@ -332,7 +332,12 @@ export function assertPendingMeshCoordinatorEventV2(raw: unknown, path = '$'): P
|
|
|
332
332
|
* Decide whether a v2 pending event should be delivered to the given drainer.
|
|
333
333
|
* Centralised so every drain implementation uses the same rule.
|
|
334
334
|
*
|
|
335
|
-
* - 'broadcast': always delivered.
|
|
335
|
+
* - 'broadcast': always delivered. NOTE: a terminal task event that reached the
|
|
336
|
+
* queue as broadcast is an ownership leak (it belongs to its dispatching
|
|
337
|
+
* coordinator). This pure helper does not have the drain-window
|
|
338
|
+
* daemon-form/session matching semantics, so the terminal+broadcast
|
|
339
|
+
* dispatchedBy filter is applied one layer up in the drainer (see
|
|
340
|
+
* mesh-events-pending routeV2EventsForDrainer) where those semantics live.
|
|
336
341
|
* - 'system': never delivered to coordinators (system handler only).
|
|
337
342
|
* - 'unicast': delivered iff intendedFor matches drainer identity.
|
|
338
343
|
*
|
|
@@ -367,6 +372,18 @@ const TERMINAL_TASK_EVENTS: ReadonlySet<string> = new Set([
|
|
|
367
372
|
'refine:accepted',
|
|
368
373
|
]);
|
|
369
374
|
|
|
375
|
+
/**
|
|
376
|
+
* True for a terminal task event (completion / stop / refine outcome). A terminal
|
|
377
|
+
* event belongs to exactly the coordinator that dispatched the task, so it must
|
|
378
|
+
* never fan out to sibling coordinators that did not dispatch it. Used by the
|
|
379
|
+
* emit-side stamp (to avoid downgrading an unaddressed terminal event to full
|
|
380
|
+
* broadcast) and by the drain-side filter (defense-in-depth for any terminal
|
|
381
|
+
* event that already reached the queue as broadcast).
|
|
382
|
+
*/
|
|
383
|
+
export function isTerminalTaskEvent(eventName: string): boolean {
|
|
384
|
+
return TERMINAL_TASK_EVENTS.has(eventName);
|
|
385
|
+
}
|
|
386
|
+
|
|
370
387
|
/**
|
|
371
388
|
* Coordinator-addressed dispatch-plane alerts. `mesh:dispatch_blocked` is the
|
|
372
389
|
* Fix (1) actionable dispatch-skip notification: it exists precisely to page the
|
|
@@ -445,9 +462,18 @@ export function coordinatorIdentityFromEmitFields(fields: {
|
|
|
445
462
|
* returns undefined: the event stays a v1 (unstamped) event and is broadcast-
|
|
446
463
|
* treated during rollout, exactly as before — no regression, no fabricated
|
|
447
464
|
* identity. When the resolved scope is 'unicast' but no `intendedFor` is
|
|
448
|
-
* available, the
|
|
449
|
-
*
|
|
450
|
-
*
|
|
465
|
+
* available, the fallback depends on the event class:
|
|
466
|
+
*
|
|
467
|
+
* - Terminal task events (completion / stop / refine outcome) MUST NOT be
|
|
468
|
+
* broadcast to every coordinator — a completion belongs to the coordinator
|
|
469
|
+
* that dispatched the task, and broadcasting it makes non-owner coordinators
|
|
470
|
+
* (e.g. sibling MAGI coordinators that never dispatched this replica's task)
|
|
471
|
+
* act on a completion that is not theirs (MAGI-REPLICA-COMPLETION-EVENT-LEAK).
|
|
472
|
+
* For these we address the event to `dispatchedBy` (the dispatching
|
|
473
|
+
* coordinator) and KEEP it unicast, so the stamp stays contract-valid and the
|
|
474
|
+
* event reaches only its originating coordinator.
|
|
475
|
+
* - Any other unicast event with no addressable target falls back to broadcast
|
|
476
|
+
* (contract-valid, still delivered, never dropped) — unchanged.
|
|
451
477
|
*/
|
|
452
478
|
export function buildPendingEventEmitStamp(opts: {
|
|
453
479
|
eventName: string;
|
|
@@ -460,9 +486,18 @@ export function buildPendingEventEmitStamp(opts: {
|
|
|
460
486
|
let scope: MeshEventScope = opts.scope ?? defaultScopeForEvent(opts.eventName);
|
|
461
487
|
let intendedFor = opts.intendedFor;
|
|
462
488
|
if (scope === 'unicast' && !intendedFor) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
489
|
+
if (isTerminalTaskEvent(opts.eventName)) {
|
|
490
|
+
// Terminal event with no explicit target: address it to the dispatching
|
|
491
|
+
// coordinator rather than broadcasting to every coordinator. dispatchedBy
|
|
492
|
+
// is the coordinator that owns the task, so this is the correct — and
|
|
493
|
+
// contract-valid (unicast requires intendedFor) — narrowing.
|
|
494
|
+
intendedFor = opts.dispatchedBy;
|
|
495
|
+
} else {
|
|
496
|
+
// No addressable target for a non-terminal unicast event — fall back to
|
|
497
|
+
// broadcast so the stamp is contract-valid and the event is still
|
|
498
|
+
// delivered (never dropped).
|
|
499
|
+
scope = 'broadcast';
|
|
500
|
+
}
|
|
466
501
|
}
|
|
467
502
|
if (scope !== 'unicast') intendedFor = undefined;
|
|
468
503
|
return {
|
|
@@ -454,6 +454,23 @@ function buildNodeConfigSection(mesh: LocalMeshEntry): string {
|
|
|
454
454
|
: [];
|
|
455
455
|
const providerRolesSuffix = providerRoles.length ? ` | caps: ${providerRoles.join(', ')}` : '';
|
|
456
456
|
lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${providerRolesSuffix}${suffix}`);
|
|
457
|
+
// Routing tags: what this node advertises for mesh_enqueue_task required_tags.
|
|
458
|
+
// Surfaced so the coordinator can route by-capability (e.g. enqueue a Windows
|
|
459
|
+
// build with required_tags:["os=win32"], or a custom "test-runner" node).
|
|
460
|
+
// os=/arch= use the same userOverrides → reported precedence as the matcher;
|
|
461
|
+
// the internal converge= tag is omitted (it is not something to target by hand).
|
|
462
|
+
const routingTags: string[] = [];
|
|
463
|
+
const custom = Array.isArray((n as any).capabilities) ? (n as any).capabilities : [];
|
|
464
|
+
for (const t of custom) { const s = typeof t === 'string' ? t.trim() : ''; if (s) routingTags.push(s); }
|
|
465
|
+
const tagOs = ((n as any).userOverrides?.platform || (n as any).reportedPlatform || '').toString().trim();
|
|
466
|
+
const tagArch = ((n as any).userOverrides?.arch || (n as any).reportedArch || '').toString().trim();
|
|
467
|
+
if (tagOs) routingTags.push(`os=${tagOs}`);
|
|
468
|
+
if (tagArch) routingTags.push(`arch=${tagArch}`);
|
|
469
|
+
const wtBranch = typeof (n as any).worktreeBranch === 'string' ? (n as any).worktreeBranch.trim() : '';
|
|
470
|
+
if (n.isLocalWorktree && wtBranch) routingTags.push(`worktree=${wtBranch}`);
|
|
471
|
+
if (routingTags.length) {
|
|
472
|
+
lines.push(` 🏷️ routing tags: ${routingTags.map(t => `\`${t}\``).join(', ')}`);
|
|
473
|
+
}
|
|
457
474
|
const nodePrompt = typeof (n as any).systemPrompt === 'string' ? (n as any).systemPrompt.trim() : '';
|
|
458
475
|
if (nodePrompt) {
|
|
459
476
|
lines.push(` 📌 Node instruction: ${indentFollowing(nodePrompt, ' ')}`);
|
|
@@ -663,6 +680,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
|
|
|
663
680
|
3. **Queue / Delegate** — The Mesh uses an autonomous pull-based Work Queue:
|
|
664
681
|
a. **General Tasks**: Enqueue tasks using \`mesh_enqueue_task\`. Idle node agents will automatically pull tasks from the queue and begin working.
|
|
665
682
|
b. **Node Preparation**: Reuse an existing idle session on the correct node/provider before launching a new chat/session. Call \`mesh_launch_session\` only when no suitable session exists, when the user explicitly asks for a fresh provider/session, or when branch/worktree isolation requires it. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
|
|
683
|
+
b1. **Keep a branch's work on its worktree (worktree affinity).** A worktree node is a durable per-branch workspace, not a one-task throwaway — implement, review, and fix for the same branch all belong on the SAME worktree, and it lives until its work is converged (merged/pushed) and it is cleaned up. So once you clone a worktree for a branch, route every subsequent \`code_change\`/\`validation\`/fix task for that branch back to that same node: pass \`required_tags: ["worktree=<branch>"]\` or \`target_node_id: <that worktree node's id>\`. **Where to get the node id / tag:** the \`mesh_clone_node\` result returns the new node's \`id\` and \`worktreeBranch\` directly — use them immediately. The Configured Nodes list in this prompt is a launch-time snapshot and will NOT list a worktree you cloned after this session started, so do not rely on it for freshly-cloned worktrees; take the id/branch from the \`mesh_clone_node\` result, or call \`mesh_status\` to re-list the live nodes (each worktree there advertises its \`worktree=<branch>\` tag). Do NOT leave same-branch follow-ups untargeted — an untargeted task is claimed by whichever node polls first (usually the base machine node), which strands the work off the branch's worktree. The ONE exception is a \`convergence\` task (merge/push): that is base-only and must NOT be pinned to the worktree.
|
|
666
684
|
c. **Targeted Tasks**: Use \`mesh_send_task\` only when you need to bypass the queue and force a specific node to execute a task immediately.
|
|
667
685
|
d. For the first dispatch of a new task, provide a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
|
|
668
686
|
e. For a continuation of the same issue in an existing session, send a concise **delta instruction**: current verified state, the exact failed/blocked step, the newly approved action, and final reporting requirements. Do not resend the full original task or open a new chat solely to continue the same work; that wastes coordinator and worker context.
|
|
@@ -718,6 +736,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
718
736
|
- **Route, don't implement.** Delegate all code reading, analysis, and execution to node agents. Never read source files or run commands in the coordinator — keep context lean.
|
|
719
737
|
- **Front-load task messages.** Include everything the agent needs (files, problem, expected fix) in \`mesh_enqueue_task\` / \`mesh_send_task\`. Append a structured result request at the end: ask the worker to conclude with a JSON block containing \`status\`, \`changedFiles\`, \`gitStatus\`, \`validationResults\`, \`errors\`, \`nextAction\`. The daemon parses this automatically; you can read it from \`mesh_task_history\`.
|
|
720
738
|
- **Reuse idle sessions.** For follow-up, retry, commit/push, or cleanup on the same issue, send only the delta to the existing idle session. Start fresh only for independent work, provider mismatch, transcript contamination, or required worktree isolation.
|
|
739
|
+
- **Worktree affinity.** A worktree is a durable per-branch workspace; keep all of a branch's code_change/fix/review work on its worktree node by targeting \`required_tags: ["worktree=<branch>"]\` or \`target_node_id\`. Get the id/branch from the \`mesh_clone_node\` result or a live \`mesh_status\` — the Configured Nodes snapshot won't list a worktree cloned after launch. Untargeted same-branch follow-ups drift to the base node. Only \`convergence\` (merge/push) runs on the base, never pinned to the worktree.
|
|
721
740
|
- **Respect explicit provider requests.** Map: Hermes → \`hermes-cli\`, Claude/Claude Code → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`, Antigravity → \`antigravity-cli\`. Never substitute the coordinator's own runtime.
|
|
722
741
|
- **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
|
|
723
742
|
- **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing. This caps *concurrent* load — it does not mean serialize independent work: when a new, independent request arrives and there is headroom under \`maxParallelTasks\`, dispatch it right away rather than waiting for an in-flight task or a user nudge (read-only diagnosis especially, since it has no merge cost).
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
coordinatorIdentityFromEmitFields,
|
|
15
15
|
coordinatorIdentityKey,
|
|
16
16
|
isMeshEventScope,
|
|
17
|
+
isTerminalTaskEvent,
|
|
17
18
|
MESH_PROTOCOL_VERSION_V2,
|
|
18
19
|
shouldDeliverPendingEventToCoordinator,
|
|
19
20
|
type CoordinatorIdentity,
|
|
@@ -408,6 +409,25 @@ function routeV2EventsForDrainer(
|
|
|
408
409
|
// Broadcast → any coordinator; system → daemon handler only (never a
|
|
409
410
|
// coordinator). Delegates to the contract helper for those two scopes.
|
|
410
411
|
if (validated.scope !== 'unicast') {
|
|
412
|
+
// Defense-in-depth (MAGI-REPLICA-COMPLETION-EVENT-LEAK): a TERMINAL task
|
|
413
|
+
// event that reached the queue as broadcast is an ownership leak — a
|
|
414
|
+
// completion/stop belongs to the coordinator that dispatched the task, so
|
|
415
|
+
// a sibling coordinator that never dispatched it must NOT act on it. The
|
|
416
|
+
// emit-side stamp now narrows unaddressed terminal events to unicast, but a
|
|
417
|
+
// legacy/version-skewed/other-path broadcast can still arrive here; filter
|
|
418
|
+
// it by dispatchedBy vs the drainer using the SAME daemon-form/session
|
|
419
|
+
// matching semantics as unicast (identityDeliversTo), so the true owner —
|
|
420
|
+
// possibly addressed under a different daemon-id form — still receives it.
|
|
421
|
+
if (validated.scope === 'broadcast' && isTerminalTaskEvent(validated.event)) {
|
|
422
|
+
if (identityDeliversTo(validated.dispatchedBy, drainer)) {
|
|
423
|
+
ctx.batchSeen.add(eventId);
|
|
424
|
+
bump('v2Delivered');
|
|
425
|
+
kept.push(event);
|
|
426
|
+
} else {
|
|
427
|
+
bump('v2RoutedAway');
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
411
431
|
if (shouldDeliverPendingEventToCoordinator(validated, drainer)) {
|
|
412
432
|
ctx.batchSeen.add(eventId);
|
|
413
433
|
bump('v2Delivered');
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -527,7 +527,7 @@ const DIRTY_WORKSPACE_BEHAVIORS = new Set<RepoMeshPolicy['dirtyWorkspaceBehavior
|
|
|
527
527
|
|
|
528
528
|
/** Min/max bounds for the global write-task parallel cap. */
|
|
529
529
|
export const MESH_MAX_PARALLEL_TASKS_MIN = 1;
|
|
530
|
-
export const MESH_MAX_PARALLEL_TASKS_MAX =
|
|
530
|
+
export const MESH_MAX_PARALLEL_TASKS_MAX = 64;
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
533
|
* Default multiplier applied to the write cap to derive the read-only diagnosis
|