toolroll 0.9.76 → 0.9.78

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 (144) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +46 -10
  5. package/dist/assignment.js +3 -0
  6. package/dist/backup-ui.d.ts +9 -1
  7. package/dist/backup-ui.js +28 -3
  8. package/dist/backup.js +4 -3
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  10. package/dist/browser/workspace.css +2 -2
  11. package/dist/browser/workspace.js +31 -31
  12. package/dist/browser-workspace.d.ts +2 -0
  13. package/dist/builder.d.ts +18 -3
  14. package/dist/builder.js +152 -47
  15. package/dist/chat-decide.js +4 -0
  16. package/dist/check-failure.d.ts +92 -0
  17. package/dist/check-failure.js +295 -0
  18. package/dist/check-progress.d.ts +28 -1
  19. package/dist/check-progress.js +105 -6
  20. package/dist/check-report.d.ts +67 -6
  21. package/dist/check-report.js +254 -9
  22. package/dist/checkout-cleanup.js +2 -2
  23. package/dist/claim.d.ts +9 -0
  24. package/dist/claim.js +9 -1
  25. package/dist/cli.js +8 -5
  26. package/dist/coding-handoff.d.ts +1 -0
  27. package/dist/coding-handoff.js +20 -11
  28. package/dist/commit-extract.d.ts +6 -0
  29. package/dist/commit-extract.js +63 -8
  30. package/dist/console-chrome.d.ts +41 -0
  31. package/dist/console-chrome.js +166 -0
  32. package/dist/contracts/console-api.d.ts +1 -1
  33. package/dist/contracts/console-api.js +1 -1
  34. package/dist/contracts/release-check.d.ts +12 -0
  35. package/dist/contracts/release-check.js +17 -0
  36. package/dist/contracts/release-terms.d.ts +41 -0
  37. package/dist/contracts/release-terms.js +53 -0
  38. package/dist/contracts/verification-receipt.d.ts +20 -2
  39. package/dist/contracts/verification-receipt.js +15 -3
  40. package/dist/contracts/verification-view.d.ts +8 -0
  41. package/dist/controller-service.js +4 -1
  42. package/dist/controller-supervisor.d.ts +8 -0
  43. package/dist/controller-supervisor.js +33 -1
  44. package/dist/demo.js +3 -2
  45. package/dist/deploy-gate.js +5 -0
  46. package/dist/deploy-rescue.d.ts +41 -0
  47. package/dist/deploy-rescue.js +94 -0
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/dispose.d.ts +3 -0
  50. package/dist/dispose.js +9 -5
  51. package/dist/doctor.d.ts +7 -0
  52. package/dist/doctor.js +10 -2
  53. package/dist/envelope.js +2 -1
  54. package/dist/evidence.d.ts +9 -0
  55. package/dist/evidence.js +19 -4
  56. package/dist/exec.d.ts +9 -0
  57. package/dist/exec.js +22 -2
  58. package/dist/git-floor.d.ts +16 -0
  59. package/dist/git-floor.js +36 -0
  60. package/dist/grant.js +5 -1
  61. package/dist/guides.js +22 -11
  62. package/dist/latency-timeline.d.ts +46 -0
  63. package/dist/latency-timeline.js +120 -0
  64. package/dist/live-backup.d.ts +39 -0
  65. package/dist/live-backup.js +271 -0
  66. package/dist/operate.d.ts +13 -0
  67. package/dist/operate.js +969 -328
  68. package/dist/planner.js +2 -2
  69. package/dist/prepared-evidence.d.ts +2 -1
  70. package/dist/prepared-evidence.js +8 -3
  71. package/dist/project-knowledge.js +6 -2
  72. package/dist/project-learning.js +2 -2
  73. package/dist/proof.js +6 -0
  74. package/dist/read-executor-worker.js +8 -4
  75. package/dist/read-executor.d.ts +18 -1
  76. package/dist/read-executor.js +8 -0
  77. package/dist/release-adapters.d.ts +80 -2
  78. package/dist/release-adapters.js +441 -16
  79. package/dist/release-authority.js +6 -2
  80. package/dist/release-auto.d.ts +38 -7
  81. package/dist/release-auto.js +131 -15
  82. package/dist/release-card.d.ts +15 -3
  83. package/dist/release-card.js +25 -9
  84. package/dist/release-carry.d.ts +21 -1
  85. package/dist/release-carry.js +22 -0
  86. package/dist/release-ci-hold.d.ts +41 -0
  87. package/dist/release-ci-hold.js +53 -0
  88. package/dist/release-cli.d.ts +3 -0
  89. package/dist/release-cli.js +95 -17
  90. package/dist/release-coverage.js +5 -2
  91. package/dist/release-gate.d.ts +5 -0
  92. package/dist/release-gate.js +1 -1
  93. package/dist/release-recheck.d.ts +1 -1
  94. package/dist/release-recheck.js +8 -2
  95. package/dist/release.d.ts +144 -14
  96. package/dist/release.js +421 -81
  97. package/dist/review-context.js +8 -6
  98. package/dist/runner-process.d.ts +67 -0
  99. package/dist/runner-process.js +219 -0
  100. package/dist/runner.d.ts +48 -6
  101. package/dist/runner.js +73 -9
  102. package/dist/sandbox-runtime.d.ts +8 -0
  103. package/dist/sandbox-runtime.js +14 -2
  104. package/dist/scope.d.ts +19 -1
  105. package/dist/scope.js +26 -3
  106. package/dist/scout.js +2 -2
  107. package/dist/secret-mint.d.ts +1 -0
  108. package/dist/secret-mint.js +14 -0
  109. package/dist/serve.js +112 -51
  110. package/dist/server/chat.js +5 -5
  111. package/dist/server/http.d.ts +2 -0
  112. package/dist/server/pages.js +7 -3
  113. package/dist/server/remote-hooks.d.ts +1 -1
  114. package/dist/server/remote-hooks.js +5 -3
  115. package/dist/server/render-results.d.ts +2 -0
  116. package/dist/server/render-results.js +1 -0
  117. package/dist/server/render-tasks.d.ts +2 -0
  118. package/dist/server/render-tasks.js +2 -1
  119. package/dist/server/request-context.d.ts +8 -0
  120. package/dist/server/runtime.d.ts +2 -0
  121. package/dist/server/settings.js +22 -2
  122. package/dist/server/task-views.js +7 -2
  123. package/dist/server/tasks.js +4 -1
  124. package/dist/setup-guide.d.ts +2 -2
  125. package/dist/storage-sweep.js +1 -1
  126. package/dist/storage.d.ts +5 -0
  127. package/dist/storage.js +28 -1
  128. package/dist/store.d.ts +135 -2
  129. package/dist/store.js +275 -13
  130. package/dist/surface.js +3 -1
  131. package/dist/task-base.d.ts +4 -0
  132. package/dist/task-base.js +22 -2
  133. package/dist/teardown-git.d.ts +52 -3
  134. package/dist/teardown-git.js +145 -33
  135. package/dist/test-infra.js +1 -1
  136. package/dist/toolroll-update.js +2 -2
  137. package/dist/up-processes.d.ts +137 -0
  138. package/dist/up-processes.js +165 -0
  139. package/dist/up-role-child.d.ts +1 -0
  140. package/dist/up-role-child.js +81 -0
  141. package/dist/verification-evidence.d.ts +1 -0
  142. package/dist/verification-evidence.js +13 -1
  143. package/dist/workspace-revision.js +3 -1
  144. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
2
  import { networkWords } from "../egress.js";
3
3
  import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
4
+ import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
4
5
  import { writesWords } from "../writes-mode.js";
5
6
  import { checkAccessWords } from "../check-access.js";
6
7
  import { sandboxTermsWords } from "../project-sandbox.js";
@@ -1501,7 +1502,7 @@ export function taskBodyParts(data) {
1501
1502
  scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.join(", ")}</p>`,
1502
1503
  html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
1503
1504
  acceptanceCeremonyHtml(scope.acceptance),
1504
- scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
1505
+ scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
1505
1506
  approval.approved && data.autoRelease != null
1506
1507
  ? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
1507
1508
  : approval.approved && data.release?.kind === "carried"
@@ -18,14 +18,22 @@ export type RequestFacts = {
18
18
  returnTo: string;
19
19
  actor?: string;
20
20
  createdTask?: string;
21
+ /** This request's latency timeline (latency-timeline.ts): kept when the answer was slow. */
22
+ timeline?: import("../latency-timeline.js").Timeline;
21
23
  browser?: boolean;
22
24
  workspaceRead?: boolean;
25
+ /** Receipt reconciliation, task and result reads (`request`, `task`, `result`): work is read afresh, never cached. */
26
+ freshWork?: boolean;
27
+ /** A workspace re-read's If-None-Match: the content validator it holds from its last read. */
28
+ workspaceIfNoneMatch?: string;
23
29
  workspaceRequest?: string | null;
24
30
  workCounts?: ReturnType<typeof workCountsByProject>;
25
31
  workCrew?: {
26
32
  project: string | null;
27
33
  page: WorkIndexPage;
28
34
  };
35
+ /** The chrome this request already read (console-chrome.ts keys): one snapshot per request, however often it asks. */
36
+ chromeReads?: Map<string, unknown>;
29
37
  lens?: string | null;
30
38
  workspaceValidator?: {
31
39
  key: string;
@@ -153,6 +153,8 @@ export interface ServerRuntime {
153
153
  saturated: boolean;
154
154
  };
155
155
  liveRefreshSeconds: () => number;
156
+ /** One work index page for an already-admitted reader, read at most once per workspace revision (console-chrome.ts). */
157
+ sharedIndexPage: (access: import("../work-summary.js").WorkSummaryAccess, options: import("../work-index.js").WorkIndexOptions) => import("../work-index.js").WorkIndexPage;
156
158
  chatKeyFor: (provider: DirectChatProviderId) => {
157
159
  key: string;
158
160
  source: "environment" | "stored";
@@ -117,7 +117,9 @@ import { sweepSchedule } from "../storage-sweep-schedule.js";
117
117
  import { storageHtml } from "../storage-ui.js";
118
118
  import { landedSummary } from "../landed-cleanup.js";
119
119
  import { bytesWords, parseCleanup } from "../storage.js";
120
- import { isDigestTime, RESULT_SCREENSHOTS } from "../store.js";
120
+ import { isDigestTime, liveBackupState, RESULT_SCREENSHOTS, setLiveBackupSettings } from "../store.js";
121
+ import { defaultLiveFolder, liveFolderOf, liveFolderProblem } from "../live-backup.js";
122
+ import { LIVE_EVERY } from "../backup-ui.js";
121
123
  import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
122
124
  import { teamsSettingsHtml } from "../teams-settings.js";
123
125
  import { telegramSettingsHtml } from "../telegram-settings.js";
@@ -497,7 +499,9 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
497
499
  if (databaseFile === null)
498
500
  return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
499
501
  const settings = store.backupSettings();
500
- const view = { settings, folder: backupFolderOf(settings, databaseFile), defaultFolder: defaultBackupFolder(databaseFile), runs: store.backupRuns(20), csrf: who.session.csrf, now };
502
+ const live = liveBackupState(store);
503
+ const view = { settings, folder: backupFolderOf(settings, databaseFile), defaultFolder: defaultBackupFolder(databaseFile), runs: store.backupRuns(20), csrf: who.session.csrf, now,
504
+ live: { state: live, folder: liveFolderOf(live.settings, databaseFile), defaultFolder: defaultLiveFolder(databaseFile) } };
501
505
  return sendScreen(response, 200, screen("Backups", html `<p><a href="/settings">Settings</a></p><h1>Backups</h1>${(backupHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
502
506
  }
503
507
  // v105: Settings → Data, where an instance operator downloads everything. The download is a POST with the password.
@@ -1325,8 +1329,24 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1325
1329
  if (problem !== null)
1326
1330
  return back("problem", problem);
1327
1331
  }
1332
+ // The continuous copy (live-backup.ts): how often, and a folder that is a plain, private folder of this user's
1333
+ // outside every project and checkout. A form without these fields leaves it as it is.
1334
+ const liveBefore = liveBackupState(store).settings;
1335
+ const liveEvery = body.get("live_every") ?? null;
1336
+ const liveMinutes = Number(liveEvery);
1337
+ if (liveEvery !== null && liveEvery !== "off" && !LIVE_EVERY.includes(liveMinutes))
1338
+ return back("problem", "Choose how often to copy continuously.");
1339
+ const liveTyped = body.get("live_folder") === null ? liveBefore.folder ?? "" : (body.get("live_folder") ?? "").trim();
1340
+ const liveFolder = liveTyped === "" || liveTyped === defaultLiveFolder(databaseFile) ? null : liveTyped;
1341
+ if (liveFolder !== null) {
1342
+ const problem = liveFolderProblem(liveFolder, [...new Set([...managedRepos(), ...store.knownRepos(), ...(options.poolRoot === undefined ? [] : [options.poolRoot])])]);
1343
+ if (problem !== null)
1344
+ return back("problem", problem);
1345
+ }
1328
1346
  const before = store.backupSettings();
1329
1347
  store.setBackupSettings({ enabled: every !== "off", everyHours: every === "off" ? before.everyHours : hours, keep, folder }, who.name, now);
1348
+ setLiveBackupSettings(store, { enabled: liveEvery === null ? liveBefore.enabled : liveEvery !== "off", everyMinutes: liveEvery === null || liveEvery === "off" ? liveBefore.everyMinutes : liveMinutes,
1349
+ keep: liveBefore.keep, folder: liveFolder }, who.name, now);
1330
1350
  return back("said", every === "off" ? "Saved. Scheduled backups are off." : "Saved.");
1331
1351
  }
1332
1352
  // v105: download everything as a .zip. An instance operator, with a step-up; the ledger records it.
@@ -1,9 +1,10 @@
1
1
  import { releaseViewOf } from "../release-view.js";
2
+ import { releaseCiRunningOf } from "../release-ci-hold.js";
2
3
  import { releaseObligationsOf } from "../release-recheck.js";
3
4
  import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
4
5
  import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
5
6
  import { originOf } from "../task-source.js";
6
- import { taskBaseOf } from "../task-base.js";
7
+ import { recentGitReads, taskBaseOf } from "../task-base.js";
7
8
  import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
8
9
  import { html, joinHtml } from "../html.js";
9
10
  import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
@@ -53,6 +54,9 @@ import {} from "../store.js";
53
54
  import { taskControlOf } from "../task-control.js";
54
55
  import { lastErrorLineOf, missedRequirementOf, workStatusOf } from "../workspace-ui.js";
55
56
  export function createTaskViews(runtime) {
57
+ // The task page's base line reads Git through a frozen view of the project (~0.2-0.5 s on a large repository) and
58
+ // the page renders it twice per view and again on every live refresh: its answers are kept for a moment.
59
+ const baseGit = recentGitReads();
56
60
  /** The first task's way to Ready shows until the first Ready result, and stays on the installation's first task. */
57
61
  function guided(rootId, now) {
58
62
  if (runtime.store.isDemo())
@@ -491,12 +495,13 @@ export function createTaskViews(runtime) {
491
495
  return { rows, decisions: decisionsFor(runtime.store, rows), now };
492
496
  })(),
493
497
  steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
494
- base: taskBaseOf(runtime.store, taskId),
498
+ base: taskBaseOf(runtime.store, taskId, baseGit),
495
499
  steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
496
500
  steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
497
501
  release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
498
502
  canStopRelease: who.role === "approver" && who.via === "cookie",
499
503
  autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
504
+ releaseCiRunning: ref !== null && releaseCiRunningOf(runtime.store, ref.externalId) !== null,
500
505
  approvalProof: (() => {
501
506
  // The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
502
507
  const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
@@ -66,6 +66,7 @@ import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, t
66
66
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
67
67
  import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
68
68
  import { runEfficiencyOf } from "../run-efficiency.js";
69
+ import { timingFor } from "../latency-timeline.js";
69
70
  import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
70
71
  import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
71
72
  import {} from "./session.js";
@@ -2837,7 +2838,7 @@ export function createTasksHandlers(runtime) {
2837
2838
  ...(store.isDemo() ? { demo: true } : {}),
2838
2839
  };
2839
2840
  if (run === null) {
2840
- return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
2841
+ return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [], timing: timingFor(store, null, row.taskId) };
2841
2842
  }
2842
2843
  const detail = resultDetailOf(run, who, now);
2843
2844
  return {
@@ -2866,6 +2867,8 @@ export function createTasksHandlers(runtime) {
2866
2867
  } })(),
2867
2868
  // What its network was and what its proxy saw (egress.ts), display only.
2868
2869
  network: store.runNetwork(run.id),
2870
+ // Where the time went: its release check's parts and the task's deploys (latency-timeline.ts), display only.
2871
+ timing: timingFor(store, run.id, row.taskId),
2869
2872
  // How far its checks reached (check-access.ts), display only.
2870
2873
  checkAccess: store.runCheckAccess(run.id),
2871
2874
  reviewRetry: store.reviewRetryStateOf(run.id),
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
- content?: never;
19
18
  ok: false;
20
19
  message: string;
21
20
  plan?: never;
21
+ content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
- content?: never;
34
33
  ok: false;
35
34
  message: string;
36
35
  plan?: never;
36
+ content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
@@ -8,7 +8,7 @@
8
8
  * Nothing live work uses goes (`liveWork`): while a run is open, a lease current or a checkout in use, each removal
9
9
  * and each signal first asks again whether that work uses the folder, and when its processes can't be read nothing
10
10
  * goes. Every scan and removal runs in a separate child: synchronous disk work must never delay the worker's lease
11
- * heartbeats (90 s). On Oct 12 the sweep starved those heartbeats and three builds ended interrupted half a second
11
+ * heartbeats (a lease lasts a minute). On Oct 12 the sweep starved those heartbeats and three builds ended interrupted half a second
12
12
  * after it finished. Once a run has ended, the next sweep removes what
13
13
  * it no longer uses.
14
14
  *
package/dist/storage.d.ts CHANGED
@@ -36,3 +36,8 @@ export declare function storageReport(store: Store, databaseFile: string, checko
36
36
  lines: StorageLine[];
37
37
  };
38
38
  export declare function bytesWords(bytes: number): string;
39
+ /** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
40
+ * measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
41
+ export declare function diskBytesAsync(paths: readonly string[], run?: typeof duOnce): Promise<Map<string, number>>;
42
+ /** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
43
+ declare function duOnce(batch: readonly string[]): Promise<string>;
package/dist/storage.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * what slips through: stale test temp folders, leftover processes, extra
8
8
  * staged runtimes and dependency installs unused for a week.
9
9
  */
10
- import { spawnSync } from "node:child_process";
10
+ import { execFile, spawnSync } from "node:child_process";
11
11
  import { lstatSync, readdirSync } from "node:fs";
12
12
  import { basename, dirname, join } from "node:path";
13
13
  import { DEPS_FOLDER, sharedCopies } from "./shared-deps.js";
@@ -190,3 +190,30 @@ export function bytesWords(bytes) {
190
190
  return `${Math.round(bytes / 1024 ** 2)} MB`;
191
191
  return `${Math.round(bytes / 1024)} KB`;
192
192
  }
193
+ /** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
194
+ * measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
195
+ export async function diskBytesAsync(paths, run = duOnce) {
196
+ const sizes = new Map();
197
+ if (paths.length === 0)
198
+ return sizes;
199
+ if (process.platform !== "win32") {
200
+ for (let at = 0; at < paths.length; at += 200) {
201
+ const batch = paths.slice(at, at + 200);
202
+ for (const line of (await run(batch)).split("\n")) {
203
+ const match = /^(\d+)\t(.+)$/.exec(line);
204
+ if (match !== null && batch.includes(match[2]))
205
+ sizes.set(match[2], Number(match[1]) * 1024);
206
+ }
207
+ }
208
+ }
209
+ for (const path of paths)
210
+ if (!sizes.has(path))
211
+ sizes.set(path, treeBytes(path));
212
+ return sizes;
213
+ }
214
+ /** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
215
+ function duOnce(batch) {
216
+ return new Promise(resolve => {
217
+ execFile("du", ["-sk", ...batch], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 60_000 }, (_error, stdout) => resolve(typeof stdout === "string" ? stdout : ""));
218
+ });
219
+ }
package/dist/store.d.ts CHANGED
@@ -26,7 +26,7 @@ import type { Scope } from "./scope.js";
26
26
  import type { QualityMode } from "./quality.js";
27
27
  import type { ProgressSnapshot } from "./plan.js";
28
28
  import { type CheckProgressSnapshot } from "./check-progress.js";
29
- import { type ReleaseTerms } from "./contracts/release-terms.js";
29
+ import { type ReleaseCheckPurpose, type ReleaseTerms } from "./contracts/release-terms.js";
30
30
  import { type ProjectAccess } from "./project-access.js";
31
31
  import { type LedgerEntry } from "./action-ledger.js";
32
32
  import type { LimitReading, LimitWindow } from "./provider-limits.js";
@@ -106,6 +106,10 @@ export declare const LIVE_STEER_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_pause
106
106
  /** v136: a run's own "Show commands in the live view" answer, captured once before its live file opens (live-detail.ts).
107
107
  * Absent, unreadable or anything but 1 means off. */
108
108
  export declare const RUN_LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_live_detail (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n captured_at TEXT NOT NULL\n);\n-- The Agents view's Live grid (docs/plans/agents-view.md \u00A72.5): every role's running runs and its recently ended ones.\n-- (id > 0 keeps them to the grid's own query, which names it: no other query's plan is offered them.)\nCREATE INDEX IF NOT EXISTS run_live_open ON run (id) WHERE outcome IS NULL AND id > 0;\nCREATE INDEX IF NOT EXISTS run_live_recent ON run (finished_at) WHERE outcome IS NOT NULL AND id > 0;\n";
109
+ /** v137 (docs/plans/actors-lessons.md): explicit lifecycle, latency timelines and the continuous database copy. New
110
+ * tables only. A lifecycle row's `holder` binds it to one registration (a worker's registered_at) or one console
111
+ * process; a row whose holder no longer matches says nothing. Timelines are bounded and pruned as they are written. */
112
+ export declare const V137_SCHEMA = "\nCREATE TABLE IF NOT EXISTS service_lifecycle (\n component TEXT PRIMARY KEY,\n state TEXT NOT NULL CHECK (state IN ('started','draining','stopped')),\n holder TEXT NOT NULL,\n pid INTEGER,\n changed_at TEXT NOT NULL,\n beat_at TEXT NOT NULL,\n drain_until TEXT\n);\nCREATE TABLE IF NOT EXISTS latency_timeline (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('console','release-check','deploy')),\n subject TEXT NOT NULL,\n task_id TEXT,\n run_id INTEGER,\n started_at TEXT NOT NULL,\n total_ms INTEGER NOT NULL,\n milestones_json TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS latency_timeline_by_kind ON latency_timeline (kind, id);\nCREATE INDEX IF NOT EXISTS latency_timeline_by_run ON latency_timeline (run_id) WHERE run_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS latency_timeline_by_task ON latency_timeline (task_id) WHERE task_id IS NOT NULL;\nCREATE TABLE IF NOT EXISTS live_backup (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n enabled INTEGER NOT NULL DEFAULT 1,\n every_minutes INTEGER NOT NULL DEFAULT 5,\n keep INTEGER NOT NULL DEFAULT 24,\n folder TEXT,\n holder TEXT,\n held_until TEXT,\n latest_file TEXT,\n latest_sha256 TEXT,\n latest_at TEXT,\n latest_schema INTEGER,\n latest_bytes INTEGER,\n tried_at TEXT,\n problem TEXT\n);\n";
109
113
  /** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
110
114
  export declare const LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS live_detail_setting (\n repo TEXT PRIMARY KEY,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n-- Every run a steering note reached, kept as it happens (live-notes.ts): a\n-- note's own attached/mailed fields move to the next attempt, this does not.\nCREATE TABLE IF NOT EXISTS steer_note_run (\n note INTEGER NOT NULL REFERENCES task_steer(id) ON DELETE CASCADE,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n how TEXT NOT NULL CHECK (how IN ('attached','mailed','read')),\n at TEXT NOT NULL,\n PRIMARY KEY (note, run, how)\n);\nCREATE INDEX IF NOT EXISTS steer_note_run_by_run ON steer_note_run (run, note);\n";
111
115
  export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
@@ -186,7 +190,7 @@ export type SealedLedgerEntry = LedgerEntry & {
186
190
  hash: string;
187
191
  } | null;
188
192
  };
189
- export declare const SCHEMA_VERSION = 136;
193
+ export declare const SCHEMA_VERSION = 138;
190
194
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
191
195
  export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
192
196
  /**
@@ -331,6 +335,12 @@ export declare const V135_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
331
335
  definition: string;
332
336
  saved: string;
333
337
  }>>>>;
338
+ /** v138 (docs/plans/pipeline-speed.md, P0): a scope's release check purpose, part of its digest when present. Saved
339
+ * rows read NULL. */
340
+ export declare const V138_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
341
+ definition: string;
342
+ saved: string;
343
+ }>>>>;
334
344
  /**
335
345
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
336
346
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -2339,6 +2349,8 @@ export declare class Store {
2339
2349
  proposedVia?: ScopeAuthor | null;
2340
2350
  /** v135: release terms (release-once.md); undefined keeps the ones the scope carries, null removes them. */
2341
2351
  releaseTerms?: ReleaseTerms | null;
2352
+ /** v138: a release check's purpose; undefined keeps the scope's, null removes it. Never beside terms. */
2353
+ releaseCheck?: ReleaseCheckPurpose | null;
2342
2354
  }): void;
2343
2355
  /**
2344
2356
  * Re-file a scope under its OWN current words (v47): the route is
@@ -6600,6 +6612,9 @@ export declare class Store {
6600
6612
  };
6601
6613
  heartbeatWatchLease(runner: string, repo: string, owner: string, ttlMs: number, now: Date): boolean;
6602
6614
  releaseWatchLease(runner: string, repo: string, owner: string, now: Date): void;
6615
+ /** Lapse every watch lease a runner holds now: its holder was proved gone (runner.ts registerRunnerIfIdle). The rows
6616
+ * stay, so the next acquire takes them over and recovers the superseded incarnation as after any crash. */
6617
+ expireWatchLeasesOf(runner: string, now: Date): number;
6603
6618
  /** The first still-unexpired watch lease this runner holds, if any (arc 2:
6604
6619
  * the takeover door refuses while one exists). */
6605
6620
  liveWatchLeaseOf(runner: string, now: Date): {
@@ -7780,3 +7795,121 @@ export type AgentsLiveRun = Run & {
7780
7795
  * Visibility is the caller's: it filters each by the console's ceiling and the viewer's access.
7781
7796
  */
7782
7797
  export declare function agentsLiveRuns(store: Store, endedSince: Date): AgentsLiveRun[];
7798
+ /**
7799
+ * This runner's watches a predecessor left needing recovery (Codex review round 1, finding 2): a watch lease still
7800
+ * live, or one whose holder left a claim open (or a run open under a claim a reaper already released). A replacement
7801
+ * worker takes each over — recovering the holder's incarnation in the same transaction — before it admits new work,
7802
+ * whichever projects its start names (a project being removed when the predecessor died is in none of them).
7803
+ */
7804
+ export declare function predecessorWatches(store: Store, runner: string, now: Date): {
7805
+ repo: string;
7806
+ owner: string;
7807
+ }[];
7808
+ /** The longest a watch lease is taken or renewed for (durable-actors' MAX_HOST_LEASE_DURATION_MS). */
7809
+ export declare const MAX_WATCH_LEASE_MS = 60000;
7810
+ /** A service component's lifecycle (durable-actors' host lifecycle reports): started, then heartbeats, then draining
7811
+ * (no new admission; its own drain ends by drain_until), then stopped (retired). */
7812
+ export type LifecycleState = "started" | "draining" | "stopped";
7813
+ export type LifecycleRow = {
7814
+ component: string;
7815
+ state: LifecycleState;
7816
+ holder: string;
7817
+ pid: number | null;
7818
+ changedAt: string;
7819
+ beatAt: string;
7820
+ drainUntil: string | null;
7821
+ };
7822
+ /** One component's lifecycle row, or null (never reported: a component from before v137 says nothing). */
7823
+ export declare function lifecycleOf(store: Store, component: string): LifecycleRow | null;
7824
+ /** Every reported component, by name. */
7825
+ export declare function lifecycles(store: Store): LifecycleRow[];
7826
+ /**
7827
+ * Report a component's state. `started` replaces whatever an earlier holder left; `draining` and `stopped` only move the
7828
+ * current holder's row (a stale process can't mark its successor draining), and never back from stopped. Returns
7829
+ * whether the row now says what was asked.
7830
+ */
7831
+ export declare function reportLifecycle(store: Store, component: string, state: LifecycleState, holder: string, now: Date, extra?: {
7832
+ pid?: number | null;
7833
+ drainUntil?: Date | null;
7834
+ }): boolean;
7835
+ /** The short heartbeat: the current holder's row, while it is not stopped. False means another holder took it. */
7836
+ export declare function beatLifecycle(store: Store, component: string, holder: string, now: Date): boolean;
7837
+ /** A milestone in a latency timeline: what happened, how long after the operation began (ms), and an optional short
7838
+ * reason. Never a secret: route ids, phase and part names, fixed reason words. */
7839
+ export type Milestone = {
7840
+ name: string;
7841
+ ms: number;
7842
+ detail?: string;
7843
+ };
7844
+ export type LatencyKind = "console" | "release-check" | "deploy";
7845
+ export type LatencyTimeline = {
7846
+ id: number;
7847
+ kind: LatencyKind;
7848
+ subject: string;
7849
+ taskId: string | null;
7850
+ runId: number | null;
7851
+ startedAt: string;
7852
+ totalMs: number;
7853
+ milestones: Milestone[];
7854
+ };
7855
+ /** At most this many milestones per timeline (the rest are dropped, the last kept), and timelines kept per kind. */
7856
+ export declare const MAX_MILESTONES = 40;
7857
+ export declare const TIMELINES_KEPT = 200;
7858
+ /** Timelines older than this go whatever their count. */
7859
+ export declare const TIMELINE_DAYS = 14;
7860
+ /** Milestones made safe to keep: bounded in number and length, offsets whole non-negative milliseconds, in order. */
7861
+ export declare function boundedMilestones(milestones: readonly Milestone[]): Milestone[];
7862
+ /** Keep one timeline, then prune its kind to the newest TIMELINES_KEPT within TIMELINE_DAYS. */
7863
+ export declare function recordLatencyTimeline(store: Store, entry: {
7864
+ kind: LatencyKind;
7865
+ subject: string;
7866
+ taskId?: string | null;
7867
+ runId?: number | null;
7868
+ startedAt: Date;
7869
+ totalMs: number;
7870
+ milestones: readonly Milestone[];
7871
+ }, now: Date): number;
7872
+ /** Timelines, newest first: of one kind, one run's, or one task's. A row that doesn't read is left out. */
7873
+ export declare function latencyTimelines(store: Store, filter: {
7874
+ kind?: LatencyKind;
7875
+ runId?: number;
7876
+ taskId?: string;
7877
+ limit?: number;
7878
+ }): LatencyTimeline[];
7879
+ /** The continuous copy (live-backup.ts): how often, how many, where (null: `backups/live` beside the database). */
7880
+ export type LiveBackupSettings = {
7881
+ enabled: boolean;
7882
+ everyMinutes: number;
7883
+ keep: number;
7884
+ folder: string | null;
7885
+ };
7886
+ export declare const DEFAULT_LIVE_BACKUP: LiveBackupSettings;
7887
+ /** The latest copy that passed its integrity check, as it was when it was made. */
7888
+ export type LiveBackupCopy = {
7889
+ file: string;
7890
+ sha256: string;
7891
+ madeAt: string;
7892
+ schemaVersion: number;
7893
+ bytes: number;
7894
+ };
7895
+ export type LiveBackupState = {
7896
+ settings: LiveBackupSettings;
7897
+ latest: LiveBackupCopy | null;
7898
+ triedAt: string | null;
7899
+ problem: string | null;
7900
+ };
7901
+ export declare function liveBackupState(store: Store): LiveBackupState;
7902
+ /** Change the continuous copy's settings; the ledger keeps before → after. A new folder forgets the latest copy (it is
7903
+ * in the old one). */
7904
+ export declare function setLiveBackupSettings(store: Store, next: LiveBackupSettings, by: string, now: Date): LiveBackupSettings;
7905
+ /** Take (or keep) the continuous copy's lease until `until`: one process copies at a time. */
7906
+ export declare function holdLiveBackup(store: Store, holder: string, now: Date, until: Date): boolean;
7907
+ export declare function releaseLiveBackup(store: Store, holder: string): void;
7908
+ /** A copy's outcome: a verified copy becomes the latest; a failure is kept as the problem (the latest stays). */
7909
+ export declare function recordLiveBackup(store: Store, outcome: {
7910
+ ok: true;
7911
+ copy: LiveBackupCopy;
7912
+ } | {
7913
+ ok: false;
7914
+ problem: string;
7915
+ }, now: Date): void;