crewx-agent-cli 0.2.3 → 0.2.5
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 +79 -4
- package/dist/adapters.d.ts +12 -0
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +112 -40
- package/dist/adapters.js.map +1 -1
- package/dist/api.d.ts +40 -17
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +187 -11
- package/dist/api.js.map +1 -1
- package/dist/bridge-watchdog.d.ts +10 -0
- package/dist/bridge-watchdog.d.ts.map +1 -0
- package/dist/bridge-watchdog.js +45 -0
- package/dist/bridge-watchdog.js.map +1 -0
- package/dist/config.d.ts +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +6 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -1
- package/dist/constants.js.map +1 -1
- package/dist/daemon.d.ts +34 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +507 -102
- package/dist/daemon.js.map +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/join.d.ts +1 -0
- package/dist/join.d.ts.map +1 -1
- package/dist/join.js +13 -0
- package/dist/join.js.map +1 -1
- package/dist/outbox.d.ts +22 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +189 -0
- package/dist/outbox.js.map +1 -0
- package/dist/process-lock.d.ts +31 -0
- package/dist/process-lock.d.ts.map +1 -0
- package/dist/process-lock.js +272 -0
- package/dist/process-lock.js.map +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +7 -0
- package/dist/prompt.js.map +1 -1
- package/dist/runtime-proxy.d.ts +18 -0
- package/dist/runtime-proxy.d.ts.map +1 -0
- package/dist/runtime-proxy.js +221 -0
- package/dist/runtime-proxy.js.map +1 -0
- package/package.json +6 -2
package/dist/daemon.js
CHANGED
|
@@ -3,13 +3,17 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { hostname, platform, arch } from "node:os";
|
|
4
4
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { runAdapter, verifyOpenClawRuntime, } from "./adapters.js";
|
|
6
|
-
import { ApiError } from "./api.js";
|
|
7
|
-
import { CLI_VERSION, MAX_EVENT_CONTENT_LENGTH } from "./constants.js";
|
|
8
|
-
import { errorMessage, redactSecrets } from "./errors.js";
|
|
6
|
+
import { ApiError, serializeOutboundEventBatch, } from "./api.js";
|
|
7
|
+
import { CLI_VERSION, MAX_BUFFERED_EVENT_BYTES, MAX_BUFFERED_EVENTS, MAX_EVENT_BATCH_BYTES, MAX_EVENT_CONTENT_LENGTH, } from "./constants.js";
|
|
8
|
+
import { CliError, errorMessage, redactSecrets } from "./errors.js";
|
|
9
9
|
import { assemblePrompt, assignmentFromEvent } from "./prompt.js";
|
|
10
|
+
import { startRuntimeCredentialProxy, } from "./runtime-proxy.js";
|
|
11
|
+
import { DurableEventOutbox, agentEventOutboxPath, eventOutboxLockPath, } from "./outbox.js";
|
|
12
|
+
import { AgentProcessLock, } from "./process-lock.js";
|
|
10
13
|
const ASSIGNMENT_CONTROL_INTERVAL_MS = 5_000;
|
|
11
14
|
const EVENT_RETRY_BASE_DELAY_MS = 2_000;
|
|
12
15
|
const EVENT_RETRY_MAX_DELAY_MS = 30_000;
|
|
16
|
+
const MAX_IDLE_POLL_DELAY_MS = 8_000;
|
|
13
17
|
const MAX_RUNTIME_DIAGNOSTICS = 20;
|
|
14
18
|
function timestamp() {
|
|
15
19
|
return new Date().toISOString();
|
|
@@ -22,6 +26,19 @@ function event(type, payload) {
|
|
|
22
26
|
occurred_at: timestamp(),
|
|
23
27
|
};
|
|
24
28
|
}
|
|
29
|
+
function assignmentEvent(assignment, type, payload) {
|
|
30
|
+
return {
|
|
31
|
+
id: crypto.randomUUID(),
|
|
32
|
+
type,
|
|
33
|
+
payload: {
|
|
34
|
+
...payload,
|
|
35
|
+
...(assignment.claim_token
|
|
36
|
+
? { claim_token: assignment.claim_token }
|
|
37
|
+
: {}),
|
|
38
|
+
},
|
|
39
|
+
occurred_at: timestamp(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
25
42
|
function truncate(value) {
|
|
26
43
|
return value.length > MAX_EVENT_CONTENT_LENGTH
|
|
27
44
|
? `${value.slice(0, MAX_EVENT_CONTENT_LENGTH)}… [truncated]`
|
|
@@ -43,6 +60,50 @@ function record(value) {
|
|
|
43
60
|
function nonEmptyString(value) {
|
|
44
61
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
45
62
|
}
|
|
63
|
+
function runtimeFailure(adapter, detail) {
|
|
64
|
+
if (adapter !== "openclaw") {
|
|
65
|
+
return { code: "runtime_exit_nonzero", message: detail };
|
|
66
|
+
}
|
|
67
|
+
if (/\b429\b|rate[ -]?limit|quota|too many requests|cool(?:ing)?[ -]?down/i.test(detail)) {
|
|
68
|
+
return {
|
|
69
|
+
code: "openclaw_provider_rate_limited",
|
|
70
|
+
message: `${detail} Check \`openclaw models status --agent <agent-id> --json\` for provider cooldowns, then retry after the provider limit resets.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (/unauthori[sz]ed|\bauth\b|authentication|credential|oauth|invalid token|expired/i.test(detail)) {
|
|
74
|
+
return {
|
|
75
|
+
code: "openclaw_auth_failed",
|
|
76
|
+
message: `${detail} Run \`openclaw models auth --agent <agent-id> list --json\` and repair the selected provider credentials.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (/model.+(?:not found|unknown|unavailable|not allowed)/i.test(detail)) {
|
|
80
|
+
return {
|
|
81
|
+
code: "openclaw_model_unavailable",
|
|
82
|
+
message: `${detail} Run \`openclaw models status --agent <agent-id> --json\` and select an available model.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { code: "openclaw_runtime_failed", message: detail };
|
|
86
|
+
}
|
|
87
|
+
const OPENCLAW_DIAGNOSTIC_NOISE = /^\[openclaw\]\s+(?:Debug|Try|Help):/i;
|
|
88
|
+
export function runtimeFailureDetail(adapter, stderr, exitCode) {
|
|
89
|
+
const lines = stderr.map((line) => line.trim()).filter(Boolean);
|
|
90
|
+
if (adapter === "openclaw") {
|
|
91
|
+
const reason = [...lines]
|
|
92
|
+
.reverse()
|
|
93
|
+
.find((line) => /^\[openclaw\]\s+Reason:/i.test(line));
|
|
94
|
+
if (reason) {
|
|
95
|
+
return reason.replace(/^\[openclaw\]\s+Reason:\s*/i, "").trim();
|
|
96
|
+
}
|
|
97
|
+
const actionable = [...lines]
|
|
98
|
+
.reverse()
|
|
99
|
+
.find((line) => !OPENCLAW_DIAGNOSTIC_NOISE.test(line));
|
|
100
|
+
if (actionable) {
|
|
101
|
+
return actionable.replace(/^\[openclaw\]\s*/i, "").trim();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return (lines.at(-1) ??
|
|
105
|
+
`${adapter} exited with code ${String(exitCode)}`);
|
|
106
|
+
}
|
|
46
107
|
function identifier(value) {
|
|
47
108
|
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
48
109
|
return String(value);
|
|
@@ -130,7 +191,7 @@ export function assignmentWorkingDirectory(assignment, context, fallback, locked
|
|
|
130
191
|
if (relativePath === ".." ||
|
|
131
192
|
relativePath.startsWith(`..${sep}`) ||
|
|
132
193
|
isAbsolute(relativePath)) {
|
|
133
|
-
throw new Error(
|
|
194
|
+
throw new Error("CrewX Bridge rejected the assignment directory because it is outside the approved folder.");
|
|
134
195
|
}
|
|
135
196
|
return canonicalCandidate;
|
|
136
197
|
}
|
|
@@ -138,18 +199,65 @@ export class EventBuffer {
|
|
|
138
199
|
api;
|
|
139
200
|
sessionId;
|
|
140
201
|
queue = [];
|
|
202
|
+
inFlight = new Set();
|
|
141
203
|
flushing;
|
|
142
204
|
retryNotBefore = 0;
|
|
143
205
|
consecutiveFailures = 0;
|
|
144
|
-
|
|
206
|
+
maxEvents;
|
|
207
|
+
maxBytes;
|
|
208
|
+
random;
|
|
209
|
+
durableSnapshot;
|
|
210
|
+
constructor(api, sessionId, options = {}) {
|
|
145
211
|
this.api = api;
|
|
146
212
|
this.sessionId = sessionId;
|
|
213
|
+
this.maxEvents = options.maxEvents ?? MAX_BUFFERED_EVENTS;
|
|
214
|
+
this.maxBytes = options.maxBytes ?? MAX_BUFFERED_EVENT_BYTES;
|
|
215
|
+
this.random = options.random ?? Math.random;
|
|
216
|
+
this.outbox = options.outbox;
|
|
217
|
+
if (this.outbox) {
|
|
218
|
+
this.queue.push(...this.outbox.load());
|
|
219
|
+
this.enforceBounds();
|
|
220
|
+
}
|
|
221
|
+
this.durableSnapshot = this.serializeDurableEvents();
|
|
147
222
|
}
|
|
223
|
+
outbox;
|
|
148
224
|
push(item) {
|
|
149
|
-
this.
|
|
225
|
+
this.pushMany([item]);
|
|
226
|
+
}
|
|
227
|
+
pushMany(items) {
|
|
228
|
+
if (items.length === 0)
|
|
229
|
+
return;
|
|
230
|
+
const previous = [...this.queue];
|
|
231
|
+
try {
|
|
232
|
+
for (const item of items) {
|
|
233
|
+
if (!this.coalesce(item))
|
|
234
|
+
this.queue.push(item);
|
|
235
|
+
}
|
|
236
|
+
this.enforceBounds();
|
|
237
|
+
this.persistDurableEvents();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.queue.splice(0, this.queue.length, ...previous);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
150
243
|
if (this.queue.length >= 25)
|
|
151
244
|
void this.flush().catch(() => undefined);
|
|
152
245
|
}
|
|
246
|
+
pendingCount() {
|
|
247
|
+
return this.queue.length;
|
|
248
|
+
}
|
|
249
|
+
pendingBytes() {
|
|
250
|
+
return this.queue.reduce((total, item) => total + this.eventBytes(item), 0);
|
|
251
|
+
}
|
|
252
|
+
hasPendingTerminalOutcome(assignmentId) {
|
|
253
|
+
const isTerminalOutcome = (item) => item.type === "lifecycle" &&
|
|
254
|
+
item.payload.assignment_id === assignmentId &&
|
|
255
|
+
(item.payload.stage === "completed" ||
|
|
256
|
+
item.payload.stage === "failed" ||
|
|
257
|
+
item.payload.stage === "cancelled");
|
|
258
|
+
return (this.queue.some(isTerminalOutcome) ||
|
|
259
|
+
[...this.inFlight].some(isTerminalOutcome));
|
|
260
|
+
}
|
|
153
261
|
async flush(signal) {
|
|
154
262
|
if (this.flushing) {
|
|
155
263
|
await this.flushing;
|
|
@@ -171,21 +279,143 @@ export class EventBuffer {
|
|
|
171
279
|
await sleep(retryDelay, signal);
|
|
172
280
|
if (signal?.aborted)
|
|
173
281
|
return;
|
|
174
|
-
const batch =
|
|
282
|
+
const batch = [];
|
|
283
|
+
for (const item of this.queue) {
|
|
284
|
+
if (batch.length >= 100) {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
const candidate = [...batch, item];
|
|
288
|
+
const candidateBytes = Buffer.byteLength(serializeOutboundEventBatch(this.sessionId, candidate), "utf8");
|
|
289
|
+
if (candidateBytes > MAX_EVENT_BATCH_BYTES) {
|
|
290
|
+
if (batch.length === 0) {
|
|
291
|
+
throw new CliError(`CrewX cannot send event ${item.id ?? "(without an ID)"} because its exact serialized batch exceeds ${String(MAX_EVENT_BATCH_BYTES)} bytes.`, 1, { code: "event_batch_too_large" });
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
batch.push(item);
|
|
296
|
+
}
|
|
297
|
+
for (const item of batch)
|
|
298
|
+
this.inFlight.add(item);
|
|
175
299
|
try {
|
|
176
300
|
await this.api.sendEvents(this.sessionId, batch, signal);
|
|
177
|
-
this.queue.splice(0, batch.length);
|
|
301
|
+
const delivered = this.queue.splice(0, batch.length);
|
|
302
|
+
try {
|
|
303
|
+
this.persistDurableEvents();
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
this.queue.unshift(...delivered);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
178
309
|
this.consecutiveFailures = 0;
|
|
179
310
|
this.retryNotBefore = 0;
|
|
180
311
|
}
|
|
181
312
|
catch (error) {
|
|
182
313
|
this.consecutiveFailures += 1;
|
|
183
314
|
const exponentialDelay = Math.min(EVENT_RETRY_MAX_DELAY_MS, EVENT_RETRY_BASE_DELAY_MS * 2 ** (this.consecutiveFailures - 1));
|
|
315
|
+
const jitterDelay = Math.floor(this.random() * (exponentialDelay + 1));
|
|
184
316
|
const serverDelay = error instanceof ApiError ? error.retryAfterMs : undefined;
|
|
185
317
|
this.retryNotBefore =
|
|
186
|
-
Date.now() + Math.max(
|
|
318
|
+
Date.now() + Math.max(jitterDelay, serverDelay ?? 0);
|
|
187
319
|
throw error;
|
|
188
320
|
}
|
|
321
|
+
finally {
|
|
322
|
+
for (const item of batch)
|
|
323
|
+
this.inFlight.delete(item);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
coalesce(item) {
|
|
328
|
+
if (item.id) {
|
|
329
|
+
const duplicateIndex = this.findNewestIndex((candidate) => candidate.id === item.id);
|
|
330
|
+
if (duplicateIndex >= 0) {
|
|
331
|
+
this.queue[duplicateIndex] = item;
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (item.type === "heartbeat") {
|
|
336
|
+
const existingIndex = this.findNewestIndex((candidate) => candidate.type === "heartbeat");
|
|
337
|
+
if (existingIndex >= 0) {
|
|
338
|
+
this.queue[existingIndex] = item;
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (item.type === "output") {
|
|
343
|
+
const existingIndex = this.findNewestIndex((candidate) => candidate.type === "output" &&
|
|
344
|
+
candidate.payload.assignment_id === item.payload.assignment_id &&
|
|
345
|
+
candidate.payload.stream === item.payload.stream);
|
|
346
|
+
const existing = this.queue[existingIndex];
|
|
347
|
+
if (existingIndex >= 0 && existing?.type === "output") {
|
|
348
|
+
const combined = `${existing.payload.content}\n${item.payload.content}`;
|
|
349
|
+
const content = combined.length <= MAX_EVENT_CONTENT_LENGTH
|
|
350
|
+
? combined
|
|
351
|
+
: `[earlier output omitted during backpressure]\n${combined.slice(-(MAX_EVENT_CONTENT_LENGTH - 47))}`;
|
|
352
|
+
this.queue[existingIndex] = {
|
|
353
|
+
...item,
|
|
354
|
+
payload: {
|
|
355
|
+
...item.payload,
|
|
356
|
+
content,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
findNewestIndex(predicate) {
|
|
365
|
+
for (let index = this.queue.length - 1; index >= 0; index -= 1) {
|
|
366
|
+
const candidate = this.queue[index];
|
|
367
|
+
if (candidate &&
|
|
368
|
+
!this.inFlight.has(candidate) &&
|
|
369
|
+
predicate(candidate)) {
|
|
370
|
+
return index;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return -1;
|
|
374
|
+
}
|
|
375
|
+
eventPriority(item) {
|
|
376
|
+
if (item.type === "heartbeat" || item.type === "output")
|
|
377
|
+
return 0;
|
|
378
|
+
if (item.type === "error" ||
|
|
379
|
+
(item.type === "message" && item.payload.role === "assistant") ||
|
|
380
|
+
(item.type === "lifecycle" &&
|
|
381
|
+
["completed", "failed", "cancelled", "stopped"].includes(item.payload.stage))) {
|
|
382
|
+
return 2;
|
|
383
|
+
}
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
eventBytes(item) {
|
|
387
|
+
return Buffer.byteLength(JSON.stringify(item));
|
|
388
|
+
}
|
|
389
|
+
persistDurableEvents() {
|
|
390
|
+
if (!this.outbox)
|
|
391
|
+
return;
|
|
392
|
+
const durable = this.queue.filter((item) => this.eventPriority(item) === 2);
|
|
393
|
+
const snapshot = JSON.stringify(durable);
|
|
394
|
+
if (snapshot === this.durableSnapshot)
|
|
395
|
+
return;
|
|
396
|
+
this.outbox.replace(durable);
|
|
397
|
+
this.durableSnapshot = snapshot;
|
|
398
|
+
}
|
|
399
|
+
serializeDurableEvents() {
|
|
400
|
+
return JSON.stringify(this.queue.filter((item) => this.eventPriority(item) === 2));
|
|
401
|
+
}
|
|
402
|
+
enforceBounds() {
|
|
403
|
+
let bytes = this.pendingBytes();
|
|
404
|
+
while (this.queue.length > this.maxEvents ||
|
|
405
|
+
bytes > this.maxBytes) {
|
|
406
|
+
let removeIndex = -1;
|
|
407
|
+
for (const priority of [0, 1]) {
|
|
408
|
+
removeIndex = this.queue.findIndex((item) => !this.inFlight.has(item) &&
|
|
409
|
+
this.eventPriority(item) === priority);
|
|
410
|
+
if (removeIndex >= 0)
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if (removeIndex < 0) {
|
|
414
|
+
throw new CliError("CrewX cannot buffer another final agent outcome while event delivery is unavailable. Existing final outcomes were retained; restore connectivity before accepting more work.", 1, { code: "event_buffer_capacity_exceeded" });
|
|
415
|
+
}
|
|
416
|
+
const [removed] = this.queue.splice(removeIndex, 1);
|
|
417
|
+
if (removed)
|
|
418
|
+
bytes -= this.eventBytes(removed);
|
|
189
419
|
}
|
|
190
420
|
}
|
|
191
421
|
}
|
|
@@ -202,6 +432,18 @@ function sleep(milliseconds, signal) {
|
|
|
202
432
|
signal?.addEventListener("abort", finish, { once: true });
|
|
203
433
|
});
|
|
204
434
|
}
|
|
435
|
+
export function adaptiveIdlePollDelay(baseDelayMs, consecutiveEmptyPolls, options = {}) {
|
|
436
|
+
const base = Math.max(250, Math.floor(baseDelayMs));
|
|
437
|
+
const maximum = Math.max(base, MAX_IDLE_POLL_DELAY_MS);
|
|
438
|
+
const exponent = Math.max(0, Math.min(8, consecutiveEmptyPolls - 1));
|
|
439
|
+
const cap = Math.min(maximum, base * 2 ** exponent);
|
|
440
|
+
const random = options.random ?? Math.random;
|
|
441
|
+
const jittered = cap <= base
|
|
442
|
+
? base
|
|
443
|
+
: base +
|
|
444
|
+
Math.round(Math.max(0, Math.min(1, random())) * (cap - base));
|
|
445
|
+
return Math.max(jittered, options.retryAfterMs ?? 0);
|
|
446
|
+
}
|
|
205
447
|
function linkedAbortController(parent) {
|
|
206
448
|
const controller = new AbortController();
|
|
207
449
|
const abort = () => controller.abort(parent?.reason);
|
|
@@ -222,17 +464,24 @@ async function monitorAssignmentControl(options) {
|
|
|
222
464
|
while (!options.isFinished() && !options.controller.signal.aborted) {
|
|
223
465
|
let nextCheckDelay = ASSIGNMENT_CONTROL_INTERVAL_MS;
|
|
224
466
|
try {
|
|
225
|
-
const control = await options.api.control(options.sessionId, options.assignmentId, options.controller.signal);
|
|
467
|
+
const control = await options.api.control(options.sessionId, options.assignmentId, options.controller.signal, options.assignmentEventCursor);
|
|
226
468
|
if (control.cancelled) {
|
|
227
469
|
options.log(`CrewX cancelled ${options.assignmentId}; stopping the local agent…`);
|
|
228
470
|
options.controller.abort(new Error("Assignment cancelled by CrewX"));
|
|
229
|
-
return;
|
|
471
|
+
return undefined;
|
|
230
472
|
}
|
|
231
473
|
lastLoggedError = undefined;
|
|
232
474
|
}
|
|
233
475
|
catch (error) {
|
|
234
|
-
if (options.controller.signal.aborted || options.isFinished())
|
|
235
|
-
return;
|
|
476
|
+
if (options.controller.signal.aborted || options.isFinished()) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
if (error instanceof ApiError &&
|
|
480
|
+
(error.status === 401 || error.status === 403)) {
|
|
481
|
+
options.log("CrewX revoked this agent connection; stopping the local agent and daemon…");
|
|
482
|
+
options.controller.abort(error);
|
|
483
|
+
return error;
|
|
484
|
+
}
|
|
236
485
|
const message = errorMessage(error);
|
|
237
486
|
const now = Date.now();
|
|
238
487
|
if (message !== lastLoggedError || now - lastErrorLoggedAt >= 30_000) {
|
|
@@ -246,38 +495,68 @@ async function monitorAssignmentControl(options) {
|
|
|
246
495
|
}
|
|
247
496
|
await sleep(nextCheckDelay, options.controller.signal);
|
|
248
497
|
}
|
|
498
|
+
return undefined;
|
|
249
499
|
}
|
|
250
500
|
export async function executeAssignment(options) {
|
|
251
501
|
const { assignment, events } = options;
|
|
252
|
-
const secrets =
|
|
253
|
-
|
|
254
|
-
|
|
502
|
+
const secrets = [
|
|
503
|
+
...(options.environment?.CREWX_TOKEN
|
|
504
|
+
? [options.environment.CREWX_TOKEN]
|
|
505
|
+
: []),
|
|
506
|
+
...(assignment.claim_token ? [assignment.claim_token] : []),
|
|
507
|
+
];
|
|
255
508
|
const nativeJsonRuntime = !options.codingCommand &&
|
|
256
509
|
(options.adapter === "codex" ||
|
|
257
510
|
options.adapter === "claude" ||
|
|
258
511
|
options.adapter === "pi");
|
|
259
512
|
const runtimeDiagnostics = new Set();
|
|
260
|
-
|
|
513
|
+
const pendingRuntimeMessages = [];
|
|
514
|
+
let terminalizationStarted = false;
|
|
515
|
+
events.push(assignmentEvent(assignment, "lifecycle", {
|
|
261
516
|
stage: "claimed",
|
|
262
517
|
assignment_id: assignment.id,
|
|
263
518
|
adapter: options.adapter,
|
|
264
519
|
}));
|
|
265
|
-
events.push(
|
|
520
|
+
events.push(assignmentEvent(assignment, "message", {
|
|
266
521
|
assignment_id: assignment.id,
|
|
267
522
|
role: "user",
|
|
268
523
|
content: assignment.prompt,
|
|
269
524
|
}));
|
|
270
|
-
events.push(
|
|
525
|
+
events.push(assignmentEvent(assignment, "lifecycle", {
|
|
271
526
|
stage: "started",
|
|
272
527
|
assignment_id: assignment.id,
|
|
273
528
|
adapter: options.adapter,
|
|
274
529
|
}));
|
|
275
530
|
await events.flush(options.signal);
|
|
531
|
+
let runtimeProxy;
|
|
276
532
|
try {
|
|
277
533
|
const agent = record(options.context.agent);
|
|
278
534
|
const permissionPreset = agent?.permission_preset;
|
|
279
535
|
const configuredModel = nonEmptyString(agent?.model);
|
|
280
536
|
const runtimeAgentId = nonEmptyString(agent?.runtime_agent_id);
|
|
537
|
+
const upstreamToken = options.environment?.CREWX_TOKEN?.trim();
|
|
538
|
+
const cliPath = options.environment?.CREWX_CLI_PATH?.trim();
|
|
539
|
+
if (upstreamToken && cliPath) {
|
|
540
|
+
runtimeProxy = await startRuntimeCredentialProxy({
|
|
541
|
+
upstreamUrl: options.serverUrl,
|
|
542
|
+
upstreamToken,
|
|
543
|
+
cliPath,
|
|
544
|
+
assignmentId: assignment.id,
|
|
545
|
+
...(assignment.claim_token
|
|
546
|
+
? { claimToken: assignment.claim_token }
|
|
547
|
+
: {}),
|
|
548
|
+
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
549
|
+
permissionPreset: permissionPreset === "read_only" ||
|
|
550
|
+
permissionPreset === "standard" ||
|
|
551
|
+
permissionPreset === "full_access"
|
|
552
|
+
? permissionPreset
|
|
553
|
+
: "read_only",
|
|
554
|
+
chatOnly: agent?.chat_only === true,
|
|
555
|
+
});
|
|
556
|
+
const proxyToken = runtimeProxy.environment.CREWX_TOKEN;
|
|
557
|
+
if (proxyToken)
|
|
558
|
+
secrets.push(proxyToken);
|
|
559
|
+
}
|
|
281
560
|
const result = await (options.runAgent ?? runAdapter)({
|
|
282
561
|
adapter: options.adapter,
|
|
283
562
|
prompt: assemblePrompt(assignment, options.context),
|
|
@@ -297,6 +576,9 @@ export async function executeAssignment(options) {
|
|
|
297
576
|
},
|
|
298
577
|
...(options.codingCommand ? { codingCommand: options.codingCommand } : {}),
|
|
299
578
|
...(options.environment ? { environment: options.environment } : {}),
|
|
579
|
+
...(runtimeProxy
|
|
580
|
+
? { controlEnvironment: runtimeProxy.environment }
|
|
581
|
+
: {}),
|
|
300
582
|
...(options.signal ? { signal: options.signal } : {}),
|
|
301
583
|
onStdout(line, parsed) {
|
|
302
584
|
if (nativeJsonRuntime) {
|
|
@@ -309,7 +591,7 @@ export async function executeAssignment(options) {
|
|
|
309
591
|
return;
|
|
310
592
|
}
|
|
311
593
|
runtimeDiagnostics.add(content);
|
|
312
|
-
events.push(
|
|
594
|
+
events.push(assignmentEvent(assignment, "output", {
|
|
313
595
|
assignment_id: assignment.id,
|
|
314
596
|
stream: "stdout",
|
|
315
597
|
content,
|
|
@@ -318,7 +600,7 @@ export async function executeAssignment(options) {
|
|
|
318
600
|
}
|
|
319
601
|
const content = truncate(redactSecrets(line, secrets));
|
|
320
602
|
const raw = sanitizedRaw(parsed.raw, secrets);
|
|
321
|
-
events.push(
|
|
603
|
+
events.push(assignmentEvent(assignment, "output", {
|
|
322
604
|
assignment_id: assignment.id,
|
|
323
605
|
stream: "stdout",
|
|
324
606
|
content,
|
|
@@ -330,14 +612,20 @@ export async function executeAssignment(options) {
|
|
|
330
612
|
}));
|
|
331
613
|
},
|
|
332
614
|
onMessage(message, role) {
|
|
333
|
-
|
|
615
|
+
const runtimeMessage = assignmentEvent(assignment, "message", {
|
|
334
616
|
assignment_id: assignment.id,
|
|
335
617
|
role,
|
|
336
618
|
content: truncate(redactSecrets(message, secrets)),
|
|
337
|
-
})
|
|
619
|
+
});
|
|
620
|
+
if (role === "assistant" || role === "tool") {
|
|
621
|
+
pendingRuntimeMessages.push(runtimeMessage);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
events.push(runtimeMessage);
|
|
625
|
+
}
|
|
338
626
|
},
|
|
339
627
|
onStderr(line) {
|
|
340
|
-
events.push(
|
|
628
|
+
events.push(assignmentEvent(assignment, "output", {
|
|
341
629
|
assignment_id: assignment.id,
|
|
342
630
|
stream: "stderr",
|
|
343
631
|
content: truncate(redactSecrets(line, secrets)),
|
|
@@ -345,55 +633,111 @@ export async function executeAssignment(options) {
|
|
|
345
633
|
},
|
|
346
634
|
});
|
|
347
635
|
if (options.signal?.aborted) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
636
|
+
terminalizationStarted = true;
|
|
637
|
+
events.pushMany([
|
|
638
|
+
...pendingRuntimeMessages,
|
|
639
|
+
assignmentEvent(assignment, "lifecycle", {
|
|
640
|
+
stage: "cancelled",
|
|
641
|
+
assignment_id: assignment.id,
|
|
642
|
+
adapter: options.adapter,
|
|
643
|
+
}),
|
|
644
|
+
]);
|
|
353
645
|
}
|
|
354
646
|
else if (result.exitCode === 0) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
647
|
+
terminalizationStarted = true;
|
|
648
|
+
events.pushMany([
|
|
649
|
+
...pendingRuntimeMessages,
|
|
650
|
+
assignmentEvent(assignment, "lifecycle", {
|
|
651
|
+
stage: "completed",
|
|
652
|
+
assignment_id: assignment.id,
|
|
653
|
+
adapter: options.adapter,
|
|
654
|
+
exit_code: result.exitCode,
|
|
655
|
+
}),
|
|
656
|
+
]);
|
|
361
657
|
}
|
|
362
658
|
else {
|
|
363
|
-
const detail = result.stderr.
|
|
364
|
-
|
|
365
|
-
const safeDetail = redactSecrets(
|
|
366
|
-
|
|
659
|
+
const detail = runtimeFailureDetail(options.adapter, result.stderr, result.exitCode);
|
|
660
|
+
const failure = runtimeFailure(options.adapter, detail);
|
|
661
|
+
const safeDetail = redactSecrets(failure.message, secrets);
|
|
662
|
+
if (options.adapter === "openclaw") {
|
|
663
|
+
options.log?.(`OpenClaw task ${assignment.id} failed: ${truncate(safeDetail)}`);
|
|
664
|
+
}
|
|
665
|
+
terminalizationStarted = true;
|
|
666
|
+
events.pushMany([
|
|
667
|
+
...pendingRuntimeMessages,
|
|
668
|
+
assignmentEvent(assignment, "error", {
|
|
669
|
+
assignment_id: assignment.id,
|
|
670
|
+
message: truncate(safeDetail),
|
|
671
|
+
code: failure.code,
|
|
672
|
+
retryable: false,
|
|
673
|
+
}),
|
|
674
|
+
assignmentEvent(assignment, "lifecycle", {
|
|
675
|
+
stage: "failed",
|
|
676
|
+
assignment_id: assignment.id,
|
|
677
|
+
adapter: options.adapter,
|
|
678
|
+
exit_code: result.exitCode,
|
|
679
|
+
detail: truncate(safeDetail),
|
|
680
|
+
}),
|
|
681
|
+
]);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (terminalizationStarted)
|
|
686
|
+
throw error;
|
|
687
|
+
const message = truncate(redactSecrets(errorMessage(error), secrets));
|
|
688
|
+
if (options.adapter === "openclaw") {
|
|
689
|
+
options.log?.(`OpenClaw task ${assignment.id} failed: ${message}`);
|
|
690
|
+
}
|
|
691
|
+
terminalizationStarted = true;
|
|
692
|
+
events.pushMany([
|
|
693
|
+
...pendingRuntimeMessages,
|
|
694
|
+
assignmentEvent(assignment, "error", {
|
|
367
695
|
assignment_id: assignment.id,
|
|
368
|
-
message
|
|
696
|
+
message,
|
|
697
|
+
...(error instanceof CliError && error.code
|
|
698
|
+
? { code: error.code }
|
|
699
|
+
: {}),
|
|
369
700
|
retryable: false,
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
stage: "failed",
|
|
701
|
+
}),
|
|
702
|
+
assignmentEvent(assignment, "lifecycle", {
|
|
703
|
+
stage: options.signal?.aborted ? "cancelled" : "failed",
|
|
373
704
|
assignment_id: assignment.id,
|
|
374
705
|
adapter: options.adapter,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
706
|
+
detail: message,
|
|
707
|
+
}),
|
|
708
|
+
]);
|
|
379
709
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
events.push(event("error", {
|
|
383
|
-
assignment_id: assignment.id,
|
|
384
|
-
message,
|
|
385
|
-
retryable: false,
|
|
386
|
-
}));
|
|
387
|
-
events.push(event("lifecycle", {
|
|
388
|
-
stage: options.signal?.aborted ? "cancelled" : "failed",
|
|
389
|
-
assignment_id: assignment.id,
|
|
390
|
-
adapter: options.adapter,
|
|
391
|
-
detail: message,
|
|
392
|
-
}));
|
|
710
|
+
finally {
|
|
711
|
+
await runtimeProxy?.close();
|
|
393
712
|
}
|
|
394
713
|
await events.flush(options.signal?.aborted ? undefined : options.signal);
|
|
395
714
|
}
|
|
396
715
|
export async function runDaemon(options) {
|
|
716
|
+
const agentToken = options.environment?.CREWX_TOKEN?.trim();
|
|
717
|
+
const automaticOutboxPath = agentToken
|
|
718
|
+
? agentEventOutboxPath(options.api.baseUrl, agentToken, options.adapter, options.environment)
|
|
719
|
+
: undefined;
|
|
720
|
+
const durablePath = options.outbox?.path ?? automaticOutboxPath;
|
|
721
|
+
const processLock = options.processLock ??
|
|
722
|
+
(durablePath
|
|
723
|
+
? new AgentProcessLock(eventOutboxLockPath(durablePath))
|
|
724
|
+
: undefined);
|
|
725
|
+
// The lock precedes both session registration and outbox loading. This is
|
|
726
|
+
// intentionally wider than the network loop: two same-profile daemons must
|
|
727
|
+
// never hold independent cached snapshots of the same durable outcomes.
|
|
728
|
+
await processLock?.acquire();
|
|
729
|
+
try {
|
|
730
|
+
const durableOutbox = options.outbox ??
|
|
731
|
+
(automaticOutboxPath
|
|
732
|
+
? new DurableEventOutbox(automaticOutboxPath)
|
|
733
|
+
: undefined);
|
|
734
|
+
await runOwnedDaemon(options, durableOutbox);
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
await processLock?.release();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function runOwnedDaemon(options, durableOutbox) {
|
|
397
741
|
const log = options.log ?? (() => undefined);
|
|
398
742
|
const startedAt = Date.now();
|
|
399
743
|
const session = await options.api.createSession({
|
|
@@ -409,14 +753,20 @@ export async function runDaemon(options) {
|
|
|
409
753
|
cwd: options.cwd,
|
|
410
754
|
},
|
|
411
755
|
}, options.signal);
|
|
412
|
-
const events = new EventBuffer(options.api, session.session_id);
|
|
413
756
|
let cursor = session.cursor;
|
|
414
757
|
let activeAssignmentId;
|
|
758
|
+
let activeAssignmentClaimToken;
|
|
415
759
|
let stopping = false;
|
|
416
760
|
const seenAssignmentEvents = new Set();
|
|
417
761
|
let workspaceContext = await options.api.context(session.session_id, options.signal);
|
|
762
|
+
const agent = record(workspaceContext.agent);
|
|
763
|
+
const events = new EventBuffer(options.api, session.session_id, {
|
|
764
|
+
...(durableOutbox ? { outbox: durableOutbox } : {}),
|
|
765
|
+
});
|
|
766
|
+
// Reuse original event UUIDs against the new session so the server can
|
|
767
|
+
// idempotently acknowledge outcomes persisted before a crash or restart.
|
|
768
|
+
await events.flush(options.signal);
|
|
418
769
|
if (options.adapter === "openclaw") {
|
|
419
|
-
const agent = record(workspaceContext.agent);
|
|
420
770
|
const runtimeAgentId = nonEmptyString(agent?.runtime_agent_id);
|
|
421
771
|
if (!runtimeAgentId) {
|
|
422
772
|
throw new Error("CrewX did not provide an OpenClaw runtime agent ID for this profile.");
|
|
@@ -439,6 +789,9 @@ export async function runDaemon(options) {
|
|
|
439
789
|
events.push(event("heartbeat", {
|
|
440
790
|
status: activeAssignmentId ? "running" : stopping ? "stopping" : "idle",
|
|
441
791
|
...(activeAssignmentId ? { assignment_id: activeAssignmentId } : {}),
|
|
792
|
+
...(activeAssignmentClaimToken
|
|
793
|
+
? { claim_token: activeAssignmentClaimToken }
|
|
794
|
+
: {}),
|
|
442
795
|
uptime_seconds: Math.floor((Date.now() - startedAt) / 1_000),
|
|
443
796
|
}));
|
|
444
797
|
void events
|
|
@@ -448,6 +801,8 @@ export async function runDaemon(options) {
|
|
|
448
801
|
heartbeatTimer.unref();
|
|
449
802
|
try {
|
|
450
803
|
let polls = 0;
|
|
804
|
+
let consecutivePollFailures = 0;
|
|
805
|
+
let consecutiveEmptyPolls = 0;
|
|
451
806
|
while (!options.signal?.aborted) {
|
|
452
807
|
try {
|
|
453
808
|
// Never acknowledge a claimed page while its lifecycle/output batch is
|
|
@@ -455,7 +810,7 @@ export async function runDaemon(options) {
|
|
|
455
810
|
await events.flush(options.signal);
|
|
456
811
|
const requestedCursor = cursor;
|
|
457
812
|
const response = await options.api.poll(session.session_id, cursor, options.signal);
|
|
458
|
-
|
|
813
|
+
consecutivePollFailures = 0;
|
|
459
814
|
polls += 1;
|
|
460
815
|
for (const inbound of response.events) {
|
|
461
816
|
const assignment = assignmentFromEvent(inbound);
|
|
@@ -469,49 +824,85 @@ export async function runDaemon(options) {
|
|
|
469
824
|
continue;
|
|
470
825
|
seenAssignmentEvents.add(executionKey);
|
|
471
826
|
activeAssignmentId = assignment.id;
|
|
827
|
+
activeAssignmentClaimToken = assignment.claim_token;
|
|
472
828
|
log(`Running ${assignment.title ?? assignment.id}…`);
|
|
473
829
|
try {
|
|
474
|
-
|
|
830
|
+
try {
|
|
831
|
+
workspaceContext = await options.api.context(session.session_id, options.signal);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
log(`Context refresh failed; using the previous snapshot: ${errorMessage(error)}`);
|
|
835
|
+
}
|
|
836
|
+
const linked = linkedAbortController(options.signal);
|
|
837
|
+
let assignmentFinished = false;
|
|
838
|
+
const assignmentEventCursor = typeof assignment.metadata?.assignment_event_cursor === "number"
|
|
839
|
+
? assignment.metadata.assignment_event_cursor
|
|
840
|
+
: undefined;
|
|
841
|
+
const canMonitor = typeof options.api.control === "function" &&
|
|
842
|
+
/^\d+$/.test(assignment.id);
|
|
843
|
+
const monitor = canMonitor
|
|
844
|
+
? monitorAssignmentControl({
|
|
845
|
+
api: options.api,
|
|
846
|
+
sessionId: session.session_id,
|
|
847
|
+
assignmentId: assignment.id,
|
|
848
|
+
assignmentEventCursor,
|
|
849
|
+
controller: linked.controller,
|
|
850
|
+
isFinished: () => assignmentFinished,
|
|
851
|
+
log,
|
|
852
|
+
})
|
|
853
|
+
: Promise.resolve(undefined);
|
|
854
|
+
let fatalControlError;
|
|
855
|
+
try {
|
|
856
|
+
await (options.executeAssignment ?? executeAssignment)({
|
|
857
|
+
assignment,
|
|
858
|
+
adapter: options.adapter,
|
|
859
|
+
cwd: assignmentWorkingDirectory(assignment, workspaceContext, options.cwd, options.lockedRoot ?? process.env.CREWX_BRIDGE_ROOT),
|
|
860
|
+
context: workspaceContext,
|
|
861
|
+
events,
|
|
862
|
+
serverUrl: options.api.baseUrl,
|
|
863
|
+
sessionId: session.session_id,
|
|
864
|
+
...(options.codingCommand
|
|
865
|
+
? { codingCommand: options.codingCommand }
|
|
866
|
+
: {}),
|
|
867
|
+
...(options.environment
|
|
868
|
+
? { environment: options.environment }
|
|
869
|
+
: {}),
|
|
870
|
+
log,
|
|
871
|
+
signal: linked.controller.signal,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
assignmentFinished = true;
|
|
876
|
+
linked.controller.abort();
|
|
877
|
+
linked.detach();
|
|
878
|
+
fatalControlError = await monitor;
|
|
879
|
+
}
|
|
880
|
+
if (fatalControlError)
|
|
881
|
+
throw fatalControlError;
|
|
882
|
+
await events.flush(options.signal);
|
|
475
883
|
}
|
|
476
884
|
catch (error) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
assignmentId: assignment.id,
|
|
488
|
-
controller: linked.controller,
|
|
489
|
-
isFinished: () => assignmentFinished,
|
|
490
|
-
log,
|
|
491
|
-
})
|
|
492
|
-
: Promise.resolve();
|
|
493
|
-
try {
|
|
494
|
-
await (options.executeAssignment ?? executeAssignment)({
|
|
495
|
-
assignment,
|
|
496
|
-
adapter: options.adapter,
|
|
497
|
-
cwd: assignmentWorkingDirectory(assignment, workspaceContext, options.cwd, options.lockedRoot ?? process.env.CREWX_BRIDGE_ROOT),
|
|
498
|
-
context: workspaceContext,
|
|
499
|
-
events,
|
|
500
|
-
serverUrl: options.api.baseUrl,
|
|
501
|
-
...(options.codingCommand ? { codingCommand: options.codingCommand } : {}),
|
|
502
|
-
...(options.environment ? { environment: options.environment } : {}),
|
|
503
|
-
signal: linked.controller.signal,
|
|
504
|
-
});
|
|
885
|
+
// A failed pre-run lifecycle flush means the runtime never
|
|
886
|
+
// started. Leave the page unacknowledged and allow that event to
|
|
887
|
+
// execute after delivery recovers. If a terminal outcome is
|
|
888
|
+
// queued, retain the seen fence: the next loop first delivers
|
|
889
|
+
// that durable outcome, then acknowledges the page without
|
|
890
|
+
// executing the runtime twice.
|
|
891
|
+
if (!events.hasPendingTerminalOutcome(assignment.id)) {
|
|
892
|
+
seenAssignmentEvents.delete(executionKey);
|
|
893
|
+
}
|
|
894
|
+
throw error;
|
|
505
895
|
}
|
|
506
896
|
finally {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
linked.detach();
|
|
510
|
-
await monitor;
|
|
897
|
+
activeAssignmentId = undefined;
|
|
898
|
+
activeAssignmentClaimToken = undefined;
|
|
511
899
|
}
|
|
512
|
-
await events.flush(options.signal);
|
|
513
|
-
activeAssignmentId = undefined;
|
|
514
900
|
}
|
|
901
|
+
// Advancing the local cursor is the acknowledgement boundary. Do it
|
|
902
|
+
// only after every assignment on the page has a delivered terminal
|
|
903
|
+
// outcome (or was previously terminalized and fenced).
|
|
904
|
+
cursor = response.cursor;
|
|
905
|
+
seenAssignmentEvents.clear();
|
|
515
906
|
if (options.once && polls >= 1) {
|
|
516
907
|
if (String(cursor) !== String(requestedCursor)) {
|
|
517
908
|
// A final poll acknowledges the page that was just processed. Any
|
|
@@ -520,10 +911,21 @@ export async function runDaemon(options) {
|
|
|
520
911
|
}
|
|
521
912
|
break;
|
|
522
913
|
}
|
|
523
|
-
if (response.has_more)
|
|
914
|
+
if (response.has_more) {
|
|
915
|
+
consecutiveEmptyPolls = 0;
|
|
524
916
|
continue;
|
|
917
|
+
}
|
|
918
|
+
consecutiveEmptyPolls =
|
|
919
|
+
response.events.length === 0 ? consecutiveEmptyPolls + 1 : 0;
|
|
525
920
|
const serverDelay = response.retry_after_ms;
|
|
526
|
-
|
|
921
|
+
const pollDelay = consecutiveEmptyPolls > 0
|
|
922
|
+
? adaptiveIdlePollDelay(options.pollIntervalMs, consecutiveEmptyPolls, {
|
|
923
|
+
...(serverDelay !== undefined
|
|
924
|
+
? { retryAfterMs: serverDelay }
|
|
925
|
+
: {}),
|
|
926
|
+
})
|
|
927
|
+
: Math.max(options.pollIntervalMs, serverDelay ?? 0);
|
|
928
|
+
await sleep(pollDelay, options.signal);
|
|
527
929
|
}
|
|
528
930
|
catch (error) {
|
|
529
931
|
if (options.signal?.aborted)
|
|
@@ -532,9 +934,12 @@ export async function runDaemon(options) {
|
|
|
532
934
|
throw error;
|
|
533
935
|
const message = redactSecrets(errorMessage(error), [options.api.token]);
|
|
534
936
|
log(`Polling failed: ${message}. Retrying…`);
|
|
535
|
-
|
|
937
|
+
consecutivePollFailures += 1;
|
|
536
938
|
const serverDelay = error instanceof ApiError ? error.retryAfterMs : undefined;
|
|
537
|
-
|
|
939
|
+
const retryCap = Math.min(EVENT_RETRY_MAX_DELAY_MS, Math.max(options.pollIntervalMs, EVENT_RETRY_BASE_DELAY_MS) *
|
|
940
|
+
2 ** Math.min(8, consecutivePollFailures - 1));
|
|
941
|
+
const jitterDelay = Math.floor(Math.random() * (retryCap + 1));
|
|
942
|
+
await sleep(Math.max(250, jitterDelay, serverDelay ?? 0), options.signal);
|
|
538
943
|
}
|
|
539
944
|
}
|
|
540
945
|
}
|