@bridge4dev/runner 0.42.0 → 0.44.1
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 +218 -12
- package/dist/adapters/codex.js +46 -0
- 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/supervisor.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { log } from './log.js';
|
|
4
4
|
import { claimAutoResume, clearAutoResume, pruneAutoResume } from './auto-resume.js';
|
|
5
|
+
import { classifyFailure, isRepeatOfSameFailure, MAX_RETRIES_PER_SESSION, retryDelayMs, } from './adapters/error-policy.js';
|
|
5
6
|
import { evaluateRecipeCommand, maskSecrets, maskString } from './policy.js';
|
|
6
7
|
import { agentPromptSizeLabel, inspectAgentPrompt, quotePath, readAgentPrompt, } from './agent-prompt.js';
|
|
7
8
|
import { JournalStore } from './journal.js';
|
|
@@ -480,6 +481,10 @@ export class Supervisor {
|
|
|
480
481
|
return LAUNCH_REFUSED;
|
|
481
482
|
}
|
|
482
483
|
running.lastPrompt = prompt;
|
|
484
|
+
// #252: a launch is also the start of a turn, so the retry path needs it too.
|
|
485
|
+
// Both are set here and only here they agree; `deliverMessage` moves the
|
|
486
|
+
// second one on its own afterwards.
|
|
487
|
+
running.lastTurnPrompt = prompt;
|
|
483
488
|
// Facts about this session only, plus the one file the project named. The
|
|
484
489
|
// rest of the project's documentation is read by each agent itself — see
|
|
485
490
|
// `composeWorkspaceContext`.
|
|
@@ -897,6 +902,7 @@ export class Supervisor {
|
|
|
897
902
|
// full disk, a daemon already shutting down) must not take the exit path
|
|
898
903
|
// with it, because everything below it is cleanup.
|
|
899
904
|
try {
|
|
905
|
+
this.clearApiRetry(running);
|
|
900
906
|
this.withdrawOpenQuestions(running, 'session_stopped');
|
|
901
907
|
}
|
|
902
908
|
catch (error) {
|
|
@@ -1328,6 +1334,13 @@ export class Supervisor {
|
|
|
1328
1334
|
});
|
|
1329
1335
|
return;
|
|
1330
1336
|
case 'turn_end': {
|
|
1337
|
+
// #252: decided BEFORE the frame goes out. A `turn_end{ok:false}` files
|
|
1338
|
+
// the session as FAILED, and FAILED is terminal — a session we intend to
|
|
1339
|
+
// retry must never be told it has ended. So a turn that is going to be
|
|
1340
|
+
// retried emits no `turn_end` at all; exactly one leaves this session,
|
|
1341
|
+
// when it finally succeeds or finally gives up.
|
|
1342
|
+
if (!event.ok && this.armApiRetry(running, descriptor, event))
|
|
1343
|
+
return;
|
|
1331
1344
|
this.sendEvent(running, 'turn_end', {
|
|
1332
1345
|
ok: event.ok,
|
|
1333
1346
|
errorMessage: event.errorMessage,
|
|
@@ -1813,7 +1826,138 @@ export class Supervisor {
|
|
|
1813
1826
|
* resolves them and a refusal puts exactly those records back in the queue —
|
|
1814
1827
|
* the message is retired from disk only once an agent has it.
|
|
1815
1828
|
*/
|
|
1829
|
+
/**
|
|
1830
|
+
* Should this failed turn be retried by itself, and if so, arm it (#252, #257).
|
|
1831
|
+
*
|
|
1832
|
+
* Returns `true` when a retry is armed, and the caller must then emit NOTHING —
|
|
1833
|
+
* a `turn_end{ok:false}` files the session as FAILED, which is terminal, and a
|
|
1834
|
+
* session we intend to retry cannot be told it has ended.
|
|
1835
|
+
*
|
|
1836
|
+
* The owner's standing priority governs every branch here: «лучше, чтобы ретрай
|
|
1837
|
+
* не сработал, чем сработал там, где не нужно». So this reads as a list of
|
|
1838
|
+
* reasons to decline, and the permission is the last thing it reaches.
|
|
1839
|
+
*/
|
|
1840
|
+
armApiRetry(running, descriptor, event) {
|
|
1841
|
+
// A refusal by the plan is not a failure to repeat — it is a pause measured
|
|
1842
|
+
// in hours, and the API has already armed a clock for it (#258). Retrying
|
|
1843
|
+
// into a spent limit changes nothing and would fight the pause.
|
|
1844
|
+
if (event.limitBlocked === true)
|
|
1845
|
+
return false;
|
|
1846
|
+
// The session is on its way out with a meaning of its own.
|
|
1847
|
+
if (running.stopRequested || running.budgetSpent || running.parkRequested)
|
|
1848
|
+
return false;
|
|
1849
|
+
if (Supervisor.isPaused(running))
|
|
1850
|
+
return false;
|
|
1851
|
+
// An open card means the agent asked the person something. Retrying would
|
|
1852
|
+
// orphan the card, and — worse — the suppressed `turn_end` is what clears the
|
|
1853
|
+
// API's open-ask counter, so the session would sit unanswerable.
|
|
1854
|
+
if (running.openQuestions.size > 0)
|
|
1855
|
+
return false;
|
|
1856
|
+
// The ceiling that does not depend on the table being right.
|
|
1857
|
+
if ((running.apiRetriesUsed ?? 0) >= MAX_RETRIES_PER_SESSION)
|
|
1858
|
+
return false;
|
|
1859
|
+
const decision = classifyFailure({
|
|
1860
|
+
provider: descriptor.agent === 'CODEX' ? 'codex' : 'claude',
|
|
1861
|
+
code: typeof event['failureCode'] === 'string' ? event['failureCode'] : null,
|
|
1862
|
+
status: typeof event['failureStatus'] === 'number' ? event['failureStatus'] : null,
|
|
1863
|
+
text: event.errorMessage ?? null,
|
|
1864
|
+
produced: event['produced'] === true,
|
|
1865
|
+
irreversible: event['irreversible'] === true,
|
|
1866
|
+
});
|
|
1867
|
+
if (decision.bucket === 'stop')
|
|
1868
|
+
return false;
|
|
1869
|
+
// Pinned to a local: the narrowing above does not survive into the timer's
|
|
1870
|
+
// closure, and widening it back to `RetryBucket` there would let a `stop`
|
|
1871
|
+
// through as if it were a permission.
|
|
1872
|
+
const bucket = decision.bucket;
|
|
1873
|
+
// The same failure came straight back. Spending the rest of the allowance on
|
|
1874
|
+
// it proves nothing — it is a deterministic fault wearing a transient face.
|
|
1875
|
+
const previous = running.apiRetry?.ruleId ?? null;
|
|
1876
|
+
const attempt = (running.apiRetry?.attempt ?? 0) + 1;
|
|
1877
|
+
if (attempt > 1 && isRepeatOfSameFailure(previous, decision)) {
|
|
1878
|
+
this.clearApiRetry(running);
|
|
1879
|
+
this.sendEvent(running, 'system_note', {
|
|
1880
|
+
code: 'agent_retry',
|
|
1881
|
+
text: 'The same failure came back after an automatic retry — stopping so a person can look.',
|
|
1882
|
+
});
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
if (attempt > decision.attempts) {
|
|
1886
|
+
this.clearApiRetry(running);
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
const delay = retryDelayMs(decision.backoff, attempt);
|
|
1890
|
+
const at = new Date(Date.now() + delay);
|
|
1891
|
+
const timer = setTimeout(() => {
|
|
1892
|
+
this.runApiRetry(running, bucket);
|
|
1893
|
+
}, delay);
|
|
1894
|
+
timer.unref();
|
|
1895
|
+
running.apiRetry = { timer, attempt, ruleId: decision.ruleId };
|
|
1896
|
+
running.apiRetriesUsed = (running.apiRetriesUsed ?? 0) + 1;
|
|
1897
|
+
// Supervisor-minted rather than an adapter notice: adapter notices are
|
|
1898
|
+
// de-duplicated for the session's whole life, so «attempt 2» would appear
|
|
1899
|
+
// once and never again (gotcha #148).
|
|
1900
|
+
this.sendEvent(running, 'system_note', {
|
|
1901
|
+
code: 'agent_retry',
|
|
1902
|
+
text: `${bucket === 'continue' ? 'The answer was cut off part-way' : 'The provider could not be reached'} — ` +
|
|
1903
|
+
`attempt ${attempt} of ${decision.attempts}, next at ${at.toISOString().slice(11, 16)} UTC. ` +
|
|
1904
|
+
'This is a fault on the provider’s side, not in the task.',
|
|
1905
|
+
});
|
|
1906
|
+
this.reportStatus(descriptor.id, 'RUNNING', {});
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
/** Fire an armed retry — re-checking, at FIRE time, everything that could have changed. */
|
|
1910
|
+
runApiRetry(running, bucket) {
|
|
1911
|
+
const armed = running.apiRetry;
|
|
1912
|
+
if (!armed)
|
|
1913
|
+
return;
|
|
1914
|
+
// Everything below changed while we waited, and every one of them means the
|
|
1915
|
+
// retry must not happen. Checked here rather than only at arm time: the wait
|
|
1916
|
+
// is minutes long, which is plenty of time for a person to press Stop, for a
|
|
1917
|
+
// budget to run out, or for a limit pause to start.
|
|
1918
|
+
if (running.stopRequested ||
|
|
1919
|
+
running.budgetSpent ||
|
|
1920
|
+
running.parkRequested ||
|
|
1921
|
+
Supervisor.isPaused(running) ||
|
|
1922
|
+
running.openQuestions.size > 0) {
|
|
1923
|
+
this.clearApiRetry(running);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (!running.session) {
|
|
1927
|
+
// The process died while we waited. Relaunching is `launchAgent`'s job and
|
|
1928
|
+
// it needs a prompt; without one there is nothing honest to do here.
|
|
1929
|
+
this.clearApiRetry(running);
|
|
1930
|
+
this.reportStatus(running.descriptor.id, 'FAILED', {
|
|
1931
|
+
errorMessage: 'The agent process ended while waiting to retry',
|
|
1932
|
+
});
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
// Never a re-send of the prompt when work was already done: `continue` asks
|
|
1936
|
+
// the agent to look at what it did and carry on, which is exactly what the
|
|
1937
|
+
// owner used to type by hand. `auto-resume.ts` records the cost of getting
|
|
1938
|
+
// this backwards — a re-sent prompt ran a fifteen-minute command twice.
|
|
1939
|
+
const text = bucket === 'continue'
|
|
1940
|
+
? 'The connection to the model dropped part-way through your previous answer — not by ' +
|
|
1941
|
+
'anything you did, and nothing is wrong with the work. Check what you had already ' +
|
|
1942
|
+
'finished before redoing any of it, then continue from where you stopped.'
|
|
1943
|
+
: (running.lastTurnPrompt ?? 'Continue.');
|
|
1944
|
+
running.session.send(text);
|
|
1945
|
+
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
1946
|
+
}
|
|
1947
|
+
/** Disarm a pending retry — always through here, so no timer is ever orphaned. */
|
|
1948
|
+
clearApiRetry(running) {
|
|
1949
|
+
if (!running.apiRetry)
|
|
1950
|
+
return;
|
|
1951
|
+
clearTimeout(running.apiRetry.timer);
|
|
1952
|
+
delete running.apiRetry;
|
|
1953
|
+
}
|
|
1816
1954
|
deliverMessage(running, text, held = [], originSeq) {
|
|
1955
|
+
// #252: a person typing is the clearest possible evidence that the work is
|
|
1956
|
+
// back on track — the same reasoning `clearAutoResume` is built on. Their
|
|
1957
|
+
// words also supersede whatever we were about to retry, so any armed timer
|
|
1958
|
+
// is disarmed rather than left to fire into a conversation that has moved on.
|
|
1959
|
+
this.clearApiRetry(running);
|
|
1960
|
+
running.apiRetriesUsed = 0;
|
|
1817
1961
|
// Older instructions are already waiting: send them together, in order.
|
|
1818
1962
|
// Without this a message that CAN go now jumps the queue — and the ones it
|
|
1819
1963
|
// jumped are stranded, because the drain only runs when a process exits
|
|
@@ -1844,6 +1988,11 @@ export class Supervisor {
|
|
|
1844
1988
|
}
|
|
1845
1989
|
};
|
|
1846
1990
|
if (running.session && !running.parkRequested) {
|
|
1991
|
+
// #252: the words this turn is actually running. `lastPrompt` deliberately
|
|
1992
|
+
// stays put — three older latches relaunch a PROCESS with it, and handing
|
|
1993
|
+
// them a follow-up line instead of the task would start a fresh
|
|
1994
|
+
// conversation with «продолжай».
|
|
1995
|
+
running.lastTurnPrompt = text;
|
|
1847
1996
|
running.session.send(text);
|
|
1848
1997
|
settle();
|
|
1849
1998
|
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
@@ -2281,6 +2430,7 @@ export class Supervisor {
|
|
|
2281
2430
|
running.session.stop('session_stopped'); // pumpEvents finishes the cleanup
|
|
2282
2431
|
}
|
|
2283
2432
|
else {
|
|
2433
|
+
this.clearApiRetry(running);
|
|
2284
2434
|
this.withdrawOpenQuestions(running, 'session_stopped');
|
|
2285
2435
|
this.reportStatus(sessionId, 'STOPPED', { activeMs: running.activeMs });
|
|
2286
2436
|
this.sessions.delete(sessionId);
|
|
@@ -2318,6 +2468,7 @@ export class Supervisor {
|
|
|
2318
2468
|
running.session.stop('session_stopped'); // pumpEvents finishes the cleanup
|
|
2319
2469
|
}
|
|
2320
2470
|
else {
|
|
2471
|
+
this.clearApiRetry(running);
|
|
2321
2472
|
this.withdrawOpenQuestions(running, 'session_stopped');
|
|
2322
2473
|
this.sessions.delete(sessionId);
|
|
2323
2474
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.44.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED