@commonlyai/cli 0.1.9 → 0.1.10
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/package.json +1 -1
- package/src/commands/agent.js +82 -12
- package/src/lib/adapters/claude.js +13 -2
- package/src/lib/enforcement.js +30 -9
- package/src/lib/environment.js +24 -1
- package/src/lib/spawn-retry.js +31 -5
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -710,6 +710,7 @@ export const performRun = ({
|
|
|
710
710
|
retryJitterRatio,
|
|
711
711
|
claimLeaseSeconds = 90,
|
|
712
712
|
cascadeCap = 3,
|
|
713
|
+
cascadeAddressedGrace = 2,
|
|
713
714
|
cascadeResetMs = 10 * 60 * 1000,
|
|
714
715
|
chatCharLimit = 400,
|
|
715
716
|
maxChatChunks = 3,
|
|
@@ -729,7 +730,11 @@ export const performRun = ({
|
|
|
729
730
|
const spawnJitterRatio = retryJitterRatio ?? spawnRetryJitter(agentName);
|
|
730
731
|
// Per-seat cascade state — lives with the process, like the session store.
|
|
731
732
|
// A wrapper restart forgets the streak; the decay window covers that gap.
|
|
732
|
-
const cascadeGovernor = createCascadeGovernor({
|
|
733
|
+
const cascadeGovernor = createCascadeGovernor({
|
|
734
|
+
cap: cascadeCap,
|
|
735
|
+
addressedGrace: cascadeAddressedGrace,
|
|
736
|
+
resetMs: cascadeResetMs,
|
|
737
|
+
});
|
|
733
738
|
// Fairness: recent broadcast-race winners start the next race from the back.
|
|
734
739
|
const claimHandicap = createClaimHandicap({ delayMs: claimYieldDelayMs });
|
|
735
740
|
|
|
@@ -798,11 +803,13 @@ export const performRun = ({
|
|
|
798
803
|
// capped agent-triggered event is exactly the traffic we want dropped.
|
|
799
804
|
// Human-triggered turns are never capped.
|
|
800
805
|
const trigger = classifyTrigger(event, preSpawn);
|
|
801
|
-
const admission = cascadeGovernor.admit(eventPodId, trigger);
|
|
806
|
+
const admission = cascadeGovernor.admit(eventPodId, trigger, event.type);
|
|
802
807
|
if (!admission.allowed) {
|
|
803
808
|
log(
|
|
804
809
|
`[${event.type}] cascade cap: ${admission.streak} consecutive agent-triggered `
|
|
805
|
-
+ `turns in pod ${eventPodId}
|
|
810
|
+
+ `turns in pod ${eventPodId}`
|
|
811
|
+
+ (admission.addressed ? ' (addressed grace also spent)' : '')
|
|
812
|
+
+ ' — standing down until a human speaks or the pod goes quiet for the reset window',
|
|
806
813
|
);
|
|
807
814
|
return { outcome: 'no_action', reason: 'cascade-cap' };
|
|
808
815
|
}
|
|
@@ -1082,8 +1089,26 @@ export const performRun = ({
|
|
|
1082
1089
|
circuitOpen: retry.circuitOpen,
|
|
1083
1090
|
eventId: event._id,
|
|
1084
1091
|
});
|
|
1085
|
-
|
|
1086
|
-
|
|
1092
|
+
// ONE emission, not two. `wrapped.message` already opens with the
|
|
1093
|
+
// event type, so the log copy added a second prefix and a second
|
|
1094
|
+
// line of identical text into the same merged stream — enough that
|
|
1095
|
+
// `grep -c 'session limit'` returned 8 for 4 failures, and a reader
|
|
1096
|
+
// counting hits per event id saw 2 and inferred two deliveries.
|
|
1097
|
+
// On 2026-08-18 that count pointed at the wrong cause for #993:
|
|
1098
|
+
// two deliveries per event puts `attempts` at 2 and the requeue cap
|
|
1099
|
+
// one step away. It did not decide anything — the cap was ruled out
|
|
1100
|
+
// from the DB, where a capped event would have left a `failed` row
|
|
1101
|
+
// and none existed — but it corroborated the wrong theory, and the
|
|
1102
|
+
// doubling had to be spotted by hand before the count could be
|
|
1103
|
+
// discounted. A log that inflates is worse than one that is silent,
|
|
1104
|
+
// because it argues.
|
|
1105
|
+
//
|
|
1106
|
+
// Routed to the error channel when a caller provides one, and to
|
|
1107
|
+
// the log when it doesn't, so neither contract loses the failure:
|
|
1108
|
+
// an embedder that passes no `onError` still sees it, and `agent
|
|
1109
|
+
// run` — which always passes one — stops printing it twice.
|
|
1110
|
+
if (onError) onError(wrapped);
|
|
1111
|
+
else log(`[${event.type}] ${wrapped.message}`);
|
|
1087
1112
|
break;
|
|
1088
1113
|
}
|
|
1089
1114
|
// Only a completed model turn proves the local runtime and delivery
|
|
@@ -1300,6 +1325,24 @@ export const performDetach = async ({
|
|
|
1300
1325
|
return { backend: backendResult, localCleaned: true };
|
|
1301
1326
|
};
|
|
1302
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* Every line a seat emits lands in one file, because the fleet is launched as
|
|
1330
|
+
* `nohup commonly agent run <name> > <log> 2>&1`. Until now none of those lines
|
|
1331
|
+
* carried a time.
|
|
1332
|
+
*
|
|
1333
|
+
* That is not a cosmetic gap. On 2026-08-18 a fleet-wide quota stall destroyed
|
|
1334
|
+
* 38 queued events (#993), and the seat log was the ONLY surviving trace —
|
|
1335
|
+
* the kernel rows were deleted, so nothing else recorded that those turns had
|
|
1336
|
+
* been attempted. The log records `(4 consecutive)` and `next probe in 2.0m`
|
|
1337
|
+
* and gives no way to place either on a clock: the investigation had to date
|
|
1338
|
+
* events by decoding ObjectId prefixes instead, because the file that named
|
|
1339
|
+
* them could not say when.
|
|
1340
|
+
*
|
|
1341
|
+
* ISO-8601 so stamps sort lexically, diff cleanly, and line up with the
|
|
1342
|
+
* kernel's own timestamps without conversion.
|
|
1343
|
+
*/
|
|
1344
|
+
const stamp = () => new Date().toISOString();
|
|
1345
|
+
|
|
1303
1346
|
export const registerAgent = (program) => {
|
|
1304
1347
|
const agent = program.command('agent').description('Manage agents');
|
|
1305
1348
|
|
|
@@ -1649,15 +1692,15 @@ Docs:
|
|
|
1649
1692
|
record = await bootstrapAgentRecordFromEnv({
|
|
1650
1693
|
name,
|
|
1651
1694
|
adapterOverride: opts.adapter || null,
|
|
1652
|
-
log: (line) => console.log(
|
|
1695
|
+
log: (line) => console.log(`${stamp()} [${name}] ${line}`),
|
|
1653
1696
|
});
|
|
1654
1697
|
} catch (err) {
|
|
1655
|
-
console.error(err.message);
|
|
1698
|
+
console.error(`${stamp()} [${name}] ${err.message}`);
|
|
1656
1699
|
process.exit(1);
|
|
1657
1700
|
}
|
|
1658
1701
|
if (record) {
|
|
1659
1702
|
saveAgentToken(record.agentName, record);
|
|
1660
|
-
console.log(
|
|
1703
|
+
console.log(`${stamp()} [${name}] bootstrapped ${tokenFile(record.agentName)} from COMMONLY_AGENT_TOKEN (adapter: ${record.adapter})`);
|
|
1661
1704
|
} else {
|
|
1662
1705
|
console.error(
|
|
1663
1706
|
`No token for '${name}'. Either export COMMONLY_API_URL + COMMONLY_AGENT_TOKEN`
|
|
@@ -1674,7 +1717,14 @@ Docs:
|
|
|
1674
1717
|
process.exit(1);
|
|
1675
1718
|
}
|
|
1676
1719
|
|
|
1677
|
-
|
|
1720
|
+
// THE line to stamp, not just one of them. It is the first line of every
|
|
1721
|
+
// seat log and the truncation boundary — the fleet is launched as
|
|
1722
|
+
// `nohup … > log 2>&1`, so this banner is written at boot and everything
|
|
1723
|
+
// before it is gone. Stamped, it dates the restart from the log itself.
|
|
1724
|
+
// Unstamped, it took `ps -o lstart` to establish when nine seats came
|
|
1725
|
+
// back on 2026-08-18, and that only worked because the processes were
|
|
1726
|
+
// still alive — after the next restart that route is gone too.
|
|
1727
|
+
console.log(`${stamp()} [${name}] polling ${record.instanceUrl} for events (ctrl+c to stop)`);
|
|
1678
1728
|
|
|
1679
1729
|
const { stop } = performRun({
|
|
1680
1730
|
instanceUrl: record.instanceUrl,
|
|
@@ -1686,12 +1736,32 @@ Docs:
|
|
|
1686
1736
|
environment: record.environment || null,
|
|
1687
1737
|
workspacePath: record.workspacePath || null,
|
|
1688
1738
|
intervalMs: parseInt(opts.interval, 10),
|
|
1689
|
-
log: (line) => console.log(
|
|
1690
|
-
|
|
1739
|
+
log: (line) => console.log(`${stamp()} [${name}] ${line}`),
|
|
1740
|
+
// Both sinks are stamped, and both must be — but not for the reason
|
|
1741
|
+
// this comment gave until now, which its own PR falsified.
|
|
1742
|
+
//
|
|
1743
|
+
// A spawn failure is routed to `onError` when a caller provides one and
|
|
1744
|
+
// to `log` only as the fallback, so in `agent run` — which always passes
|
|
1745
|
+
// one — EVERY failure line comes out of the error channel and none out
|
|
1746
|
+
// of the log. Stamping only `log:` would leave the entire failure class
|
|
1747
|
+
// undated, which is the class these stamps exist for.
|
|
1748
|
+
//
|
|
1749
|
+
// The two sinks also carry genuinely different text elsewhere: the
|
|
1750
|
+
// no-prompt skip logs a terse line and sends a detailed diagnostic, so
|
|
1751
|
+
// one stamped and one bare would date half of that pair.
|
|
1752
|
+
//
|
|
1753
|
+
// (History, because the reasoning moved twice. This originally argued
|
|
1754
|
+
// from a DOUBLE emission — the retry path called both sinks with
|
|
1755
|
+
// identical text, so `grep -c 'session limit'` returned 8 for 4 failures
|
|
1756
|
+
// and a per-event count read as two deliveries. That was true when the
|
|
1757
|
+
// stamps landed and false eleven minutes later, when the same PR
|
|
1758
|
+
// collapsed the duplication. The conclusion survived the premise; the
|
|
1759
|
+
// sentence did not, and nothing in the diff pointed at it.)
|
|
1760
|
+
onError: (err) => console.error(`${stamp()} [${name}] ${err.message}`),
|
|
1691
1761
|
});
|
|
1692
1762
|
|
|
1693
1763
|
process.on('SIGINT', () => {
|
|
1694
|
-
console.log(`\n[${name}] stopping...`);
|
|
1764
|
+
console.log(`\n${stamp()} [${name}] stopping...`);
|
|
1695
1765
|
stop();
|
|
1696
1766
|
process.exit(0);
|
|
1697
1767
|
});
|
|
@@ -474,7 +474,18 @@ export default {
|
|
|
474
474
|
const sessionId = ctx.sessionId || randomUUID();
|
|
475
475
|
const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm || '');
|
|
476
476
|
const sessionFlag = isResume ? '--resume' : '--session-id';
|
|
477
|
-
|
|
477
|
+
// Model pin from the ADR-008 environment spec. Absent it, claude picks its
|
|
478
|
+
// own default — which is how a fleet of ten agents ended up running three
|
|
479
|
+
// different Opus versions that nobody chose and nothing recorded, with one
|
|
480
|
+
// seat named after a model it does not run.
|
|
481
|
+
//
|
|
482
|
+
// Computed ONCE and spread into BOTH arg builders. The session-recovery
|
|
483
|
+
// path at the bottom of this function constructs its own array, and a model
|
|
484
|
+
// present in one but not the other means a retry silently runs a different
|
|
485
|
+
// model than the turn it is replacing — the same drifting-copy shape that
|
|
486
|
+
// has bitten this codebase repeatedly.
|
|
487
|
+
const modelArgs = ctx.environment?.model ? ['--model', String(ctx.environment.model)] : [];
|
|
488
|
+
const baseArgs = ['-p', fullPrompt, '--output-format', 'text', sessionFlag, sessionId, ...modelArgs];
|
|
478
489
|
|
|
479
490
|
if (ctx.environment && ctx.cwd) {
|
|
480
491
|
const skills = await mountSkills(ctx.environment, ctx.cwd);
|
|
@@ -529,7 +540,7 @@ export default {
|
|
|
529
540
|
// session id poisons every subsequent event re-delivery.
|
|
530
541
|
if (isResume && /already in use|no conversation|no session/i.test(String(err.message))) {
|
|
531
542
|
const freshId = randomUUID();
|
|
532
|
-
const retryBase = ['-p', fullPrompt, '--output-format', 'text', '--session-id', freshId];
|
|
543
|
+
const retryBase = ['-p', fullPrompt, '--output-format', 'text', '--session-id', freshId, ...modelArgs];
|
|
533
544
|
const retry = await prepareArgv(retryBase, {
|
|
534
545
|
...ctx,
|
|
535
546
|
mcpConfigPath: mcpConfig?.file || null,
|
package/src/lib/enforcement.js
CHANGED
|
@@ -55,6 +55,13 @@ export const classifyTrigger = (event, recentMessages) => {
|
|
|
55
55
|
return trigger.isBot ? 'agent' : 'human';
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// Direct-address event types: the seat was NAMED (explicit @, implicit human
|
|
59
|
+
// reply, or DM routing). A lost claim on these does not silence the seat —
|
|
60
|
+
// being chosen by a human outranks being beaten to a CAS. Broadcast wakes
|
|
61
|
+
// (message.posted) are the opposite: nobody asked for THIS seat, so a lost
|
|
62
|
+
// race is a free stand-down.
|
|
63
|
+
export const ADDRESSED_EVENT_TYPES = new Set(['chat.mention', 'thread.mention', 'dm.message']);
|
|
64
|
+
|
|
58
65
|
// ── cascade governor ────────────────────────────────────────────────────────
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -68,12 +75,19 @@ export const classifyTrigger = (event, recentMessages) => {
|
|
|
68
75
|
* pod recovers on its own — a legitimate a2a handoff an hour later must not
|
|
69
76
|
* inherit a stale cap).
|
|
70
77
|
*
|
|
78
|
+
* NOTE on `resetMs`: it is a SILENCE window, not a decay. It requires
|
|
79
|
+
* `resetMs` with ZERO agent-triggered turns in the pod. Three chatty seats
|
|
80
|
+
* never leave a ten-minute hole, so in a busy room the only live release is a
|
|
81
|
+
* human turn. Say so plainly rather than letting "or the streak decays" imply
|
|
82
|
+
* a timer that will save you.
|
|
83
|
+
*
|
|
71
84
|
* Split into admit/record so a spawn that fails (and will be redelivered)
|
|
72
85
|
* never double-counts: admit() only reads, record() runs after a turn
|
|
73
86
|
* actually completed. Human-triggered turns are always admitted.
|
|
74
87
|
*/
|
|
75
88
|
export const createCascadeGovernor = ({
|
|
76
89
|
cap = 3,
|
|
90
|
+
addressedGrace = 2,
|
|
77
91
|
resetMs = 10 * 60 * 1000,
|
|
78
92
|
now = Date.now,
|
|
79
93
|
} = {}) => {
|
|
@@ -88,10 +102,23 @@ export const createCascadeGovernor = ({
|
|
|
88
102
|
};
|
|
89
103
|
|
|
90
104
|
return {
|
|
91
|
-
admit(podId, trigger) {
|
|
92
|
-
if (trigger !== 'agent') return { allowed: true, streak: 0 };
|
|
105
|
+
admit(podId, trigger, eventType) {
|
|
106
|
+
if (trigger !== 'agent') return { allowed: true, streak: 0, addressed: false };
|
|
93
107
|
const s = stateFor(podId);
|
|
94
|
-
|
|
108
|
+
// Being NAMED outranks a mechanical brake — the same judgement the claim
|
|
109
|
+
// path already makes forty lines down in agent.js. Without this, a peer
|
|
110
|
+
// can @mention a capped seat and get silence, with no signal to either
|
|
111
|
+
// side that anything was suppressed. Observed 2026-08-18: one seat took
|
|
112
|
+
// 51 wakes and 28 consecutive cap refusals, five of them chat.mention,
|
|
113
|
+
// and answered none of them.
|
|
114
|
+
//
|
|
115
|
+
// A GRACE, not an exemption: an unbounded pass would restore the exact
|
|
116
|
+
// A-mentions-B-mentions-A echo this governor exists to kill. Addressed
|
|
117
|
+
// turns still count toward the streak, so a mention loop terminates at
|
|
118
|
+
// cap + addressedGrace instead of never.
|
|
119
|
+
const addressed = ADDRESSED_EVENT_TYPES.has(eventType);
|
|
120
|
+
const limit = addressed ? cap + addressedGrace : cap;
|
|
121
|
+
return { allowed: s.streak < limit, streak: s.streak, addressed };
|
|
95
122
|
},
|
|
96
123
|
record(podId, trigger) {
|
|
97
124
|
if (trigger === 'human') {
|
|
@@ -107,12 +134,6 @@ export const createCascadeGovernor = ({
|
|
|
107
134
|
|
|
108
135
|
// ── claim fairness ──────────────────────────────────────────────────────────
|
|
109
136
|
|
|
110
|
-
// Direct-address event types: the seat was NAMED (explicit @, implicit human
|
|
111
|
-
// reply, or DM routing). A lost claim on these does not silence the seat —
|
|
112
|
-
// being chosen by a human outranks being beaten to a CAS. Broadcast wakes
|
|
113
|
-
// (message.posted) are the opposite: nobody asked for THIS seat, so a lost
|
|
114
|
-
// race is a free stand-down.
|
|
115
|
-
export const ADDRESSED_EVENT_TYPES = new Set(['chat.mention', 'thread.mention', 'dm.message']);
|
|
116
137
|
|
|
117
138
|
// Frame prepended when an ADDRESSED seat lost the claim race: it still gets
|
|
118
139
|
// its turn, but knows a peer is (probably) already answering — the bar for
|
package/src/lib/environment.js
CHANGED
|
@@ -29,8 +29,19 @@ import { homedir } from 'os';
|
|
|
29
29
|
// exposes `$HOME` layout for zero server-side benefit. Callers pass
|
|
30
30
|
// `envFileDir` as a separate argument to resolveWorkspace / mountSkills.
|
|
31
31
|
|
|
32
|
+
// `model` closes issue #774. Before it, the only way to pin a wrapper's model
|
|
33
|
+
// was `ANTHROPIC_MODEL` exported at `commonly agent run` time — per-process,
|
|
34
|
+
// never persisted, and silently dropped by any restart. Measured consequence on
|
|
35
|
+
// 2026-08-16: six of nine live seats had lost their assigned model, including
|
|
36
|
+
// ux-lead, which was supposed to be on Fable and had been running the CLI
|
|
37
|
+
// default for days with nothing recording the drift.
|
|
38
|
+
//
|
|
39
|
+
// Putting it in the env spec makes the model a persisted, server-side fact that
|
|
40
|
+
// survives restarts and is readable by the platform — which is what ADR-022's
|
|
41
|
+
// "persona and runtime are chosen separately" requires to mean anything for a
|
|
42
|
+
// BYO seat, and what lets an identity card answer "what is this running".
|
|
32
43
|
const ALLOWED_TOP_KEYS = new Set([
|
|
33
|
-
'version', 'workspace', 'sandbox', 'skills', 'mcp',
|
|
44
|
+
'version', 'workspace', 'sandbox', 'skills', 'mcp', 'model',
|
|
34
45
|
]);
|
|
35
46
|
const ALLOWED_SANDBOX_MODES = new Set([
|
|
36
47
|
'none', 'workspace', 'read-only', 'bwrap', 'firejail', 'container', 'managed',
|
|
@@ -112,6 +123,18 @@ export const validateEnvironmentSpec = (spec) => {
|
|
|
112
123
|
errors.push(`version must be 1, got ${JSON.stringify(spec.version)}`);
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
// Validated as an opaque non-empty string, deliberately not against a list of
|
|
127
|
+
// known model ids. A whitelist here would need editing every time a provider
|
|
128
|
+
// ships a model, and would reject a valid id the local CLI understands and
|
|
129
|
+
// this file does not — failing the user's attach for a fact it is not the
|
|
130
|
+
// authority on. The adapter passes it through to `--model`; the CLI is the
|
|
131
|
+
// thing that knows what is valid, and its error is the honest one.
|
|
132
|
+
if (spec.model !== undefined) {
|
|
133
|
+
if (typeof spec.model !== 'string' || spec.model.trim() === '') {
|
|
134
|
+
errors.push('model must be a non-empty string');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
if (spec.workspace !== undefined) {
|
|
116
139
|
if (typeof spec.workspace !== 'object' || spec.workspace === null) {
|
|
117
140
|
errors.push('workspace must be an object');
|
package/src/lib/spawn-retry.js
CHANGED
|
@@ -19,11 +19,37 @@ export const SPAWN_CIRCUIT_THRESHOLD = 3;
|
|
|
19
19
|
export const SPAWN_RETRY_MAX_MS = 15 * 60 * 1000;
|
|
20
20
|
export const SPAWN_RETRY_JITTER_MAX_RATIO = 0.2;
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
22
|
+
// This list is a per-provider allowlist, and it only ever grows after an
|
|
23
|
+
// outage has already been misclassified. Twice now:
|
|
24
|
+
// 2026-08-03 codex "Your workspace is out of credits." → `out of credits`
|
|
25
|
+
// 2026-08-18 claude "You've hit your session limit" → `session limit`
|
|
26
|
+
// The second one is the instructive failure: `usage limit` was already here —
|
|
27
|
+
// it is Claude's OTHER exhaustion wording — so the fleet stalled for an hour on
|
|
28
|
+
// a string one word away from a pattern we had. Both times the miss meant
|
|
29
|
+
// RUNTIME, the weakest class with the shortest backoff, against a provider that
|
|
30
|
+
// was not going to answer for hours.
|
|
31
|
+
//
|
|
32
|
+
// What a miss costs, measured against these constants (intervalMs 5000,
|
|
33
|
+
// jitter 0; `*` = circuit open):
|
|
34
|
+
//
|
|
35
|
+
// quota n=1,2,3 -> 900s* 900s* 900s*
|
|
36
|
+
// configuration n=1,2,3 -> 900s* 900s* 900s*
|
|
37
|
+
// rate_limit n=1,2,3 -> 60s* 120s* 240s*
|
|
38
|
+
// runtime n=1,2,3 -> 5s 10s 60s*
|
|
39
|
+
//
|
|
40
|
+
// So an unmatched wording is not one class off — it is 180x faster on the
|
|
41
|
+
// first retry than the class it belonged in, and the only class that does not
|
|
42
|
+
// open the circuit at n=1. That is the price of a missing string, and it is
|
|
43
|
+
// why the entry below is a list of exact wordings rather than a loose pattern.
|
|
44
|
+
//
|
|
45
|
+
// Note RUNTIME is also `classifySpawnFailure`'s fallthrough, so "unrecognised"
|
|
46
|
+
// and "transient local fault" resolve to the same, most aggressive schedule.
|
|
47
|
+
// That is a structural issue rather than a vocabulary one — see #996.
|
|
48
|
+
//
|
|
49
|
+
// Deliberately NOT loosened to a bare `limit`: QUOTA is tested before
|
|
50
|
+
// RATE_LIMIT, so that would swallow every "rate limit" error into the 15-minute
|
|
51
|
+
// cooldown. Add exact wordings, not looser ones.
|
|
52
|
+
const QUOTA_RE = /(?:quota|usage limit|session limit|credit balance|out of credits|billing|insufficient[_ -]?quota|resource exhausted|spending limit)/i;
|
|
27
53
|
const RATE_LIMIT_RE = /(?:rate[ -]?limit|too many requests|\b429\b|overloaded|capacity)/i;
|
|
28
54
|
const CONFIGURATION_RE = /(?:ENOENT|command not found|not on PATH|login required|not logged in|invalid api key|authentication failed|unauthori[sz]ed|forbidden|\b40[13]\b)/i;
|
|
29
55
|
|