@bridge4dev/runner 0.42.1 → 0.44.2
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/adapters/claude-usage.d.ts +35 -4
- package/dist/adapters/claude-usage.js +138 -14
- package/dist/adapters/claude.js +205 -10
- package/dist/adapters/codex.d.ts +14 -0
- package/dist/adapters/codex.js +103 -8
- package/dist/adapters/error-policy.d.ts +178 -0
- package/dist/adapters/error-policy.js +370 -0
- package/dist/adapters/rate-limits.d.ts +22 -0
- package/dist/adapters/rate-limits.js +24 -0
- package/dist/adapters/types.d.ts +30 -0
- package/dist/claude-settings.d.ts +107 -0
- package/dist/claude-settings.js +415 -0
- package/dist/index.js +88 -1
- package/dist/recipe-schema.d.ts +6 -6
- package/dist/regex-guard-hook.d.ts +3 -0
- package/dist/regex-guard-hook.js +48 -0
- package/dist/regex-guard.d.ts +86 -0
- package/dist/regex-guard.js +359 -0
- package/dist/supervisor.d.ts +16 -0
- package/dist/supervisor.js +151 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/codex.js
CHANGED
|
@@ -153,6 +153,16 @@ class CodexSession {
|
|
|
153
153
|
threadId = null;
|
|
154
154
|
threadModel = null;
|
|
155
155
|
activeTurnId = null;
|
|
156
|
+
/**
|
|
157
|
+
* Did this turn put anything on the wire before it broke, and was any of it
|
|
158
|
+
* beyond taking back? (#252, #257)
|
|
159
|
+
*
|
|
160
|
+
* The Claude adapter counts the same two facts for the same reason: a failure
|
|
161
|
+
* that arrives after six rounds of tools cannot be answered by re-sending the
|
|
162
|
+
* turn. Reset in `beginTurnWork`, called wherever a turn starts.
|
|
163
|
+
*/
|
|
164
|
+
turnProduced = false;
|
|
165
|
+
turnIrreversible = false;
|
|
156
166
|
/**
|
|
157
167
|
* Last turn this thread finished (ticket #126).
|
|
158
168
|
*
|
|
@@ -1099,6 +1109,9 @@ class CodexSession {
|
|
|
1099
1109
|
}
|
|
1100
1110
|
case 'turn/started': {
|
|
1101
1111
|
this.activeTurnId = str(params['turnId']) ?? str(asRecord(params['turn'])['id']) ?? null;
|
|
1112
|
+
// #252: Codex announces its turns, so this is the exact boundary. Any
|
|
1113
|
+
// later and one turn's «work was done» would veto every retry after it.
|
|
1114
|
+
this.beginTurnWork();
|
|
1102
1115
|
return;
|
|
1103
1116
|
}
|
|
1104
1117
|
case 'turn/completed': {
|
|
@@ -1276,6 +1289,8 @@ class CodexSession {
|
|
|
1276
1289
|
case 'commandExecution': {
|
|
1277
1290
|
const command = str(item['command']) ?? '';
|
|
1278
1291
|
if (!done) {
|
|
1292
|
+
// #252/#257: what separates «send the turn again» from «resume it».
|
|
1293
|
+
this.noteWork(command);
|
|
1279
1294
|
this.emit({
|
|
1280
1295
|
type: 'tool',
|
|
1281
1296
|
phase: 'use',
|
|
@@ -1384,16 +1399,28 @@ class CodexSession {
|
|
|
1384
1399
|
const usedPercent = num(window['usedPercent'] ?? window['used_percent']);
|
|
1385
1400
|
if (usedPercent === undefined)
|
|
1386
1401
|
continue;
|
|
1387
|
-
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1402
|
+
// `windowDurationMins` is the name codex-cli 0.147.0 actually sends —
|
|
1403
|
+
// captured from a live `account/rateLimits/updated` on 2026-08-16:
|
|
1404
|
+
//
|
|
1405
|
+
// primary: {usedPercent: 4, windowDurationMins: 10080, resetsAt: 1787221437}
|
|
1406
|
+
// secondary: null
|
|
1407
|
+
//
|
|
1408
|
+
// The two older spellings are kept behind it, not in front: a Codex that
|
|
1409
|
+
// still sends them keeps working, and the current one stops being read as
|
|
1410
|
+
// «length unknown». That misreading is what made the panel label a WEEKLY
|
|
1411
|
+
// window «5 hours» — the number was right and the heading was not.
|
|
1412
|
+
const minutes = num(window['windowDurationMins'] ?? window['windowMinutes'] ?? window['window_minutes']) ??
|
|
1413
|
+
null;
|
|
1393
1414
|
// Only when the length is ABSENT. A stated length we do not recognise is
|
|
1394
1415
|
// information — 43 200 minutes is a monthly window, and calling it «5
|
|
1395
1416
|
// hours» because it arrived first would be worse than admitting we have
|
|
1396
1417
|
// no name for it.
|
|
1418
|
+
//
|
|
1419
|
+
// The slot fallback stays for exactly that case, but it is now the last
|
|
1420
|
+
// resort rather than the usual path. And note what the live payload shows:
|
|
1421
|
+
// `primary` is NOT reliably the short window. Here it is the weekly one
|
|
1422
|
+
// and `secondary` is null, so guessing by slot was wrong in the only case
|
|
1423
|
+
// we have ever observed.
|
|
1397
1424
|
const key = minutes === null
|
|
1398
1425
|
? slot === 'primary'
|
|
1399
1426
|
? 'five_hour'
|
|
@@ -1403,7 +1430,12 @@ class CodexSession {
|
|
|
1403
1430
|
key,
|
|
1404
1431
|
windowMinutes: minutes,
|
|
1405
1432
|
usedPercent: clampPercent(usedPercent),
|
|
1406
|
-
|
|
1433
|
+
// Epoch SECONDS on the wire, not an ISO string — reading it with `str()`
|
|
1434
|
+
// returned undefined for every snapshot ever received, which is why no
|
|
1435
|
+
// Codex window has ever shown a reset time. The string form is still
|
|
1436
|
+
// accepted in case a future version switches. Same fact, two encodings,
|
|
1437
|
+
// one place that knows it — exactly as the Claude adapter already does.
|
|
1438
|
+
resetsAt: codexResetsAt(window['resetsAt'] ?? window['resets_at']),
|
|
1407
1439
|
});
|
|
1408
1440
|
}
|
|
1409
1441
|
this.rateLimitWindows = windows.sort((a, b) => (a.windowMinutes ?? Number.MAX_SAFE_INTEGER) - (b.windowMinutes ?? Number.MAX_SAFE_INTEGER));
|
|
@@ -1438,9 +1470,36 @@ class CodexSession {
|
|
|
1438
1470
|
* calls a human instead. Guessing a time here is the one outcome worth
|
|
1439
1471
|
* avoiding: waking early burns a retry and changes nothing.
|
|
1440
1472
|
*/
|
|
1473
|
+
/** A new turn is starting — forget what the previous one did (#252). */
|
|
1474
|
+
beginTurnWork() {
|
|
1475
|
+
this.turnProduced = false;
|
|
1476
|
+
this.turnIrreversible = false;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Record a command as work that happened, and judge whether it can be undone.
|
|
1480
|
+
*
|
|
1481
|
+
* The same blunt list as the Claude adapter: writing git subcommands only.
|
|
1482
|
+
* Read-only git is excluded deliberately — the agent runs `git status` all day
|
|
1483
|
+
* and treating that as a write would suppress nearly every retry.
|
|
1484
|
+
*/
|
|
1485
|
+
noteWork(command) {
|
|
1486
|
+
this.turnProduced = true;
|
|
1487
|
+
if (this.turnIrreversible)
|
|
1488
|
+
return;
|
|
1489
|
+
if (/\bgit\s+(commit|push|tag|merge|rebase|reset|revert|cherry-pick)\b/.test(command)) {
|
|
1490
|
+
this.turnIrreversible = true;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1441
1493
|
rateLimitRefusal(error) {
|
|
1494
|
+
// The machine-readable cause FIRST. Codex 0.147.0 says `usageLimitExceeded`
|
|
1495
|
+
// — Exceeded, not Reached — so not one of the four substrings below ever
|
|
1496
|
+
// matched it, and the auto-pause never armed on this agent at all: a refused
|
|
1497
|
+
// turn went to FAILED instead of sleeping until the window reopened.
|
|
1498
|
+
// Demonstrated by test, 2026-08-16, not inferred.
|
|
1499
|
+
const code = str(asRecord(error)['codexErrorInfo']);
|
|
1442
1500
|
const haystack = JSON.stringify(error).toLowerCase();
|
|
1443
|
-
const refused =
|
|
1501
|
+
const refused = code === 'usageLimitExceeded' ||
|
|
1502
|
+
haystack.includes('ratelimitreached') ||
|
|
1444
1503
|
haystack.includes('rate_limit_reached') ||
|
|
1445
1504
|
haystack.includes('usage_limit_reached') ||
|
|
1446
1505
|
haystack.includes('usagelimitreached');
|
|
@@ -1472,6 +1531,13 @@ class CodexSession {
|
|
|
1472
1531
|
const blocked = this.rateLimitRefusal(error);
|
|
1473
1532
|
if (blocked)
|
|
1474
1533
|
this.emitRateLimits(blocked);
|
|
1534
|
+
// #252: the machine-readable cause, beside the sentence. `codexErrorInfo`
|
|
1535
|
+
// is Codex's own enum (`usageLimitExceeded`, `httpConnectionFailed`,
|
|
1536
|
+
// `responseStreamConnectionFailed`, `contextWindowExceeded`, …) and it is
|
|
1537
|
+
// the ONLY thing allowed to open the door to an automatic retry — the
|
|
1538
|
+
// message next to it is prose, and prose can be written by agents.
|
|
1539
|
+
const failureCode = str(error['codexErrorInfo']);
|
|
1540
|
+
const failureStatus = error['httpStatusCode'];
|
|
1475
1541
|
this.emit({
|
|
1476
1542
|
type: 'turn_end',
|
|
1477
1543
|
ok: false,
|
|
@@ -1480,6 +1546,10 @@ class CodexSession {
|
|
|
1480
1546
|
// one line earlier would ring into a dead row and throw the person's
|
|
1481
1547
|
// queued words away instead of sending them.
|
|
1482
1548
|
...(blocked ? { limitBlocked: true } : {}),
|
|
1549
|
+
...(failureCode !== undefined ? { failureCode } : {}),
|
|
1550
|
+
...(typeof failureStatus === 'number' ? { failureStatus } : {}),
|
|
1551
|
+
...(this.turnProduced ? { produced: true } : {}),
|
|
1552
|
+
...(this.turnIrreversible ? { irreversible: true } : {}),
|
|
1483
1553
|
});
|
|
1484
1554
|
return;
|
|
1485
1555
|
}
|
|
@@ -1867,6 +1937,31 @@ function truncateRecord(value) {
|
|
|
1867
1937
|
}
|
|
1868
1938
|
return out;
|
|
1869
1939
|
}
|
|
1940
|
+
/**
|
|
1941
|
+
* When a Codex window resets → ISO, or null if it did not say.
|
|
1942
|
+
*
|
|
1943
|
+
* Epoch SECONDS on the wire, captured live from codex-cli 0.147.0 on 2026-08-16:
|
|
1944
|
+
* `resetsAt: 1787221437` is 2026-08-20. Read as a string it was silently dropped,
|
|
1945
|
+
* which is why no Codex window has ever shown a reset time and why #258 had
|
|
1946
|
+
* nothing to schedule a wake-up from on this agent.
|
|
1947
|
+
*
|
|
1948
|
+
* The range check is not decoration. A clock built from a misread number is
|
|
1949
|
+
* worse than no clock at all: milliseconds mistaken for seconds would promise a
|
|
1950
|
+
* reset in the year 58600, and #258 would arm a timer on it. Outside the range,
|
|
1951
|
+
* we say we do not know — which is the honest answer and the safe one.
|
|
1952
|
+
*/
|
|
1953
|
+
export function codexResetsAt(raw) {
|
|
1954
|
+
if (typeof raw === 'string' && raw) {
|
|
1955
|
+
const parsed = Date.parse(raw);
|
|
1956
|
+
return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();
|
|
1957
|
+
}
|
|
1958
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw))
|
|
1959
|
+
return null;
|
|
1960
|
+
// 2001-09-09 … 2286-11-20 in epoch seconds.
|
|
1961
|
+
if (raw < 1_000_000_000 || raw > 9_999_999_999)
|
|
1962
|
+
return null;
|
|
1963
|
+
return new Date(raw * 1000).toISOString();
|
|
1964
|
+
}
|
|
1870
1965
|
function stringifyMcpResult(item) {
|
|
1871
1966
|
const errorMessage = str(asRecord(item['error'])['message']);
|
|
1872
1967
|
if (errorMessage)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed turn is allowed to do next — as DATA, not as branching (#252, #257).
|
|
3
|
+
*
|
|
4
|
+
* It lives beside `rate-limits.ts` and for the same reason: two CLIs describe the
|
|
5
|
+
* same class of accident in two vocabularies, a third provider is expected, and
|
|
6
|
+
* one table is easier to audit than two adapters' worth of `if`.
|
|
7
|
+
*
|
|
8
|
+
* ## How to read the table
|
|
9
|
+
*
|
|
10
|
+
* Every cause either CLI is known to report has its own row, including the ones
|
|
11
|
+
* that are never retried. Owner's instruction, 2026-08-16: the config should list
|
|
12
|
+
* them all with an explicit `retry: true | false`, so the answer to «why did the
|
|
13
|
+
* session not pick itself back up» is one line in one file rather than an absence.
|
|
14
|
+
*
|
|
15
|
+
* The dividing line is **whose fault it is**. Automatic retry is for failures that
|
|
16
|
+
* belong to the provider's own servers and have nothing to do with what the user
|
|
17
|
+
* asked for — a busy cluster, a 500, a dropped connection. Everything that depends
|
|
18
|
+
* on the account or the request — a dead key, an empty wallet, a refused policy, a
|
|
19
|
+
* context that no longer fits — is `retry: false` and stops the session, because
|
|
20
|
+
* asking again produces the same answer while burning tokens and, for policy and
|
|
21
|
+
* billing, sending a bad signal toward the account.
|
|
22
|
+
*
|
|
23
|
+
* ## The unit is the TURN, not the request
|
|
24
|
+
*
|
|
25
|
+
* The tempting table — «529 and 5xx are retryable» — is written for a single HTTP
|
|
26
|
+
* call. Ours is a turn: one turn is many calls, and by the time a 529 surfaces the
|
|
27
|
+
* agent may have run six rounds of tools, with a `git commit` and a `git push`
|
|
28
|
+
* already on disk. So a row here is a PERMISSION, never an instruction: the caller
|
|
29
|
+
* still has to ask «did this turn already do something» and narrow it. `refine()`
|
|
30
|
+
* below is that step, and it is not optional.
|
|
31
|
+
*
|
|
32
|
+
* ## Why codes and not sentences
|
|
33
|
+
*
|
|
34
|
+
* Both CLIs hand us a machine-readable cause and the runner used to throw it away.
|
|
35
|
+
* Sentences are not matched at all — see the note on `match`. We already have a
|
|
36
|
+
* scar from matching prose: `isAuthError` had to be strengthened because a bare
|
|
37
|
+
* `401` matched an MCP server's error and «unauthorized» contains «auth».
|
|
38
|
+
*
|
|
39
|
+
* ## Unknown means STOP
|
|
40
|
+
*
|
|
41
|
+
* A code that matches no row behaves exactly as it does today: the session stops
|
|
42
|
+
* and calls a human. The costs are wildly asymmetric — a wrong STOP costs one
|
|
43
|
+
* click, a wrong RETRY costs a second `git push`, a duplicated MCP write, tokens
|
|
44
|
+
* paid twice, and a transcript with duplicate tool_use ids (which is itself a 400
|
|
45
|
+
* that no retry can clear). It also makes an endless retry loop impossible by
|
|
46
|
+
* construction rather than by a counter someone might raise later.
|
|
47
|
+
*/
|
|
48
|
+
/** Verified against Claude Code CLI 2.1.233 and codex-cli 0.147.0 on 2026-08-16. */
|
|
49
|
+
export type RetryBucket =
|
|
50
|
+
/** Nothing ran. The same turn may be sent again. */
|
|
51
|
+
'retry'
|
|
52
|
+
/** Work was partly done. Resume it; never re-send the prompt. */
|
|
53
|
+
| 'continue'
|
|
54
|
+
/** A repeat cannot help. Stop and tell the person. */
|
|
55
|
+
| 'stop';
|
|
56
|
+
export type BackoffProfile = 'standard' | 'slow';
|
|
57
|
+
export interface ErrorRule {
|
|
58
|
+
/** Stable id — also the key the «same failure twice» guard compares. */
|
|
59
|
+
id: string;
|
|
60
|
+
provider: 'claude' | 'codex';
|
|
61
|
+
match: {
|
|
62
|
+
/** The provider's own machine-readable cause. */
|
|
63
|
+
codeIn?: readonly string[];
|
|
64
|
+
/** HTTP status, when the provider reported one. */
|
|
65
|
+
statusFrom?: number;
|
|
66
|
+
statusTo?: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The switch: may this cause EVER be retried automatically?
|
|
70
|
+
*
|
|
71
|
+
* `false` means today's behaviour — the session stops and asks for a person.
|
|
72
|
+
* Turning one on is a deliberate act with a reason written in `note`.
|
|
73
|
+
*/
|
|
74
|
+
retry: boolean;
|
|
75
|
+
/** Read only when `retry` is true. */
|
|
76
|
+
bucket?: Exclude<RetryBucket, 'stop'>;
|
|
77
|
+
attempts?: number;
|
|
78
|
+
backoff?: BackoffProfile;
|
|
79
|
+
/**
|
|
80
|
+
* `false` → the row is INERT: counted, never acted on.
|
|
81
|
+
*
|
|
82
|
+
* How a provider we have not run yet gets into the table safely. Gemini and the
|
|
83
|
+
* Chinese providers are expected next, and several of them answer with HTTP 200
|
|
84
|
+
* and put the failure in the body, so their rows must not go live on a guess.
|
|
85
|
+
*/
|
|
86
|
+
verified: boolean;
|
|
87
|
+
note: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The one place a provider is added, and the one place a retry is switched on.
|
|
91
|
+
*
|
|
92
|
+
* First match wins, so named causes precede status ranges.
|
|
93
|
+
*/
|
|
94
|
+
export declare const ERROR_RULES: readonly ErrorRule[];
|
|
95
|
+
export interface FailureSignal {
|
|
96
|
+
provider: 'claude' | 'codex';
|
|
97
|
+
/**
|
|
98
|
+
* The provider's machine-readable cause — `assistant.error` for Claude,
|
|
99
|
+
* `codexErrorInfo` for Codex.
|
|
100
|
+
*
|
|
101
|
+
* **A decision can only start here or at `status`.** If both are absent the
|
|
102
|
+
* answer is `stop`, no matter what the text says.
|
|
103
|
+
*/
|
|
104
|
+
code?: string | null;
|
|
105
|
+
/** HTTP status, when the provider reported one. */
|
|
106
|
+
status?: number | null;
|
|
107
|
+
/**
|
|
108
|
+
* The CLI's own sentence about the failure.
|
|
109
|
+
*
|
|
110
|
+
* Read in ONE direction: it can turn a `retry` into a `continue`, and it can
|
|
111
|
+
* never turn anything into a `retry`.
|
|
112
|
+
*/
|
|
113
|
+
text?: string | null;
|
|
114
|
+
/** Did the turn emit anything at all before it broke — text, thinking, a tool call? */
|
|
115
|
+
produced?: boolean;
|
|
116
|
+
/** Did the turn run something a repeat cannot take back — git, or a write through MCP? */
|
|
117
|
+
irreversible?: boolean;
|
|
118
|
+
}
|
|
119
|
+
export interface RetryDecision {
|
|
120
|
+
bucket: RetryBucket;
|
|
121
|
+
attempts: number;
|
|
122
|
+
backoff: BackoffProfile;
|
|
123
|
+
/** Which row decided, or `null` when nothing matched and the default applied. */
|
|
124
|
+
ruleId: string | null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What may this failure do next?
|
|
128
|
+
*
|
|
129
|
+
* First match wins. A row that is inert (`verified: false`) or switched off
|
|
130
|
+
* (`retry: false`) resolves to `stop` — but still reports its id, so the feed and
|
|
131
|
+
* the logs can say WHICH known cause stopped the session rather than «unknown».
|
|
132
|
+
*/
|
|
133
|
+
export declare function classifyFailure(signal: FailureSignal): RetryDecision;
|
|
134
|
+
/**
|
|
135
|
+
* The ceiling no row can raise: total automatic retries in one session's life.
|
|
136
|
+
*
|
|
137
|
+
* Every other guard depends on the table being right. This one does not — it is
|
|
138
|
+
* arithmetic. If a code were misclassified, if a provider started answering with
|
|
139
|
+
* a cause we read wrongly, if two mechanisms both decided to retry, the session
|
|
140
|
+
* still cannot spin: after this many attempts it stops and asks for a person,
|
|
141
|
+
* exactly as it does today.
|
|
142
|
+
*
|
|
143
|
+
* Reset when a HUMAN sends a message, on the same reasoning as `clearAutoResume`:
|
|
144
|
+
* somebody typing into the session is the clearest possible evidence that the
|
|
145
|
+
* work is on track again.
|
|
146
|
+
*/
|
|
147
|
+
export declare const MAX_RETRIES_PER_SESSION = 12;
|
|
148
|
+
/**
|
|
149
|
+
* Has this exact failure already been retried, and come straight back?
|
|
150
|
+
*
|
|
151
|
+
* Cheap insurance against a deterministic failure wearing a transient face — a
|
|
152
|
+
* context overflow surfacing as a 500, a malformed transcript that will fail
|
|
153
|
+
* identically forever. One repeat of the same row is enough to conclude the
|
|
154
|
+
* repeat is not helping, and spending the remaining attempts proves nothing.
|
|
155
|
+
*/
|
|
156
|
+
export declare function isRepeatOfSameFailure(previousRuleId: string | null | undefined, decision: RetryDecision): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* How long to wait before attempt `attempt` (1-based).
|
|
159
|
+
*
|
|
160
|
+
* standard: max(10s, U(0, min(300s, 30s · 2^(n-1))))
|
|
161
|
+
* slow: max(30s, U(0, min(600s, 60s · 2^(n-1))))
|
|
162
|
+
*
|
|
163
|
+
* Full jitter, with a floor. The floor matters: pure full jitter can collapse to
|
|
164
|
+
* nearly zero, and a zero-length wait before restarting a process is pointless —
|
|
165
|
+
* the process takes longer than that to come up.
|
|
166
|
+
*
|
|
167
|
+
* The base is tens of seconds rather than one, because our attempt is not an HTTP
|
|
168
|
+
* request: it is a process re-reading a conversation, costing seconds and input
|
|
169
|
+
* tokens. By the time we see the failure at all, the CLI has already run its own
|
|
170
|
+
* retry ladder — `api_retry` reports up to ten of them — so answering that with
|
|
171
|
+
* another try one second later would just be noise.
|
|
172
|
+
*
|
|
173
|
+
* Jitter is not decoration: one machine runs several sessions and there are many
|
|
174
|
+
* machines, all watching the same API. Without it a recovering provider gets a
|
|
175
|
+
* synchronised volley from the whole fleet.
|
|
176
|
+
*/
|
|
177
|
+
export declare function retryDelayMs(profile: BackoffProfile, attempt: number, random?: () => number): number;
|
|
178
|
+
//# sourceMappingURL=error-policy.d.ts.map
|