@enderfga/claw-orchestrator 6.4.0 → 7.0.0
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/README.md +19 -4
- package/dist/bin/mcp-server.js +5 -3
- package/dist/bin/mcp-server.js.map +1 -1
- package/dist/src/autoloop/dispatcher.d.ts +37 -3
- package/dist/src/autoloop/dispatcher.js +171 -30
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/messages.d.ts +19 -1
- package/dist/src/autoloop/messages.js +4 -0
- package/dist/src/autoloop/messages.js.map +1 -1
- package/dist/src/autoloop/runner.d.ts +57 -2
- package/dist/src/autoloop/runner.js +260 -26
- package/dist/src/autoloop/runner.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +62 -2
- package/dist/src/autoloop/types.js +63 -0
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/embedded-server.d.ts +8 -0
- package/dist/src/embedded-server.js +65 -4
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.d.ts +9 -7
- package/dist/src/index.js +120 -80
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/engine.js +19 -14
- package/dist/src/kernel/engine.js.map +1 -1
- package/dist/src/models.js +16 -4
- package/dist/src/models.js.map +1 -1
- package/dist/src/persistent-agy-session.d.ts +4 -2
- package/dist/src/persistent-agy-session.js +12 -3
- package/dist/src/persistent-agy-session.js.map +1 -1
- package/dist/src/persistent-session.js +11 -1
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/session-manager.d.ts +6 -0
- package/dist/src/session-manager.js +246 -13
- package/dist/src/session-manager.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/SKILL.md +1 -1
- package/skills/references/autoloop.md +34 -3
- package/skills/references/claude-cli-tracking.md +2 -1
- package/skills/references/multi-engine.md +14 -2
- package/skills/references/tools.md +24 -3
package/README.md
CHANGED
|
@@ -110,17 +110,32 @@ block, and the cancellation and permission limitations are in
|
|
|
110
110
|
|
|
111
111
|
| Engine | CLI | Tested Version |
|
|
112
112
|
| ----------- | ---------- | -------------- |
|
|
113
|
-
| Claude Code | `claude` | 2.1.
|
|
114
|
-
| Codex | `codex` | 0.
|
|
115
|
-
| Antigravity | `agy` | 1.1.
|
|
113
|
+
| Claude Code | `claude` | 2.1.259 |
|
|
114
|
+
| Codex | `codex` | 0.153.0 |
|
|
115
|
+
| Antigravity | `agy` | 1.1.25 |
|
|
116
116
|
| Grok Build | `grok` | 1.0.13 |
|
|
117
|
-
| OpenCode | `opencode` | 1.18.
|
|
117
|
+
| OpenCode | `opencode` | 1.18.27 |
|
|
118
118
|
| Custom CLI | any | — |
|
|
119
119
|
|
|
120
120
|
Any coding CLI that runs as a subprocess can be wired up as a custom engine — see [`multi-engine.md`](./skills/references/multi-engine.md#custom-engine-enginecustom).
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## How the engine table stays honest
|
|
125
|
+
|
|
126
|
+
The versions above are not typed in — they are what the weekly sweep last ran. `scripts/sweep.ts`
|
|
127
|
+
measures each core engine's installed, pinned and upstream version, diffs the flags the wrapper
|
|
128
|
+
passes against the binary's `--help`, runs one live turn **through the real wrapper class**, and
|
|
129
|
+
smokes the ACP and MCP entry points. It has no LLM in it, so the thing that reports a wrapper as
|
|
130
|
+
broken cannot share the wrapper's failure modes.
|
|
131
|
+
|
|
132
|
+
`scripts/sweep-workflow.json` wraps it as a durable run on this project's own kernel: verifier →
|
|
133
|
+
router → an agent that drafts the alignment on a `sweep/<date>` branch → a human gate. That is the
|
|
134
|
+
bounded form of recursive self-improvement this project practises — the loop measures, proposes and
|
|
135
|
+
verifies; a person merges. The orchestrator never edits its own code unattended, on purpose: the
|
|
136
|
+
recovery path has to stay simpler than what it recovers. Its first scripted run found a default
|
|
137
|
+
model an engine had silently dropped, which three weeks of by-hand sweeps had walked past.
|
|
138
|
+
|
|
124
139
|
## Contributing
|
|
125
140
|
|
|
126
141
|
See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Run `npm run build && npm run lint && npm run format:check && npm run test` before submitting.
|
package/dist/bin/mcp-server.js
CHANGED
|
@@ -148,9 +148,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
148
148
|
}
|
|
149
149
|
try {
|
|
150
150
|
const result = await tool.execute(`mcp-${Date.now()}`, (req.params.arguments ?? {}));
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
// The plugin entry already formatted the payload as MCP-shaped text blocks,
|
|
152
|
+
// so hand them straight over. Stringifying `result` here instead would ship
|
|
153
|
+
// the data twice — once escaped inside `content[0].text`, once again under
|
|
154
|
+
// `details` — which is what this line did before the results were normalised.
|
|
155
|
+
return { content: result.content };
|
|
154
156
|
}
|
|
155
157
|
catch (err) {
|
|
156
158
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../../bin/mcp-server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,4EAA4E;AAC5E,6EAA6E;AAC7E,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,GAAG,CAAC;AAE7C,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../../bin/mcp-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -47,6 +47,8 @@ export interface ClaudeAgentDispatcherConfig {
|
|
|
47
47
|
reviewerCustomEngine?: CustomEngineConfig;
|
|
48
48
|
/** Per-message wall-clock cap. Default 10 min. */
|
|
49
49
|
sendTimeoutMs?: number;
|
|
50
|
+
/** Internal failure-atomic resume marker; never accepted from an agent. */
|
|
51
|
+
suppressFailedStartAudit?: boolean;
|
|
50
52
|
/**
|
|
51
53
|
* Optional acceptance contract. When present the Reviewer's `advance` is no
|
|
52
54
|
* longer sufficient on its own: the contract runs against the workspace and a
|
|
@@ -110,6 +112,23 @@ export declare class ClaudeAgentDispatcher extends EventEmitter implements Agent
|
|
|
110
112
|
/** Where Reviewer reads from. Created lazily by stageReviewSandbox(). */
|
|
111
113
|
private reviewerSandboxDir;
|
|
112
114
|
private ledgerDir;
|
|
115
|
+
/**
|
|
116
|
+
* One promise per immutable logical dispatch, so a re-delivered message is
|
|
117
|
+
* coalesced onto the first send instead of spending a second agent turn.
|
|
118
|
+
*
|
|
119
|
+
* Bounded, because what these promises resolve to is not small: a Coder
|
|
120
|
+
* dispatch returns `iter_artifacts` carrying the iteration's entire diff, and
|
|
121
|
+
* a run may legitimately last up to `MAX_AUTOLOOP_HARD_TIMEOUT_MS` (72 h).
|
|
122
|
+
* Retaining every one of those for the run's lifetime grows without limit.
|
|
123
|
+
*
|
|
124
|
+
* Evicting the oldest is safe: the queue consumes each message once, and the
|
|
125
|
+
* only replay path (`restorePausedMessages`) re-queues messages that were
|
|
126
|
+
* parked *before* delivery. Re-delivery therefore happens within a few
|
|
127
|
+
* messages of the original, far inside this window — while an entry still
|
|
128
|
+
* in flight is never evicted, so concurrent duplicates always coalesce.
|
|
129
|
+
*/
|
|
130
|
+
private logicalDispatches;
|
|
131
|
+
private readonly settledDispatches;
|
|
113
132
|
constructor(config: ClaudeAgentDispatcherConfig);
|
|
114
133
|
get sessionNames(): {
|
|
115
134
|
planner: string;
|
|
@@ -121,6 +140,17 @@ export declare class ClaudeAgentDispatcher extends EventEmitter implements Agent
|
|
|
121
140
|
purge?: boolean;
|
|
122
141
|
}): Promise<void>;
|
|
123
142
|
deliver(env: AnyAutoloopMessage): Promise<AnyAutoloopMessage[]>;
|
|
143
|
+
/** Drop the oldest settled dispatches once the retained set exceeds its cap. */
|
|
144
|
+
private trimLogicalDispatches;
|
|
145
|
+
/** Current per-agent deadline, including the backward-compatible default. */
|
|
146
|
+
get effectiveSendTimeoutMs(): number;
|
|
147
|
+
/**
|
|
148
|
+
* Apply an already-authorized timeout migration. This seam intentionally has
|
|
149
|
+
* no decrease mode: even an internal caller must supply a valid strict
|
|
150
|
+
* increase, while SessionManager owns persistence and dispatch matching.
|
|
151
|
+
*/
|
|
152
|
+
increaseSendTimeoutMs(next: number): void;
|
|
153
|
+
private deliverOnce;
|
|
124
154
|
private roleModel;
|
|
125
155
|
private validateSelection;
|
|
126
156
|
/**
|
|
@@ -171,10 +201,14 @@ export declare class ClaudeAgentDispatcher extends EventEmitter implements Agent
|
|
|
171
201
|
force?: boolean;
|
|
172
202
|
eagerRestart?: boolean;
|
|
173
203
|
}): Promise<void>;
|
|
204
|
+
private pendingSendTimeout;
|
|
205
|
+
private recoverableSendTimeout;
|
|
206
|
+
/** One physical send attempt with strict timeout classification. */
|
|
207
|
+
private sendAttempt;
|
|
174
208
|
/**
|
|
175
|
-
* Wrap a subagent send.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
209
|
+
* Wrap a subagent send. Genuine timeouts are never retried because the first
|
|
210
|
+
* turn may still finish with side effects. Other throws/error results retain
|
|
211
|
+
* the established reset-once/retry-once subprocess recovery path.
|
|
178
212
|
*/
|
|
179
213
|
private sendWithRecovery;
|
|
180
214
|
/**
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* into the runner queue.
|
|
18
18
|
*/
|
|
19
19
|
import { EventEmitter } from 'node:events';
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
20
21
|
import * as fs from 'node:fs';
|
|
21
22
|
import * as path from 'node:path';
|
|
22
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -27,7 +28,7 @@ import { capturePatch, changedFilesSince } from '../verify/baseline.js';
|
|
|
27
28
|
import { runContract } from '../verify/runner.js';
|
|
28
29
|
import { writeEvidence } from '../verify/evidence.js';
|
|
29
30
|
import { Msg } from './messages.js';
|
|
30
|
-
import { LEDGER_SCHEMA_VERSION, } from './types.js';
|
|
31
|
+
import { DEFAULT_SEND_TIMEOUT_MS, LEDGER_SCHEMA_VERSION, validateAutoloopTimeoutConfig, } from './types.js';
|
|
31
32
|
import { applyPlannerToolCalls, parsePlannerReply, } from './planner-tools.js';
|
|
32
33
|
import { extractIterComplete, extractReviewComplete, parseAgentReply } from './agent-tools.js';
|
|
33
34
|
/**
|
|
@@ -66,6 +67,35 @@ function resolveConfigByName(filename) {
|
|
|
66
67
|
const resolveDefaultPlannerPrompt = () => resolveConfigByName('autoloop-planner-prompt.md');
|
|
67
68
|
const resolveDefaultCoderPrompt = () => resolveConfigByName('autoloop-coder-prompt.md');
|
|
68
69
|
const resolveDefaultReviewerPrompt = () => resolveConfigByName('autoloop-reviewer-prompt.md');
|
|
70
|
+
/**
|
|
71
|
+
* Adapter send deadlines use one of these explicit signals. Deliberately do
|
|
72
|
+
* not classify arbitrary messages containing "timeout": configuration errors
|
|
73
|
+
* and other subprocess failures must retain reset-once/retry-once recovery.
|
|
74
|
+
*/
|
|
75
|
+
function genuineSendTimeoutMessage(error) {
|
|
76
|
+
const record = error;
|
|
77
|
+
const message = typeof error === 'string' ? error : typeof record?.message === 'string' ? record.message : '';
|
|
78
|
+
const namedTimeout = record?.name === 'TimeoutError';
|
|
79
|
+
const codedTimeout = record?.code === 'ETIMEDOUT';
|
|
80
|
+
const adapterTimeout = /^Timeout waiting for (?:(?:.+ )?response|.+ turn to complete)$/i.test(message);
|
|
81
|
+
return namedTimeout || codedTimeout || adapterTimeout ? message || 'Agent send timed out' : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* How many settled logical dispatches stay cached for dedup. One iteration
|
|
85
|
+
* spends a handful, so this holds many iterations' worth of replay window while
|
|
86
|
+
* keeping the retained diffs bounded.
|
|
87
|
+
*/
|
|
88
|
+
const MAX_RETAINED_DISPATCHES = 64;
|
|
89
|
+
/**
|
|
90
|
+
* Hash only immutable logical routing identity. `msg_id` distinguishes two
|
|
91
|
+
* intentional sends with otherwise identical content; envelope timestamps,
|
|
92
|
+
* wall-clock time, random values, retry attempts, and mutable counters are not
|
|
93
|
+
* inputs, so re-delivery in this or another dispatcher derives the same ID.
|
|
94
|
+
*/
|
|
95
|
+
function deriveDispatchId(runId, env) {
|
|
96
|
+
const identity = JSON.stringify([runId, env.msg_id, env.iter, env.from, env.to, env.type]);
|
|
97
|
+
return `dispatch_${createHash('sha256').update(identity).digest('hex')}`;
|
|
98
|
+
}
|
|
69
99
|
export class ClaudeAgentDispatcher extends EventEmitter {
|
|
70
100
|
config;
|
|
71
101
|
logger;
|
|
@@ -85,6 +115,23 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
85
115
|
/** Where Reviewer reads from. Created lazily by stageReviewSandbox(). */
|
|
86
116
|
reviewerSandboxDir;
|
|
87
117
|
ledgerDir;
|
|
118
|
+
/**
|
|
119
|
+
* One promise per immutable logical dispatch, so a re-delivered message is
|
|
120
|
+
* coalesced onto the first send instead of spending a second agent turn.
|
|
121
|
+
*
|
|
122
|
+
* Bounded, because what these promises resolve to is not small: a Coder
|
|
123
|
+
* dispatch returns `iter_artifacts` carrying the iteration's entire diff, and
|
|
124
|
+
* a run may legitimately last up to `MAX_AUTOLOOP_HARD_TIMEOUT_MS` (72 h).
|
|
125
|
+
* Retaining every one of those for the run's lifetime grows without limit.
|
|
126
|
+
*
|
|
127
|
+
* Evicting the oldest is safe: the queue consumes each message once, and the
|
|
128
|
+
* only replay path (`restorePausedMessages`) re-queues messages that were
|
|
129
|
+
* parked *before* delivery. Re-delivery therefore happens within a few
|
|
130
|
+
* messages of the original, far inside this window — while an entry still
|
|
131
|
+
* in flight is never evicted, so concurrent duplicates always coalesce.
|
|
132
|
+
*/
|
|
133
|
+
logicalDispatches = new Map();
|
|
134
|
+
settledDispatches = new Set();
|
|
88
135
|
constructor(config) {
|
|
89
136
|
super();
|
|
90
137
|
this.config = config;
|
|
@@ -122,11 +169,13 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
122
169
|
await this.ensurePlanner();
|
|
123
170
|
}
|
|
124
171
|
async shutdown(reason, opts = {}) {
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
if (!(reason === 'start-failed' && this.config.suppressFailedStartAudit)) {
|
|
173
|
+
this.appendDecisionLog({
|
|
174
|
+
kind: 'terminate',
|
|
175
|
+
actor: reason === 'phase_error_circuit' ? 'runner' : 'planner',
|
|
176
|
+
payload: { reason },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
130
179
|
// Best-effort cleanup. Stopping a non-existent session is a no-op.
|
|
131
180
|
// keepPersisted: true keeps the persistedSessions entry on disk so a
|
|
132
181
|
// later /autoloop/<id>/resume can re-attach the Planner's Claude
|
|
@@ -141,13 +190,61 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
141
190
|
}
|
|
142
191
|
}
|
|
143
192
|
async deliver(env) {
|
|
193
|
+
const dispatchId = deriveDispatchId(this.config.runId, env);
|
|
194
|
+
const existing = this.logicalDispatches.get(dispatchId);
|
|
195
|
+
if (existing)
|
|
196
|
+
return await existing;
|
|
197
|
+
const pending = this.deliverOnce(env, dispatchId);
|
|
198
|
+
this.logicalDispatches.set(dispatchId, pending);
|
|
199
|
+
// Mark settled before trimming so eviction can tell an in-flight dispatch
|
|
200
|
+
// from a finished one. A rejection settles too; `deliver` still rethrows it
|
|
201
|
+
// to this caller, and the entry is only a dedup record afterwards.
|
|
202
|
+
void pending.then(() => this.settledDispatches.add(dispatchId), () => this.settledDispatches.add(dispatchId));
|
|
203
|
+
try {
|
|
204
|
+
return await pending;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
this.trimLogicalDispatches();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Drop the oldest settled dispatches once the retained set exceeds its cap. */
|
|
211
|
+
trimLogicalDispatches() {
|
|
212
|
+
if (this.logicalDispatches.size <= MAX_RETAINED_DISPATCHES)
|
|
213
|
+
return;
|
|
214
|
+
for (const id of this.logicalDispatches.keys()) {
|
|
215
|
+
if (this.logicalDispatches.size <= MAX_RETAINED_DISPATCHES)
|
|
216
|
+
break;
|
|
217
|
+
if (!this.settledDispatches.has(id))
|
|
218
|
+
continue; // still in flight — must stay
|
|
219
|
+
this.logicalDispatches.delete(id);
|
|
220
|
+
this.settledDispatches.delete(id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Current per-agent deadline, including the backward-compatible default. */
|
|
224
|
+
get effectiveSendTimeoutMs() {
|
|
225
|
+
return this.config.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Apply an already-authorized timeout migration. This seam intentionally has
|
|
229
|
+
* no decrease mode: even an internal caller must supply a valid strict
|
|
230
|
+
* increase, while SessionManager owns persistence and dispatch matching.
|
|
231
|
+
*/
|
|
232
|
+
increaseSendTimeoutMs(next) {
|
|
233
|
+
validateAutoloopTimeoutConfig({ sendTimeoutMs: next });
|
|
234
|
+
const current = this.effectiveSendTimeoutMs;
|
|
235
|
+
if (next <= current) {
|
|
236
|
+
throw new Error(`sendTimeoutMs must be strictly greater than the current effective value ${current}`);
|
|
237
|
+
}
|
|
238
|
+
this.config.sendTimeoutMs = next;
|
|
239
|
+
}
|
|
240
|
+
async deliverOnce(env, dispatchId) {
|
|
144
241
|
switch (env.to) {
|
|
145
242
|
case 'planner':
|
|
146
|
-
return await this.deliverToPlanner(env);
|
|
243
|
+
return await this.deliverToPlanner(env, dispatchId);
|
|
147
244
|
case 'coder':
|
|
148
|
-
return await this.deliverToCoder(env);
|
|
245
|
+
return await this.deliverToCoder(env, dispatchId);
|
|
149
246
|
case 'reviewer':
|
|
150
|
-
return await this.deliverToReviewer(env);
|
|
247
|
+
return await this.deliverToReviewer(env, dispatchId);
|
|
151
248
|
default:
|
|
152
249
|
throw new Error(`[autoloop] unexpected dispatcher target: ${env.to}`);
|
|
153
250
|
}
|
|
@@ -375,17 +472,54 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
375
472
|
await this.ensureReviewer();
|
|
376
473
|
}
|
|
377
474
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
475
|
+
pendingSendTimeout(env, agent, dispatchId) {
|
|
476
|
+
return {
|
|
477
|
+
status: 'awaiting_resume',
|
|
478
|
+
dispatch_id: dispatchId,
|
|
479
|
+
agent,
|
|
480
|
+
message_id: env.msg_id,
|
|
481
|
+
message_type: env.type,
|
|
482
|
+
iter: env.iter,
|
|
483
|
+
timeout_ms: this.config.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
recoverableSendTimeout(pending, error) {
|
|
487
|
+
const timeout = { ...pending, error };
|
|
488
|
+
this.appendDecisionLog({
|
|
489
|
+
kind: 'send_timeout',
|
|
490
|
+
actor: 'dispatcher',
|
|
491
|
+
payload: { ...timeout },
|
|
492
|
+
});
|
|
493
|
+
return { output: '', error, recoverable_timeout: timeout };
|
|
494
|
+
}
|
|
495
|
+
/** One physical send attempt with strict timeout classification. */
|
|
496
|
+
async sendAttempt(name, promptText, pending) {
|
|
384
497
|
try {
|
|
385
|
-
|
|
386
|
-
timeout: this.config.sendTimeoutMs ??
|
|
498
|
+
const result = (await this.config.manager.sendMessage(name, promptText, {
|
|
499
|
+
timeout: this.config.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS,
|
|
387
500
|
parentRunId: this.config.runId,
|
|
388
501
|
}));
|
|
502
|
+
const timeoutMessage = result.error ? genuineSendTimeoutMessage(result.error) : null;
|
|
503
|
+
return timeoutMessage ? this.recoverableSendTimeout(pending, timeoutMessage) : result;
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
const timeoutMessage = genuineSendTimeoutMessage(err);
|
|
507
|
+
if (timeoutMessage)
|
|
508
|
+
return this.recoverableSendTimeout(pending, timeoutMessage);
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Wrap a subagent send. Genuine timeouts are never retried because the first
|
|
514
|
+
* turn may still finish with side effects. Other throws/error results retain
|
|
515
|
+
* the established reset-once/retry-once subprocess recovery path.
|
|
516
|
+
*/
|
|
517
|
+
async sendWithRecovery(agent, name, promptText, pending) {
|
|
518
|
+
try {
|
|
519
|
+
const result = await this.sendAttempt(name, promptText, pending);
|
|
520
|
+
if (result.recoverable_timeout || !result.error)
|
|
521
|
+
return result;
|
|
522
|
+
throw new Error(result.error);
|
|
389
523
|
}
|
|
390
524
|
catch (err) {
|
|
391
525
|
this.logger.warn?.(`[autoloop] ${agent} send threw, attempting reset+retry: ${err.message}`);
|
|
@@ -396,10 +530,10 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
396
530
|
// lockstep retries across concurrent runs.
|
|
397
531
|
await new Promise((r) => setTimeout(r, 500 + Math.floor(Math.random() * 250)));
|
|
398
532
|
try {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
533
|
+
const result = await this.sendAttempt(name, promptText, pending);
|
|
534
|
+
if (result.recoverable_timeout || !result.error)
|
|
535
|
+
return result;
|
|
536
|
+
throw new Error(result.error);
|
|
403
537
|
}
|
|
404
538
|
catch (err2) {
|
|
405
539
|
this.logger.error?.(`[autoloop] ${agent} second attempt failed after reset: ${err2.message}`);
|
|
@@ -530,7 +664,7 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
530
664
|
});
|
|
531
665
|
this.plannerStarted = true;
|
|
532
666
|
}
|
|
533
|
-
async deliverToPlanner(env) {
|
|
667
|
+
async deliverToPlanner(env, dispatchId) {
|
|
534
668
|
if (env.type !== 'chat' && env.type !== 'directive_ack' && env.type !== 'iter_done') {
|
|
535
669
|
// Other types (push_user / pause / resume / terminate) are runner-only
|
|
536
670
|
// or planner-emitted; they should never arrive *to* planner.
|
|
@@ -553,10 +687,11 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
553
687
|
// iter_done
|
|
554
688
|
promptText = `[system] iter ${env.iter} done. verdict=${env.payload.verdict} metric=${env.payload.metric}`;
|
|
555
689
|
}
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
690
|
+
const pendingTimeout = this.pendingSendTimeout(env, 'planner', dispatchId);
|
|
691
|
+
const result = await this.sendAttempt(this.plannerName, this.withRoleInstructions('planner', this.plannerSelection, this.plannerSystemPrompt, promptText), pendingTimeout);
|
|
692
|
+
if (result.recoverable_timeout) {
|
|
693
|
+
return [Msg.sendTimeout(env.iter, result.recoverable_timeout)];
|
|
694
|
+
}
|
|
560
695
|
if (result.error) {
|
|
561
696
|
this.logger.error?.(`[autoloop] planner send error: ${result.error}`);
|
|
562
697
|
this.emit('planner_error', new Error(result.error));
|
|
@@ -681,7 +816,7 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
681
816
|
});
|
|
682
817
|
this.coderStarted = true;
|
|
683
818
|
}
|
|
684
|
-
async deliverToCoder(env) {
|
|
819
|
+
async deliverToCoder(env, dispatchId) {
|
|
685
820
|
if (env.type !== 'directive') {
|
|
686
821
|
throw new Error(`[autoloop] coder does not accept message type=${env.type}`);
|
|
687
822
|
}
|
|
@@ -727,7 +862,10 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
727
862
|
text: `🔨 Coder iter ${env.iter} working…`,
|
|
728
863
|
ts: new Date().toISOString(),
|
|
729
864
|
});
|
|
730
|
-
const result = await this.sendWithRecovery('coder', this.coderName, this.withRoleInstructions('coder', this.coderSelection, this.coderSystemPrompt, promptText));
|
|
865
|
+
const result = await this.sendWithRecovery('coder', this.coderName, this.withRoleInstructions('coder', this.coderSelection, this.coderSystemPrompt, promptText), this.pendingSendTimeout(env, 'coder', dispatchId));
|
|
866
|
+
if (result.recoverable_timeout) {
|
|
867
|
+
return [Msg.sendTimeout(env.iter, result.recoverable_timeout)];
|
|
868
|
+
}
|
|
731
869
|
this.recordTurn('coder', 'user', promptText);
|
|
732
870
|
this.recordTurn('coder', 'agent', (result.output ?? '').trim());
|
|
733
871
|
// A3: subprocess died (recovery retry exhausted). Surface as phase_error
|
|
@@ -921,7 +1059,7 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
921
1059
|
}
|
|
922
1060
|
}
|
|
923
1061
|
}
|
|
924
|
-
async deliverToReviewer(env) {
|
|
1062
|
+
async deliverToReviewer(env, dispatchId) {
|
|
925
1063
|
if (env.type !== 'review_request') {
|
|
926
1064
|
throw new Error(`[autoloop] reviewer does not accept message type=${env.type}`);
|
|
927
1065
|
}
|
|
@@ -942,7 +1080,10 @@ export class ClaudeAgentDispatcher extends EventEmitter {
|
|
|
942
1080
|
text: `🔍 Reviewer iter ${env.payload.iter} auditing…`,
|
|
943
1081
|
ts: new Date().toISOString(),
|
|
944
1082
|
});
|
|
945
|
-
const result = await this.sendWithRecovery('reviewer', this.reviewerName, this.withRoleInstructions('reviewer', this.reviewerSelection, this.reviewerSessionPrompt ?? this.reviewerSystemPrompt, promptText));
|
|
1083
|
+
const result = await this.sendWithRecovery('reviewer', this.reviewerName, this.withRoleInstructions('reviewer', this.reviewerSelection, this.reviewerSessionPrompt ?? this.reviewerSystemPrompt, promptText), this.pendingSendTimeout(env, 'reviewer', dispatchId));
|
|
1084
|
+
if (result.recoverable_timeout) {
|
|
1085
|
+
return [Msg.sendTimeout(env.iter, result.recoverable_timeout)];
|
|
1086
|
+
}
|
|
946
1087
|
this.recordTurn('reviewer', 'user', promptText);
|
|
947
1088
|
this.recordTurn('reviewer', 'agent', (result.output ?? '').trim());
|
|
948
1089
|
if (result.fatal) {
|