@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.
@@ -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 getCompletedFinalizationBlockReason;
107
+ private getCompletedFinalizationBlock;
108
108
  private scheduleCompletedDebounceFlush;
109
109
  private flushCompletedDebounceIfFinalized;
110
110
  private maybeAutoApproveStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.76",
3
+ "version": "0.9.82-rc.78",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 = normalizeAgentStatus(adapter.getStatus?.()?.status);
1126
+ const currentStatus = getEffectiveAgentSendStatus(adapter);
1083
1127
  if (BUSY_AGENT_STATUSES.has(currentStatus)) {
1084
1128
  return {
1085
1129
  success: false,
@@ -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 their configured submodule remote(s), then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from its configured remote.`;
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 verifyRemoteCommitReachable = async (remoteUrl: string, commit: string): Promise<void> => {
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, commit]);
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
- await verifyRemoteCommitReachable(remoteUrl, gitlink.commit);
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 its configured remote.',
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.machineLabel || node.id || node.nodeId,
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 = ['## Current Node Status', ''];
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
- lines.push(`- ${healthIcon} **${n.machineLabel}** (${n.nodeId})`);
89
- lines.push(` workspace: \`${n.workspace}\` | ${branch} | ${sessions}`);
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 = ['## Configured Nodes', ''];
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
- lines.push(`- **${n.workspace}** (${n.id})${suffix}`);
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 its configured remote. 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), then rerun \`mesh_refine_node\`; do not merge the root branch until the submodule commit(s) are reachable. 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.
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 | undefined {
68
- if (!nodeId || !sessionId || !Array.isArray(nodes)) return undefined;
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 undefined;
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 undefined;
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 undefined;
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 liveStatus = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
177
- const status = terminalStatus || liveStatus || 'assigned';
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
- records.push({
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
- return { activeWork: records, summary: buildMeshActiveWorkSummary(records) };
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
  }
@@ -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 details = [jobId ? `job_id=${jobId}` : '', code ? `code=${code}` : ''].filter(Boolean).join('; ');
769
- return `[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'} Review the terminal refine event/ledger before retrying.`;
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 getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
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') return `parsed_status:${parsedStatus}`;
841
- if (parsed?.activeModal || parsed?.modal) return 'parsed_modal_active';
842
- if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return 'missing_final_assistant';
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 blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
870
- if (blockReason) {
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;