@drakon-systems/multi-clawd 2.3.0 → 2.5.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 +121 -31
- package/SECURITY.md +3 -0
- package/dist/credential-state.js +8 -24
- package/dist/direct-report.js +12 -1
- package/dist/direct-route.js +12 -6
- package/dist/direct-sync.js +12 -4
- package/dist/explain-core.js +33 -10
- package/dist/health-journal.js +123 -0
- package/dist/health.js +13 -1
- package/dist/history.js +103 -0
- package/dist/index.js +89 -38
- package/dist/retry-plan.js +2 -0
- package/dist/session-affinity.js +108 -0
- package/dist/shim-core.js +197 -34
- package/dist/shim-lock.js +106 -0
- package/dist/shim.js +170 -79
- package/dist/spread.js +54 -0
- package/dist/sticky.js +2 -0
- package/dist/update-core.js +75 -1
- package/openclaw.plugin.json +26 -3
- package/package.json +1 -1
- package/scripts/account-config.mjs +115 -0
- package/scripts/cli.mjs +131 -25
- package/scripts/doctor.mjs +113 -6
package/dist/shim-core.js
CHANGED
|
@@ -192,9 +192,9 @@ export function limitFamilyFor(displayName, modelId) {
|
|
|
192
192
|
return displayName.trim().toLowerCase() === family ? family : undefined;
|
|
193
193
|
}
|
|
194
194
|
const LIMIT_TEXT_RE = /reached your (.{1,40}?) limit/i;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
const USAGE_LIMIT_API_ERROR = "usage_limit_reached";
|
|
196
|
+
const AUTH_FAILED_ERROR_KIND = "authentication_failed";
|
|
197
|
+
function parseRecord(line) {
|
|
198
198
|
let record;
|
|
199
199
|
try {
|
|
200
200
|
record = JSON.parse(line);
|
|
@@ -204,12 +204,15 @@ export function parseModelLimitError(line) {
|
|
|
204
204
|
}
|
|
205
205
|
if (typeof record !== "object" || record === null)
|
|
206
206
|
return undefined;
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
return record;
|
|
208
|
+
}
|
|
209
|
+
function isErrorRecord(r) {
|
|
210
|
+
return (r.type === "error" ||
|
|
209
211
|
r.is_error === true ||
|
|
210
|
-
(typeof r.subtype === "string" && r.subtype.startsWith("error"))
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
(typeof r.subtype === "string" && r.subtype.startsWith("error")) ||
|
|
213
|
+
(r.type === "assistant" && r.is_api_error_message === true));
|
|
214
|
+
}
|
|
215
|
+
function errorRecordTexts(r) {
|
|
213
216
|
const texts = [];
|
|
214
217
|
if (typeof r.result === "string")
|
|
215
218
|
texts.push(r.result);
|
|
@@ -220,13 +223,193 @@ export function parseModelLimitError(line) {
|
|
|
220
223
|
if (typeof msg === "string")
|
|
221
224
|
texts.push(msg);
|
|
222
225
|
}
|
|
223
|
-
|
|
226
|
+
if (r.type === "assistant" && r.is_api_error_message === true && Array.isArray(r.message?.content)) {
|
|
227
|
+
for (const block of r.message.content) {
|
|
228
|
+
if (block && block.type === "text" && typeof block.text === "string")
|
|
229
|
+
texts.push(block.text);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return texts;
|
|
233
|
+
}
|
|
234
|
+
const TRANSIENT_TEXT_PATTERNS = [
|
|
235
|
+
{ re: /repeated 529 overloaded errors/i, kind: "overloaded" },
|
|
236
|
+
{ re: /overloaded_error/i, kind: "overloaded" },
|
|
237
|
+
{ re: /the api is at capacity/i, kind: "overloaded" },
|
|
238
|
+
{ re: /server-side issue, usually temporary/i, kind: "server_error" },
|
|
239
|
+
{ re: /temporarily limiting requests \(not your usage limit\)/i, kind: "rate_limit_transient" },
|
|
240
|
+
{ re: /request rejected \(429\)/i, kind: "rate_limit_transient" },
|
|
241
|
+
];
|
|
242
|
+
const TRANSIENT_TEXT_MAX_CHARS = 160;
|
|
243
|
+
export function parseTransientApiError(line) {
|
|
244
|
+
if (!/("error"|is_error|api_error_status|overloaded|server-side|temporarily limiting|request rejected|at capacity)/i.test(line)) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const r = parseRecord(line);
|
|
248
|
+
if (!r || !isErrorRecord(r))
|
|
249
|
+
return undefined;
|
|
250
|
+
if (parseModelLimitError(line) || parseAuthFailure(line))
|
|
251
|
+
return undefined;
|
|
252
|
+
const joined = errorRecordTexts(r).join("\n");
|
|
253
|
+
const status = typeof r.api_error_status === "number" ? r.api_error_status : undefined;
|
|
254
|
+
const text = joined.trim().slice(0, TRANSIENT_TEXT_MAX_CHARS);
|
|
255
|
+
const kindFromCli = typeof r.error === "string" ? r.error : undefined;
|
|
256
|
+
if (status === 529 || kindFromCli === "overloaded")
|
|
257
|
+
return { kind: "overloaded", status, text };
|
|
258
|
+
if ((status !== undefined && status >= 500) || kindFromCli === "server_error") {
|
|
259
|
+
return { kind: "server_error", status, text };
|
|
260
|
+
}
|
|
261
|
+
for (const { re, kind } of TRANSIENT_TEXT_PATTERNS) {
|
|
262
|
+
if (re.test(joined))
|
|
263
|
+
return { kind, status, text };
|
|
264
|
+
}
|
|
265
|
+
if (status === 429)
|
|
266
|
+
return { kind: "rate_limit_transient", status, text };
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
export function parseApiRetryNotice(line) {
|
|
270
|
+
if (!line.includes('"api_retry"'))
|
|
271
|
+
return undefined;
|
|
272
|
+
let record;
|
|
273
|
+
try {
|
|
274
|
+
record = JSON.parse(line);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
if (typeof record !== "object" || record === null)
|
|
280
|
+
return undefined;
|
|
281
|
+
const r = record;
|
|
282
|
+
if (r.type !== "system" || r.subtype !== "api_retry")
|
|
283
|
+
return undefined;
|
|
284
|
+
const delay = typeof r.retry_delay_ms === "number" && r.retry_delay_ms >= 0 ? r.retry_delay_ms : 0;
|
|
285
|
+
return { retryDelayMs: delay };
|
|
286
|
+
}
|
|
287
|
+
export function parseModelLimitError(line) {
|
|
288
|
+
if (!/(reached your |usage_limit_reached)/i.test(line))
|
|
289
|
+
return undefined;
|
|
290
|
+
const r = parseRecord(line);
|
|
291
|
+
if (!r || !isErrorRecord(r))
|
|
292
|
+
return undefined;
|
|
293
|
+
for (const text of errorRecordTexts(r)) {
|
|
224
294
|
const match = LIMIT_TEXT_RE.exec(text);
|
|
225
295
|
if (match)
|
|
226
296
|
return { displayName: match[1].trim() };
|
|
227
297
|
}
|
|
298
|
+
if (r.api_error === USAGE_LIMIT_API_ERROR)
|
|
299
|
+
return { displayName: USAGE_LIMIT_API_ERROR };
|
|
228
300
|
return undefined;
|
|
229
301
|
}
|
|
302
|
+
export const LIMIT_PASSTHROUGH_MARKER = " (rate limit: 429)";
|
|
303
|
+
const HOST_DIAGNOSTIC_ERROR_PREFIX = "[ede_diagnostic]";
|
|
304
|
+
export function markForwardedLimitLine(line) {
|
|
305
|
+
if (!parseModelLimitError(line))
|
|
306
|
+
return line;
|
|
307
|
+
let record;
|
|
308
|
+
try {
|
|
309
|
+
record = JSON.parse(line);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return line;
|
|
313
|
+
}
|
|
314
|
+
if (typeof record !== "object" || record === null || Array.isArray(record))
|
|
315
|
+
return line;
|
|
316
|
+
const r = record;
|
|
317
|
+
const subtype = typeof r.subtype === "string" ? r.subtype : "";
|
|
318
|
+
if (r.type !== "result" || !(r.is_error === true || subtype.startsWith("error")))
|
|
319
|
+
return line;
|
|
320
|
+
const mark = (text) => text.includes(LIMIT_PASSTHROUGH_MARKER) ? undefined : `${text.trimEnd()}${LIMIT_PASSTHROUGH_MARKER}`;
|
|
321
|
+
if (Array.isArray(r.errors)) {
|
|
322
|
+
const idx = r.errors.findIndex((e) => typeof e === "string" && e.trim() !== "" && !e.trim().startsWith(HOST_DIAGNOSTIC_ERROR_PREFIX));
|
|
323
|
+
if (idx >= 0) {
|
|
324
|
+
const marked = mark(r.errors[idx]);
|
|
325
|
+
if (marked === undefined)
|
|
326
|
+
return line;
|
|
327
|
+
const errors = [...r.errors];
|
|
328
|
+
errors[idx] = marked;
|
|
329
|
+
return JSON.stringify({ ...r, errors });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (typeof r.result === "string" && r.result.trim() !== "") {
|
|
333
|
+
const marked = mark(r.result);
|
|
334
|
+
return marked === undefined ? line : JSON.stringify({ ...r, result: marked });
|
|
335
|
+
}
|
|
336
|
+
if (typeof r.error === "object" && r.error !== null && !Array.isArray(r.error)) {
|
|
337
|
+
const err = r.error;
|
|
338
|
+
if (typeof err.message === "string" && err.message.trim() !== "") {
|
|
339
|
+
const marked = mark(err.message);
|
|
340
|
+
return marked === undefined ? line : JSON.stringify({ ...r, error: { ...err, message: marked } });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (typeof r.error === "string" && r.error.trim() !== "") {
|
|
344
|
+
const marked = mark(r.error);
|
|
345
|
+
return marked === undefined ? line : JSON.stringify({ ...r, error: marked });
|
|
346
|
+
}
|
|
347
|
+
return line;
|
|
348
|
+
}
|
|
349
|
+
const RESULT_LINE_PREFIX = Buffer.from('{"type":"result"', "utf8");
|
|
350
|
+
const FORWARD_HOLD_CAP_BYTES = 256 * 1024;
|
|
351
|
+
function couldBeResultLine(pending) {
|
|
352
|
+
let i = 0;
|
|
353
|
+
while (i < pending.length && (pending[i] === 0x20 || pending[i] === 0x09 || pending[i] === 0x0d))
|
|
354
|
+
i++;
|
|
355
|
+
const rest = pending.subarray(i);
|
|
356
|
+
const n = Math.min(rest.length, RESULT_LINE_PREFIX.length);
|
|
357
|
+
return rest.subarray(0, n).equals(RESULT_LINE_PREFIX.subarray(0, n));
|
|
358
|
+
}
|
|
359
|
+
function forwardCompleteLine(line, onMark) {
|
|
360
|
+
const text = line.toString("utf8");
|
|
361
|
+
const ending = text.endsWith("\r\n") ? "\r\n" : text.endsWith("\n") ? "\n" : "";
|
|
362
|
+
const body = text.slice(0, text.length - ending.length).trim();
|
|
363
|
+
const marked = markForwardedLimitLine(body);
|
|
364
|
+
if (marked === body)
|
|
365
|
+
return line;
|
|
366
|
+
onMark?.();
|
|
367
|
+
return Buffer.from(marked + ending, "utf8");
|
|
368
|
+
}
|
|
369
|
+
export function createForwardWriter(write, onMark) {
|
|
370
|
+
let pending;
|
|
371
|
+
let streaming = false;
|
|
372
|
+
return {
|
|
373
|
+
push(chunk) {
|
|
374
|
+
const out = [];
|
|
375
|
+
let start = 0;
|
|
376
|
+
while (start < chunk.length) {
|
|
377
|
+
const nl = chunk.indexOf(0x0a, start);
|
|
378
|
+
const end = nl < 0 ? chunk.length : nl + 1;
|
|
379
|
+
const piece = chunk.subarray(start, end);
|
|
380
|
+
start = end;
|
|
381
|
+
if (streaming) {
|
|
382
|
+
out.push(piece);
|
|
383
|
+
if (nl >= 0)
|
|
384
|
+
streaming = false;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const line = pending ? Buffer.concat([pending, piece]) : piece;
|
|
388
|
+
pending = undefined;
|
|
389
|
+
if (nl >= 0) {
|
|
390
|
+
out.push(forwardCompleteLine(line, onMark));
|
|
391
|
+
}
|
|
392
|
+
else if (!couldBeResultLine(line) || line.length > FORWARD_HOLD_CAP_BYTES) {
|
|
393
|
+
out.push(line);
|
|
394
|
+
streaming = true;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
pending = Buffer.from(line);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (out.length > 0)
|
|
401
|
+
write(out.length === 1 ? out[0] : Buffer.concat(out));
|
|
402
|
+
},
|
|
403
|
+
flush() {
|
|
404
|
+
if (pending) {
|
|
405
|
+
const line = pending;
|
|
406
|
+
pending = undefined;
|
|
407
|
+
write(forwardCompleteLine(line, onMark));
|
|
408
|
+
}
|
|
409
|
+
streaming = false;
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
230
413
|
export function recordModelLimit(state, modelId, now, resetsAt, family) {
|
|
231
414
|
return {
|
|
232
415
|
...state,
|
|
@@ -300,36 +483,16 @@ export function parseAuthFailure(line) {
|
|
|
300
483
|
if (!/(authenticat|oauth|logged in|\/login|unauthori|api key|access token|bearer token)/i.test(line)) {
|
|
301
484
|
return undefined;
|
|
302
485
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
record = JSON.parse(line);
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
if (typeof record !== "object" || record === null)
|
|
311
|
-
return undefined;
|
|
312
|
-
const r = record;
|
|
313
|
-
const isErrorRecord = r.type === "error" ||
|
|
314
|
-
r.is_error === true ||
|
|
315
|
-
(typeof r.subtype === "string" && r.subtype.startsWith("error"));
|
|
316
|
-
if (!isErrorRecord)
|
|
486
|
+
const r = parseRecord(line);
|
|
487
|
+
if (!r || !isErrorRecord(r))
|
|
317
488
|
return undefined;
|
|
318
|
-
const
|
|
319
|
-
if (typeof r.result === "string")
|
|
320
|
-
texts.push(r.result);
|
|
321
|
-
if (typeof r.error === "string")
|
|
322
|
-
texts.push(r.error);
|
|
323
|
-
if (typeof r.error === "object" && r.error !== null) {
|
|
324
|
-
const msg = r.error.message;
|
|
325
|
-
if (typeof msg === "string")
|
|
326
|
-
texts.push(msg);
|
|
327
|
-
}
|
|
328
|
-
for (const text of texts) {
|
|
489
|
+
for (const text of errorRecordTexts(r)) {
|
|
329
490
|
if (AUTH_FAILURE_PATTERNS.some((p) => p.test(text))) {
|
|
330
491
|
return { reason: text.trim().slice(0, AUTH_REASON_MAX_CHARS) };
|
|
331
492
|
}
|
|
332
493
|
}
|
|
494
|
+
if (r.error === AUTH_FAILED_ERROR_KIND)
|
|
495
|
+
return { reason: AUTH_FAILED_ERROR_KIND };
|
|
333
496
|
return undefined;
|
|
334
497
|
}
|
|
335
498
|
export function recordCredentialFailure(state, reason, now) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute } from "node:path";
|
|
4
|
+
const WAIT_MS = 150;
|
|
5
|
+
const LEASE_MS = 5_000;
|
|
6
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
7
|
+
export function acquireShimLock(file, waitMs = WAIT_MS, hooks = {}) {
|
|
8
|
+
if (!file || !isAbsolute(file))
|
|
9
|
+
return undefined;
|
|
10
|
+
const lock = `${file}.lock`;
|
|
11
|
+
const deadline = Date.now() + waitMs;
|
|
12
|
+
try {
|
|
13
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
do {
|
|
19
|
+
try {
|
|
20
|
+
const fd = openSync(lock, "wx", 0o600);
|
|
21
|
+
const token = randomUUID();
|
|
22
|
+
const content = Buffer.from(`${process.pid} ${Date.now()} ${token}\n`);
|
|
23
|
+
let ino;
|
|
24
|
+
try {
|
|
25
|
+
ino = fstatSync(fd).ino;
|
|
26
|
+
writeFileSync(fd, content);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
try {
|
|
30
|
+
if (ino !== undefined && statSync(lock).ino === ino) {
|
|
31
|
+
const found = readFileSync(lock);
|
|
32
|
+
if (found.includes(Buffer.from(token)) || found.length === 0)
|
|
33
|
+
unlinkSync(lock);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
try {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const release = () => {
|
|
44
|
+
try {
|
|
45
|
+
if (statSync(lock).ino === ino && readFileSync(lock).equals(content))
|
|
46
|
+
unlinkSync(lock);
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
(hooks.close ?? closeSync)(fd);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
try {
|
|
55
|
+
closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
return release;
|
|
59
|
+
}
|
|
60
|
+
return release;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err.code !== "EEXIST")
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
let staleFd;
|
|
67
|
+
try {
|
|
68
|
+
staleFd = openSync(lock, "r");
|
|
69
|
+
const before = fstatSync(staleFd);
|
|
70
|
+
hooks.beforeStaleSnapshot?.();
|
|
71
|
+
const observed = readFileSync(staleFd);
|
|
72
|
+
const [pidText, timeText] = observed.toString("utf8").trim().split(" ");
|
|
73
|
+
const pid = Number(pidText);
|
|
74
|
+
const age = Date.now() - Number(timeText);
|
|
75
|
+
let dead = false;
|
|
76
|
+
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
dead = err.code === "ESRCH";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (dead || (Number.isFinite(age) && age > LEASE_MS) || Date.now() - before.mtimeMs > LEASE_MS) {
|
|
85
|
+
hooks.beforeStaleUnlink?.();
|
|
86
|
+
const current = statSync(lock);
|
|
87
|
+
if (current.ino === before.ino && current.dev === before.dev && readFileSync(lock).equals(observed)) {
|
|
88
|
+
unlinkSync(lock);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
finally {
|
|
95
|
+
if (staleFd !== undefined)
|
|
96
|
+
try {
|
|
97
|
+
closeSync(staleFd);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
if (Date.now() >= deadline)
|
|
102
|
+
break;
|
|
103
|
+
Atomics.wait(pause, 0, 0, Math.min(10, deadline - Date.now()));
|
|
104
|
+
} while (Date.now() <= deadline);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|