@bivy/bivy 0.7.0-staging.102 → 0.7.0-staging.103
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/metadata.js +14 -0
- package/dist/policy/session-reroute.js +16 -2
- package/dist/server.js +35 -5
- package/package.json +1 -1
package/dist/metadata.js
CHANGED
|
@@ -182,6 +182,20 @@ export class MetadataStore {
|
|
|
182
182
|
this.data.sessions[id] = { ...prev, resumeAt: next, updatedAt: nowIso() };
|
|
183
183
|
this.save();
|
|
184
184
|
}
|
|
185
|
+
/** Set the durable consecutive auto-resume counter (the restart-safe backstop
|
|
186
|
+
* for the in-memory reroute budget). Pass 0 to clear. No-op when the row is
|
|
187
|
+
* missing or already in the requested state, so a normal turn (counter already
|
|
188
|
+
* 0) never churns the file. */
|
|
189
|
+
setResumeAttempts(id, attempts) {
|
|
190
|
+
const prev = this.data.sessions[id];
|
|
191
|
+
if (!prev)
|
|
192
|
+
return;
|
|
193
|
+
const next = attempts > 0 ? attempts : undefined;
|
|
194
|
+
if ((prev.resumeAttempts ?? undefined) === next)
|
|
195
|
+
return;
|
|
196
|
+
this.data.sessions[id] = { ...prev, resumeAttempts: next, updatedAt: nowIso() };
|
|
197
|
+
this.save();
|
|
198
|
+
}
|
|
185
199
|
/** Sessions with a durable auto-resume time set — the resume sweep re-arms
|
|
186
200
|
* these after a restart. */
|
|
187
201
|
sessionsWithResumeAt() {
|
|
@@ -92,8 +92,22 @@ export class SessionRerouteController {
|
|
|
92
92
|
// long enough that it's clearly a limit rather than routine backoff.
|
|
93
93
|
if (decision.resetsAt === undefined && decision.delayMs < MIN_RESUME_DELAY_MS)
|
|
94
94
|
return null;
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Resolve the due time (provider reset when known, else backoff) and floor it
|
|
96
|
+
// to at least MIN_RESUME_DELAY_MS in the FUTURE. A reset time can be in the
|
|
97
|
+
// past or ~now — a stale/elapsed reset, clock skew, or (most often) a window
|
|
98
|
+
// that already lapsed while the daemon was down — and using it verbatim yields
|
|
99
|
+
// a 0ms delay. The caller arms a timer at that delay, so a 0ms resume re-sends
|
|
100
|
+
// instantly, re-hits the still-standing limit, and re-schedules 0ms again: a
|
|
101
|
+
// tight loop that pins a CPU core and never settles. Flooring turns a
|
|
102
|
+
// not-yet-cleared limit into a slow retry the attempt budget can still park.
|
|
103
|
+
const rawDueMs = decision.resetsAt ? Date.parse(decision.resetsAt) : now + decision.delayMs;
|
|
104
|
+
const dueMs = Math.max(Number.isFinite(rawDueMs) ? rawDueMs : now, now + MIN_RESUME_DELAY_MS);
|
|
105
|
+
return {
|
|
106
|
+
condition: decision.condition,
|
|
107
|
+
summary: decision.summary,
|
|
108
|
+
delayMs: dueMs - now,
|
|
109
|
+
resumeAt: new Date(dueMs).toISOString(),
|
|
110
|
+
};
|
|
97
111
|
}
|
|
98
112
|
/** Advance the attempt budget once the caller has committed to a resume, so a
|
|
99
113
|
* limit that re-fires after the reset counts toward `maxAttempts` and can
|
package/dist/server.js
CHANGED
|
@@ -6886,6 +6886,14 @@ const SESSION_RESUME_SWEEP_MS = 60_000;
|
|
|
6886
6886
|
/** Slack around "due": a capped timer may fire a touch early — drive only when
|
|
6887
6887
|
* within this of the target, else re-arm. */
|
|
6888
6888
|
const SESSION_RESUME_TICK_MS = 15_000;
|
|
6889
|
+
/** Hard ceiling on consecutive auto-resumes for one session before we give up and
|
|
6890
|
+
* surface the limit. The reroute controller already caps per turn, but its budget
|
|
6891
|
+
* is in-memory: a session re-resolved after its child exits on the limit (or a
|
|
6892
|
+
* daemon restart) gets a fresh controller, so without a durable count a limit that
|
|
6893
|
+
* never actually clears would re-send every MIN_RESUME_DELAY_MS indefinitely.
|
|
6894
|
+
* Generous enough to ride out a mis-parsed multi-day window (each wait is ≥1 min,
|
|
6895
|
+
* usually far longer), low enough to bound a genuinely stuck limit. */
|
|
6896
|
+
const MAX_DURABLE_RESUME_ATTEMPTS = 10;
|
|
6889
6897
|
const sessionResumeTimers = new Map();
|
|
6890
6898
|
// Fire due auto-resumes (a usage/rate limit that has since reset) and re-arm the
|
|
6891
6899
|
// tail of long waits whose in-process timer was capped or lost to a restart.
|
|
@@ -7192,10 +7200,20 @@ function armSessionResumeTimer(id, dueMs) {
|
|
|
7192
7200
|
timer.unref?.();
|
|
7193
7201
|
sessionResumeTimers.set(id, timer);
|
|
7194
7202
|
}
|
|
7195
|
-
/** Persist + arm an auto-resume decided by the session policy. Synchronous so
|
|
7196
|
-
*
|
|
7203
|
+
/** Persist + arm an auto-resume decided by the session policy. Synchronous so the
|
|
7204
|
+
* caller can atomically suppress the turn's error toast. Returns false when the
|
|
7205
|
+
* session has already exhausted its durable auto-resume budget (a limit that never
|
|
7206
|
+
* clears) — the caller then lets the error surface instead of looping. */
|
|
7197
7207
|
function scheduleSessionResume(record, plan) {
|
|
7208
|
+
const attempts = metadata.getSession(record.id)?.resumeAttempts ?? 0;
|
|
7209
|
+
if (attempts >= MAX_DURABLE_RESUME_ATTEMPTS) {
|
|
7210
|
+
console.warn(`[resume] session ${record.id} hit the durable auto-resume cap (${MAX_DURABLE_RESUME_ATTEMPTS}) without the limit clearing — giving up`);
|
|
7211
|
+
clearSessionResume(record.id);
|
|
7212
|
+
metadata.setResumeAttempts(record.id, 0);
|
|
7213
|
+
return false;
|
|
7214
|
+
}
|
|
7198
7215
|
metadata.setResumeAt(record.id, plan.resumeAt);
|
|
7216
|
+
metadata.setResumeAttempts(record.id, attempts + 1);
|
|
7199
7217
|
const when = Date.parse(plan.resumeAt);
|
|
7200
7218
|
const cond = plan.condition.replace(/_/g, " ");
|
|
7201
7219
|
broadcast({
|
|
@@ -7205,6 +7223,7 @@ function scheduleSessionResume(record, plan) {
|
|
|
7205
7223
|
message: `Hit a ${cond} limit — I'll resume this automatically when it resets (${plan.resumeAt}).`,
|
|
7206
7224
|
});
|
|
7207
7225
|
armSessionResumeTimer(record.id, Number.isFinite(when) ? when : Date.now());
|
|
7226
|
+
return true;
|
|
7208
7227
|
}
|
|
7209
7228
|
/** Fire a due auto-resume: re-open the session if needed and re-send the turn's
|
|
7210
7229
|
* last prompt. Clears the durable marker BEFORE driving so a crash mid-resume
|
|
@@ -7484,6 +7503,10 @@ function attachSessionListeners(record) {
|
|
|
7484
7503
|
resetsAtHint: limitResetHint(record, Date.now()),
|
|
7485
7504
|
}) ?? null
|
|
7486
7505
|
: null;
|
|
7506
|
+
// Did this turn end by scheduling another auto-resume? If not, the session
|
|
7507
|
+
// made forward progress (a user turn, a resume that cleared the limit, a
|
|
7508
|
+
// reroute, or a surfaced error), so its durable resume streak resets below.
|
|
7509
|
+
let scheduledResume = false;
|
|
7487
7510
|
if (reroutePlan) {
|
|
7488
7511
|
void record.reroute.applyReroute(reroutePlan, {
|
|
7489
7512
|
getCurrentModelName: () => record.session.getCurrentModel()?.name,
|
|
@@ -7493,12 +7516,14 @@ function attachSessionListeners(record) {
|
|
|
7493
7516
|
},
|
|
7494
7517
|
});
|
|
7495
7518
|
}
|
|
7496
|
-
else if (resumePlan) {
|
|
7519
|
+
else if (resumePlan && scheduleSessionResume(record, resumePlan)) {
|
|
7497
7520
|
// Charge the attempt budget so a limit that re-fires after the reset can
|
|
7498
7521
|
// eventually exhaust (→ surface) instead of looping, then park the turn
|
|
7499
|
-
// as a scheduled resume rather than a dead error.
|
|
7522
|
+
// as a scheduled resume rather than a dead error. scheduleSessionResume
|
|
7523
|
+
// returns false once the durable cap is hit, so this falls through to
|
|
7524
|
+
// surface the limit instead of resuming forever.
|
|
7500
7525
|
record.reroute.noteResumeApplied();
|
|
7501
|
-
|
|
7526
|
+
scheduledResume = true;
|
|
7502
7527
|
}
|
|
7503
7528
|
else if (messageError) {
|
|
7504
7529
|
// Only the server-owned (pi-ai) path surfaces here; a Claude Code error
|
|
@@ -7527,6 +7552,11 @@ function attachSessionListeners(record) {
|
|
|
7527
7552
|
body: `${sessionNotifyLabel(record)} finished — tap to review the result.`,
|
|
7528
7553
|
});
|
|
7529
7554
|
}
|
|
7555
|
+
// Any turn that didn't schedule another resume broke the limit streak —
|
|
7556
|
+
// clear the durable counter so a future limit starts with a full budget
|
|
7557
|
+
// (no-op when it's already 0, so a normal turn never touches the file).
|
|
7558
|
+
if (!scheduledResume)
|
|
7559
|
+
metadata.setResumeAttempts(record.id, 0);
|
|
7530
7560
|
// First real commit on a repo-backed worktree → publish the branch to the
|
|
7531
7561
|
// remote (sets upstream), so the work is visible on GitHub. No-op until
|
|
7532
7562
|
// there's a commit, and only pushes once. Then adopt a PR the agent opened
|
package/package.json
CHANGED