@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.
@@ -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(resolvedCommand, candidate.args, {
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, candidate.command, spawnResolutionFailed) };
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(resolvedCommand, candidate.args, {
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, candidate.command, true);
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(resolvedCommand, command.args, {
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,
@@ -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 {