@lumi.ai/runner 0.6.0 → 0.6.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/cli.js +70 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -764,7 +764,7 @@ function mcpUrl(config2) {
|
|
|
764
764
|
}
|
|
765
765
|
|
|
766
766
|
// src/version.ts
|
|
767
|
-
var RUNNER_VERSION = true ? "0.6.
|
|
767
|
+
var RUNNER_VERSION = true ? "0.6.2" : "0.0.0-dev";
|
|
768
768
|
|
|
769
769
|
// src/auth.ts
|
|
770
770
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2328,6 +2328,48 @@ function subscribeEngineLimits(db, shipId, cb, onError) {
|
|
|
2328
2328
|
);
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
|
+
// src/jobs/claimBackoff.ts
|
|
2332
|
+
var BACKOFF_BASE_MS = 3e3;
|
|
2333
|
+
var BACKOFF_MAX_MS = 6e4;
|
|
2334
|
+
var BACKOFF_RETENTION_MS = 10 * 6e4;
|
|
2335
|
+
function backoffFor(releases) {
|
|
2336
|
+
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, releases - 1));
|
|
2337
|
+
}
|
|
2338
|
+
function noteTransientRelease(backoff, jobId, now) {
|
|
2339
|
+
const releases = (backoff.get(jobId)?.releases ?? 0) + 1;
|
|
2340
|
+
const entry = { releases, eligibleAt: now + backoffFor(releases), poked: false };
|
|
2341
|
+
backoff.set(jobId, entry);
|
|
2342
|
+
return entry;
|
|
2343
|
+
}
|
|
2344
|
+
function isBackedOff(backoff, jobId, now) {
|
|
2345
|
+
const entry = backoff.get(jobId);
|
|
2346
|
+
return !!entry && entry.eligibleAt > now;
|
|
2347
|
+
}
|
|
2348
|
+
function clearBackoff(backoff, jobId) {
|
|
2349
|
+
backoff.delete(jobId);
|
|
2350
|
+
}
|
|
2351
|
+
function pruneExpired2(backoff, now) {
|
|
2352
|
+
const cleared = [];
|
|
2353
|
+
for (const [key, entry] of backoff) {
|
|
2354
|
+
if (entry.eligibleAt > now) continue;
|
|
2355
|
+
if (!entry.poked) {
|
|
2356
|
+
cleared.push(key);
|
|
2357
|
+
entry.poked = true;
|
|
2358
|
+
}
|
|
2359
|
+
if (now - entry.eligibleAt >= BACKOFF_RETENTION_MS) backoff.delete(key);
|
|
2360
|
+
}
|
|
2361
|
+
return cleared;
|
|
2362
|
+
}
|
|
2363
|
+
function nextEligibleAt(backoff, now) {
|
|
2364
|
+
let soonest = null;
|
|
2365
|
+
for (const entry of backoff.values()) {
|
|
2366
|
+
if (entry.eligibleAt > now && (soonest === null || entry.eligibleAt < soonest)) {
|
|
2367
|
+
soonest = entry.eligibleAt;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return soonest;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2331
2373
|
// src/jobs/capacity.ts
|
|
2332
2374
|
var DEFAULT_PARALLEL_JOBS = 1;
|
|
2333
2375
|
var PARALLEL_MAX = 8;
|
|
@@ -3445,6 +3487,7 @@ async function startDaemon() {
|
|
|
3445
3487
|
}
|
|
3446
3488
|
const pending = /* @__PURE__ */ new Map();
|
|
3447
3489
|
const engineLimits = /* @__PURE__ */ new Map();
|
|
3490
|
+
const claimBackoff = /* @__PURE__ */ new Map();
|
|
3448
3491
|
const agentEngines = /* @__PURE__ */ new Map();
|
|
3449
3492
|
const unsubsByShip = /* @__PURE__ */ new Map();
|
|
3450
3493
|
const listenerError = (shipId, what) => (e) => {
|
|
@@ -3627,6 +3670,23 @@ async function startDaemon() {
|
|
|
3627
3670
|
limitTimer = setTimeout(sweepLimits, Math.max(0, at - Date.now()) + 1e3);
|
|
3628
3671
|
limitTimer.unref();
|
|
3629
3672
|
}
|
|
3673
|
+
let backoffTimer = null;
|
|
3674
|
+
function armBackoffTimer() {
|
|
3675
|
+
if (backoffTimer) {
|
|
3676
|
+
clearTimeout(backoffTimer);
|
|
3677
|
+
backoffTimer = null;
|
|
3678
|
+
}
|
|
3679
|
+
const at = nextEligibleAt(claimBackoff, Date.now());
|
|
3680
|
+
if (at === null) return;
|
|
3681
|
+
backoffTimer = setTimeout(sweepBackoff, Math.max(0, at - Date.now()) + 1e3);
|
|
3682
|
+
backoffTimer.unref();
|
|
3683
|
+
}
|
|
3684
|
+
function sweepBackoff() {
|
|
3685
|
+
backoffTimer = null;
|
|
3686
|
+
const cleared = pruneExpired2(claimBackoff, Date.now());
|
|
3687
|
+
armBackoffTimer();
|
|
3688
|
+
if (cleared.length > 0) poke();
|
|
3689
|
+
}
|
|
3630
3690
|
function sweepLimits() {
|
|
3631
3691
|
limitTimer = null;
|
|
3632
3692
|
const cleared = pruneExpired(engineLimits, Date.now());
|
|
@@ -3658,7 +3718,11 @@ async function startDaemon() {
|
|
|
3658
3718
|
// the limit map is keyed by (Ship, engine) and the engine comes from the registry via
|
|
3659
3719
|
// the agent, so the job loop still knows nothing about Claude. Skipped entries STAY in
|
|
3660
3720
|
// `pending`, which is what lets a reset resume them with only a poke.
|
|
3661
|
-
eligible: (p) => approved.get(p.shipId) === true && !isLimited(engineLimits, p.shipId, engineForJob(p.shipId, p.job), now)
|
|
3721
|
+
eligible: (p) => approved.get(p.shipId) === true && !isLimited(engineLimits, p.shipId, engineForJob(p.shipId, p.job), now) && // The connection cooldown. Same "skip, don't stop" shape as the limit gate above, and
|
|
3722
|
+
// skipped entries STAY in `pending` for the same reason — `sweepBackoff` resumes them
|
|
3723
|
+
// with a poke. Without this the `transient` release below re-claims what it just put
|
|
3724
|
+
// down, every three seconds, forever (jobs/claimBackoff.ts).
|
|
3725
|
+
!isBackedOff(claimBackoff, p.job.id, now)
|
|
3662
3726
|
});
|
|
3663
3727
|
for (const pick of picks) {
|
|
3664
3728
|
pending.delete(`${pick.shipId}/${pick.job.id}`);
|
|
@@ -4014,14 +4078,16 @@ async function startDaemon() {
|
|
|
4014
4078
|
target.kind === "task" ? `Task ${target.taskId} is done.` : delivery === "agent-replied" ? "Agent replied in a chat." : "Agent finished a chat run without replying \u2014 its answer was posted for it."
|
|
4015
4079
|
);
|
|
4016
4080
|
} else if (transient) {
|
|
4081
|
+
const held = noteTransientRelease(claimBackoff, job.id, Date.now());
|
|
4017
4082
|
await releaseJob(
|
|
4018
4083
|
sess(shipId).fb.db,
|
|
4019
4084
|
shipId,
|
|
4020
4085
|
job,
|
|
4021
4086
|
`Lost the connection to Firestore \u2014 released without consuming a retry: ${failure.slice(0, 120)}`
|
|
4022
4087
|
);
|
|
4088
|
+
armBackoffTimer();
|
|
4023
4089
|
log2(
|
|
4024
|
-
`Job ${job.id} released: lost the connection to Firestore. Attempt ${job.attempt} preserved \u2014 ${failure.slice(0, 120)}
|
|
4090
|
+
`Job ${job.id} released: lost the connection to Firestore. Attempt ${job.attempt} preserved \u2014 ${failure.slice(0, 120)}. Not re-claiming it here for ${Math.round((held.eligibleAt - Date.now()) / 1e3)}s (drop ${held.releases} on this machine); another daemon may take it meanwhile.`
|
|
4025
4091
|
);
|
|
4026
4092
|
} else if (!terminal && job.attempt < MAX_ATTEMPTS) {
|
|
4027
4093
|
log2(`Job ${job.id} failed (attempt ${job.attempt}) \u2014 re-queueing: ${failure.slice(0, 120)}`);
|
|
@@ -4043,6 +4109,7 @@ async function startDaemon() {
|
|
|
4043
4109
|
log2(`Job ${job.id} FAILED terminally: ${failure.slice(0, 120)}`);
|
|
4044
4110
|
notify("Crew job failed", `${targetLabel}: ${failure.slice(0, 120)}`);
|
|
4045
4111
|
}
|
|
4112
|
+
if (!transient) clearBackoff(claimBackoff, job.id);
|
|
4046
4113
|
} catch (e) {
|
|
4047
4114
|
log2(`finalize failed for ${job.id}: ${e instanceof Error ? e.message : e}`);
|
|
4048
4115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|