toolroll 0.9.77 → 0.9.79

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/agent-fence.js +18 -2
  3. package/dist/backup-ui.d.ts +9 -1
  4. package/dist/backup-ui.js +28 -3
  5. package/dist/backup.js +4 -3
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +18 -18
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/builder.d.ts +4 -3
  10. package/dist/builder.js +14 -8
  11. package/dist/check-failure.js +31 -3
  12. package/dist/check-report.d.ts +18 -3
  13. package/dist/check-report.js +67 -7
  14. package/dist/claim.d.ts +9 -0
  15. package/dist/claim.js +9 -1
  16. package/dist/contracts/console-api.d.ts +1 -1
  17. package/dist/contracts/console-api.js +1 -1
  18. package/dist/contracts/release-terms.d.ts +41 -0
  19. package/dist/contracts/release-terms.js +53 -0
  20. package/dist/controller-service.js +4 -1
  21. package/dist/deploy-rescue.d.ts +41 -0
  22. package/dist/deploy-rescue.js +94 -0
  23. package/dist/dispose.js +4 -3
  24. package/dist/evidence.d.ts +9 -0
  25. package/dist/evidence.js +15 -3
  26. package/dist/exec.d.ts +2 -0
  27. package/dist/exec.js +15 -1
  28. package/dist/git-views.d.ts +1 -1
  29. package/dist/git-views.js +2 -2
  30. package/dist/guides.js +18 -8
  31. package/dist/latency-timeline.d.ts +46 -0
  32. package/dist/latency-timeline.js +120 -0
  33. package/dist/live-backup.d.ts +39 -0
  34. package/dist/live-backup.js +271 -0
  35. package/dist/operate.js +203 -16
  36. package/dist/planner.js +2 -2
  37. package/dist/project-knowledge.d.ts +7 -7
  38. package/dist/release-adapters.d.ts +41 -3
  39. package/dist/release-adapters.js +275 -73
  40. package/dist/release-carry.d.ts +21 -1
  41. package/dist/release-carry.js +22 -0
  42. package/dist/release-cli.d.ts +3 -0
  43. package/dist/release-cli.js +74 -5
  44. package/dist/release-gate.d.ts +5 -0
  45. package/dist/release-gate.js +1 -1
  46. package/dist/release-net.d.ts +90 -0
  47. package/dist/release-net.js +253 -0
  48. package/dist/release-recheck.js +3 -2
  49. package/dist/release.d.ts +40 -5
  50. package/dist/release.js +142 -52
  51. package/dist/runner-process.d.ts +67 -0
  52. package/dist/runner-process.js +219 -0
  53. package/dist/runner.d.ts +48 -6
  54. package/dist/runner.js +71 -8
  55. package/dist/scope.d.ts +19 -1
  56. package/dist/scope.js +19 -2
  57. package/dist/scout.js +2 -2
  58. package/dist/serve.js +11 -0
  59. package/dist/server/http.d.ts +2 -0
  60. package/dist/server/pages.js +7 -3
  61. package/dist/server/remote-hooks.d.ts +1 -1
  62. package/dist/server/remote-hooks.js +5 -3
  63. package/dist/server/render-results.d.ts +2 -0
  64. package/dist/server/render-results.js +1 -0
  65. package/dist/server/request-context.d.ts +2 -0
  66. package/dist/server/settings.js +22 -2
  67. package/dist/server/tasks.js +4 -1
  68. package/dist/storage-sweep.js +1 -1
  69. package/dist/store.d.ts +125 -2
  70. package/dist/store.js +250 -12
  71. package/dist/surface.js +3 -1
  72. package/dist/teardown-git.d.ts +1 -1
  73. package/dist/teardown-git.js +12 -12
  74. package/dist/toolroll-update.js +2 -2
  75. package/dist/up-role-child.js +6 -2
  76. package/dist/workspace-revision.js +3 -1
  77. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.79 — 2026-10-29
4
+
5
+ - **`toolroll release` rides out a short network failure instead of
6
+ stopping.** A read from GitHub or git (`gh api`, `gh pr list`,
7
+ `git ls-remote`, `git fetch`) that fails for a passing reason (no
8
+ connection, a name that didn't resolve, a timeout, a 5xx, an answer cut
9
+ short, GitHub's secondary rate limit) is asked again with backoff for up
10
+ to about three minutes, saying why once; then it stops with the original
11
+ error. A definitive answer (404, 401/403) is never asked again. A merge or
12
+ a tag is never sent twice blindly: after a lost answer the release reads
13
+ GitHub to learn whether it happened, and asks again only when it didn't.
14
+ When main's branch protection can't be read, the release now says
15
+ "Couldn't read GitHub's branch protection" instead of "main's protection
16
+ doesn't hold", and still stops.
17
+
18
+ ## 0.9.78 — 2026-10-29
19
+
20
+ - **Toolroll says when it is restarting.** The worker and console now record
21
+ when they start, keep a heartbeat every 15 seconds, and record when they
22
+ begin stopping and when they have stopped. While one is stopping it takes no
23
+ new work, `/healthz` answers "draining" at once, pages say "Toolroll is
24
+ restarting", and a new start waits only until the old one has finished
25
+ stopping instead of a fixed window. A worker that stopped cleanly never
26
+ holds its projects. A second live `up` is still refused.
27
+ - **The database is copied every few minutes.** Besides scheduled backups,
28
+ the worker copies the database online every 5 minutes (change it, or the
29
+ folder, in Settings → Backups), checks each copy and keeps the newest 24
30
+ and one a day for a week. It never holds up the console or the worker. A
31
+ deploy uses the latest copy when it is recent and matches the database
32
+ exactly, instead of making a new one. Restoring stays a manual
33
+ `toolroll restore`.
34
+ - **See where the time went.** A task's page has a "Where the time went"
35
+ section under Details: when each part of its release check started and
36
+ ended, and each step of its deploys, including what a deploy waited for.
37
+ Console pages that take over a second keep a short record of where the
38
+ time went, which an instance operator reads on `/health`. Only page names,
39
+ step names and reasons are kept, never addresses or form contents.
40
+ - **A crashed worker is noticed in a minute, not three.** Workers check in
41
+ every 15 seconds and are treated as gone after a minute of silence. A
42
+ project lease is never taken for longer than a minute.
43
+ - **A release's version bump alone no longer owes the upgrade path.** Toolroll
44
+ now decides from both commits' verified files that package.json and
45
+ package-lock.json changed only in the project's own version (the check's
46
+ own rule); any other change to them still owes it.
47
+ - **The macOS suite runs on the candidate before it merges.** Pull requests
48
+ and main pushes now run the unit suite on hosted macOS with Node 22 and 24,
49
+ in four shards each, joined into `node 22 · macos-latest` and
50
+ `node 24 · macos-latest` exactly like the Linux checks. `toolroll release`
51
+ waits for them; main's protection not requiring them yet is reported, not
52
+ refused. After the merge, publication waits only for the tag's publish job,
53
+ no longer for main's own CI run on the merge commit. The nightly and tag
54
+ macOS jobs are renamed so no other workflow shares a required name.
55
+ - **A release is published without waiting for Homebrew.** `toolroll release`
56
+ ends with "Published; Homebrew pending" once npm and the GitHub release are
57
+ verified, instead of waiting up to 45 minutes for the tap's checks. Homebrew
58
+ is its own state in the release journal (pending, failed or current), and
59
+ running the same command again finishes it: it waits for the tap change's
60
+ checks, then merges exactly that change, with the checksum still taken from
61
+ npm's tarball. A tap already on a newer version is never moved back, checked
62
+ again right before the merge.
63
+ - **A release whose checks run outside the sandbox now reruns its own check
64
+ after Toolroll's machine fails.** Such a release can't carry later commits,
65
+ so it used to file without release terms, and a check stopped by Toolroll's
66
+ own shutdown, a lost build or a command that never started waited for a
67
+ person. Its approval now seals a release check purpose instead: the check
68
+ is held to the release's rules (a complete report, no CI standing in for
69
+ its units), and only a failure Toolroll itself confirmed as its own
70
+ infrastructure reruns, on the same commit under the same approval, at most
71
+ twice. Another commit, a changed scope, release check or check access, or
72
+ an approval that no longer stands never reruns. It never carries a later
73
+ commit. (Schema v138 adds the column; saved scopes are unchanged.)
74
+ - **A deploy no longer gets stuck behind an `up` that won't stop.** When the
75
+ old service's processes outlive their stop (a supervisor that exited while
76
+ its `up` ignored SIGTERM), the deployment stops them itself, but only
77
+ processes proved, by exact runtime path and arguments, to be that service's
78
+ supervisor, `up` or console and worker. Anything it can't prove refuses and
79
+ nothing is signalled.
80
+ - **A killed `up` no longer blocks the next start for three minutes.** `up`
81
+ records each process it gives its worker credential to. A new start that
82
+ proves every one of them has exited signs the old worker off and takes over
83
+ at once; a second live `up` is still refused.
84
+ - **`up` signs off however it is stopped.** SIGTERM, Ctrl-C, its supervisor
85
+ dying or its channel closing all end with the worker retired. A stop that
86
+ hangs past the worker's grace stops the console and worker outright
87
+ instead of waiting for launchd to kill it.
88
+ - **Deployments wait long enough, and say why.** Restoring the previous
89
+ service, starting the new one and waiting for its project leases now allow
90
+ two minutes (past a stale worker heartbeat's one) and name what they are
91
+ waiting for. "Current work must finish first" now says what is running and
92
+ what to do.
93
+ - **One command puts back a deployment that stopped half way.**
94
+ `toolroll release --recover-deploy` shows what recovery will do for the
95
+ unfinished deployment; `--yes` does it: stops only that service's proved
96
+ leftovers, starts the previous service again and lifts the update pause.
97
+ It is safe to run again.
98
+
3
99
  ## 0.9.77 — 2026-10-29
4
100
 
5
101
  - **CI finishes in about a third of the time.** The unit suite runs in four
@@ -33,6 +33,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
33
33
  import { readMonitoring } from "./monitoring-settings.js";
34
34
  import { existingFolders } from "./names.js";
35
35
  import { ensureGitViewsRoot, gitViewsRoot } from "./git-views.js";
36
+ import { ensureRunnerProcessFolder, RUNNER_PROCESS_DIR } from "./runner-process.js";
36
37
  import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
37
38
  import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
38
39
  import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
@@ -167,6 +168,21 @@ export function agentFence(options) {
167
168
  catch { /* fenced below either way */ }
168
169
  }
169
170
  fence.add(runFolder);
171
+ const runnerFolder = join(state, RUNNER_PROCESS_DIR);
172
+ try {
173
+ ensureRunnerProcessFolder(runnerFolder);
174
+ }
175
+ catch { /* both the name and its target stay denied */ }
176
+ fence.add(runnerFolder);
177
+ fence.add(real(runnerFolder));
178
+ // A copy may first appear after launch. Fence its permanent namespace even when empty.
179
+ const backups = join(state, "backups");
180
+ try {
181
+ mkdirSync(backups, { mode: 0o700 });
182
+ }
183
+ catch { /* name and target remain denied */ }
184
+ fence.add(backups);
185
+ fence.add(real(backups));
170
186
  // Only a folder that is Toolroll's own (~/.config/toolroll or an older name,
171
187
  // the desktop app's "Toolroll") is fenced whole: a database kept
172
188
  // in a shared folder (a projects folder, a test's temp folder) must not
@@ -214,7 +230,7 @@ export function agentFence(options) {
214
230
  // Secret, state, database, explicit-extra and home-config denies are never exemptible.
215
231
  for (const path of options.boundary?.protectedShell ?? [])
216
232
  fence.add(real(path));
217
- const protectedPaths = new Set([...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]));
233
+ const protectedPaths = new Set([...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]));
218
234
  // Protected storage (chat apps, browser profiles, password managers): every launch, every policy, never exemptible,
219
235
  // kept by the name it was resolved under as well as where it leads.
220
236
  const storage = options.protectedStorage ?? protectedStorageDenies(home);
@@ -226,7 +242,7 @@ export function agentFence(options) {
226
242
  fence.add(path);
227
243
  const binding = options.binding;
228
244
  const needed = binding === undefined || binding === null ? [] : [binding.gitdir, real(join(binding.main, ".git"))];
229
- return [...new Set([...[...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
245
+ return [...new Set([...[...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
230
246
  if (protectedPaths.has(path) || !needed.some(target => target === path || inside(target, path)))
231
247
  return true;
232
248
  console.warn(`Dropped build fence entry covering this checkout's own Git data: ${path}`);
@@ -4,9 +4,11 @@
4
4
  * disclosures.
5
5
  */
6
6
  import { type Html } from "./html.js";
7
- import type { BackupRun, BackupSettings } from "./store.js";
7
+ import type { BackupRun, BackupSettings, LiveBackupState } from "./store.js";
8
8
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
9
9
  export declare const BACKUP_EVERY_HOURS: readonly [1, 6, 12, 24];
10
+ /** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
11
+ export declare const LIVE_EVERY: readonly [1, 5, 15, 30];
10
12
  export declare const MAX_KEEP = 100;
11
13
  export declare const BACKUP_CSS: string;
12
14
  export type BackupView = {
@@ -16,6 +18,12 @@ export type BackupView = {
16
18
  runs: BackupRun[];
17
19
  csrf: string;
18
20
  now: Date;
21
+ /** The continuous copy (live-backup.ts): its settings, latest verified copy and folder. */
22
+ live?: {
23
+ state: LiveBackupState;
24
+ folder: string;
25
+ defaultFolder: string;
26
+ };
19
27
  };
20
28
  export declare function backupHtml(view: BackupView, notice: {
21
29
  said?: string | null;
package/dist/backup-ui.js CHANGED
@@ -8,6 +8,8 @@ import { bytesWords } from "./storage.js";
8
8
  import { whenUtc } from "./when-html.js";
9
9
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
10
10
  export const BACKUP_EVERY_HOURS = [1, 6, 12, 24];
11
+ /** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
12
+ export const LIVE_EVERY = [1, 5, 15, 30];
11
13
  export const MAX_KEEP = 100;
12
14
  const when = (at) => whenUtc(at);
13
15
  const whenWords = (at) => `${at.slice(0, 16).replace("T", " ")} UTC`;
@@ -23,6 +25,19 @@ export const BACKUP_CSS = `.backups{max-width:720px;min-width:0;overflow-wrap:an
23
25
  `.backups li{display:flex;gap:8px 12px;flex-wrap:wrap;padding:8px 0;border-top:1px solid var(--line, rgba(0,0,0,.08));font-size:.875rem}` +
24
26
  `.backups li .failed{color:var(--danger)}.backups code{white-space:nowrap}.backups pre{overflow-x:auto;margin:8px 0}` +
25
27
  `@media (max-width:900px){.backups .backup-state form,.backups .backup-state button{width:100%}.backups .pair{grid-template-columns:1fr}}`;
28
+ /** The continuous copy in one line: its latest verified copy, or why there is none. */
29
+ function liveLine(view) {
30
+ const live = view.live;
31
+ if (live === undefined)
32
+ return "";
33
+ if (!live.state.settings.enabled)
34
+ return html `<p class="meta">Continuous copy is off.</p>`;
35
+ if (live.state.problem !== null)
36
+ return html `<p class="problem" role="alert">The continuous copy failed: ${live.state.problem}</p>`;
37
+ if (live.state.latest === null)
38
+ return html `<p class="meta">The first continuous copy is made within a few minutes of Toolroll starting.</p>`;
39
+ return html `<p class="meta" data-live-copy>Continuous copy checked ${when(live.state.latest.madeAt)}, every ${live.state.settings.everyMinutes === 1 ? "minute" : `${live.state.settings.everyMinutes} minutes`}.</p>`;
40
+ }
26
41
  /** The state in one line, and the next backup (if any), for the top of the page. */
27
42
  function state(view) {
28
43
  const last = view.runs[0];
@@ -31,13 +46,13 @@ function state(view) {
31
46
  : good === undefined ? "The first scheduled backup runs within a minute or two of the console starting."
32
47
  : `Next one about ${whenWords(new Date(Date.parse(good.startedAt) + view.settings.everyHours * 3_600_000).toISOString())}.`;
33
48
  if (last === undefined)
34
- return html `<h2>No backups yet</h2><p class="meta">${next}</p>`;
49
+ return html `<h2>No backups yet</h2><p class="meta">${next}</p>${liveLine(view)}`;
35
50
  if (last.ok === null)
36
51
  return html `<h2>Backing up</h2><p class="meta">Started ${when(last.startedAt)}.</p>`;
37
52
  if (last.ok === false) {
38
53
  return html `<h2 data-backup-state="failed">Last backup failed</h2><p class="problem" role="alert">${when(last.startedAt)}: ${last.error ?? "no reason was recorded"}</p><p class="meta">${good === undefined ? "No backup has succeeded yet." : html `Last good backup ${when(good.startedAt)}.`} ${next}</p>`;
39
54
  }
40
- return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>`;
55
+ return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>${liveLine(view)}`;
41
56
  }
42
57
  export function backupHtml(view, notice) {
43
58
  const s = view.settings;
@@ -46,5 +61,15 @@ export function backupHtml(view, notice) {
46
61
  ...BACKUP_EVERY_HOURS.map(hours => html `<option value="${hours}"${s.enabled && s.everyHours === hours ? html ` selected` : ""}>${everyWords(hours)}</option>`)];
47
62
  const runs = view.runs.map(run => html `<li data-backup-run="${run.id}"><span>${when(run.startedAt)}</span>${run.ok === true ? html `<span>Succeeded${run.bytes === null ? "" : ` · ${bytesWords(run.bytes)}`}</span>${run.file === null ? "" : html `<span class="mono meta">${fileName(run.file)}</span>`}`
48
63
  : run.ok === false ? html `<span class="failed">Failed: ${run.error ?? "no reason recorded"}</span>` : html `<span>Running</span>`}<span class="meta">${run.trigger === "manual" ? "by hand" : "scheduled"}</span></li>`);
49
- return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p></details></section>`;
64
+ return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${liveFields(view)}${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p>${view.live === undefined ? "" : html `<p class="meta">Continuous copies are in <span class="mono">${view.live.folder}</span>; restore one the same way.</p>`}</details></section>`;
65
+ }
66
+ /** The continuous copy's two choices, beside the schedule's: how often, and where (blank: beside the database). */
67
+ function liveFields(view) {
68
+ const live = view.live;
69
+ if (live === undefined)
70
+ return "";
71
+ const on = live.state.settings;
72
+ const choices = [html `<option value="off"${on.enabled ? "" : html ` selected`}>Off</option>`,
73
+ ...LIVE_EVERY.map(minutes => html `<option value="${minutes}"${on.enabled && on.everyMinutes === minutes ? html ` selected` : ""}>${minutes === 1 ? "Every minute" : `Every ${minutes} minutes`}</option>`)];
74
+ return html `<div class="pair"><label>Copy continuously<select name="live_every">${choices}</select></label><label>Copy folder<input type="text" name="live_folder" value="${on.folder ?? ""}" placeholder="${live.defaultFolder}" spellcheck="false"></label></div>`;
50
75
  }
package/dist/backup.js CHANGED
@@ -19,6 +19,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
19
19
  import { createRequire } from "node:module";
20
20
  import { BACKUP_EVERY_HOURS, MAX_KEEP } from "./backup-ui.js";
21
21
  import { verifyLedgerChain } from "./ledger-chain.js";
22
+ import { DEFAULT_LIVENESS_MS } from "./runner.js";
22
23
  import { openStore, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
23
24
  /** A scheduled backup's name: the database it came from (see `databaseTag`), then when it was made. Backups made
24
25
  * before the tag was added have none. Retention only ever removes files named like this. */
@@ -28,8 +29,8 @@ export const OLDEST_RESTORABLE = 103;
28
29
  export { BACKUP_EVERY_HOURS, MAX_KEEP };
29
30
  /** How long the console's lease lasts without a renewal: a console that stopped holds it no longer than this. */
30
31
  const LEASE_MS = 3 * 60_000;
31
- /** A worker that heartbeat this recently is taken to be running. */
32
- const RUNNER_FRESH_MS = 3 * 60_000;
32
+ /** A worker that heartbeat this recently is taken to be running (runner.ts DEFAULT_LIVENESS_MS). */
33
+ const RUNNER_FRESH_MS = DEFAULT_LIVENESS_MS;
33
34
  export const defaultBackupFolder = (databaseFile) => join(dirname(databaseFile), "backups");
34
35
  export const backupFolderOf = (settings, databaseFile) => settings.folder ?? defaultBackupFolder(databaseFile);
35
36
  /** 2026-09-28T05:34:39.123Z → 2026-09-28-053439 (UTC). */
@@ -287,7 +288,7 @@ export async function restoreDatabase(options) {
287
288
  try {
288
289
  const running = runningNow(store, now);
289
290
  if (running.length > 0)
290
- refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a few minutes.`);
291
+ refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a minute.`);
291
292
  folder = backupFolderOf(store.backupSettings(), databaseFile);
292
293
  }
293
294
  finally {