@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.
- package/dist/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -4
- package/dist/commands/mesh-coordinator.d.ts +10 -0
- package/dist/commands/router.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +424 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +421 -45
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +2 -7
- package/dist/mesh/mesh-work-queue.d.ts +35 -1
- package/dist/shared-types.d.ts +14 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +5 -0
- package/src/cli-adapters/provider-cli-adapter.ts +34 -4
- package/src/cli-adapters/provider-cli-shared.ts +14 -4
- package/src/commands/cli-manager.ts +0 -4
- package/src/commands/mesh-coordinator.ts +55 -7
- package/src/commands/router.ts +193 -4
- package/src/commands/stream-commands.ts +8 -1
- package/src/index.ts +2 -2
- package/src/mesh/mesh-events.ts +147 -20
- package/src/mesh/mesh-work-queue.ts +103 -6
- package/src/providers/cli-provider-instance.ts +2 -0
- package/src/shared-types.ts +14 -0
package/src/commands/router.ts
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
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
|
-
|
|
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(
|
|
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';
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -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:
|
|
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)
|
|
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
|
-
|
|
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:
|
|
92
|
-
const mesh =
|
|
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
|
|
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
|
-
//
|
|
108
|
-
|
|
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 =
|
|
174
|
-
const nodeId = readNonEmptyString(args.
|
|
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 =
|
|
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.
|
|
199
|
-
sessionId:
|
|
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:
|
|
222
|
-
nodeId: readNonEmptyString(args.
|
|
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 ?
|
|
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,
|