@adhdev/daemon-core 0.9.77-rc.4 → 0.9.77-rc.41

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.
@@ -154,6 +154,35 @@ function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Re
154
154
  return { config: baseConfig, sourceHome, sourceConfigPath };
155
155
  }
156
156
 
157
+ function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
158
+ const {
159
+ model: _model,
160
+ provider: _provider,
161
+ default_model: _defaultModel,
162
+ defaultProvider: _defaultProvider,
163
+ default_provider: _defaultProviderSnake,
164
+ modelProvider: _modelProvider,
165
+ model_provider: _modelProviderSnake,
166
+ ...sanitized
167
+ } = config;
168
+ const delegation = sanitized.delegation;
169
+ if (delegation && typeof delegation === 'object' && !Array.isArray(delegation)) {
170
+ const {
171
+ model: _delegationModel,
172
+ provider: _delegationProvider,
173
+ modelProvider: _delegationModelProvider,
174
+ model_provider: _delegationModelProviderSnake,
175
+ ...delegationRest
176
+ } = delegation;
177
+ if (Object.keys(delegationRest).length > 0) {
178
+ sanitized.delegation = delegationRest;
179
+ } else {
180
+ delete sanitized.delegation;
181
+ }
182
+ }
183
+ return sanitized;
184
+ }
185
+
157
186
  function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
158
187
  if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
159
188
  for (const fileName of ['.env', 'auth.json']) {
@@ -332,7 +361,7 @@ export class DaemonCommandRouter {
332
361
  this.deps = deps;
333
362
  }
334
363
 
335
- private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
364
+ public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
336
365
  if (inlineMesh && typeof inlineMesh === 'object') {
337
366
  this.inlineMeshCache.set(meshId, inlineMesh as any);
338
367
  return inlineMesh as any;
@@ -1331,6 +1360,56 @@ export class DaemonCommandRouter {
1331
1360
  }
1332
1361
  }
1333
1362
 
1363
+ case 'get_mesh_queue': {
1364
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1365
+ if (!meshId) return { success: false, error: 'meshId required' };
1366
+ try {
1367
+ const { getQueue } = await import('../mesh/mesh-work-queue.js');
1368
+ const status = Array.isArray(args?.status)
1369
+ ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
1370
+ : undefined;
1371
+ const queue = getQueue(meshId, { status: status as any });
1372
+ return { success: true, queue };
1373
+ } catch (e: any) {
1374
+ return { success: false, error: e.message };
1375
+ }
1376
+ }
1377
+
1378
+ case 'cancel_mesh_queue_task': {
1379
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1380
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1381
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1382
+ try {
1383
+ const { cancelTask } = await import('../mesh/mesh-work-queue.js');
1384
+ const reason = typeof args?.reason === 'string' ? args.reason : undefined;
1385
+ const task = cancelTask(meshId, taskId, { reason });
1386
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1387
+ return { success: true, task };
1388
+ } catch (e: any) {
1389
+ return { success: false, error: e.message };
1390
+ }
1391
+ }
1392
+
1393
+ case 'requeue_mesh_queue_task': {
1394
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1395
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1396
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1397
+ try {
1398
+ const { requeueTask } = await import('../mesh/mesh-work-queue.js');
1399
+ const task = requeueTask(meshId, taskId, {
1400
+ reason: typeof args?.reason === 'string' ? args.reason : undefined,
1401
+ targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
1402
+ targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
1403
+ clearTargetNode: args?.clearTargetNode === true,
1404
+ clearTargetSession: args?.clearTargetSession !== false,
1405
+ });
1406
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1407
+ return { success: true, task };
1408
+ } catch (e: any) {
1409
+ return { success: false, error: e.message };
1410
+ }
1411
+ }
1412
+
1334
1413
  case 'add_mesh_node': {
1335
1414
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1336
1415
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -1519,7 +1598,13 @@ export class DaemonCommandRouter {
1519
1598
  appendLedgerEntry(meshId, {
1520
1599
  kind: 'node_removed',
1521
1600
  nodeId,
1522
- payload: { worktree: !!node?.isLocalWorktree, sessionCleanupMode },
1601
+ payload: {
1602
+ worktree: !!node?.isLocalWorktree,
1603
+ sessionCleanupMode,
1604
+ workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
1605
+ daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
1606
+ worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
1607
+ },
1523
1608
  });
1524
1609
  } catch { /* ledger append is best-effort */ }
1525
1610
  }
@@ -1711,6 +1796,105 @@ export class DaemonCommandRouter {
1711
1796
  };
1712
1797
  }
1713
1798
 
1799
+ // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
1800
+ if (coordinatorSetup.kind === 'cli_command') {
1801
+ // Build coordinator prompt first — fail closed on errors.
1802
+ let cliCmdSystemPrompt = '';
1803
+ try {
1804
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1805
+ } catch (error: any) {
1806
+ const message = error?.message || String(error);
1807
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1808
+ return {
1809
+ success: false,
1810
+ code: 'mesh_coordinator_prompt_failed',
1811
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1812
+ meshId, cliType, workspace,
1813
+ };
1814
+ }
1815
+
1816
+ // Run the provider's MCP registration command.
1817
+ try {
1818
+ const { execFileSync: execCmdSync } = await import('node:child_process');
1819
+ const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
1820
+ const [regCmd, ...regArgs] = cmdParts;
1821
+ LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
1822
+ execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
1823
+ } catch (error: any) {
1824
+ // Non-fatal — server may already be registered (providers return exit 1 on duplicate).
1825
+ LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
1826
+ }
1827
+
1828
+ // Inject system prompt using provider-native methods.
1829
+ // Codex: -c 'instructions="..."' CLI config override
1830
+ // Gemini: write GEMINI.md to workspace (auto-loaded as context)
1831
+ const cliCmdArgs: string[] = [];
1832
+ const cliCmdEnv: Record<string, string> = {};
1833
+ if (cliCmdSystemPrompt) {
1834
+ if (cliType === 'codex-cli') {
1835
+ // Codex reads `developer_instructions` from config.toml as system instructions.
1836
+ // The -c flag overrides a config key for this session only.
1837
+ cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
1838
+ } else if (cliType === 'gemini-cli') {
1839
+ // Gemini CLI auto-loads GEMINI.md from CWD as project context.
1840
+ // Write a temporary GEMINI.md to the workspace before launch.
1841
+ try {
1842
+ const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
1843
+ const geminiMdPath = `${workspace}/GEMINI.md`;
1844
+ const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
1845
+ const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
1846
+ const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
1847
+ if (efs(geminiMdPath)) {
1848
+ const existing = rfs(geminiMdPath, 'utf-8');
1849
+ // Replace existing block or append
1850
+ const replaced = existing.replace(
1851
+ new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
1852
+ block,
1853
+ );
1854
+ wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
1855
+ } else {
1856
+ wfs(geminiMdPath, block);
1857
+ }
1858
+ LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
1859
+ } catch (e: any) {
1860
+ LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
1861
+ }
1862
+ }
1863
+ }
1864
+
1865
+ const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1866
+ cliType,
1867
+ dir: workspace,
1868
+ cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
1869
+ env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
1870
+ settings: { meshCoordinatorFor: meshId },
1871
+ });
1872
+
1873
+ if (!cliCmdLaunch?.success) {
1874
+ return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
1875
+ }
1876
+
1877
+ LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
1878
+ try {
1879
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1880
+ appendLedgerEntry(meshId, {
1881
+ kind: 'coordinator_started',
1882
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1883
+ providerType: cliType,
1884
+ payload: { workspace },
1885
+ });
1886
+ } catch { /* best-effort */ }
1887
+
1888
+ return {
1889
+ success: true,
1890
+ meshId,
1891
+ cliType,
1892
+ workspace,
1893
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1894
+ mcpRegistered: true,
1895
+ };
1896
+ }
1897
+
1714
1898
  const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1715
1899
  if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1716
1900
  return {
@@ -1777,9 +1961,11 @@ export class DaemonCommandRouter {
1777
1961
  args: coordinatorSetup.mcpServer.args,
1778
1962
  };
1779
1963
  if (args?.inlineMesh) {
1964
+ const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
1965
+ const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
1780
1966
  mcpServerEntry.env = {
1781
1967
  ADHDEV_INLINE_MESH: JSON.stringify(mesh),
1782
- ADHDEV_MCP_TRANSPORT: 'ipc',
1968
+ ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
1783
1969
  };
1784
1970
  }
1785
1971
 
@@ -1801,7 +1987,10 @@ export class DaemonCommandRouter {
1801
1987
  if (hadExistingMcpConfig) {
1802
1988
  try {
1803
1989
  const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
1804
- existingMcpConfig = { ...existingMcpConfig, ...parsedExistingMcpConfig };
1990
+ const existingCoordinatorConfig = hermesManualFallback
1991
+ ? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
1992
+ : parsedExistingMcpConfig;
1993
+ existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
1805
1994
  copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
1806
1995
  } catch (error: any) {
1807
1996
  LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
@@ -113,11 +113,18 @@ export async function handleOpenPanel(h: CommandHelpers, args: any): Promise<Com
113
113
  export async function handlePtyInput(h: CommandHelpers, args: any): Promise<CommandResult> {
114
114
  const { cliType, data, targetSessionId } = args || {};
115
115
  if (!data) return { success: false, error: 'data required' };
116
+
117
+ // Filter out VT100/VT420 Device Attributes responses (e.g. \x1b[?1;2c or \x1b[>0;276;0c)
118
+ // These are echoed by xterm.js in the dashboard in response to \x1b[c queries
119
+ // and pollute the CLI input buffer.
120
+ const cleanData = typeof data === 'string' ? data.replace(/\x1b\[[?>][0-9;]*c/g, '') : data;
121
+ if (!cleanData) return { success: true };
122
+
116
123
  const adapter = h.getCliAdapter(targetSessionId || cliType);
117
124
  if (!adapter || typeof adapter.writeRaw !== 'function') {
118
125
  return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
119
126
  }
120
- await adapter.writeRaw(data);
127
+ await adapter.writeRaw(cleanData);
121
128
  return { success: true };
122
129
  }
123
130
 
package/src/index.ts CHANGED
@@ -154,8 +154,8 @@ export { appendLedgerEntry, readLedgerEntries, getLedgerSummary, getLedgerDir, g
154
154
  export type { MeshLedgerEntry, MeshLedgerKind, MeshLedgerSummary, ReadLedgerOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
155
155
 
156
156
  // ── Mesh Work Queue (GUPP) ──
157
- export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus } from './mesh/mesh-work-queue.js';
158
- export type { MeshWorkQueueEntry, MeshTaskStatus } from './mesh/mesh-work-queue.js';
157
+ export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
158
+ export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
159
159
 
160
160
  // ── Mesh Events ──
161
161
  export { triggerMeshQueue } from './mesh/mesh-events.js';
@@ -3,7 +3,20 @@ import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
3
3
  import { LOG } from '../logging/logger.js';
4
4
  import { appendLedgerEntry, getSessionRecoveryContext } from './mesh-ledger.js';
5
5
  import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
6
- import { claimNextTask, updateSessionTaskStatus, enqueueTask } from './mesh-work-queue.js';
6
+ import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus } from './mesh-work-queue.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Remote Node Idle Session Tracking
10
+ // ---------------------------------------------------------------------------
11
+ // Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
12
+ // can assign tasks to them.
13
+ // ---------------------------------------------------------------------------
14
+ interface RemoteIdleSession {
15
+ nodeId: string;
16
+ sessionId: string;
17
+ providerType: string;
18
+ }
19
+ const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
7
20
 
8
21
  // ---------------------------------------------------------------------------
9
22
  // MCP coordinator pending-event queue
@@ -33,10 +46,19 @@ function readNonEmptyString(value: unknown): string {
33
46
  return typeof value === 'string' && value.trim() ? value.trim() : '';
34
47
  }
35
48
 
49
+ function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
50
+ return readNonEmptyString(event.targetSessionId)
51
+ || readNonEmptyString(event.sessionId)
52
+ || readNonEmptyString(event.instanceId)
53
+ || readNonEmptyString(fallback);
54
+ }
55
+
36
56
  const MESH_COORDINATOR_EVENTS = new Set([
57
+ 'agent:generating_started',
37
58
  'agent:generating_completed',
38
59
  'agent:waiting_approval',
39
60
  'agent:stopped',
61
+ 'agent:ready',
40
62
  'monitor:long_generating',
41
63
  ]);
42
64
 
@@ -60,25 +82,57 @@ function formatCompletionMetadata(event: Record<string, unknown>): string {
60
82
  return parts.length > 0 ? ` (${parts.join('; ')})` : '';
61
83
  }
62
84
 
85
+ function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined {
86
+ const localMesh = getMesh(meshId);
87
+ if (localMesh) return localMesh;
88
+ return components.router?.getCachedInlineMesh(meshId);
89
+ }
90
+
91
+
63
92
  export function tryAssignQueueTask(
64
- components: { cliManager: any },
93
+ components: DaemonComponents,
65
94
  meshId: string,
66
95
  nodeId: string,
67
96
  sessionId: string,
68
97
  providerType: string
69
98
  ): boolean {
70
99
  const task = claimNextTask(meshId, nodeId, sessionId);
71
- if (!task) return false;
100
+ if (!task) {
101
+ return false;
102
+ }
72
103
 
73
104
  LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
105
+
106
+ // Check if the node is remote
107
+ const mesh = getMeshWithCache(components, meshId);
108
+ const node = mesh?.nodes.find((n: any) => n.id === nodeId);
74
109
 
110
+ // If the node is explicitly remote and we have a dispatch mechanism, route via P2P
111
+ if (node?.daemonId && components.dispatchMeshCommand) {
112
+ const isLocalNode = components.cliManager.adapters.has(sessionId);
113
+ if (!isLocalNode) {
114
+ components.dispatchMeshCommand(node.daemonId, 'agent_command', {
115
+ targetSessionId: sessionId,
116
+ cliType: providerType,
117
+ action: 'send_chat',
118
+ message: task.message,
119
+ }).catch((e: any) => {
120
+ LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
121
+ updateTaskStatus(meshId, task.id, 'failed');
122
+ });
123
+ return true;
124
+ }
125
+ }
126
+
127
+ // Local routing
75
128
  components.cliManager.handleCliCommand('agent_command', {
76
129
  targetSessionId: sessionId,
77
130
  cliType: providerType,
78
131
  action: 'send_chat',
79
- input: task.message,
132
+ message: task.message,
80
133
  }).catch((e: any) => {
81
- LOG.error('MeshQueue', `Failed to dispatch task to node ${nodeId}: ${e?.message}`);
134
+ LOG.error('MeshQueue', `Failed to dispatch task locally to node ${nodeId}: ${e?.message}`);
135
+ updateTaskStatus(meshId, task.id, 'failed');
82
136
  });
83
137
 
84
138
  return true;
@@ -88,8 +142,8 @@ export function tryAssignQueueTask(
88
142
  * Triggers a queue check for all nodes in the mesh.
89
143
  * Called when a new task is enqueued, in case nodes are already idle.
90
144
  */
91
- export function triggerMeshQueue(components: { instanceManager: any; cliManager: any }, meshId: string) {
92
- const mesh = getMesh(meshId);
145
+ export function triggerMeshQueue(components: DaemonComponents, meshId: string) {
146
+ const mesh = getMeshWithCache(components, meshId);
93
147
  if (!mesh) return;
94
148
 
95
149
  // Find all CLI instances that belong to this mesh and are idle
@@ -99,13 +153,17 @@ export function triggerMeshQueue(components: { instanceManager: any; cliManager:
99
153
  const settings = state.settings as Record<string, unknown> || {};
100
154
 
101
155
  const instMeshId = readNonEmptyString(settings.meshNodeFor);
102
- if (instMeshId !== meshId && !settings.launchedByCoordinator) continue;
156
+ if (instMeshId !== meshId) continue;
103
157
 
104
158
  const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
105
159
  if (!nodeId) continue;
106
160
 
107
- // Is it idle? (online and waiting for input)
108
- if (state.status !== 'idle' && state.status !== 'stopped' && state.activeChat?.status !== 'waiting_input') continue;
161
+ // Only genuinely idle live sessions can pull work. Restored/stopped
162
+ // records are kept for transcript/recovery visibility, but assigning
163
+ // queue items to them strands tasks in assigned/pending without chat.
164
+ const status = readNonEmptyString(state.status).toLowerCase();
165
+ if (['stopped', 'failed', 'terminated', 'exited', 'closed'].includes(status)) continue;
166
+ if (status !== 'idle' && state.activeChat?.status !== 'waiting_input') continue;
109
167
 
110
168
  const sessionId = state.instanceId;
111
169
  const providerType = state.type || readNonEmptyString(settings.providerType);
@@ -115,6 +173,18 @@ export function triggerMeshQueue(components: { instanceManager: any; cliManager:
115
173
  tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
116
174
  }
117
175
  }
176
+
177
+ // Also check known idle remote sessions
178
+ for (const [key, idle] of remoteIdleSessions.entries()) {
179
+ // Find if this node is in the same mesh
180
+ const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
181
+ if (node) {
182
+ const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
183
+ if (assigned) {
184
+ remoteIdleSessions.delete(key);
185
+ }
186
+ }
187
+ }
118
188
  }
119
189
 
120
190
  function buildMeshSystemMessage(args: {
@@ -164,18 +234,21 @@ function buildMeshSystemMessage(args: {
164
234
  function injectMeshSystemMessage(components: DaemonComponents, args: {
165
235
  meshId: string;
166
236
  sourceInstanceId?: string;
237
+ nodeId?: string;
167
238
  nodeLabel: string;
168
239
  event: string;
169
240
  metadataEvent: Record<string, unknown>;
170
241
  }) {
171
242
  // ── Task Queue & Ledger ──
243
+ let completedTaskForLedger: { id?: string } | null = null;
172
244
  if (args.event === 'agent:generating_completed') {
173
- const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
174
- const nodeId = readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
245
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
246
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
175
247
  const providerType = readNonEmptyString(args.metadataEvent.providerType);
176
248
 
177
249
  if (sessionId) {
178
- updateSessionTaskStatus(args.meshId, sessionId, 'completed');
250
+ const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed');
251
+ completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
179
252
  if (nodeId && providerType) {
180
253
  // Short delay to allow completion event to propagate before pulling next
181
254
  setTimeout(() => {
@@ -183,8 +256,56 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
183
256
  }, 500);
184
257
  }
185
258
  }
259
+ } else if (args.event === 'agent:ready') {
260
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
261
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
262
+ const providerType = readNonEmptyString(args.metadataEvent.providerType);
263
+ const completedTask = sessionId
264
+ ? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
265
+ : null;
266
+ if (completedTask) {
267
+ completedTaskForLedger = { id: completedTask.id };
268
+ try {
269
+ appendLedgerEntry(args.meshId, {
270
+ kind: 'task_completed',
271
+ nodeId: nodeId || undefined,
272
+ sessionId,
273
+ providerType: providerType || undefined,
274
+ payload: {
275
+ event: args.event,
276
+ nodeLabel: args.nodeLabel,
277
+ taskId: completedTask.id,
278
+ completedViaReady: true,
279
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
280
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
281
+ },
282
+ });
283
+ } catch (e: any) {
284
+ LOG.warn('MeshLedger', `Failed to record task_completed from ready: ${e?.message || e}`);
285
+ }
286
+ }
287
+
288
+ if (sessionId && nodeId && providerType) {
289
+ remoteIdleSessions.set(`${nodeId}:${sessionId}`, { nodeId, sessionId, providerType });
290
+ setTimeout(() => {
291
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
292
+ if (assigned) {
293
+ remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
294
+ }
295
+ }, 500);
296
+ }
297
+ } else if (args.event === 'agent:generating_started') {
298
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
299
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
300
+ if (sessionId && nodeId) {
301
+ remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
302
+ }
186
303
  } else if (args.event === 'agent:stopped') {
187
- const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
304
+ const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
305
+ const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
306
+ if (sessionId && nodeId) {
307
+ remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
308
+ }
188
309
  if (sessionId) {
189
310
  updateSessionTaskStatus(args.meshId, sessionId, 'failed');
190
311
  }
@@ -195,13 +316,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
195
316
  try {
196
317
  appendLedgerEntry(args.meshId, {
197
318
  kind: ledgerKind,
198
- nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
199
- sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
319
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
320
+ sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
200
321
  providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
201
322
  payload: {
202
323
  event: args.event,
203
324
  nodeLabel: args.nodeLabel,
325
+ taskId: completedTaskForLedger?.id || undefined,
204
326
  providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
327
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
205
328
  },
206
329
  });
207
330
  } catch (e: any) {
@@ -218,8 +341,8 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
218
341
  const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
219
342
 
220
343
  recoveryContext = getSessionRecoveryContext(args.meshId, {
221
- sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
222
- nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
344
+ sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
345
+ nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
223
346
  maxRetries,
224
347
  });
225
348
  recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
@@ -327,12 +450,14 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
327
450
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
328
451
  return injectMeshSystemMessage(components, {
329
452
  meshId,
453
+ nodeId,
330
454
  nodeLabel,
331
455
  event: eventName,
332
456
  metadataEvent: {
333
- targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
457
+ targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId) || readNonEmptyString(payload.instanceId),
334
458
  providerType: readNonEmptyString(payload.providerType),
335
459
  providerSessionId: readNonEmptyString(payload.providerSessionId),
460
+ finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
336
461
  },
337
462
  });
338
463
  }
@@ -367,13 +492,14 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
367
492
  const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
368
493
  if (!isMeshDelegate) return;
369
494
 
370
- const mesh = meshIdFromRuntime ? getMesh(meshIdFromRuntime) : getMeshByRepo(workspace);
495
+ const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getMeshByRepo(workspace);
371
496
  const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
372
497
  if (!meshId) return;
373
498
 
374
499
  // Determine node label. Inline/cloud meshes may be unavailable here, so preserve runtime node id.
375
500
  const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
376
501
  const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
502
+ const resolvedNodeId = targetNode?.id || runtimeNodeId;
377
503
  const nodeLabel = targetNode
378
504
  ? `Node '${targetNode.id}'`
379
505
  : runtimeNodeId
@@ -383,6 +509,7 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
383
509
  injectMeshSystemMessage(components, {
384
510
  meshId,
385
511
  sourceInstanceId: instanceId,
512
+ nodeId: resolvedNodeId,
386
513
  nodeLabel,
387
514
  event: event.event,
388
515
  metadataEvent: event,