@adhdev/daemon-core 0.9.77-rc.1 → 0.9.77-rc.11
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/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -4
- package/dist/commands/mesh-coordinator.d.ts +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1018 -216
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1014 -224
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +11 -1
- package/dist/mesh/mesh-ledger.d.ts +90 -0
- package/dist/mesh/mesh-sync.d.ts +10 -0
- package/dist/mesh/mesh-work-queue.d.ts +50 -0
- package/dist/repo-mesh-types.d.ts +6 -0
- package/dist/shared-types.d.ts +12 -0
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +10 -4
- package/src/cli-adapters/provider-cli-shared.ts +14 -4
- package/src/commands/mesh-coordinator.ts +28 -6
- package/src/commands/router.ts +222 -0
- package/src/commands/stream-commands.ts +8 -1
- package/src/index.ts +11 -0
- package/src/mesh/coordinator-prompt.ts +27 -12
- package/src/mesh/mesh-events.ts +200 -1
- package/src/mesh/mesh-ledger.ts +378 -0
- package/src/mesh/mesh-sync.ts +32 -0
- package/src/mesh/mesh-work-queue.ts +164 -0
- package/src/repo-mesh-types.ts +7 -0
- package/src/shared-types.ts +12 -0
- package/src/status/builders.ts +13 -0
|
@@ -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)
|
|
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
|
@@ -149,6 +149,17 @@ export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
|
|
|
149
149
|
export { syncMeshes } from './mesh/mesh-sync.js';
|
|
150
150
|
export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
|
|
151
151
|
|
|
152
|
+
// ── Mesh Task Ledger ──
|
|
153
|
+
export { appendLedgerEntry, readLedgerEntries, getLedgerSummary, getLedgerDir, getSessionRecoveryContext } from './mesh/mesh-ledger.js';
|
|
154
|
+
export type { MeshLedgerEntry, MeshLedgerKind, MeshLedgerSummary, ReadLedgerOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
|
|
155
|
+
|
|
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';
|
|
159
|
+
|
|
160
|
+
// ── Mesh Events ──
|
|
161
|
+
export { triggerMeshQueue } from './mesh/mesh-events.js';
|
|
162
|
+
|
|
152
163
|
// ── State Store ──
|
|
153
164
|
export { loadState, saveState, resetState } from './config/state-store.js';
|
|
154
165
|
export type { DaemonState } from './config/state-store.js';
|
|
@@ -133,6 +133,7 @@ const TOOLS_SECTION = `## Available Tools
|
|
|
133
133
|
| \`mesh_launch_session\` | Start a new agent session on a node |
|
|
134
134
|
| \`mesh_send_task\` | Send a task (natural language) to a running agent |
|
|
135
135
|
| \`mesh_read_chat\` | Read an agent's recent messages to check progress |
|
|
136
|
+
| \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
|
|
136
137
|
| \`mesh_git_status\` | Check git status on a specific node |
|
|
137
138
|
| \`mesh_checkpoint\` | Create a git checkpoint on a node |
|
|
138
139
|
| \`mesh_approve\` | Approve/reject a pending agent action |
|
|
@@ -145,18 +146,31 @@ Before doing any coordinator work, confirm that the actual callable tool list in
|
|
|
145
146
|
|
|
146
147
|
const WORKFLOW_SECTION = `## Orchestration Workflow
|
|
147
148
|
|
|
148
|
-
1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available.
|
|
149
|
-
2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist.
|
|
150
|
-
3. **Delegate** —
|
|
151
|
-
a.
|
|
152
|
-
b. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
|
|
153
|
-
c.
|
|
154
|
-
d.
|
|
155
|
-
4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly
|
|
149
|
+
1. **Assess** — Call \`mesh_status\` to see which nodes are healthy and available. Check \`mesh_task_history\` to understand what has already been done in this mesh — previous delegations, completions, and failures.
|
|
150
|
+
2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist. If \`mesh_task_history\` shows a recent failure for a task, decide whether to retry or reassign.
|
|
151
|
+
3. **Queue / Delegate** — The Mesh uses an autonomous pull-based Work Queue:
|
|
152
|
+
a. **General Tasks**: Enqueue tasks using \`mesh_enqueue_task\`. Idle node agents will automatically pull tasks from the queue and begin working.
|
|
153
|
+
b. **Node Preparation**: Call \`mesh_launch_session\` to ensure enough agent sessions are active to handle the queue. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
|
|
154
|
+
c. **Targeted Tasks**: Use \`mesh_send_task\` only when you need to bypass the queue and force a specific node to execute a task immediately.
|
|
155
|
+
d. Always provide a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
|
|
156
|
+
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\`.
|
|
156
157
|
5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
|
|
157
158
|
6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
|
|
158
159
|
7. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
|
|
159
|
-
8. **Report** — Summarize what was done, what changed, and any issues
|
|
160
|
+
8. **Report** — Summarize what was done, what changed, and any issues.
|
|
161
|
+
|
|
162
|
+
## Failure Recovery
|
|
163
|
+
|
|
164
|
+
When a node agent stops unexpectedly, the daemon automatically enriches the system message with **Recovery Context** that includes:
|
|
165
|
+
- The number of consecutive failures on that node
|
|
166
|
+
- The original task message (if recorded in the ledger)
|
|
167
|
+
- A recommendation: **retry**, **reassign**, or **escalate**
|
|
168
|
+
|
|
169
|
+
Follow these recovery rules:
|
|
170
|
+
1. **If "Retry recommended"**: Re-launch the session on the same node (\`mesh_launch_session\`), then resend the original task (\`mesh_send_task\`). The system message includes the original task text.
|
|
171
|
+
2. **If "Max retries exceeded"**: Do NOT retry on the same node. Either reassign the task to a different node, or inform the user that the task requires manual intervention.
|
|
172
|
+
3. **If no recovery context**: The stop may be intentional (normal completion). Use \`mesh_read_chat\` once to verify, then move on.
|
|
173
|
+
4. **Always record what happened**: After handling a failure, briefly note the outcome in your report to the user.`;
|
|
160
174
|
|
|
161
175
|
function buildRulesSection(coordinatorCliType?: string): string {
|
|
162
176
|
const coordinatorNote = coordinatorCliType
|
|
@@ -166,12 +180,13 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
166
180
|
return `## Rules
|
|
167
181
|
|
|
168
182
|
- **Minimize coordinator context.** The coordinator's job is routing, not implementing. Do not read source files, run commands, or analyze code directly — delegate all of that to node agents. Your context should stay lean.
|
|
169
|
-
- **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to a node. Do not do it yourself.
|
|
183
|
+
- **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to the queue or a node. Do not do it yourself.
|
|
170
184
|
- **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes → \`hermes-cli\`, Claude Code/Claude → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
|
|
171
|
-
- **Front-load the task message.** When calling \`mesh_send_task\`, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
|
|
185
|
+
- **Front-load the task message.** When calling \`mesh_enqueue_task\` or \`mesh_send_task\`, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
|
|
172
186
|
- **Don't inspect code.** Treat delegated agent summaries as self-reports, not verification. Verify side effects via \`mesh_git_status\` (including related repo freshness when configured), not by reading source files.
|
|
173
187
|
- **Don't over-parallelize.** Start with 1-2 concurrent tasks. Scale up if they succeed. Never launch a duplicate session or second worker solely because \`mesh_read_chat\` has no final assistant message while the delegated session is still showing tool/terminal activity.
|
|
174
|
-
- **Handle failures
|
|
188
|
+
- **Handle failures with context.** If a task fails, check \`mesh_task_history\` first to see if this task was attempted before and how it failed. Read the chat to understand why, then decide: retry on the same node, reassign to a different node, or escalate to the user.
|
|
189
|
+
- **Check history before starting.** At the beginning of a coordination session, call \`mesh_task_history\` to understand what was previously delegated and its outcomes. This prevents duplicate work and informs recovery decisions.
|
|
175
190
|
- **Keep the user informed.** Report progress after each delegation round — one or two sentences, not a narration.
|
|
176
191
|
- **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
|
|
177
192
|
- **Never fabricate tool results.** Always call the actual tool; never pretend you did.
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
2
|
import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
3
3
|
import { LOG } from '../logging/logger.js';
|
|
4
|
+
import { appendLedgerEntry, getSessionRecoveryContext } from './mesh-ledger.js';
|
|
5
|
+
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
6
|
+
import { claimNextTask, updateSessionTaskStatus, enqueueTask } from './mesh-work-queue.js';
|
|
4
7
|
|
|
5
8
|
// ---------------------------------------------------------------------------
|
|
6
9
|
// MCP coordinator pending-event queue
|
|
@@ -37,6 +40,13 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
37
40
|
'monitor:long_generating',
|
|
38
41
|
]);
|
|
39
42
|
|
|
43
|
+
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
44
|
+
'agent:generating_completed': 'task_completed',
|
|
45
|
+
'agent:waiting_approval': 'task_approval_needed',
|
|
46
|
+
'agent:stopped': 'task_failed',
|
|
47
|
+
'monitor:long_generating': 'task_stalled',
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
41
51
|
return typeof eventName === 'string' && MESH_COORDINATOR_EVENTS.has(eventName);
|
|
42
52
|
}
|
|
@@ -50,10 +60,68 @@ function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
|
50
60
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
export function tryAssignQueueTask(
|
|
64
|
+
components: { cliManager: any },
|
|
65
|
+
meshId: string,
|
|
66
|
+
nodeId: string,
|
|
67
|
+
sessionId: string,
|
|
68
|
+
providerType: string
|
|
69
|
+
): boolean {
|
|
70
|
+
const task = claimNextTask(meshId, nodeId, sessionId);
|
|
71
|
+
if (!task) return false;
|
|
72
|
+
|
|
73
|
+
LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
74
|
+
|
|
75
|
+
components.cliManager.handleCliCommand('agent_command', {
|
|
76
|
+
targetSessionId: sessionId,
|
|
77
|
+
cliType: providerType,
|
|
78
|
+
action: 'send_chat',
|
|
79
|
+
message: task.message,
|
|
80
|
+
}).catch((e: any) => {
|
|
81
|
+
LOG.error('MeshQueue', `Failed to dispatch task to node ${nodeId}: ${e?.message}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Triggers a queue check for all nodes in the mesh.
|
|
89
|
+
* Called when a new task is enqueued, in case nodes are already idle.
|
|
90
|
+
*/
|
|
91
|
+
export function triggerMeshQueue(components: { instanceManager: any; cliManager: any }, meshId: string) {
|
|
92
|
+
const mesh = getMesh(meshId);
|
|
93
|
+
if (!mesh) return;
|
|
94
|
+
|
|
95
|
+
// Find all CLI instances that belong to this mesh and are idle
|
|
96
|
+
const cliInstances = components.instanceManager.getByCategory('cli');
|
|
97
|
+
for (const inst of cliInstances) {
|
|
98
|
+
const state = inst.getState();
|
|
99
|
+
const settings = state.settings as Record<string, unknown> || {};
|
|
100
|
+
|
|
101
|
+
const instMeshId = readNonEmptyString(settings.meshNodeFor);
|
|
102
|
+
if (instMeshId !== meshId && !settings.launchedByCoordinator) continue;
|
|
103
|
+
|
|
104
|
+
const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
|
|
105
|
+
if (!nodeId) continue;
|
|
106
|
+
|
|
107
|
+
// Is it idle? (online and waiting for input)
|
|
108
|
+
if (state.status !== 'idle' && state.status !== 'stopped' && state.activeChat?.status !== 'waiting_input') continue;
|
|
109
|
+
|
|
110
|
+
const sessionId = state.instanceId;
|
|
111
|
+
const providerType = state.type || readNonEmptyString(settings.providerType);
|
|
112
|
+
|
|
113
|
+
if (providerType) {
|
|
114
|
+
// Try to assign a task to this idle node
|
|
115
|
+
tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
53
120
|
function buildMeshSystemMessage(args: {
|
|
54
121
|
event: string;
|
|
55
122
|
nodeLabel: string;
|
|
56
123
|
metadataEvent: Record<string, unknown>;
|
|
124
|
+
recoveryContext?: SessionRecoveryContext | null;
|
|
57
125
|
}): string {
|
|
58
126
|
const metadata = formatCompletionMetadata(args.metadataEvent);
|
|
59
127
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -63,6 +131,28 @@ function buildMeshSystemMessage(args: {
|
|
|
63
131
|
return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
|
|
64
132
|
}
|
|
65
133
|
if (args.event === 'agent:stopped') {
|
|
134
|
+
const rc = args.recoveryContext;
|
|
135
|
+
if (rc && rc.consecutiveNodeFailures > 0) {
|
|
136
|
+
const parts = [
|
|
137
|
+
`[System] ${args.nodeLabel} has stopped unexpectedly${metadata}.`,
|
|
138
|
+
`\n\n**Recovery Context:**`,
|
|
139
|
+
`- Consecutive failures on this node: ${rc.consecutiveNodeFailures}`,
|
|
140
|
+
rc.taskAttemptCount > 0 ? `- This task has been attempted ${rc.taskAttemptCount} time(s)` : '',
|
|
141
|
+
`- Recommendation: ${rc.advice}`,
|
|
142
|
+
];
|
|
143
|
+
if (rc.retryRecommended && rc.lastTaskMessage) {
|
|
144
|
+
parts.push(
|
|
145
|
+
`\n\n**Original task to retry:**`,
|
|
146
|
+
`> ${rc.lastTaskMessage.length > 300 ? rc.lastTaskMessage.slice(0, 300) + '...' : rc.lastTaskMessage}`,
|
|
147
|
+
`\nTo retry: call \`mesh_launch_session\` for this node, then \`mesh_send_task\` with the original task.`,
|
|
148
|
+
);
|
|
149
|
+
} else if (!rc.retryRecommended) {
|
|
150
|
+
parts.push(
|
|
151
|
+
`\nDo NOT retry on this node. Consider reassigning to a different node or asking the user for guidance.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return parts.filter(Boolean).join('\n');
|
|
155
|
+
}
|
|
66
156
|
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
67
157
|
}
|
|
68
158
|
if (args.event === 'monitor:long_generating') {
|
|
@@ -78,6 +168,111 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
78
168
|
event: string;
|
|
79
169
|
metadataEvent: Record<string, unknown>;
|
|
80
170
|
}) {
|
|
171
|
+
// ── Task Queue & Ledger ──
|
|
172
|
+
if (args.event === 'agent:generating_completed') {
|
|
173
|
+
const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
|
|
174
|
+
const nodeId = readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
175
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
176
|
+
|
|
177
|
+
if (sessionId) {
|
|
178
|
+
updateSessionTaskStatus(args.meshId, sessionId, 'completed');
|
|
179
|
+
if (nodeId && providerType) {
|
|
180
|
+
// Short delay to allow completion event to propagate before pulling next
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
183
|
+
}, 500);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (args.event === 'agent:stopped') {
|
|
187
|
+
const sessionId = readNonEmptyString(args.metadataEvent.targetSessionId);
|
|
188
|
+
if (sessionId) {
|
|
189
|
+
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ledgerKind = EVENT_TO_LEDGER_KIND[args.event];
|
|
194
|
+
if (ledgerKind) {
|
|
195
|
+
try {
|
|
196
|
+
appendLedgerEntry(args.meshId, {
|
|
197
|
+
kind: ledgerKind,
|
|
198
|
+
nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
|
|
199
|
+
sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
|
|
200
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
201
|
+
payload: {
|
|
202
|
+
event: args.event,
|
|
203
|
+
nodeLabel: args.nodeLabel,
|
|
204
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
} catch (e: any) {
|
|
208
|
+
LOG.warn('MeshLedger', `Failed to record ${ledgerKind}: ${e?.message || e}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Recovery Context: enrich agent:stopped with retry intelligence ──
|
|
213
|
+
let recoveryContext: SessionRecoveryContext | null = null;
|
|
214
|
+
if (args.event === 'agent:stopped') {
|
|
215
|
+
try {
|
|
216
|
+
// Resolve maxTaskRetries from mesh policy
|
|
217
|
+
const mesh = getMesh(args.meshId);
|
|
218
|
+
const maxRetries = mesh?.policy?.maxTaskRetries ?? 1;
|
|
219
|
+
|
|
220
|
+
recoveryContext = getSessionRecoveryContext(args.meshId, {
|
|
221
|
+
sessionId: readNonEmptyString(args.metadataEvent.targetSessionId) || undefined,
|
|
222
|
+
nodeId: readNonEmptyString(args.metadataEvent.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
|
|
223
|
+
maxRetries,
|
|
224
|
+
});
|
|
225
|
+
recoveryContext.failedProviderType = readNonEmptyString(args.metadataEvent.providerType) || null;
|
|
226
|
+
|
|
227
|
+
// Record recovery_attempted if retry is recommended
|
|
228
|
+
if (recoveryContext.retryRecommended && recoveryContext.consecutiveNodeFailures > 0) {
|
|
229
|
+
appendLedgerEntry(args.meshId, {
|
|
230
|
+
kind: 'recovery_attempted',
|
|
231
|
+
nodeId: recoveryContext.failedNodeId || undefined,
|
|
232
|
+
sessionId: recoveryContext.failedSessionId || undefined,
|
|
233
|
+
providerType: recoveryContext.failedProviderType || undefined,
|
|
234
|
+
payload: {
|
|
235
|
+
consecutiveFailures: recoveryContext.consecutiveNodeFailures,
|
|
236
|
+
taskAttemptCount: recoveryContext.taskAttemptCount,
|
|
237
|
+
retryRecommended: recoveryContext.retryRecommended,
|
|
238
|
+
advice: recoveryContext.advice,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Auto-Recovery (Phase 5): Automatically re-enqueue the task and re-launch the session
|
|
243
|
+
if (recoveryContext.lastTaskMessage && recoveryContext.failedNodeId && recoveryContext.failedProviderType) {
|
|
244
|
+
const autoNodeId = recoveryContext.failedNodeId;
|
|
245
|
+
try {
|
|
246
|
+
const task = enqueueTask(args.meshId, recoveryContext.lastTaskMessage, {
|
|
247
|
+
targetNodeId: autoNodeId
|
|
248
|
+
});
|
|
249
|
+
LOG.info('MeshRecovery', `Auto-requeued failed task: ${task.id} for node ${autoNodeId}`);
|
|
250
|
+
|
|
251
|
+
const node = mesh?.nodes.find(n => n.id === autoNodeId);
|
|
252
|
+
if (node) {
|
|
253
|
+
components.cliManager.handleCliCommand('launch_cli', {
|
|
254
|
+
cliType: recoveryContext.failedProviderType,
|
|
255
|
+
dir: node.workspace,
|
|
256
|
+
settings: {
|
|
257
|
+
meshNodeFor: args.meshId,
|
|
258
|
+
meshNodeId: node.id,
|
|
259
|
+
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
260
|
+
launchedByCoordinator: true,
|
|
261
|
+
}
|
|
262
|
+
}).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
|
|
263
|
+
}
|
|
264
|
+
} catch (e: any) {
|
|
265
|
+
LOG.warn('MeshRecovery', `Failed to execute auto-recovery: ${e?.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
LOG.info('MeshRecovery', `Recovery context for ${args.nodeLabel}: ${recoveryContext.advice}`);
|
|
271
|
+
} catch (e: any) {
|
|
272
|
+
LOG.warn('MeshRecovery', `Failed to build recovery context: ${e?.message || e}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
81
276
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
82
277
|
const instState = inst.getState();
|
|
83
278
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
@@ -92,7 +287,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
92
287
|
event: args.event,
|
|
93
288
|
meshId: args.meshId,
|
|
94
289
|
nodeLabel: args.nodeLabel,
|
|
95
|
-
metadataEvent:
|
|
290
|
+
metadataEvent: {
|
|
291
|
+
...args.metadataEvent,
|
|
292
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
293
|
+
},
|
|
96
294
|
queuedAt: Date.now(),
|
|
97
295
|
});
|
|
98
296
|
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
@@ -104,6 +302,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
104
302
|
event: args.event,
|
|
105
303
|
nodeLabel: args.nodeLabel,
|
|
106
304
|
metadataEvent: args.metadataEvent,
|
|
305
|
+
recoveryContext,
|
|
107
306
|
});
|
|
108
307
|
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
109
308
|
|