@adhdev/daemon-core 0.9.82-rc.76 → 0.9.82-rc.78
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 +163 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -52
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +3 -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 +35 -10
- package/src/mesh/coordinator-prompt.ts +24 -7
- package/src/mesh/mesh-active-work.ts +32 -15
- package/src/mesh/mesh-events.ts +16 -3
- 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
|
};
|
|
@@ -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
|
@@ -56,7 +56,7 @@ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDa
|
|
|
56
56
|
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
57
57
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
58
58
|
import { homedir, tmpdir } from 'os';
|
|
59
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
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,12 +1281,13 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1267
1281
|
});
|
|
1268
1282
|
return String(stdout || '');
|
|
1269
1283
|
};
|
|
1270
|
-
const
|
|
1284
|
+
const verifyRemoteMainContainsCommit = async (remoteUrl: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1271
1285
|
const probeDir = fs.mkdtempSync(pathJoin(tmpdir(), 'adhdev-submodule-reachability-'));
|
|
1272
1286
|
try {
|
|
1273
1287
|
await runGit(probeDir, ['init', '-q']);
|
|
1274
|
-
await runGit(probeDir, ['-c', 'protocol.file.allow=always', 'fetch', '--depth=1', remoteUrl,
|
|
1288
|
+
await runGit(probeDir, ['-c', 'protocol.file.allow=always', 'fetch', '--depth=1', remoteUrl, `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1275
1289
|
await runGit(probeDir, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1290
|
+
await runGit(probeDir, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1276
1291
|
} finally {
|
|
1277
1292
|
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
1278
1293
|
}
|
|
@@ -1325,15 +1340,18 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1325
1340
|
entries.push(entry);
|
|
1326
1341
|
continue;
|
|
1327
1342
|
}
|
|
1328
|
-
|
|
1343
|
+
entry.remoteMainBranch = 'main';
|
|
1344
|
+
await verifyRemoteMainContainsCommit(remoteUrl, gitlink.commit, 'main');
|
|
1329
1345
|
entry.fetchedFromOrigin = true;
|
|
1330
1346
|
entry.remoteReachable = true;
|
|
1347
|
+
entry.remoteMainReachable = true;
|
|
1331
1348
|
entry.reachable = true;
|
|
1332
1349
|
} catch (e: any) {
|
|
1333
1350
|
entry.remoteReachable = false;
|
|
1351
|
+
entry.remoteMainReachable = false;
|
|
1334
1352
|
entry.publishRequired = true;
|
|
1335
1353
|
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1336
|
-
entry.error = `Submodule remote reachability check failed for origin: ${details}`;
|
|
1354
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1337
1355
|
}
|
|
1338
1356
|
} catch (e: any) {
|
|
1339
1357
|
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
@@ -2725,6 +2743,8 @@ export class DaemonCommandRouter {
|
|
|
2725
2743
|
remote: entry.remote,
|
|
2726
2744
|
remoteUrl: entry.remoteUrl,
|
|
2727
2745
|
remoteReachable: entry.remoteReachable,
|
|
2746
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2747
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2728
2748
|
error: entry.error,
|
|
2729
2749
|
})),
|
|
2730
2750
|
error: submoduleReachability.error,
|
|
@@ -2737,13 +2757,13 @@ export class DaemonCommandRouter {
|
|
|
2737
2757
|
convergenceStatus: 'blocked_review',
|
|
2738
2758
|
publishRequired: true,
|
|
2739
2759
|
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.',
|
|
2760
|
+
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
2761
|
nextStep,
|
|
2742
2762
|
nextSteps: [
|
|
2743
2763
|
'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.',
|
|
2764
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
2745
2765
|
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
2746
|
-
'Do not merge the root branch until every submodule gitlink commit is reachable from
|
|
2766
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
2747
2767
|
],
|
|
2748
2768
|
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
2749
2769
|
path: entry.path,
|
|
@@ -2751,6 +2771,8 @@ export class DaemonCommandRouter {
|
|
|
2751
2771
|
remote: entry.remote,
|
|
2752
2772
|
remoteUrl: entry.remoteUrl,
|
|
2753
2773
|
remoteReachable: entry.remoteReachable,
|
|
2774
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2775
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2754
2776
|
error: entry.error,
|
|
2755
2777
|
})),
|
|
2756
2778
|
branch,
|
|
@@ -4869,7 +4891,10 @@ export class DaemonCommandRouter {
|
|
|
4869
4891
|
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
4870
4892
|
const status: Record<string, unknown> = {
|
|
4871
4893
|
nodeId,
|
|
4872
|
-
machineLabel: node
|
|
4894
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
4895
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
4896
|
+
? 'explicit_metadata'
|
|
4897
|
+
: 'workspace_host_provider_context',
|
|
4873
4898
|
workspace: node.workspace,
|
|
4874
4899
|
repoRoot: node.repoRoot,
|
|
4875
4900
|
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
|
@@ -765,8 +765,20 @@ function buildMeshSystemMessage(args: {
|
|
|
765
765
|
const result = readRecord(args.metadataEvent.result);
|
|
766
766
|
const code = readNonEmptyString(result?.code);
|
|
767
767
|
const error = readNonEmptyString(result?.error);
|
|
768
|
-
const
|
|
769
|
-
|
|
768
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus);
|
|
769
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
770
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(readRecord(result?.finalBranchConvergenceState)?.nextStep);
|
|
771
|
+
const details = [
|
|
772
|
+
jobId ? `job_id=${jobId}` : '',
|
|
773
|
+
code ? `code=${code}` : '',
|
|
774
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
775
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
776
|
+
].filter(Boolean).join('; ');
|
|
777
|
+
const parts = [
|
|
778
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
779
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
780
|
+
];
|
|
781
|
+
return parts.join('\n');
|
|
770
782
|
}
|
|
771
783
|
return '';
|
|
772
784
|
}
|
|
@@ -904,7 +916,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
904
916
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
905
917
|
}
|
|
906
918
|
if (sessionId) {
|
|
907
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
919
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
920
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
908
921
|
}
|
|
909
922
|
}
|
|
910
923
|
|
|
@@ -43,6 +43,11 @@ type CompletedDebouncePending = {
|
|
|
43
43
|
loggedBlockReason?: string;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
type CompletedFinalizationBlock = {
|
|
47
|
+
reason: string;
|
|
48
|
+
terminal?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
46
51
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
52
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
53
|
|
|
@@ -817,29 +822,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
822
|
return !this.hasAdapterPendingResponse();
|
|
818
823
|
}
|
|
819
824
|
|
|
820
|
-
private
|
|
821
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
825
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
|
|
826
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
822
827
|
|
|
823
828
|
const adapterAny = this.adapter as any;
|
|
824
|
-
if (adapterAny?.isWaitingForResponse === true) return 'adapter_waiting_for_response';
|
|
825
|
-
if (adapterAny?.currentTurnScope) return 'adapter_turn_scope_active';
|
|
829
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
830
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
831
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
826
832
|
|
|
827
833
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
828
834
|
? this.adapter.getPartialResponse()
|
|
829
835
|
: '';
|
|
830
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
836
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
831
837
|
|
|
832
838
|
let parsed: any;
|
|
833
839
|
try {
|
|
834
840
|
parsed = this.adapter.getScriptParsedStatus();
|
|
835
841
|
} catch (error: any) {
|
|
836
|
-
return `parse_error:${error?.message || String(error)}
|
|
842
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
837
843
|
}
|
|
838
844
|
|
|
839
845
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
840
|
-
if (parsedStatus !== 'idle')
|
|
841
|
-
|
|
842
|
-
|
|
846
|
+
if (parsedStatus !== 'idle') {
|
|
847
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
848
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
849
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
850
|
+
}
|
|
851
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
852
|
+
if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
|
|
843
853
|
|
|
844
854
|
return null;
|
|
845
855
|
}
|
|
@@ -866,10 +876,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
866
876
|
return;
|
|
867
877
|
}
|
|
868
878
|
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
879
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
|
|
880
|
+
if (block) {
|
|
881
|
+
const blockReason = block.reason;
|
|
871
882
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
872
|
-
if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
883
|
+
if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
873
884
|
if (pending.loggedBlockReason !== blockReason) {
|
|
874
885
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
875
886
|
pending.loggedBlockReason = blockReason;
|