@adhdev/daemon-core 0.9.82-rc.414 → 0.9.82-rc.416
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/cli-adapters/resolve-executable.d.ts +55 -0
- package/dist/config/mesh-config.d.ts +23 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +354 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +343 -47
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +1 -1
- package/dist/mesh/mesh-events.d.ts +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +18 -0
- package/dist/mesh/mesh-work-queue.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +8 -0
- package/package.json +2 -2
- package/src/cli-adapters/resolve-executable.ts +117 -3
- package/src/commands/med-family/mesh-queue.ts +23 -1
- package/src/config/mesh-config.ts +116 -0
- package/src/index.ts +12 -0
- package/src/mesh/mesh-event-forwarding.ts +23 -7
- package/src/mesh/mesh-events-coordinator.ts +1 -0
- package/src/mesh/mesh-events-pending.ts +15 -0
- package/src/mesh/mesh-events.ts +1 -0
- package/src/mesh/mesh-ledger.ts +7 -0
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-queue-assignment.ts +24 -0
- package/src/mesh/mesh-refine-gates.ts +12 -5
- package/src/mesh/mesh-work-queue.ts +13 -0
- package/src/mesh/worktree-bootstrap-config.ts +6 -2
- package/src/repo-mesh-types.ts +8 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// MAGI (Multi-Agent Ground-truth Insight) activity reconstruction for mesh_status.
|
|
2
|
+
//
|
|
3
|
+
// MAGI cross-verification runs are persisted in the mesh ledger as `magi_dispatched`
|
|
4
|
+
// (fan-out enqueued) and `magi_synthesis` (collected + synthesized) entries, keyed by
|
|
5
|
+
// consensusGroupId. This module reconstructs the latest per-group activity from a
|
|
6
|
+
// ledger window and folds it into a BOUNDED mesh_status section so a coordinator (and
|
|
7
|
+
// the web dashboard's extractMagiActivity) can read the synthesis fields — the
|
|
8
|
+
// needs_verification counts, independence banner, and git skew — without re-running
|
|
9
|
+
// collection. Mirrors mesh-refine-status.ts (buildMeshAsyncRefineJobs / summarize…).
|
|
10
|
+
//
|
|
11
|
+
// Pure: operates on a ledger-entry array. No I/O, no Node/DOM APIs.
|
|
12
|
+
|
|
13
|
+
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
14
|
+
import type { MagiGitSkew } from '@adhdev/mesh-shared';
|
|
15
|
+
|
|
16
|
+
export type MeshMagiActivityStatus = 'running' | 'synthesized';
|
|
17
|
+
|
|
18
|
+
/** A bounded needs_verification preview item (claim text + category only). */
|
|
19
|
+
export interface MeshMagiNeedsVerificationItem {
|
|
20
|
+
claim: string;
|
|
21
|
+
category: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MeshMagiActivitySummary {
|
|
25
|
+
consensusGroupId: string;
|
|
26
|
+
status: MeshMagiActivityStatus;
|
|
27
|
+
missionId?: string;
|
|
28
|
+
panel?: string;
|
|
29
|
+
question?: string;
|
|
30
|
+
replicaCount?: number;
|
|
31
|
+
answered?: number;
|
|
32
|
+
missing?: number;
|
|
33
|
+
staleReplicas?: number;
|
|
34
|
+
needsVerificationCount?: number;
|
|
35
|
+
agreedCount?: number;
|
|
36
|
+
independenceBanner?: string | null;
|
|
37
|
+
gitSkew?: MagiGitSkew;
|
|
38
|
+
/** Bounded sample of the needs_verification clusters (claim + category). */
|
|
39
|
+
needsVerification?: MeshMagiNeedsVerificationItem[];
|
|
40
|
+
openQuestions?: string[];
|
|
41
|
+
lastLedgerKind?: string;
|
|
42
|
+
lastUpdatedAt?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readString(value: unknown): string | undefined {
|
|
46
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
+
? value as Record<string, unknown>
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readNumber(value: unknown): number | undefined {
|
|
56
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Cap on inlined needs_verification preview items per group (keeps the payload bounded). */
|
|
60
|
+
export const MAGI_NEEDS_VERIFICATION_PREVIEW_CAP = 8;
|
|
61
|
+
|
|
62
|
+
function summarizeNeedsVerification(synthesis: Record<string, unknown> | undefined): MeshMagiNeedsVerificationItem[] | undefined {
|
|
63
|
+
const list = Array.isArray(synthesis?.needsVerification) ? synthesis!.needsVerification : undefined;
|
|
64
|
+
if (!list) return undefined;
|
|
65
|
+
const items: MeshMagiNeedsVerificationItem[] = [];
|
|
66
|
+
for (const raw of list.slice(0, MAGI_NEEDS_VERIFICATION_PREVIEW_CAP)) {
|
|
67
|
+
const r = readRecord(raw);
|
|
68
|
+
const claim = readString(r?.claim);
|
|
69
|
+
if (!claim) continue;
|
|
70
|
+
items.push({ claim, category: readString(r?.category) || 'needs_verification' });
|
|
71
|
+
}
|
|
72
|
+
return items;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function mergeGroup(
|
|
76
|
+
groups: Map<string, MeshMagiActivitySummary>,
|
|
77
|
+
patch: Partial<MeshMagiActivitySummary> & { consensusGroupId?: string },
|
|
78
|
+
): void {
|
|
79
|
+
const consensusGroupId = readString(patch.consensusGroupId);
|
|
80
|
+
if (!consensusGroupId) return;
|
|
81
|
+
const previous = groups.get(consensusGroupId);
|
|
82
|
+
// synthesized is terminal-ish and must not be downgraded back to running by an
|
|
83
|
+
// out-of-order dispatch entry.
|
|
84
|
+
const status: MeshMagiActivityStatus = patch.status === 'synthesized' || previous?.status === 'synthesized'
|
|
85
|
+
? 'synthesized'
|
|
86
|
+
: 'running';
|
|
87
|
+
const definedPatch = Object.fromEntries(
|
|
88
|
+
Object.entries(patch).filter(([, v]) => v !== undefined),
|
|
89
|
+
) as Partial<MeshMagiActivitySummary>;
|
|
90
|
+
groups.set(consensusGroupId, { ...previous, ...definedPatch, consensusGroupId, status });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reconstruct per-consensusGroup MAGI activity from a ledger window. The newest
|
|
95
|
+
* `magi_synthesis` for a group supplies the synthesis fields; a `magi_dispatched`
|
|
96
|
+
* with no later synthesis stays `running`. Deduped by consensusGroupId, newest first.
|
|
97
|
+
*/
|
|
98
|
+
export function buildMeshMagiActivity(args: {
|
|
99
|
+
meshId?: string;
|
|
100
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
101
|
+
}): MeshMagiActivitySummary[] {
|
|
102
|
+
const groups = new Map<string, MeshMagiActivitySummary>();
|
|
103
|
+
|
|
104
|
+
for (const entry of args.ledgerEntries || []) {
|
|
105
|
+
const payload = readRecord(entry.payload);
|
|
106
|
+
if (payload?.source !== 'magi') continue;
|
|
107
|
+
const consensusGroupId = readString(payload.consensusGroupId);
|
|
108
|
+
if (!consensusGroupId) continue;
|
|
109
|
+
|
|
110
|
+
if (entry.kind === 'magi_synthesis') {
|
|
111
|
+
const synthesis = readRecord(payload.synthesis);
|
|
112
|
+
mergeGroup(groups, {
|
|
113
|
+
consensusGroupId,
|
|
114
|
+
status: 'synthesized',
|
|
115
|
+
missionId: readString(payload.missionId),
|
|
116
|
+
panel: readString(payload.panel),
|
|
117
|
+
question: readString(payload.question),
|
|
118
|
+
replicaCount: readNumber(synthesis?.replicasExpected) ?? readNumber(payload.replicaCount),
|
|
119
|
+
answered: readNumber(synthesis?.replicasAnswered),
|
|
120
|
+
missing: readNumber(synthesis?.replicasMissing),
|
|
121
|
+
staleReplicas: readNumber(payload.staleReplicas) ?? readNumber(synthesis?.staleReplicas),
|
|
122
|
+
needsVerificationCount: Array.isArray(synthesis?.needsVerification) ? synthesis!.needsVerification.length : undefined,
|
|
123
|
+
agreedCount: Array.isArray(synthesis?.agreed) ? synthesis!.agreed.length : undefined,
|
|
124
|
+
independenceBanner: synthesis && 'independenceBanner' in synthesis ? (synthesis.independenceBanner as string | null) : undefined,
|
|
125
|
+
gitSkew: readRecord(synthesis?.gitSkew) as unknown as MagiGitSkew | undefined,
|
|
126
|
+
needsVerification: summarizeNeedsVerification(synthesis),
|
|
127
|
+
openQuestions: Array.isArray(synthesis?.openQuestions) ? (synthesis!.openQuestions as string[]).slice(0, 10) : undefined,
|
|
128
|
+
lastLedgerKind: entry.kind,
|
|
129
|
+
lastUpdatedAt: entry.timestamp,
|
|
130
|
+
});
|
|
131
|
+
} else if (entry.kind === 'magi_dispatched') {
|
|
132
|
+
mergeGroup(groups, {
|
|
133
|
+
consensusGroupId,
|
|
134
|
+
status: 'running',
|
|
135
|
+
missionId: readString(payload.missionId),
|
|
136
|
+
panel: readString(payload.panel),
|
|
137
|
+
question: readString(payload.question),
|
|
138
|
+
replicaCount: readNumber(payload.replicaCount),
|
|
139
|
+
lastLedgerKind: entry.kind,
|
|
140
|
+
lastUpdatedAt: entry.timestamp,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Array.from(groups.values()).sort((a, b) => {
|
|
146
|
+
const at = new Date(a.lastUpdatedAt || '').getTime();
|
|
147
|
+
const bt = new Date(b.lastUpdatedAt || '').getTime();
|
|
148
|
+
return (Number.isFinite(bt) ? bt : 0) - (Number.isFinite(at) ? at : 0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Synthesized groups older than this (relative to the newest activity in the set) are
|
|
153
|
+
* folded out of the active list — already-resolved historical runs that should not keep
|
|
154
|
+
* inflating mesh_status. 6h covers a long working session. */
|
|
155
|
+
export const STALE_MAGI_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
156
|
+
|
|
157
|
+
/** Cap on recent synthesized groups kept in the active list even if all are fresh. */
|
|
158
|
+
export const RECENT_MAGI_CAP = 6;
|
|
159
|
+
|
|
160
|
+
export interface MeshMagiActivitySummaryFold {
|
|
161
|
+
total: number;
|
|
162
|
+
byStatus: Record<string, number>;
|
|
163
|
+
/** Synthesized groups dropped from the active list as stale historical residue. */
|
|
164
|
+
staleSynthesized: number;
|
|
165
|
+
/** Bounded set of recent/active groups (running first, then recent synthesized). */
|
|
166
|
+
groups: MeshMagiActivitySummary[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function activityTime(g: MeshMagiActivitySummary): number {
|
|
170
|
+
const t = new Date(g.lastUpdatedAt || '').getTime();
|
|
171
|
+
return Number.isFinite(t) ? t : 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Bound the MAGI activity list for mesh_status: running groups are always kept; synthesized
|
|
176
|
+
* groups are kept only when recent (within STALE_MAGI_WINDOW_MS of the newest activity AND
|
|
177
|
+
* among the RECENT_MAGI_CAP most-recent). Freshness is measured relative to the newest group
|
|
178
|
+
* (not wall-clock) so the result is deterministic for a given input. Mirrors
|
|
179
|
+
* summarizeMeshAsyncRefineJobs.
|
|
180
|
+
*/
|
|
181
|
+
export function summarizeMeshMagiActivity(
|
|
182
|
+
activity: MeshMagiActivitySummary[],
|
|
183
|
+
): MeshMagiActivitySummaryFold {
|
|
184
|
+
const running: MeshMagiActivitySummary[] = [];
|
|
185
|
+
const synthesized: MeshMagiActivitySummary[] = [];
|
|
186
|
+
for (const g of activity) {
|
|
187
|
+
if (g.status === 'synthesized') synthesized.push(g);
|
|
188
|
+
else running.push(g);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let newest = 0;
|
|
192
|
+
for (const g of activity) newest = Math.max(newest, activityTime(g));
|
|
193
|
+
const cutoff = newest - STALE_MAGI_WINDOW_MS;
|
|
194
|
+
|
|
195
|
+
const synthesizedByRecency = [...synthesized].sort((a, b) => activityTime(b) - activityTime(a));
|
|
196
|
+
const freshSynthesized = synthesizedByRecency
|
|
197
|
+
.filter(g => activityTime(g) >= cutoff)
|
|
198
|
+
.slice(0, RECENT_MAGI_CAP);
|
|
199
|
+
|
|
200
|
+
const byStatus: Record<string, number> = {};
|
|
201
|
+
for (const g of [...running, ...freshSynthesized]) {
|
|
202
|
+
byStatus[g.status] = (byStatus[g.status] ?? 0) + 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Running first (most actionable), then recent synthesized — both newest-first.
|
|
206
|
+
const runningByRecency = [...running].sort((a, b) => activityTime(b) - activityTime(a));
|
|
207
|
+
return {
|
|
208
|
+
total: running.length + freshSynthesized.length,
|
|
209
|
+
byStatus,
|
|
210
|
+
staleSynthesized: synthesized.length - freshSynthesized.length,
|
|
211
|
+
groups: [...runningByRecency, ...freshSynthesized],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Latest persisted synthesis activity for one consensusGroupId, or undefined. */
|
|
216
|
+
export function getMeshMagiActivityByGroup(
|
|
217
|
+
ledgerEntries: MeshLedgerEntry[],
|
|
218
|
+
consensusGroupId: string,
|
|
219
|
+
): MeshMagiActivitySummary | undefined {
|
|
220
|
+
const key = readString(consensusGroupId);
|
|
221
|
+
if (!key) return undefined;
|
|
222
|
+
return buildMeshMagiActivity({ ledgerEntries }).find(g => g.consensusGroupId === key);
|
|
223
|
+
}
|
|
@@ -987,6 +987,30 @@ export function sessionHasActiveAssignment(meshId: string, sessionId: string): b
|
|
|
987
987
|
return false;
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
+
/**
|
|
991
|
+
* CANON-IDENTITY single-flight hardening (restart-safe, observation-based).
|
|
992
|
+
*
|
|
993
|
+
* The in-memory single-flight Set (mesh-task-inflight) is process-local and is LOST on a
|
|
994
|
+
* daemon restart — after a restart, a task still being generated by a live local worker is
|
|
995
|
+
* no longer marked in-flight, so requeueTask's Set check passes and would re-open the task
|
|
996
|
+
* for a duplicate second dispatch. This recovers the "still generating" signal from
|
|
997
|
+
* observable runtime state instead of the in-memory mark: a session is actively generating
|
|
998
|
+
* when its live local CLI instance reports an active (generating/streaming/…) status — the
|
|
999
|
+
* same predicate the dispatch active-work gate uses (sessionStateLooksActive).
|
|
1000
|
+
*
|
|
1001
|
+
* Local-only by design: it inspects THIS daemon's instanceManager. The primary cross-process
|
|
1002
|
+
* fix (IpcTransport requeue delegating to the mesh-host daemon) keeps begin (dispatch) and
|
|
1003
|
+
* check (requeue guard) co-located so the in-memory mark stays authoritative in the common
|
|
1004
|
+
* path; this is the restart-safety net for sessions hosted on this daemon. A genuinely
|
|
1005
|
+
* dead/stale session is not generating → returns false → the requeue proceeds as before.
|
|
1006
|
+
*/
|
|
1007
|
+
export function isSessionActivelyGenerating(components: DaemonComponents, sessionId: string): boolean {
|
|
1008
|
+
if (!sessionId) return false;
|
|
1009
|
+
const state = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
|
|
1010
|
+
if (!state) return false;
|
|
1011
|
+
return sessionStateLooksActive(state);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
990
1014
|
function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
|
|
991
1015
|
return components.instanceManager.getByCategory('cli').filter((inst: any) => {
|
|
992
1016
|
const state = inst.getState();
|
|
@@ -21,7 +21,7 @@ import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.j
|
|
|
21
21
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import { execFileSync } from 'node:child_process';
|
|
24
|
-
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
24
|
+
import { resolveWin32Executable, buildWin32ExecFileSpawn } from '../cli-adapters/resolve-executable.js';
|
|
25
25
|
import type { CommandRouterResult } from '../commands/router.js';
|
|
26
26
|
|
|
27
27
|
// Fix (4): resolve the git executable to an absolute path once on win32. A bare `git` handed to
|
|
@@ -1573,13 +1573,18 @@ export async function runMeshRefineValidationGate(
|
|
|
1573
1573
|
// Resolve to an absolute path via the same helper the PTY path uses
|
|
1574
1574
|
// (no-op on non-win32 and when the command is already absolute).
|
|
1575
1575
|
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
1576
|
+
// A win32 .cmd/.bat shim cannot be exec'd directly — wrap it in
|
|
1577
|
+
// cmd.exe /c (no-op off win32 / for a real .exe). Keep
|
|
1578
|
+
// resolvedCommand for diagnostics.
|
|
1579
|
+
const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
|
|
1576
1580
|
try {
|
|
1577
|
-
const result = await execFileAsync(
|
|
1581
|
+
const result = await execFileAsync(spawn.file, spawn.args, {
|
|
1578
1582
|
cwd,
|
|
1579
1583
|
encoding: 'utf8',
|
|
1580
1584
|
timeout,
|
|
1581
1585
|
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1582
1586
|
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1587
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1583
1588
|
});
|
|
1584
1589
|
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1585
1590
|
} catch (error: any) {
|
|
@@ -1592,7 +1597,7 @@ export async function runMeshRefineValidationGate(
|
|
|
1592
1597
|
? { failureKind: 'spawn_resolution_failed', resolvedCommand }
|
|
1593
1598
|
: { failureKind: 'dependency_bootstrap_failed' }),
|
|
1594
1599
|
}));
|
|
1595
|
-
summary.bootstrap = { stage: 'failed', error: describeSpawnError(error,
|
|
1600
|
+
summary.bootstrap = { stage: 'failed', error: describeSpawnError(error, resolvedCommand, spawnResolutionFailed) };
|
|
1596
1601
|
summary.status = 'failed';
|
|
1597
1602
|
summary.failureKind = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
1598
1603
|
summary.failureCode = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
@@ -1622,13 +1627,15 @@ export async function runMeshRefineValidationGate(
|
|
|
1622
1627
|
// See the bootstrap loop above: resolve the win32 .cmd shim to an
|
|
1623
1628
|
// absolute path before handing it to the spawn boundary.
|
|
1624
1629
|
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
1630
|
+
const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
|
|
1625
1631
|
try {
|
|
1626
|
-
const result = await execFileAsync(
|
|
1632
|
+
const result = await execFileAsync(spawn.file, spawn.args, {
|
|
1627
1633
|
cwd,
|
|
1628
1634
|
encoding: 'utf8',
|
|
1629
1635
|
timeout,
|
|
1630
1636
|
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1631
1637
|
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1638
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1632
1639
|
});
|
|
1633
1640
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1634
1641
|
} catch (error: any) {
|
|
@@ -1652,7 +1659,7 @@ export async function runMeshRefineValidationGate(
|
|
|
1652
1659
|
if (spawnResolutionFailed) {
|
|
1653
1660
|
summary.failureKind = 'spawn_resolution_failed';
|
|
1654
1661
|
summary.failureCode = 'spawn_resolution_failed';
|
|
1655
|
-
summary.spawnResolutionError = describeSpawnError(error,
|
|
1662
|
+
summary.spawnResolutionError = describeSpawnError(error, resolvedCommand, true);
|
|
1656
1663
|
} else if (missingDependencyFailure) {
|
|
1657
1664
|
summary.failureKind = 'missing_dependencies';
|
|
1658
1665
|
summary.failureCode = 'missing_dependencies';
|
|
@@ -495,6 +495,13 @@ export interface MeshWorkQueueEntry {
|
|
|
495
495
|
dependsOn?: string[];
|
|
496
496
|
/** M1/M3: mission this task belongs to (joins mesh_missions). */
|
|
497
497
|
missionId?: string;
|
|
498
|
+
/**
|
|
499
|
+
* MAGI: consensus group id shared by every replica of one mesh_magi_review
|
|
500
|
+
* fan-out. Marks the task as part of an INTENTIONAL same-prompt quorum so the
|
|
501
|
+
* completion-event dedup (mesh-events-pending) never collapses grouped
|
|
502
|
+
* replicas. Absent on ordinary tasks. Rides in the payload JSON (no column).
|
|
503
|
+
*/
|
|
504
|
+
consensusGroupId?: string;
|
|
498
505
|
/**
|
|
499
506
|
* M1: why this task is held back (e.g. "dependency_failed:<taskId>").
|
|
500
507
|
* Only set by the system on dependency failure under the 'block' policy;
|
|
@@ -763,6 +770,8 @@ export function enqueueTask(
|
|
|
763
770
|
dependsOn?: string[];
|
|
764
771
|
/** M1/M3: mission this task belongs to. */
|
|
765
772
|
missionId?: string;
|
|
773
|
+
/** MAGI: consensus group id shared by every replica of a mesh_magi_review fan-out. */
|
|
774
|
+
consensusGroupId?: string;
|
|
766
775
|
/** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
|
|
767
776
|
id?: string;
|
|
768
777
|
/** (3) Originating coordinator session id (for session-anchored completion routing). */
|
|
@@ -806,6 +815,7 @@ export function enqueueTask(
|
|
|
806
815
|
requiredTags: resolvedRequiredTags,
|
|
807
816
|
...(dependsOn.length > 0 ? { dependsOn } : {}),
|
|
808
817
|
...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
|
|
818
|
+
...(typeof opts?.consensusGroupId === 'string' && opts.consensusGroupId.trim() ? { consensusGroupId: opts.consensusGroupId.trim() } : {}),
|
|
809
819
|
...(typeof opts?.sourceCoordinatorSessionId === 'string' && opts.sourceCoordinatorSessionId.trim()
|
|
810
820
|
? { sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId.trim() }
|
|
811
821
|
: {}),
|
|
@@ -1086,6 +1096,9 @@ export function requeueTask(
|
|
|
1086
1096
|
// STALE assigned row (dead session, dispatch never confirmed) is NOT in-flight
|
|
1087
1097
|
// — its mark was cleared on the dispatch failure — so it still requeues as
|
|
1088
1098
|
// before. An explicit operator override (`force`) bypasses this guard.
|
|
1099
|
+
// MAGI-NOTE: the future consensus group fan-out (separate mission) intentionally
|
|
1100
|
+
// re-dispatches a group-tagged task into multiple sessions and must be exempted
|
|
1101
|
+
// from this single-flight guard; the exemption hook (group-id check) belongs here.
|
|
1089
1102
|
if (!opts?.force && isTaskDispatchInFlight(meshId, taskId)) {
|
|
1090
1103
|
LOG.warn('MeshQueue', `Refusing to requeue task ${taskId} on mesh ${meshId}: it is actively dispatched/generating (single-flight in-flight). Requeueing now would open a duplicate second dispatch into another session. Pass force to override.`);
|
|
1091
1104
|
return entry;
|
|
@@ -4,7 +4,7 @@ import { execFile, execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import * as yaml from 'js-yaml';
|
|
7
|
-
import { resolveWin32Executable } from '../cli-adapters/resolve-executable.js';
|
|
7
|
+
import { resolveWin32Executable, buildWin32ExecFileSpawn } from '../cli-adapters/resolve-executable.js';
|
|
8
8
|
import {
|
|
9
9
|
isMeshConfigRecord,
|
|
10
10
|
normalizeMeshCommandConfig,
|
|
@@ -311,14 +311,18 @@ export async function runMeshWorktreeBootstrap(mesh: any, workspace: string): Pr
|
|
|
311
311
|
// (which appends only .com/.exe) cannot resolve → spawn ENOENT. Resolve
|
|
312
312
|
// to an absolute path first (no-op on non-win32 / already-absolute).
|
|
313
313
|
const resolvedCommand = resolveWin32Executable(command.command);
|
|
314
|
+
// A win32 .cmd/.bat shim (npm/npx/tsc) cannot be exec'd directly — wrap
|
|
315
|
+
// it in cmd.exe /c (no-op off win32 / for a real .exe).
|
|
316
|
+
const spawn = buildWin32ExecFileSpawn(resolvedCommand, command.args);
|
|
314
317
|
try {
|
|
315
|
-
const result = await execFileAsync(
|
|
318
|
+
const result = await execFileAsync(spawn.file, spawn.args, {
|
|
316
319
|
cwd,
|
|
317
320
|
encoding: 'utf8',
|
|
318
321
|
timeout: command.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
319
322
|
maxBuffer: command.outputLimitBytes || DEFAULT_OUTPUT_LIMIT_BYTES,
|
|
320
323
|
env: { ...process.env, CI: process.env.CI || '1', ...(command.env || {}) },
|
|
321
324
|
windowsHide: true,
|
|
325
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
322
326
|
});
|
|
323
327
|
state.commandsRun?.push({
|
|
324
328
|
command: command.command,
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
15
15
|
import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
|
|
16
|
+
import type { MagiPanelMap } from '@adhdev/mesh-shared';
|
|
16
17
|
|
|
17
18
|
// ─── Core Mesh Types ────────────────────────────
|
|
18
19
|
|
|
@@ -705,6 +706,13 @@ export interface RepoMeshCoordinatorConfig {
|
|
|
705
706
|
*/
|
|
706
707
|
export interface LocalMeshConfig {
|
|
707
708
|
meshes: LocalMeshEntry[];
|
|
709
|
+
/**
|
|
710
|
+
* MAGI cross-verification panels (machine-local). Keyed by panel name; each
|
|
711
|
+
* binds concrete `(node × provider)` members — machine-dependent facts — so
|
|
712
|
+
* panels live here in meshes.json, never in the repo-shared .adhdev/mesh.json.
|
|
713
|
+
* Optional: absent on configs written before MAGI existed.
|
|
714
|
+
*/
|
|
715
|
+
magiPanels?: MagiPanelMap;
|
|
708
716
|
}
|
|
709
717
|
|
|
710
718
|
export interface LocalMeshEntry {
|