@adhdev/daemon-core 0.9.82-rc.77 → 0.9.82-rc.79
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.js +231 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +233 -85
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +3 -0
- package/dist/mesh/mesh-events.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -1
- package/package.json +1 -1
- package/src/commands/cli-manager.ts +45 -1
- package/src/commands/router.ts +75 -34
- package/src/mesh/coordinator-prompt.ts +24 -7
- package/src/mesh/mesh-active-work.ts +32 -15
- package/src/mesh/mesh-events.ts +54 -12
- package/src/providers/cli-provider-instance.ts +23 -12
|
@@ -20,6 +20,7 @@ export interface MeshActiveWorkRecord {
|
|
|
20
20
|
terminal?: boolean;
|
|
21
21
|
terminalKind?: string;
|
|
22
22
|
terminalAt?: string;
|
|
23
|
+
staleReason?: string;
|
|
23
24
|
}
|
|
24
25
|
export interface MeshActiveWorkSummary {
|
|
25
26
|
totalActiveCount: number;
|
|
@@ -31,6 +32,7 @@ export interface MeshActiveWorkSummary {
|
|
|
31
32
|
idleCount: number;
|
|
32
33
|
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
33
34
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
35
|
+
staleDirectCount: number;
|
|
34
36
|
}
|
|
35
37
|
export interface BuildMeshActiveWorkOptions {
|
|
36
38
|
meshId: string;
|
|
@@ -44,5 +46,6 @@ export interface BuildMeshActiveWorkOptions {
|
|
|
44
46
|
export declare function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary;
|
|
45
47
|
export declare function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): {
|
|
46
48
|
activeWork: MeshActiveWorkRecord[];
|
|
49
|
+
staleDirectWork: MeshActiveWorkRecord[];
|
|
47
50
|
summary: MeshActiveWorkSummary;
|
|
48
51
|
};
|
|
@@ -6,6 +6,7 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
6
6
|
nodeId?: string;
|
|
7
7
|
workspace?: string;
|
|
8
8
|
metadataEvent: Record<string, unknown>;
|
|
9
|
+
coordinatorMessage?: string;
|
|
9
10
|
queuedAt: number;
|
|
10
11
|
}
|
|
11
12
|
export declare function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean;
|
|
@@ -104,7 +104,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
104
104
|
private buildCompletedFinalizationDiagnostic;
|
|
105
105
|
private hasAdapterPendingResponse;
|
|
106
106
|
private shouldSuppressStaleParsedBusyStatus;
|
|
107
|
-
private
|
|
107
|
+
private getCompletedFinalizationBlock;
|
|
108
108
|
private scheduleCompletedDebounceFlush;
|
|
109
109
|
private flushCompletedDebounceIfFinalized;
|
|
110
110
|
private maybeAutoApproveStatus;
|
package/package.json
CHANGED
|
@@ -86,6 +86,50 @@ function normalizeAgentStatus(value: unknown): string {
|
|
|
86
86
|
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function hasNonEmptyModalButtons(activeModal: unknown): boolean {
|
|
90
|
+
const buttons = (activeModal as any)?.buttons;
|
|
91
|
+
return Array.isArray(buttons) && buttons.some((button) => String(button || '').trim().length > 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasAdapterPendingResponse(adapter: any): boolean {
|
|
95
|
+
if (adapter?.isWaitingForResponse === true) return true;
|
|
96
|
+
if (adapter?.currentTurnScope) return true;
|
|
97
|
+
try {
|
|
98
|
+
if (typeof adapter?.isProcessing === 'function' && adapter.isProcessing()) return true;
|
|
99
|
+
} catch { /* defensive: send guard should not fail on diagnostics */ }
|
|
100
|
+
try {
|
|
101
|
+
const partial = typeof adapter?.getPartialResponse === 'function' ? adapter.getPartialResponse() : '';
|
|
102
|
+
if (typeof partial === 'string' && partial.trim()) return true;
|
|
103
|
+
} catch { /* defensive: missing partial means no pending evidence */ }
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldSuppressStaleParsedBusyStatus(adapterStatus: string, parsedStatus: any, adapter: any): boolean {
|
|
108
|
+
const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
|
|
109
|
+
if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
|
|
110
|
+
if (adapterStatus !== 'idle') return false;
|
|
111
|
+
if (hasNonEmptyModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
112
|
+
return !hasAdapterPendingResponse(adapter);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getEffectiveAgentSendStatus(adapter: any): string {
|
|
116
|
+
const adapterStatus = normalizeAgentStatus(adapter?.getStatus?.({ allowParse: false })?.status ?? adapter?.getStatus?.()?.status);
|
|
117
|
+
if (adapterStatus && adapterStatus !== 'idle') return adapterStatus;
|
|
118
|
+
if (adapterStatus !== 'idle') return adapterStatus;
|
|
119
|
+
|
|
120
|
+
if (typeof adapter?.getScriptParsedStatus !== 'function') return adapterStatus;
|
|
121
|
+
try {
|
|
122
|
+
const parsedStatus = adapter.getScriptParsedStatus();
|
|
123
|
+
const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
|
|
124
|
+
if (BUSY_AGENT_STATUSES.has(parsedRawStatus) && !shouldSuppressStaleParsedBusyStatus(adapterStatus, parsedStatus, adapter)) {
|
|
125
|
+
return parsedRawStatus;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return adapterStatus;
|
|
129
|
+
}
|
|
130
|
+
return adapterStatus;
|
|
131
|
+
}
|
|
132
|
+
|
|
89
133
|
export interface CliTransportFactoryParams {
|
|
90
134
|
runtimeId: string;
|
|
91
135
|
providerType: string;
|
|
@@ -1079,7 +1123,7 @@ export class DaemonCliManager {
|
|
|
1079
1123
|
const { adapter, key } = found;
|
|
1080
1124
|
|
|
1081
1125
|
if (action === 'send_chat') {
|
|
1082
|
-
const currentStatus =
|
|
1126
|
+
const currentStatus = getEffectiveAgentSendStatus(adapter);
|
|
1083
1127
|
if (BUSY_AGENT_STATUSES.has(currentStatus)) {
|
|
1084
1128
|
return {
|
|
1085
1129
|
success: false,
|
package/src/commands/router.ts
CHANGED
|
@@ -55,8 +55,8 @@ import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
|
55
55
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
56
56
|
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
57
57
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
58
|
-
import { homedir
|
|
59
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
58
|
+
import { homedir } from 'os';
|
|
59
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
60
60
|
import * as fs from 'fs';
|
|
61
61
|
|
|
62
62
|
type ReleaseChannel = 'stable' | 'preview';
|
|
@@ -230,6 +230,18 @@ function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmodul
|
|
|
230
230
|
return submodules.length > 0 ? submodules : undefined;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
234
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
235
|
+
if (explicit) return explicit;
|
|
236
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
237
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
238
|
+
const host = readStringValue(node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
239
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
240
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
241
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
242
|
+
return nodeId || 'unidentified mesh node';
|
|
243
|
+
}
|
|
244
|
+
|
|
233
245
|
function normalizeInlineMeshGitStatus(
|
|
234
246
|
status: Record<string, unknown>,
|
|
235
247
|
node: any,
|
|
@@ -1095,6 +1107,8 @@ type MeshRefineSubmoduleReachabilityEntry = {
|
|
|
1095
1107
|
remote?: string;
|
|
1096
1108
|
remoteUrl?: string;
|
|
1097
1109
|
remoteReachable?: boolean;
|
|
1110
|
+
remoteMainBranch?: string;
|
|
1111
|
+
remoteMainReachable?: boolean;
|
|
1098
1112
|
fetchedFromOrigin?: boolean;
|
|
1099
1113
|
error?: string;
|
|
1100
1114
|
};
|
|
@@ -1170,7 +1184,7 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1170
1184
|
const refs = entries
|
|
1171
1185
|
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1172
1186
|
.join(', ');
|
|
1173
|
-
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to
|
|
1187
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
1174
1188
|
}
|
|
1175
1189
|
|
|
1176
1190
|
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
@@ -1267,15 +1281,9 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1267
1281
|
});
|
|
1268
1282
|
return String(stdout || '');
|
|
1269
1283
|
};
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
await runGit(probeDir, ['init', '-q']);
|
|
1274
|
-
await runGit(probeDir, ['-c', 'protocol.file.allow=always', 'fetch', '--depth=1', remoteUrl, commit]);
|
|
1275
|
-
await runGit(probeDir, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1276
|
-
} finally {
|
|
1277
|
-
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
1278
|
-
}
|
|
1284
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1285
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1286
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1279
1287
|
};
|
|
1280
1288
|
|
|
1281
1289
|
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
@@ -1325,15 +1333,18 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1325
1333
|
entries.push(entry);
|
|
1326
1334
|
continue;
|
|
1327
1335
|
}
|
|
1328
|
-
|
|
1336
|
+
entry.remoteMainBranch = 'main';
|
|
1337
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1329
1338
|
entry.fetchedFromOrigin = true;
|
|
1330
1339
|
entry.remoteReachable = true;
|
|
1340
|
+
entry.remoteMainReachable = true;
|
|
1331
1341
|
entry.reachable = true;
|
|
1332
1342
|
} catch (e: any) {
|
|
1333
1343
|
entry.remoteReachable = false;
|
|
1344
|
+
entry.remoteMainReachable = false;
|
|
1334
1345
|
entry.publishRequired = true;
|
|
1335
1346
|
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1336
|
-
entry.error = `Submodule remote reachability check failed for origin: ${details}`;
|
|
1347
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1337
1348
|
}
|
|
1338
1349
|
} catch (e: any) {
|
|
1339
1350
|
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
@@ -2540,28 +2551,51 @@ export class DaemonCommandRouter {
|
|
|
2540
2551
|
}
|
|
2541
2552
|
|
|
2542
2553
|
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2543
|
-
|
|
2554
|
+
const metadataEvent = {
|
|
2555
|
+
source: 'refine_mesh_node_async_job',
|
|
2556
|
+
jobId: handle.jobId,
|
|
2557
|
+
interactionId: handle.interactionId,
|
|
2558
|
+
meshId: handle.meshId,
|
|
2559
|
+
nodeId: handle.targetNodeId,
|
|
2560
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2561
|
+
workspace: handle.workspace,
|
|
2562
|
+
status: handle.status,
|
|
2563
|
+
startedAt: handle.startedAt,
|
|
2564
|
+
completedAt: handle.completedAt,
|
|
2565
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2566
|
+
...(result ? { result } : {}),
|
|
2567
|
+
};
|
|
2568
|
+
const eventPayload = {
|
|
2544
2569
|
event,
|
|
2545
2570
|
meshId: handle.meshId,
|
|
2546
2571
|
nodeLabel: handle.targetNodeId,
|
|
2547
2572
|
nodeId: handle.targetNodeId,
|
|
2548
2573
|
workspace: handle.workspace,
|
|
2549
|
-
metadataEvent
|
|
2550
|
-
source: 'refine_mesh_node_async_job',
|
|
2551
|
-
jobId: handle.jobId,
|
|
2552
|
-
interactionId: handle.interactionId,
|
|
2553
|
-
meshId: handle.meshId,
|
|
2554
|
-
nodeId: handle.targetNodeId,
|
|
2555
|
-
targetDaemonId: handle.targetDaemonId,
|
|
2556
|
-
workspace: handle.workspace,
|
|
2557
|
-
status: handle.status,
|
|
2558
|
-
startedAt: handle.startedAt,
|
|
2559
|
-
completedAt: handle.completedAt,
|
|
2560
|
-
retryOfJobId: handle.retryOfJobId,
|
|
2561
|
-
...(result ? { result } : {}),
|
|
2562
|
-
},
|
|
2574
|
+
metadataEvent,
|
|
2563
2575
|
queuedAt: Date.now(),
|
|
2564
|
-
}
|
|
2576
|
+
};
|
|
2577
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
2578
|
+
const forwarded = handleMeshForwardEvent(
|
|
2579
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
2580
|
+
{
|
|
2581
|
+
event,
|
|
2582
|
+
meshId: handle.meshId,
|
|
2583
|
+
nodeId: handle.targetNodeId,
|
|
2584
|
+
workspace: handle.workspace,
|
|
2585
|
+
jobId: handle.jobId,
|
|
2586
|
+
interactionId: handle.interactionId,
|
|
2587
|
+
status: handle.status,
|
|
2588
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2589
|
+
startedAt: handle.startedAt,
|
|
2590
|
+
completedAt: handle.completedAt,
|
|
2591
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2592
|
+
...(result ? { result } : {}),
|
|
2593
|
+
},
|
|
2594
|
+
);
|
|
2595
|
+
if (forwarded?.success === true) return;
|
|
2596
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
2597
|
+
}
|
|
2598
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
2565
2599
|
}
|
|
2566
2600
|
|
|
2567
2601
|
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
@@ -2725,6 +2759,8 @@ export class DaemonCommandRouter {
|
|
|
2725
2759
|
remote: entry.remote,
|
|
2726
2760
|
remoteUrl: entry.remoteUrl,
|
|
2727
2761
|
remoteReachable: entry.remoteReachable,
|
|
2762
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2763
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2728
2764
|
error: entry.error,
|
|
2729
2765
|
})),
|
|
2730
2766
|
error: submoduleReachability.error,
|
|
@@ -2737,13 +2773,13 @@ export class DaemonCommandRouter {
|
|
|
2737
2773
|
convergenceStatus: 'blocked_review',
|
|
2738
2774
|
publishRequired: true,
|
|
2739
2775
|
blockedReason: 'submodule_publish_required',
|
|
2740
|
-
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote; merge/refine cleanup was not attempted.',
|
|
2776
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
2741
2777
|
nextStep,
|
|
2742
2778
|
nextSteps: [
|
|
2743
2779
|
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
2744
|
-
'Push/publish each unreachable submodule commit to the configured submodule remote shown in the evidence.',
|
|
2780
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
2745
2781
|
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
2746
|
-
'Do not merge the root branch until every submodule gitlink commit is reachable from
|
|
2782
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
2747
2783
|
],
|
|
2748
2784
|
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
2749
2785
|
path: entry.path,
|
|
@@ -2751,6 +2787,8 @@ export class DaemonCommandRouter {
|
|
|
2751
2787
|
remote: entry.remote,
|
|
2752
2788
|
remoteUrl: entry.remoteUrl,
|
|
2753
2789
|
remoteReachable: entry.remoteReachable,
|
|
2790
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2791
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2754
2792
|
error: entry.error,
|
|
2755
2793
|
})),
|
|
2756
2794
|
branch,
|
|
@@ -4869,7 +4907,10 @@ export class DaemonCommandRouter {
|
|
|
4869
4907
|
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
4870
4908
|
const status: Record<string, unknown> = {
|
|
4871
4909
|
nodeId,
|
|
4872
|
-
machineLabel: node
|
|
4910
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
4911
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
4912
|
+
? 'explicit_metadata'
|
|
4913
|
+
: 'workspace_host_provider_context',
|
|
4873
4914
|
workspace: node.workspace,
|
|
4874
4915
|
repoRoot: node.repoRoot,
|
|
4875
4916
|
isLocalWorktree: node.isLocalWorktree,
|
|
@@ -76,7 +76,12 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
|
|
|
76
76
|
// ─── Section Builders ───────────────────────────
|
|
77
77
|
|
|
78
78
|
function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
|
|
79
|
-
const lines = [
|
|
79
|
+
const lines = [
|
|
80
|
+
'## Current Node Status',
|
|
81
|
+
'',
|
|
82
|
+
'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
80
85
|
for (const n of nodes) {
|
|
81
86
|
const healthIcon = n.health === 'online' ? '🟢' :
|
|
82
87
|
n.health === 'dirty' ? '🟡' :
|
|
@@ -85,21 +90,33 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
|
|
|
85
90
|
? `sessions: ${n.activeSessions.join(', ')}`
|
|
86
91
|
: 'no active sessions';
|
|
87
92
|
const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
const context = [
|
|
94
|
+
n.daemonId ? `daemon: \`${n.daemonId}\`` : '',
|
|
95
|
+
n.providers?.length ? `providers: ${n.providers.join(', ')}` : '',
|
|
96
|
+
].filter(Boolean).join(' | ');
|
|
97
|
+
lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
|
|
98
|
+
lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
|
|
90
99
|
if (n.error) lines.push(` ⚠️ ${n.error}`);
|
|
91
100
|
}
|
|
92
101
|
return lines.join('\n');
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
function buildNodeConfigSection(mesh: LocalMeshEntry): string {
|
|
96
|
-
const lines = [
|
|
105
|
+
const lines = [
|
|
106
|
+
'## Configured Nodes',
|
|
107
|
+
'',
|
|
108
|
+
'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
|
|
109
|
+
'',
|
|
110
|
+
];
|
|
97
111
|
for (const n of mesh.nodes) {
|
|
98
112
|
const labels: string[] = [];
|
|
99
113
|
if (n.isLocalWorktree) labels.push('worktree');
|
|
100
114
|
if (n.policy?.readOnly) labels.push('read-only');
|
|
101
115
|
const suffix = labels.length ? ` [${labels.join(', ')}]` : '';
|
|
102
|
-
|
|
116
|
+
const explicitMachineLabel = typeof (n as any).machineLabel === 'string' ? (n as any).machineLabel : '';
|
|
117
|
+
const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
|
|
118
|
+
const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
|
|
119
|
+
lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${suffix}`);
|
|
103
120
|
}
|
|
104
121
|
lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
|
|
105
122
|
return lines.join('\n');
|
|
@@ -165,7 +182,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
|
|
|
165
182
|
4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
|
|
166
183
|
5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
|
|
167
184
|
6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
|
|
168
|
-
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from
|
|
185
|
+
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from the configured submodule remote main branch, not merely present on a feature ref or local checkout. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`, ask the user for explicit approval to push/publish the unreachable submodule commit(s) to submodule main, then rerun \`mesh_refine_node\`; do not merge the root branch until the submodule commit(s) are reachable from submodule origin/main. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
|
|
169
186
|
8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
|
|
170
187
|
9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
|
|
171
188
|
|
|
@@ -204,6 +221,6 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
204
221
|
- **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
|
|
205
222
|
- **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
|
|
206
223
|
- **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
|
|
207
|
-
- **Treat submodule reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote. Do not retry validation blindly or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit, then rerun \`mesh_refine_node\`.
|
|
224
|
+
- **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`.
|
|
208
225
|
- **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
|
|
209
226
|
}
|
|
@@ -22,6 +22,7 @@ export interface MeshActiveWorkRecord {
|
|
|
22
22
|
terminal?: boolean;
|
|
23
23
|
terminalKind?: string;
|
|
24
24
|
terminalAt?: string;
|
|
25
|
+
staleReason?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface MeshActiveWorkSummary {
|
|
@@ -34,6 +35,7 @@ export interface MeshActiveWorkSummary {
|
|
|
34
35
|
idleCount: number;
|
|
35
36
|
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
36
37
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
38
|
+
staleDirectCount: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface BuildMeshActiveWorkOptions {
|
|
@@ -64,10 +66,12 @@ function elapsedSince(value: string | undefined, now: number): number {
|
|
|
64
66
|
return Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): MeshActiveWorkStatus
|
|
68
|
-
if (!
|
|
69
|
+
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
|
|
70
|
+
if (!Array.isArray(nodes)) return {};
|
|
71
|
+
if (!nodeId) return { staleReason: 'direct task has no node id' };
|
|
69
72
|
const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
|
|
70
|
-
if (!node) return
|
|
73
|
+
if (!node) return { staleReason: 'direct task node is no longer in the live mesh' };
|
|
74
|
+
if (!sessionId) return {};
|
|
71
75
|
const candidates: any[] = [];
|
|
72
76
|
for (const value of [node.sessions, node.activeSessions, node.active_sessions, node.lastProbe?.sessions, node.last_probe?.sessions, node.lastProbe?.status?.sessions, node.last_probe?.status?.sessions]) {
|
|
73
77
|
if (Array.isArray(value)) candidates.push(...value);
|
|
@@ -76,16 +80,18 @@ function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessi
|
|
|
76
80
|
if (value && typeof value === 'object') candidates.push(value);
|
|
77
81
|
}
|
|
78
82
|
const session = candidates.find(item => {
|
|
83
|
+
if (typeof item === 'string') return item === sessionId;
|
|
79
84
|
const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
|
|
80
85
|
return id === sessionId;
|
|
81
86
|
});
|
|
82
|
-
if (!session) return
|
|
87
|
+
if (!session) return { staleReason: 'direct task session is not present in live session records' };
|
|
88
|
+
if (typeof session === 'string') return {};
|
|
83
89
|
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
84
|
-
if (raw.includes('approval')) return 'awaiting_approval';
|
|
85
|
-
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return 'generating';
|
|
86
|
-
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return 'failed';
|
|
87
|
-
if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return 'idle';
|
|
88
|
-
return
|
|
90
|
+
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
91
|
+
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
92
|
+
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
93
|
+
if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return { status: 'idle' };
|
|
94
|
+
return {};
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
function isDirectDispatch(entry: MeshLedgerEntry): boolean {
|
|
@@ -138,12 +144,14 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
138
144
|
idleCount: statusCounts.idle,
|
|
139
145
|
sourceCounts,
|
|
140
146
|
statusCounts,
|
|
147
|
+
staleDirectCount: activeWork.filter(item => item.source === 'direct' && item.staleReason).length,
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
151
|
+
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; staleDirectWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
145
152
|
const now = opts.now ?? Date.now();
|
|
146
153
|
const records: MeshActiveWorkRecord[] = [];
|
|
154
|
+
const staleDirectWork: MeshActiveWorkRecord[] = [];
|
|
147
155
|
|
|
148
156
|
for (const task of opts.queue || []) {
|
|
149
157
|
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
@@ -173,13 +181,13 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
173
181
|
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
174
182
|
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
175
183
|
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
176
|
-
const
|
|
177
|
-
const status = terminalStatus ||
|
|
184
|
+
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
185
|
+
const status = terminalStatus || live.status || 'assigned';
|
|
178
186
|
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
179
187
|
if (terminalRow && opts.includeTerminalDirect !== true) continue;
|
|
180
188
|
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
181
189
|
const { title, summary } = summarizeMessage(message);
|
|
182
|
-
|
|
190
|
+
const record: MeshActiveWorkRecord = {
|
|
183
191
|
taskId,
|
|
184
192
|
source: 'direct',
|
|
185
193
|
status,
|
|
@@ -197,9 +205,18 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
197
205
|
terminal: terminalRow,
|
|
198
206
|
terminalKind: terminal?.kind,
|
|
199
207
|
terminalAt: terminal?.timestamp,
|
|
200
|
-
|
|
208
|
+
staleReason: live.staleReason,
|
|
209
|
+
};
|
|
210
|
+
if (live.staleReason && !terminalRow) {
|
|
211
|
+
staleDirectWork.push(record);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
records.push(record);
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
204
|
-
|
|
218
|
+
staleDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
219
|
+
const summary = buildMeshActiveWorkSummary(records);
|
|
220
|
+
summary.staleDirectCount = staleDirectWork.length;
|
|
221
|
+
return { activeWork: records, staleDirectWork, summary };
|
|
205
222
|
}
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
55
55
|
nodeId?: string;
|
|
56
56
|
workspace?: string;
|
|
57
57
|
metadataEvent: Record<string, unknown>;
|
|
58
|
+
coordinatorMessage?: string;
|
|
58
59
|
queuedAt: number;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -750,23 +751,62 @@ function buildMeshSystemMessage(args: {
|
|
|
750
751
|
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
751
752
|
const result = readRecord(args.metadataEvent.result);
|
|
752
753
|
const validationSummary = readRecord(result?.validationSummary);
|
|
754
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
755
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
753
756
|
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
757
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
758
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
754
759
|
const into = readNonEmptyString(result?.into);
|
|
755
760
|
const branch = readNonEmptyString(result?.branch);
|
|
761
|
+
const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
|
|
762
|
+
const convergenceStatus = readNonEmptyString(finalConvergence?.status);
|
|
763
|
+
const nextStep = readNonEmptyString(result?.nextStep)
|
|
764
|
+
|| readNonEmptyString(finalConvergence?.nextStep)
|
|
765
|
+
|| 'Continue from the updated mesh state.';
|
|
756
766
|
const details = [
|
|
757
767
|
jobId ? `job_id=${jobId}` : '',
|
|
758
768
|
branch && into ? `${branch}→${into}` : '',
|
|
759
769
|
validationStatus ? `validation=${validationStatus}` : '',
|
|
770
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
771
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
772
|
+
convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
|
|
760
773
|
].filter(Boolean).join('; ');
|
|
761
|
-
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}
|
|
774
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
|
|
762
775
|
}
|
|
763
776
|
if (args.event === 'refine:failed') {
|
|
764
777
|
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
765
778
|
const result = readRecord(args.metadataEvent.result);
|
|
779
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
780
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
781
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
766
782
|
const code = readNonEmptyString(result?.code);
|
|
767
783
|
const error = readNonEmptyString(result?.error);
|
|
768
|
-
const
|
|
769
|
-
|
|
784
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
785
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
786
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
787
|
+
const mergeStatus = result?.merged === true
|
|
788
|
+
? 'merged'
|
|
789
|
+
: finalConvergence?.merged === false
|
|
790
|
+
? 'not_merged'
|
|
791
|
+
: '';
|
|
792
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
|
|
793
|
+
|| readNonEmptyString(finalConvergence?.status);
|
|
794
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
795
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
|
|
796
|
+
const details = [
|
|
797
|
+
jobId ? `job_id=${jobId}` : '',
|
|
798
|
+
code ? `code=${code}` : '',
|
|
799
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
800
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
801
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
802
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
803
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
804
|
+
].filter(Boolean).join('; ');
|
|
805
|
+
const parts = [
|
|
806
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
807
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
808
|
+
];
|
|
809
|
+
return parts.join('\n');
|
|
770
810
|
}
|
|
771
811
|
return '';
|
|
772
812
|
}
|
|
@@ -904,7 +944,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
904
944
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
905
945
|
}
|
|
906
946
|
if (sessionId) {
|
|
907
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
947
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
948
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
908
949
|
}
|
|
909
950
|
}
|
|
910
951
|
|
|
@@ -1015,6 +1056,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1015
1056
|
}
|
|
1016
1057
|
}
|
|
1017
1058
|
|
|
1059
|
+
const messageText = buildMeshSystemMessage({
|
|
1060
|
+
event: args.event,
|
|
1061
|
+
nodeLabel: args.nodeLabel,
|
|
1062
|
+
metadataEvent: args.metadataEvent,
|
|
1063
|
+
recoveryContext,
|
|
1064
|
+
});
|
|
1065
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1066
|
+
|
|
1018
1067
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
1019
1068
|
const instState = inst.getState();
|
|
1020
1069
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -1034,6 +1083,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1034
1083
|
...args.metadataEvent,
|
|
1035
1084
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
1036
1085
|
},
|
|
1086
|
+
coordinatorMessage: messageText,
|
|
1037
1087
|
queuedAt: Date.now(),
|
|
1038
1088
|
})) {
|
|
1039
1089
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
@@ -1041,14 +1091,6 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1041
1091
|
return { success: true, forwarded: 0 };
|
|
1042
1092
|
}
|
|
1043
1093
|
|
|
1044
|
-
const messageText = buildMeshSystemMessage({
|
|
1045
|
-
event: args.event,
|
|
1046
|
-
nodeLabel: args.nodeLabel,
|
|
1047
|
-
metadataEvent: args.metadataEvent,
|
|
1048
|
-
recoveryContext,
|
|
1049
|
-
});
|
|
1050
|
-
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1051
|
-
|
|
1052
1094
|
for (const coord of coordinatorInstances) {
|
|
1053
1095
|
const coordState = coord.getState();
|
|
1054
1096
|
LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
|