toolroll 0.5.0 → 0.7.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/README.md +38 -24
- package/dist/accent-colors.d.ts +6 -3
- package/dist/accent-colors.js +9 -4
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +23 -23
- package/dist/browser-workspace.d.ts +46 -0
- package/dist/builder.d.ts +18 -1
- package/dist/builder.js +177 -5
- package/dist/chat-delivery.js +6 -4
- package/dist/claim.d.ts +15 -1
- package/dist/claim.js +30 -2
- package/dist/control-ui.js +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +21 -0
- package/dist/decision.d.ts +6 -0
- package/dist/decision.js +12 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +3 -1
- package/dist/dispatch.js +13 -0
- package/dist/dispose.js +11 -7
- package/dist/exhaustion.d.ts +14 -0
- package/dist/exhaustion.js +35 -0
- package/dist/install-method.d.ts +17 -0
- package/dist/install-method.js +61 -0
- package/dist/invoke.js +26 -0
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +15 -2
- package/dist/mate.js +5 -2
- package/dist/operate.d.ts +5 -0
- package/dist/operate.js +109 -15
- package/dist/provider-auth.d.ts +138 -0
- package/dist/provider-auth.js +291 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +8 -1
- package/dist/releases.d.ts +76 -0
- package/dist/releases.js +225 -0
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +207 -76
- package/dist/setup-guide.d.ts +2 -2
- package/dist/setup-guide.js +2 -0
- package/dist/skills.d.ts +6 -0
- package/dist/skills.js +8 -0
- package/dist/store.d.ts +2 -2
- package/dist/store.js +13 -4
- package/dist/subscription-chat.js +3 -0
- package/dist/update-notice.d.ts +5 -0
- package/dist/update-notice.js +4 -0
- package/dist/work-index.js +22 -4
- package/dist/worktree.d.ts +13 -0
- package/dist/worktree.js +55 -29
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -10,6 +10,8 @@ import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, mont
|
|
|
10
10
|
import { spendCsv } from "./spend-ui.js";
|
|
11
11
|
import { buildExport, exportSummary, exportZip, writeExportFolder } from "./export.js";
|
|
12
12
|
import { startBudgetAlerts } from "./budget-alerts.js";
|
|
13
|
+
import { liftAuthPause, openAuthPauses, signInGate, signInWords, startSignInProbes } from "./provider-auth.js";
|
|
14
|
+
import { createConnectionChecker } from "./provider-connection.js";
|
|
13
15
|
import { backupFiles, backupFolderOf, backupOwner, backupNow, restoreDatabase, startBackups } from "./backup.js";
|
|
14
16
|
import { pushLimitSink } from "./provider-limits.js";
|
|
15
17
|
import { limitsView } from "./limits-ui.js";
|
|
@@ -121,7 +123,7 @@ import { verdictWords as proofVerdictWords, matrixWords, semanticCoverage, cover
|
|
|
121
123
|
import { probeRepo, isVerified } from "./probe.js";
|
|
122
124
|
import { diagnoseTaskDispatch, diagnosisIsDispatchable, scopeApprovedForDispatch, } from "./dispatch.js";
|
|
123
125
|
import { createDecisionServer } from "./serve.js";
|
|
124
|
-
import { daemonLaunchCommand, daemonStatus,
|
|
126
|
+
import { daemonLaunchCommand, daemonStatus, installReplacingLegacy, installedLegacyDaemon, planDaemon, uninstallDaemon, awaitFreshHeartbeat, } from "./daemon.js";
|
|
125
127
|
import { bridgePass, clearBotToken, createTransport, followBridge, hashPairingCode, loadBotToken, mintPairingCode, redactToken, saveBotToken, PAIRING_TTL_MS, TOKEN_ENV, telegramHookSecret, telegramPushUrl, } from "./telegram.js";
|
|
126
128
|
import { scanRepo } from "./capscan.js";
|
|
127
129
|
import { computeGaps, describeCapability } from "./gaps.js";
|
|
@@ -179,6 +181,9 @@ import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
|
179
181
|
import { beads } from "./beads.js";
|
|
180
182
|
import { githubIssues } from "./issues.js";
|
|
181
183
|
import { installationStatus, renderInstallationStatus, renderTaskWait, taskWaitSnapshot } from "./lead-status.js";
|
|
184
|
+
import { latestRelease, newerRelease, notifySecurityRelease, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
185
|
+
import { installMethod } from "./install-method.js";
|
|
186
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
182
187
|
/**
|
|
183
188
|
* 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
|
|
184
189
|
*
|
|
@@ -405,6 +410,8 @@ Agents — which provider and model each phase runs on
|
|
|
405
410
|
toolroll providers --report --runner <name> --token <t>
|
|
406
411
|
record this machine's readiness per
|
|
407
412
|
provider under its runner name
|
|
413
|
+
toolroll providers resume <provider> after signing in again, start its
|
|
414
|
+
paused tasks now
|
|
408
415
|
toolroll config set chat --provider claude-subscription|codex-subscription|anthropic-api|openrouter-api
|
|
409
416
|
[--model <m>] [--weekly-usd <n>] [--daily-turns <n>] --as <you> --token <t>
|
|
410
417
|
membership providers reuse a logged-in local harness with no dollar
|
|
@@ -716,6 +723,8 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
716
723
|
...(options.mateSeams === undefined ? {} : { mateSeams: options.mateSeams }),
|
|
717
724
|
...(options.heldCoordinator === undefined ? {} : { heldCoordinator: options.heldCoordinator }),
|
|
718
725
|
...(options.waitSleep === undefined ? {} : { waitSleep: options.waitSleep }),
|
|
726
|
+
...(options.releaseIo === undefined ? {} : { releaseIo: options.releaseIo }),
|
|
727
|
+
...(options.installBin === undefined ? {} : { installBin: options.installBin }),
|
|
719
728
|
});
|
|
720
729
|
}
|
|
721
730
|
catch (error) {
|
|
@@ -913,6 +922,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
913
922
|
case "intake":
|
|
914
923
|
return intakeCommand(positional, flags, context);
|
|
915
924
|
case "providers":
|
|
925
|
+
if (positional[0] === "resume")
|
|
926
|
+
return resumeProviderCommand(positional.slice(1), flags, context);
|
|
916
927
|
return providersCommand(flags, context);
|
|
917
928
|
case "template":
|
|
918
929
|
return templateCommand(positional, flags, context);
|
|
@@ -1007,6 +1018,7 @@ async function readyCommand(flags, context) {
|
|
|
1007
1018
|
const ready = store.listReady(now);
|
|
1008
1019
|
const described = ready.map(ref => describeRef(store, ref, now));
|
|
1009
1020
|
const dispatchableCount = described.filter(one => diagnosisIsDispatchable(one.dispatch)).length;
|
|
1021
|
+
const pauses = openAuthPauses(store);
|
|
1010
1022
|
if (json) {
|
|
1011
1023
|
write(envelopeJson({
|
|
1012
1024
|
ok: ready.length > 0,
|
|
@@ -1014,10 +1026,14 @@ async function readyCommand(flags, context) {
|
|
|
1014
1026
|
...(ready.length > 0 ? {} : { reason: "empty", message: "nothing is task-locally ready" }),
|
|
1015
1027
|
count: ready.length,
|
|
1016
1028
|
dispatchableCount,
|
|
1029
|
+
signIn: pauses.map(one => ({ provider: one.provider, message: signInWords(one), since: one.openedAt })),
|
|
1017
1030
|
tasks: described,
|
|
1018
1031
|
}));
|
|
1019
1032
|
return ready.length > 0 ? EXIT.ok : EXIT.refused;
|
|
1020
1033
|
}
|
|
1034
|
+
// A paused provider first: nothing on it runs until someone signs in.
|
|
1035
|
+
for (const one of pauses)
|
|
1036
|
+
write(signInWords(one));
|
|
1021
1037
|
if (ready.length === 0) {
|
|
1022
1038
|
write("Nothing is ready to dispatch.");
|
|
1023
1039
|
return EXIT.refused;
|
|
@@ -1026,7 +1042,7 @@ async function readyCommand(flags, context) {
|
|
|
1026
1042
|
for (const ref of ready) {
|
|
1027
1043
|
const task = store.getTask(ref.externalId);
|
|
1028
1044
|
const diagnosis = diagnoseTaskDispatch(store, ref.externalId, now);
|
|
1029
|
-
write(` ${ref.externalId} ${task === null ? "" : task.title}${ref.assignedRunner === null ? "" : ` (reserved for ${ref.assignedRunner})`}${diagnosis === null ? "" : ` — ${diagnosis.summary.toLowerCase()}`}`);
|
|
1045
|
+
write(` ${ref.externalId} ${task === null ? "" : task.title}${ref.assignedRunner === null ? "" : ` (reserved for ${ref.assignedRunner})`}${diagnosis === null ? "" : ` — ${diagnosis.code === "signed-out" ? diagnosis.summary : diagnosis.summary.toLowerCase()}`}`);
|
|
1030
1046
|
}
|
|
1031
1047
|
return EXIT.ok;
|
|
1032
1048
|
}
|
|
@@ -1826,6 +1842,7 @@ async function buildCommand(positional, flags, context) {
|
|
|
1826
1842
|
const broke = result.reason === "agent" ||
|
|
1827
1843
|
result.reason === "agent-reported" ||
|
|
1828
1844
|
result.reason === "no-op" ||
|
|
1845
|
+
result.reason === "no-handoff" ||
|
|
1829
1846
|
result.reason === "moved-head" ||
|
|
1830
1847
|
result.reason === "timeout" ||
|
|
1831
1848
|
result.reason === "git" ||
|
|
@@ -2114,12 +2131,20 @@ async function tickCommand(flags, context) {
|
|
|
2114
2131
|
resumed.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(laneBudget) });
|
|
2115
2132
|
continue;
|
|
2116
2133
|
}
|
|
2134
|
+
// The sign-in pause holds a resumed lane too: it waits, parked, instead of failing.
|
|
2135
|
+
const laneGate = signInGate(store, [racer.provider], clock());
|
|
2136
|
+
if (laneGate.waiting !== null) {
|
|
2137
|
+
backToParked();
|
|
2138
|
+
resumed.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(laneGate.waiting) });
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2117
2141
|
// Legacy comparison lanes carried an absolute clock. New profiles use
|
|
2118
2142
|
// a progress watchdog and may remain alive indefinitely while useful
|
|
2119
2143
|
// work is observable; only legacy approvals retain their cumulative
|
|
2120
2144
|
// wall-clock contract.
|
|
2121
2145
|
const remaining = waiting.kind === "comparison" ? null : racer.budgetMicrousd - racer.accountedMicrousd;
|
|
2122
2146
|
if (remaining !== null && remaining <= 0) {
|
|
2147
|
+
laneGate.giveBack();
|
|
2123
2148
|
const current = store.getContestant(racer.id);
|
|
2124
2149
|
if (current !== null)
|
|
2125
2150
|
store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
|
|
@@ -2131,6 +2156,7 @@ async function tickCommand(flags, context) {
|
|
|
2131
2156
|
const laneProfile = racer.profile ?? contestantProfileOf(racer.provider, racer.model, racer.repairModel);
|
|
2132
2157
|
const clockCapMs = 3 * laneProfile.timeoutSeconds * 1000;
|
|
2133
2158
|
if (laneProfile.timeoutKind !== "idle" && store.contestantCumulativeMs(racer.id) >= clockCapMs) {
|
|
2159
|
+
laneGate.giveBack();
|
|
2134
2160
|
const current = store.getContestant(racer.id);
|
|
2135
2161
|
if (current !== null)
|
|
2136
2162
|
store.casContestantState(racer.id, ["ready"], "stopped", current.generation);
|
|
@@ -2141,12 +2167,14 @@ async function tickCommand(flags, context) {
|
|
|
2141
2167
|
}
|
|
2142
2168
|
const reclaimed = acquire(store, waiting.taskRef, runner, { now: clock(), token, ttlMs: leaseTtlMs });
|
|
2143
2169
|
if (!reclaimed.ok) {
|
|
2170
|
+
laneGate.giveBack();
|
|
2144
2171
|
backToParked();
|
|
2145
2172
|
continue;
|
|
2146
2173
|
}
|
|
2147
2174
|
const freshContest = store.getContest(waiting.id);
|
|
2148
2175
|
if (freshContest === null || !store.casContestState(waiting.id, ["decision-wait", "racing"], "racing", freshContest.generation)) {
|
|
2149
2176
|
release(store, reclaimed.claim.leaseId, clock());
|
|
2177
|
+
laneGate.giveBack();
|
|
2150
2178
|
backToParked();
|
|
2151
2179
|
continue;
|
|
2152
2180
|
}
|
|
@@ -2365,7 +2393,10 @@ async function tickCommand(flags, context) {
|
|
|
2365
2393
|
// gateway re-checks freshly at spawn; this cache only keeps a skipped
|
|
2366
2394
|
// queue from probing once per task.
|
|
2367
2395
|
const attestedThisPass = new Map();
|
|
2396
|
+
let untakenTrial = null;
|
|
2368
2397
|
for (const ref of ready) {
|
|
2398
|
+
untakenTrial?.giveBack();
|
|
2399
|
+
untakenTrial = null;
|
|
2369
2400
|
// The build budget governs UNATTENDED admissions (round-1 finding 4):
|
|
2370
2401
|
// once it is spent, the pass keeps SCANNING for attended
|
|
2371
2402
|
// authorizations — operator-invoked sessions launch regardless —
|
|
@@ -2575,6 +2606,21 @@ async function tickCommand(flags, context) {
|
|
|
2575
2606
|
dispatched.push({ id, outcome: "skipped", reason: "auth-mode", detail: brokenMode.problem });
|
|
2576
2607
|
continue;
|
|
2577
2608
|
}
|
|
2609
|
+
// THE SIGN-IN PAUSE: a provider whose sign-in stopped working takes no
|
|
2610
|
+
// new work — no claim, no run, never a substitute (a lapsed login must not
|
|
2611
|
+
// reach a paid fallback) — until a run or check on it works again or a
|
|
2612
|
+
// person resumes it. Other providers keep working.
|
|
2613
|
+
// One task every AUTH_TRIAL_MS goes ahead as the trial: a sign-in check
|
|
2614
|
+
// can say "logged in" for a session that cannot refresh, so a real run
|
|
2615
|
+
// decides — it lifts the pause, or fails into the same incident.
|
|
2616
|
+
const gate = signInGate(store, modeProviders, clock(), ref.id);
|
|
2617
|
+
if (gate.waiting !== null) {
|
|
2618
|
+
dispatched.push({ id, outcome: "skipped", reason: "signed-out", detail: signInWords(gate.waiting) });
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
// A trial whose task is skipped before its claim succeeds is given back
|
|
2622
|
+
// at the next task (or after the pass), so the next pass may take it.
|
|
2623
|
+
untakenTrial = gate;
|
|
2578
2624
|
// The leg is the authority: what resolved must BE the leg, exactly —
|
|
2579
2625
|
// the sealed build leg, or the parked fallback entry's own pair.
|
|
2580
2626
|
const governingLeg = wantsPlan ? planLeg : parkedEntry !== null ? { provider: parkedEntry.provider, model: parkedEntry.model, chosen: "fallback" } : sealedBuildLeg;
|
|
@@ -2741,6 +2787,8 @@ async function tickCommand(flags, context) {
|
|
|
2741
2787
|
? {}
|
|
2742
2788
|
: { maxOpenDecisions: Number(text(flags, "max-open-decisions")) }),
|
|
2743
2789
|
});
|
|
2790
|
+
if (claimed.ok)
|
|
2791
|
+
untakenTrial = null;
|
|
2744
2792
|
if (!claimed.ok) {
|
|
2745
2793
|
// Losing a race, finding the task no longer ready, and a machine that
|
|
2746
2794
|
// lacks what the task needs are all the system working. None fails the
|
|
@@ -3678,7 +3726,9 @@ async function tickCommand(flags, context) {
|
|
|
3678
3726
|
id,
|
|
3679
3727
|
outcome: "failed",
|
|
3680
3728
|
reason: disposition.sealed
|
|
3681
|
-
?
|
|
3729
|
+
? disposition.disposition === "auth-expired"
|
|
3730
|
+
? "auth-expired — requeued, sign-in needed"
|
|
3731
|
+
: `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
|
|
3682
3732
|
: "fenced",
|
|
3683
3733
|
...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
|
|
3684
3734
|
worktree: leased.worktree.path,
|
|
@@ -3699,6 +3749,7 @@ async function tickCommand(flags, context) {
|
|
|
3699
3749
|
break;
|
|
3700
3750
|
}
|
|
3701
3751
|
}
|
|
3752
|
+
untakenTrial?.giveBack();
|
|
3702
3753
|
// THE CHAIN ADMISSION PASS (E3d): cycles a recognized exhaustion advanced
|
|
3703
3754
|
// to pending-admission dispatch their NEXT approved entry here — the ONLY
|
|
3704
3755
|
// road that runs a fallback entry. Every authority is re-derived inside
|
|
@@ -3745,6 +3796,12 @@ async function tickCommand(flags, context) {
|
|
|
3745
3796
|
dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "budget-unenforceable" });
|
|
3746
3797
|
continue;
|
|
3747
3798
|
}
|
|
3799
|
+
// The sign-in pause holds a fallback entry too: the admission stays pending until its provider works.
|
|
3800
|
+
const entryGate = signInGate(store, [peek.profile.provider], clock(), pending.taskRef);
|
|
3801
|
+
if (entryGate.waiting !== null) {
|
|
3802
|
+
dispatched.push({ id: pending.taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(entryGate.waiting) });
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3748
3805
|
// The FALLBACK claim (finding 4): every acquireIfReady gate — task
|
|
3749
3806
|
// state, non-backoff holds, blockers, approved scope + mode belt,
|
|
3750
3807
|
// capability, capacity, and quota keyed by the PINNED credential —
|
|
@@ -3767,6 +3824,7 @@ async function tickCommand(flags, context) {
|
|
|
3767
3824
|
: { maxOpenDecisions: Number(text(flags, "max-open-decisions")) }),
|
|
3768
3825
|
});
|
|
3769
3826
|
if (!claimed.ok) {
|
|
3827
|
+
entryGate.giveBack();
|
|
3770
3828
|
dispatched.push({ id: pending.taskId, outcome: "skipped", reason: claimed.reason });
|
|
3771
3829
|
continue;
|
|
3772
3830
|
}
|
|
@@ -3934,8 +3992,15 @@ async function tickCommand(flags, context) {
|
|
|
3934
3992
|
dispatched.push({ id: taskId, outcome: "skipped", reason: "budget", detail: budgetWords(continuationBudget) });
|
|
3935
3993
|
continue;
|
|
3936
3994
|
}
|
|
3995
|
+
// The sign-in pause holds a continuation too: its authorization stays open and it starts once the sign-in works.
|
|
3996
|
+
const continuationGate = signInGate(store, [pinned.provider], clock(), parent.taskRef);
|
|
3997
|
+
if (continuationGate.waiting !== null) {
|
|
3998
|
+
dispatched.push({ id: taskId, outcome: "skipped", reason: "signed-out", detail: signInWords(continuationGate.waiting) });
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
3937
4001
|
const claimed = acquireContinuation(store, continuation, runner, { now: clock(), token, ttlMs: leaseTtlMs });
|
|
3938
4002
|
if (!claimed.ok) {
|
|
4003
|
+
continuationGate.giveBack();
|
|
3939
4004
|
dispatched.push({ id: taskId, outcome: "skipped", reason: claimed.reason, ...("message" in claimed ? { detail: claimed.message } : {}) });
|
|
3940
4005
|
continue;
|
|
3941
4006
|
}
|
|
@@ -4633,9 +4698,13 @@ async function startConsole(options) {
|
|
|
4633
4698
|
server.on("close", stopMonitoring);
|
|
4634
4699
|
// v105: budget alerts at 50/80/100 %, a pass a minute.
|
|
4635
4700
|
server.on("close", startBudgetAlerts(context.store));
|
|
4701
|
+
// A paused provider's sign-in is checked every two minutes (no model, nothing spent); the pause lifts when it works.
|
|
4702
|
+
server.on("close", startSignInProbes(context.store, createConnectionChecker()));
|
|
4636
4703
|
// Sprint 8: scheduled backups, a pass a minute; its lease also tells a restore that the console is running.
|
|
4637
4704
|
server.on("close", startBackups(context.store, context.databaseFile));
|
|
4638
4705
|
server.on("close", startCodexLimits(context.store));
|
|
4706
|
+
// At most one anonymous look a day for a newer Toolroll (Settings → Updates can switch it off).
|
|
4707
|
+
server.on("close", startUpdateChecks(context.store, dirname(context.databaseFile), PACKAGE_VERSION, installMethod(context.installBin).updateCommand, context.releaseIo ?? {}));
|
|
4639
4708
|
const bound = server.address();
|
|
4640
4709
|
const port = typeof bound === "object" && bound !== null ? bound.port : options.port;
|
|
4641
4710
|
// A bind-everywhere address is not a place a browser can go: the URL
|
|
@@ -5009,6 +5078,27 @@ async function webhookCommand(positional, flags, context) {
|
|
|
5009
5078
|
`Send yourself a proof: toolroll webhook test`,
|
|
5010
5079
|
]);
|
|
5011
5080
|
}
|
|
5081
|
+
/**
|
|
5082
|
+
* `toolroll providers resume <provider>` — a person says the sign-in works
|
|
5083
|
+
* again (after `claude /login`, say): the provider's sign-in pause lifts, its
|
|
5084
|
+
* tasks may start, and one short message says how many resumed. A pause that
|
|
5085
|
+
* lifted by itself answers so; nothing is paused twice.
|
|
5086
|
+
*/
|
|
5087
|
+
function resumeProviderCommand(positional, flags, context) {
|
|
5088
|
+
const { store, write, json } = context;
|
|
5089
|
+
const command = "providers resume";
|
|
5090
|
+
const provider = positional[0];
|
|
5091
|
+
if (provider === undefined || !isProviderId(provider) || positional.length > 1) {
|
|
5092
|
+
return fail(write, json, command, "usage", `Use \`toolroll providers resume <${PROVIDER_IDS.join("|")}>\`.`, EXIT.usage);
|
|
5093
|
+
}
|
|
5094
|
+
const who = text(flags, "as") ?? process.env["USER"] ?? "operator";
|
|
5095
|
+
const lifted = liftAuthPause(store, provider, "person", who, context.clock());
|
|
5096
|
+
if (lifted === null)
|
|
5097
|
+
return succeed(write, json, command, { provider, resumed: 0, paused: false }, () => [`${provider} is not paused.`]);
|
|
5098
|
+
return succeed(write, json, command, { provider, resumed: lifted.resumed, paused: false }, () => [
|
|
5099
|
+
`${provider} resumed — ${lifted.resumed} ${lifted.resumed === 1 ? "task" : "tasks"} can start again.`,
|
|
5100
|
+
]);
|
|
5101
|
+
}
|
|
5012
5102
|
/**
|
|
5013
5103
|
* `toolroll providers` — identification, never integration theater.
|
|
5014
5104
|
*
|
|
@@ -6884,18 +6974,12 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6884
6974
|
write("Nothing was written. Re-run without --dry-run to install.");
|
|
6885
6975
|
return EXIT.ok;
|
|
6886
6976
|
}
|
|
6887
|
-
|
|
6888
|
-
try {
|
|
6889
|
-
await uninstallDaemon(legacy, supervise);
|
|
6890
|
-
}
|
|
6891
|
-
catch (error) {
|
|
6892
|
-
return fail(write, json, "daemon install", "supervisor", `could not stop ${legacy.label}, the same watch under its older name: ${error.message}`, EXIT.failed);
|
|
6893
|
-
}
|
|
6894
|
-
}
|
|
6895
|
-
const installed = await installDaemon(plan, token, supervise);
|
|
6977
|
+
const installed = await installReplacingLegacy(plan, token, legacy, supervise);
|
|
6896
6978
|
if (!installed.ok) {
|
|
6897
6979
|
return fail(write, json, "daemon install", "supervisor", installed.message, EXIT.failed);
|
|
6898
6980
|
}
|
|
6981
|
+
// The legacy watch beat for this runner until it stopped: only a beat after that proves the new one.
|
|
6982
|
+
const readySince = legacy === null ? heartbeatBefore : store.getRunner(runnerName)?.runner.heartbeatAt ?? null;
|
|
6899
6983
|
const started = await daemonStatus(plan, supervise);
|
|
6900
6984
|
if (started.state !== "running") {
|
|
6901
6985
|
return fail(write, json, "daemon install", "not-running", `the service was installed but did not stay running (${started.detail}) — read ${plan.logPath}; no work will be claimed until this is fixed`, EXIT.failed);
|
|
@@ -6906,7 +6990,7 @@ async function daemonCommand(positional, flags, context) {
|
|
|
6906
6990
|
// readiness receipt that proves this service reached the work loop —
|
|
6907
6991
|
// a FRESH one when the service was (re)started; the one already
|
|
6908
6992
|
// standing when a healthy running service was left alone.
|
|
6909
|
-
const fresh = await awaitFreshHeartbeat(store, runnerName, installed.action === "running" ? null :
|
|
6993
|
+
const fresh = await awaitFreshHeartbeat(store, runnerName, installed.action === "running" && legacy === null ? null : readySince);
|
|
6910
6994
|
const liveRunner = fresh.ok ? store.getRunner(runnerName)?.runner ?? null : null;
|
|
6911
6995
|
if (liveRunner === null) {
|
|
6912
6996
|
const macHint = process.platform === "darwin" && repo.startsWith(join(homedir(), "Documents"))
|
|
@@ -7013,6 +7097,7 @@ async function runWatchLoop(args) {
|
|
|
7013
7097
|
// attributes to this episode by runner and window, and `brief
|
|
7014
7098
|
// --latest-watch` bounds itself to exactly it.
|
|
7015
7099
|
store.startWatchEpisode({ repo, runner, incarnation }, new Date());
|
|
7100
|
+
recordRunnerVersion(dirname(context.databaseFile), runner, PACKAGE_VERSION, new Date());
|
|
7016
7101
|
args.onReady?.();
|
|
7017
7102
|
// A false renewal is FATAL (arc 2 finding 15): the lease or the
|
|
7018
7103
|
// credential is gone, and admitting one more pass would be work done for
|
|
@@ -8002,6 +8087,7 @@ async function upCommand(flags, context) {
|
|
|
8002
8087
|
});
|
|
8003
8088
|
};
|
|
8004
8089
|
observeReadiness();
|
|
8090
|
+
recordRunnerVersion(dirname(context.databaseFile), runnerName, PACKAGE_VERSION, clock());
|
|
8005
8091
|
const runnerHeartbeat = setInterval(() => {
|
|
8006
8092
|
const beat = heartbeatRunner(store, runnerName, runnerToken, clock());
|
|
8007
8093
|
if (!beat.ok && !stopping) {
|
|
@@ -8887,7 +8973,7 @@ function readPaths(flags, backend) {
|
|
|
8887
8973
|
return backend === BUILT_IN ? [BUILT_IN] : [];
|
|
8888
8974
|
}
|
|
8889
8975
|
// ---- authoring ------------------------------------------------------------
|
|
8890
|
-
function statusCommand(positional, flags, context) {
|
|
8976
|
+
async function statusCommand(positional, flags, context) {
|
|
8891
8977
|
const command = "status";
|
|
8892
8978
|
const allowed = new Set(["db", "json"]);
|
|
8893
8979
|
for (const name of flags.keys()) {
|
|
@@ -8897,7 +8983,15 @@ function statusCommand(positional, flags, context) {
|
|
|
8897
8983
|
if (positional.length > 0)
|
|
8898
8984
|
return fail(context.write, context.json, command, "usage", "Use `toolroll status [--json]`.", EXIT.usage);
|
|
8899
8985
|
const status = installationStatus(context.store, context.clock());
|
|
8900
|
-
|
|
8986
|
+
// One line, only when a newer Toolroll exists; offline or switched off says nothing.
|
|
8987
|
+
const current = PACKAGE_VERSION;
|
|
8988
|
+
const method = installMethod(context.installBin);
|
|
8989
|
+
const release = newerRelease(await latestRelease(dirname(context.databaseFile), { now: context.clock, ...context.releaseIo }), current);
|
|
8990
|
+
if (release !== null)
|
|
8991
|
+
notifySecurityRelease(context.store, release, current, method.updateCommand, context.clock());
|
|
8992
|
+
const line = updateLine(release, current, method.updateCommand);
|
|
8993
|
+
const update = release === null ? {} : { update: { current, latest: release.version, security: release.security, updateCommand: method.updateCommand, url: release.url } };
|
|
8994
|
+
return succeed(context.write, context.json, command, { ...status, ...update }, () => [...renderInstallationStatus(status), ...(line === null ? [] : [line])]);
|
|
8901
8995
|
}
|
|
8902
8996
|
async function waitTask(positional, flags, context) {
|
|
8903
8997
|
const command = "task wait";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in pauses: when a provider's sign-in (or API key) stops working, its
|
|
3
|
+
* runs fail in seconds and every retry fails the same way. So an
|
|
4
|
+
* `auth-expired` run takes no strike and no retry: its task goes back to the
|
|
5
|
+
* queue, dispatch for THAT provider pauses (the others keep working), and a
|
|
6
|
+
* person is told once per incident, on every connected messaging channel, what
|
|
7
|
+
* to run. The pause lifts when a run or a sign-in probe on that provider
|
|
8
|
+
* succeeds, or when a person resumes it; one short message says so.
|
|
9
|
+
*
|
|
10
|
+
* One row per incident. At most one open incident per provider (a partial
|
|
11
|
+
* unique index), so however many runs hit it, one notification goes out:
|
|
12
|
+
* its key is `signin:auth-expired:<provider>:<ordinal>:<person>`, the ordinal
|
|
13
|
+
* being the incident's own number for that provider.
|
|
14
|
+
*/
|
|
15
|
+
import type { Store } from "./store.js";
|
|
16
|
+
import { type ProviderId } from "./provider.js";
|
|
17
|
+
/** Every sign-in message's dedupe key starts here: `signin:<kind>:<provider>:<ordinal>:<person>`.
|
|
18
|
+
* Not a lifecycle (`life:`) key: those are progress chatter, and an undelivered
|
|
19
|
+
* one stays out of the attention list — this one needs a person. */
|
|
20
|
+
export declare const SIGN_IN_KEY_PREFIX = "signin:";
|
|
21
|
+
export declare const PROVIDER_AUTH_SCHEMA = "\nCREATE TABLE IF NOT EXISTS provider_auth_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key')),\n opened_at TEXT NOT NULL,\n first_run INTEGER,\n task_ref INTEGER,\n runs INTEGER NOT NULL DEFAULT 1,\n -- 1 once a sign-in probe during this pause said the provider is signed out:\n -- only a probe that then sees it signed in again may lift the pause.\n probe_saw_out INTEGER NOT NULL DEFAULT 0,\n lifted_at TEXT,\n lifted_by TEXT,\n lifted_how TEXT CHECK (lifted_how IS NULL OR lifted_how IN ('run', 'probe', 'person')),\n resumed INTEGER,\n -- The last task let through as a trial while paused: a sign-in check can\n -- say \"logged in\" for a session that no longer works, so a real run decides.\n last_trial_at TEXT,\n UNIQUE (provider, ordinal)\n);\nCREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;\n";
|
|
22
|
+
export type AuthPause = {
|
|
23
|
+
id: number;
|
|
24
|
+
provider: ProviderId;
|
|
25
|
+
ordinal: number;
|
|
26
|
+
authMode: "subscription" | "api-key";
|
|
27
|
+
openedAt: string;
|
|
28
|
+
firstRun: number | null;
|
|
29
|
+
taskRef: number | null;
|
|
30
|
+
runs: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function providerName(provider: string): string;
|
|
33
|
+
/** The plain reason: "Claude needs you to sign in again". */
|
|
34
|
+
export declare function signInReason(pause: Pick<AuthPause, "provider" | "authMode">): string;
|
|
35
|
+
/** What to run, as a command. */
|
|
36
|
+
export declare function signInCommand(pause: Pick<AuthPause, "provider" | "authMode">): string;
|
|
37
|
+
/** One line for status, ready and the console. */
|
|
38
|
+
export declare function signInWords(pause: AuthPause): string;
|
|
39
|
+
/** The open pause for a provider, or null. */
|
|
40
|
+
export declare function authPauseOf(store: Store, provider: string): AuthPause | null;
|
|
41
|
+
/**
|
|
42
|
+
* SQL: whether the pause `pause` (a provider_auth_pause alias) is on one of
|
|
43
|
+
* the task_ref `taskRef`'s current providers — its pins, its working profile,
|
|
44
|
+
* route legs and fallback entries — so a task re-routed to another provider
|
|
45
|
+
* stops saying it waits on the old one. A task that names none yet (an
|
|
46
|
+
* unpinned planner, whose agent comes from configuration) keeps what the gate
|
|
47
|
+
* found.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pauseOnTaskProviders(pause: string, taskRef: string): string;
|
|
50
|
+
/** The open pause the dispatch gate last left this task waiting on, while it
|
|
51
|
+
* is still on one of the task's providers; or null. */
|
|
52
|
+
export declare function authWaitOf(store: Store, taskRef: number): AuthPause | null;
|
|
53
|
+
/** Every open pause, oldest first. */
|
|
54
|
+
export declare function openAuthPauses(store: Store): AuthPause[];
|
|
55
|
+
/**
|
|
56
|
+
* Record an auth-expired run against its provider's pause, opening the
|
|
57
|
+
* incident (and its one notification) when none is open. Called inside the
|
|
58
|
+
* run's own failure transaction. Returns whether this run opened it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function pauseForAuth(store: Store, args: {
|
|
61
|
+
provider: string;
|
|
62
|
+
authMode: "subscription" | "api-key" | null;
|
|
63
|
+
runId: number;
|
|
64
|
+
taskRef: number;
|
|
65
|
+
now: Date;
|
|
66
|
+
}): {
|
|
67
|
+
opened: boolean;
|
|
68
|
+
pause: AuthPause;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Lift a provider's open pause — a run or probe on it succeeded, or a person
|
|
72
|
+
* resumed it — and send the one short "signed in again" message. Returns the
|
|
73
|
+
* number of tasks its incident sent back to the queue, or null when nothing
|
|
74
|
+
* was paused.
|
|
75
|
+
*/
|
|
76
|
+
export declare function liftAuthPause(store: Store, provider: string, how: "run" | "probe" | "person", by: string, now: Date,
|
|
77
|
+
/** For a run: when it started. A run that began before the pause opened proves nothing about the sign-in now. */
|
|
78
|
+
runStartedAt?: Date): {
|
|
79
|
+
resumed: number;
|
|
80
|
+
pause: AuthPause;
|
|
81
|
+
} | null;
|
|
82
|
+
/**
|
|
83
|
+
* What a non-spending sign-in probe said. A probe lifts the pause only after
|
|
84
|
+
* one during the same pause saw the provider signed out: a CLI that reports
|
|
85
|
+
* "logged in" while its session cannot refresh must not lift and re-open the
|
|
86
|
+
* pause over and over.
|
|
87
|
+
*/
|
|
88
|
+
export declare function noteSignInProbe(store: Store, provider: string, state: string, now: Date): {
|
|
89
|
+
resumed: number;
|
|
90
|
+
} | null;
|
|
91
|
+
/** How often a paused provider lets one task through as a trial. */
|
|
92
|
+
export declare const AUTH_TRIAL_MS: number;
|
|
93
|
+
/** A trial taken: the pause, when it was taken, and the trial time it replaced. */
|
|
94
|
+
export type AuthTrial = {
|
|
95
|
+
pauseId: number;
|
|
96
|
+
takenAt: string;
|
|
97
|
+
previous: string | null;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Whether this task may go ahead as the paused provider's trial: at most one
|
|
101
|
+
* every AUTH_TRIAL_MS. A trial that works lifts the pause (its run proves the
|
|
102
|
+
* sign-in); one that fails the same way only counts against the open
|
|
103
|
+
* incident, so nobody is told twice. Null when no trial is due.
|
|
104
|
+
*/
|
|
105
|
+
export declare function claimAuthTrial(store: Store, pause: AuthPause, now: Date): AuthTrial | null;
|
|
106
|
+
/** Give a trial back when its task never started (its claim failed), so the next pass may take it. */
|
|
107
|
+
export declare function giveBackAuthTrial(store: Store, trial: AuthTrial): void;
|
|
108
|
+
/** What the sign-in gate decided: the open pause the work waits on, or null
|
|
109
|
+
* when it may go ahead; and, when it goes ahead as a pause's trial, how to
|
|
110
|
+
* give that trial back if its claim then fails. */
|
|
111
|
+
export type SignInGate = {
|
|
112
|
+
waiting: AuthPause | null;
|
|
113
|
+
giveBack: () => void;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The sign-in gate every road that starts an agent asks before it claims
|
|
117
|
+
* anything: the tick's queue, and the roads beside it (fallback entries,
|
|
118
|
+
* attended continuations, contest resumes). Waits when a provider is paused,
|
|
119
|
+
* unless this is the pause's one trial. A waiting task is noted on its
|
|
120
|
+
* task_ref, so the work index says exactly what the gate decided; a task let
|
|
121
|
+
* through is un-noted. A road whose claim fails after a trial calls giveBack.
|
|
122
|
+
*/
|
|
123
|
+
export declare function signInGate(store: Store, providers: readonly string[], now: Date, taskRef?: number): SignInGate;
|
|
124
|
+
/** The loop beside the console: every two minutes, probe each paused
|
|
125
|
+
* provider's sign-in (no model, nothing spent) and lift the pause when it
|
|
126
|
+
* works again. Nothing is probed while nothing is paused. */
|
|
127
|
+
export declare function startSignInProbes(store: Store, check: (provider: ProviderId, fresh?: boolean) => Promise<{
|
|
128
|
+
state: string;
|
|
129
|
+
}>, everyMs?: number): () => void;
|
|
130
|
+
/** The console's view of every open pause: title, command, one action. */
|
|
131
|
+
export declare function signInNotices(store: Store): {
|
|
132
|
+
provider: string;
|
|
133
|
+
title: string;
|
|
134
|
+
command: string;
|
|
135
|
+
detail: string;
|
|
136
|
+
resumeLabel: string;
|
|
137
|
+
resumeHref: string;
|
|
138
|
+
}[];
|