flowviant 0.9.1 → 0.10.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/bin/lib/config.mjs +1 -1
- package/bin/lib/live.mjs +77 -1
- package/package.json +1 -1
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
export const VERSION = '0.
|
|
7
|
+
export const VERSION = '0.10.0';
|
|
8
8
|
|
|
9
9
|
// Credential stored by `flowviant login` (device auth) — the no-token,
|
|
10
10
|
// no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
|
package/bin/lib/live.mjs
CHANGED
|
@@ -255,6 +255,52 @@ async function waitForMessage(mcpUrl, token, runId, afterId, isAlive) {
|
|
|
255
255
|
|
|
256
256
|
// One task: claim → seed → stream/mirror/inject/park → complete. Returns
|
|
257
257
|
// { outcome: 'nothing' | 'done' | 'blocked' | 'stalled' | 'error' }.
|
|
258
|
+
// Distinguish "YOUR OWN Claude account is out of quota" (the user must wait or
|
|
259
|
+
// hand off — a park) from a transient Anthropic-side hiccup (retry soon — a
|
|
260
|
+
// plain error). Only the former parks. resetAt is lifted from the limit
|
|
261
|
+
// response's retry headers when present, so the thread can say when it's back.
|
|
262
|
+
export function classifyRateLimit(e) {
|
|
263
|
+
const status = e?.status ?? e?.statusCode ?? e?.response?.status;
|
|
264
|
+
const msg = String(e?.message ?? e ?? '').toLowerCase();
|
|
265
|
+
const overloaded = status === 529 || msg.includes('overloaded');
|
|
266
|
+
const isRateLimit =
|
|
267
|
+
!overloaded &&
|
|
268
|
+
(status === 429 ||
|
|
269
|
+
/rate.?limit|usage limit|quota|too many requests|exceeded your|reached your|limit reached/.test(
|
|
270
|
+
msg,
|
|
271
|
+
));
|
|
272
|
+
if (!isRateLimit) return { isRateLimit: false };
|
|
273
|
+
let resetAt;
|
|
274
|
+
const hdrs = e?.headers ?? e?.response?.headers;
|
|
275
|
+
const get = (k) => hdrs?.get?.(k) ?? hdrs?.[k];
|
|
276
|
+
const retryAfter = Number(get?.('retry-after'));
|
|
277
|
+
const resetHdr = get?.('anthropic-ratelimit-unified-reset');
|
|
278
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
279
|
+
resetAt = new Date(Date.now() + retryAfter * 1000).toISOString();
|
|
280
|
+
} else if (resetHdr != null) {
|
|
281
|
+
const epoch = Number(resetHdr);
|
|
282
|
+
if (Number.isFinite(epoch) && epoch > 0) resetAt = new Date(epoch * 1000).toISOString();
|
|
283
|
+
else if (!Number.isNaN(Date.parse(resetHdr))) resetAt = new Date(resetHdr).toISOString();
|
|
284
|
+
}
|
|
285
|
+
return { isRateLimit: true, resetAt };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Wait out a Claude-account limit, heartbeating so the 30-min lease stays warm
|
|
289
|
+
// and the task isn't reclaimed while paused. Caps the wait so an unknown or very
|
|
290
|
+
// distant reset still retries eventually (and re-parks if still limited).
|
|
291
|
+
async function parkUntilReset(resetAt, { mcpUrl, getToken, runId, isAlive }) {
|
|
292
|
+
const MAX_PARK_MS = 60 * 60 * 1000; // never sit longer than an hour before retrying
|
|
293
|
+
const DEFAULT_PARK_MS = 15 * 60 * 1000; // no reset given → try again in 15 min
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
const target = resetAt ? Date.parse(resetAt) : now + DEFAULT_PARK_MS;
|
|
296
|
+
const until = Math.min(Number.isFinite(target) ? target : now + DEFAULT_PARK_MS, now + MAX_PARK_MS);
|
|
297
|
+
while (isAlive() && Date.now() < until) {
|
|
298
|
+
const token = getToken();
|
|
299
|
+
if (token) await mcpCall(mcpUrl, token, 'heartbeat', { runId }).catch(() => {});
|
|
300
|
+
await sleep(IDLE_SECONDS);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
258
304
|
export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive, resumeIntentId, onChild }) {
|
|
259
305
|
const claim = await mcpCall(mcpUrl, token, 'claim_next_intent', {}).catch(() => null);
|
|
260
306
|
if (!claim || claim.claimed !== true) return { outcome: 'nothing' };
|
|
@@ -493,6 +539,14 @@ export async function runLiveTask({ mcpUrl, token, cwd, baseRef, isAlive, resume
|
|
|
493
539
|
}
|
|
494
540
|
return { outcome: completed ? 'done' : 'stalled', title, intentId };
|
|
495
541
|
} catch (e) {
|
|
542
|
+
const rl = classifyRateLimit(e);
|
|
543
|
+
if (rl.isRateLimit) {
|
|
544
|
+
// The user's OWN Claude account is tapped. Park the run so the thread shows
|
|
545
|
+
// it as their plan's limit (not a Flowviant error) and the lease stays warm
|
|
546
|
+
// for a resume in place — never reset this worktree's work.
|
|
547
|
+
await mcpCall(mcpUrl, token, 'report_paused', { runId, resetAt: rl.resetAt }).catch(() => {});
|
|
548
|
+
return { outcome: 'rate_limited', resetAt: rl.resetAt, runId, title, intentId };
|
|
549
|
+
}
|
|
496
550
|
return { outcome: 'error', error: e?.message ?? String(e), title, intentId };
|
|
497
551
|
} finally {
|
|
498
552
|
onChild?.(null); // no longer busy — token may rotate between tasks
|
|
@@ -617,7 +671,10 @@ export async function runLiveWorker({
|
|
|
617
671
|
// stalled / errored → same worktree resumes). Finishing or finding no work
|
|
618
672
|
// clears it so the next fresh task starts from a clean base.
|
|
619
673
|
lastIntentId =
|
|
620
|
-
res.outcome === 'parked' ||
|
|
674
|
+
res.outcome === 'parked' ||
|
|
675
|
+
res.outcome === 'stalled' ||
|
|
676
|
+
res.outcome === 'error' ||
|
|
677
|
+
res.outcome === 'rate_limited'
|
|
621
678
|
? res.intentId
|
|
622
679
|
: null;
|
|
623
680
|
if (res.outcome === 'nothing') {
|
|
@@ -651,6 +708,25 @@ export async function runLiveWorker({
|
|
|
651
708
|
// on reconnect. Nothing to do but stop cleanly.
|
|
652
709
|
break;
|
|
653
710
|
}
|
|
711
|
+
if (res.outcome === 'rate_limited') {
|
|
712
|
+
// The agent's OWN Claude account hit its limit — not a Flowviant failure.
|
|
713
|
+
// Hold the worktree + lease and wait it out (heartbeating so it isn't
|
|
714
|
+
// reclaimed), then resume the SAME task in place. Never reset the worktree.
|
|
715
|
+
const when = res.resetAt ? ` until ~${new Date(res.resetAt).toLocaleTimeString()}` : '';
|
|
716
|
+
enter(
|
|
717
|
+
'paused',
|
|
718
|
+
warn,
|
|
719
|
+
`${c.yellow('paused')} ${c.dim(`— your Claude account hit its usage limit; holding your work${when}`)}`,
|
|
720
|
+
);
|
|
721
|
+
await parkUntilReset(res.resetAt, {
|
|
722
|
+
mcpUrl: getMcpUrl() ?? MCP_URL,
|
|
723
|
+
getToken: () => getToken(agentId),
|
|
724
|
+
runId: res.runId,
|
|
725
|
+
isAlive,
|
|
726
|
+
});
|
|
727
|
+
phase = '';
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
654
730
|
// stalled / error — usually a stale token or a stuck turn. Refresh + retry.
|
|
655
731
|
enter('reconnect', warn, `${c.yellow(res.outcome)} ${c.dim('— refreshing token, retrying')}`);
|
|
656
732
|
onTokenSuspect?.(agentId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|