toolroll 0.9.92 → 0.9.94
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/CHANGELOG.md +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/dist/store.js
CHANGED
|
@@ -9,6 +9,8 @@ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
|
|
|
9
9
|
import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
|
|
10
10
|
import { reclaimDatabase } from "./database-reclaim.js";
|
|
11
11
|
import { BASELINE_SCHEMA, BASELINE_SCHEMA_VERSION } from "./store-baseline.js";
|
|
12
|
+
import { codeMapRemoteChanged, codeMapShapeProblem, forgetCodeMap, V152_ADDED_COLUMNS, V152_SCHEMA } from "./code-map-store.js";
|
|
13
|
+
import { readStamps, stampDigest, stampRefusal, V154_SCHEMA } from "./check-stamp.js";
|
|
12
14
|
import { refArchived, taskArchived } from "./task-archive.js";
|
|
13
15
|
export { refArchived, taskArchived } from "./task-archive.js";
|
|
14
16
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
@@ -73,8 +75,10 @@ import { ownedProcessCount, runOwnerTag } from "./exec.js";
|
|
|
73
75
|
import { worktreeProcessOccupancy } from "./worktree.js";
|
|
74
76
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
75
77
|
import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
78
|
+
import { currentMachineId, isThisMachine, liveHostAliases, rememberHostAliases, thisMachine } from "./machine-identity.js";
|
|
76
79
|
import { containerEmptiness } from "./container-state.js";
|
|
77
80
|
import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
|
|
81
|
+
import { ACTIVITY_ONLY_KINDS } from "./needs-you.js";
|
|
78
82
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
79
83
|
import { foldReview, manualReviewOnly } from "./proof.js";
|
|
80
84
|
import { hashPassword, liveReleaseOf } from "./scope.js";
|
|
@@ -92,6 +96,10 @@ import { authenticate as runnerAuthenticate } from "./runner.js";
|
|
|
92
96
|
import { stampNewProjectLevel } from "./check-levels.js";
|
|
93
97
|
import { parseCheckProgressSnapshot } from "./check-progress.js";
|
|
94
98
|
import { authorityStandsSql } from "./approval-authority.js";
|
|
99
|
+
import { clearRetryEpisode, settleVanished } from "./vanished-retry.js";
|
|
100
|
+
import { issueImported } from "./task-source.js";
|
|
101
|
+
import { BUILDER_SERVICE_SCHEMA, CHECK_ONLY_SCHEMA, FAST_PREVIEW_SCHEMA, ROUTE_CHANGE_SCHEMA, V153_ADDED_COLUMNS, VANISHED_SCHEMA } from "./plain-schema.js";
|
|
102
|
+
export { V153_ADDED_COLUMNS } from "./plain-schema.js";
|
|
95
103
|
import { releasePolicyDigest, serializeReleaseCheckPurpose, storedReleaseCheckPurpose, storedReleaseTerms, readReleaseTerms, readReleasePolicy } from "./contracts/release-terms.js";
|
|
96
104
|
import { accountReachesSql, normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
97
105
|
import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
@@ -107,7 +115,7 @@ import { REQUEST_BUDGET_SCHEMA } from "./request-budget.js";
|
|
|
107
115
|
import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
108
116
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, subagentFilers, usd } from "./spend.js";
|
|
109
117
|
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
110
|
-
import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
118
|
+
import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, DEFAULT_IDLE, V151_ADDED_COLUMNS, cleanupWords, idleWords } from "./storage.js";
|
|
111
119
|
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
112
120
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
113
121
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
@@ -1114,7 +1122,21 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1114
1122
|
// v148 adds, and changes no saved row (docs/plans/results-routing.md): the results-routing tables (V148_SCHEMA, new
|
|
1115
1123
|
// tables only) and a run's execution class, a revision's origin and a project's "Suggest cheaper models" switch
|
|
1116
1124
|
// (V148_ADDED_COLUMNS: saved rows read NULL, NULL and 1).
|
|
1117
|
-
|
|
1125
|
+
// v149 adds, and changes no saved row (docs/plans/learning-loops.md): every learning-loops table (V149_SCHEMA, new
|
|
1126
|
+
// tables only) — each check's recorded conditions and results, mixed-outcome observations, tracked identities, lesson
|
|
1127
|
+
// notes and their tallies, memory use, tidy-up proposals and decision merges.
|
|
1128
|
+
// v150 adds, and changes no saved row: this machine's stable id on each process witness (run_process.machine_id;
|
|
1129
|
+
// saved witnesses read NULL and keep being matched by host name) and the hostnames each machine id was seen under
|
|
1130
|
+
// (machine_host_alias, a new table; machine-identity.ts). V150_SCHEMA, V150_ADDED_COLUMNS.
|
|
1131
|
+
// v151 adds, and changes no saved row: how long a task stays idle before its clean folder goes
|
|
1132
|
+
// (V151_ADDED_COLUMNS: a saved cleanup setting reads NULL, the default).
|
|
1133
|
+
// v152 adds, and changes no saved row (docs/plans/code-map.md): the code map's tables (V152_SCHEMA, code-map-store.ts) and
|
|
1134
|
+
// a project's map switch and enrollment end (V152_ADDED_COLUMNS: a saved project reads on, not enrolled).
|
|
1135
|
+
// v153 adds, and changes no saved row (docs/plans/plain-toolroll.md Phase 2): new tables and columns only.
|
|
1136
|
+
// V153_SCHEMA, V153_ADDED_COLUMNS.
|
|
1137
|
+
// v154 adds, and changes no saved row (docs/plans/trusted-release-reports.md, H2): each check launch's execution stamp
|
|
1138
|
+
// (check_stamp, a new table; check-stamp.ts). A run from before reads no stamp: how its checks ran is unknown.
|
|
1139
|
+
export const SCHEMA_VERSION = 154;
|
|
1118
1140
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1119
1141
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1120
1142
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1247,7 +1269,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1247
1269
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1248
1270
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1249
1271
|
*/
|
|
1250
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148]);
|
|
1272
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154]);
|
|
1251
1273
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1252
1274
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1253
1275
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1650,6 +1672,262 @@ export const V148_ADDED_COLUMNS = Object.freeze({
|
|
|
1650
1672
|
});
|
|
1651
1673
|
/** The v148 migration's steps after v147's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1652
1674
|
export const V148_STEPS = Object.freeze(["results-tables", "exec-class", "revision-origin", "suggest-models"]);
|
|
1675
|
+
/**
|
|
1676
|
+
* v149 (docs/plans/learning-loops.md): every table the learning-loops arc uses, landed at once because a current store
|
|
1677
|
+
* runs no DDL. Slice 1 writes check_fact, check_result and learning_watch; the rest are written from later slices.
|
|
1678
|
+
* Each table has one writer module (the migration aside), which release-auto's learning scan (eval-hooks.ts
|
|
1679
|
+
* learningWriteProblem) holds a release to: check_fact and check_result → check-facts.ts; local_observation → flaky.ts;
|
|
1680
|
+
* ci_flaky_observation and ci_pairing_rejected → flaky-ci.ts; learning_watch → flaky.ts and lesson-notes.ts;
|
|
1681
|
+
* lesson_note and lesson_note_tally → lesson-notes.ts; memory_use → memory-use.ts; memory_tidy_proposal and
|
|
1682
|
+
* decision_merge → memory-tidy.ts.
|
|
1683
|
+
*
|
|
1684
|
+
* Nothing here is ever read for authority: a check's outcome, a verdict and an approval never change because of it.
|
|
1685
|
+
*
|
|
1686
|
+
* Deletion follows today's semantics (runs are never deleted by ordinary work; removing a project keeps its results).
|
|
1687
|
+
* The tables are keyed by repo path with no foreign key to `project`; their run keys are plain REFERENCES with no ON
|
|
1688
|
+
* DELETE action. check_fact, learning_watch, lesson_note, lesson_note_tally, memory_use, memory_tidy_proposal and
|
|
1689
|
+
* decision_merge are kept and never deleted (their triggers allow only the columns their one writer sets: a watch's
|
|
1690
|
+
* `until`, a tally's counters, a proposal's decision, a merge's undo). check_result, local_observation,
|
|
1691
|
+
* ci_flaky_observation and ci_pairing_rejected are pruned after 180 days by the storage sweep's own step (a later
|
|
1692
|
+
* slice), the only DELETE on them; their rows never change. The whole-project deletion (project-delete.ts), which
|
|
1693
|
+
* lifts every guard for its one transaction, takes them with the project as it takes everything else.
|
|
1694
|
+
*/
|
|
1695
|
+
export const V149_SCHEMA = `
|
|
1696
|
+
CREATE TABLE IF NOT EXISTS check_fact (
|
|
1697
|
+
run INTEGER PRIMARY KEY REFERENCES run(id),
|
|
1698
|
+
repo TEXT,
|
|
1699
|
+
task_ref INTEGER NOT NULL,
|
|
1700
|
+
head TEXT,
|
|
1701
|
+
tree TEXT,
|
|
1702
|
+
command_digest TEXT,
|
|
1703
|
+
access TEXT,
|
|
1704
|
+
harness_digest TEXT,
|
|
1705
|
+
lockfile TEXT,
|
|
1706
|
+
conditions_digest TEXT,
|
|
1707
|
+
conditions_class TEXT,
|
|
1708
|
+
purpose TEXT NOT NULL CHECK (purpose IN ('ordinary','release','nightly')),
|
|
1709
|
+
learn_state TEXT NOT NULL CHECK (learn_state IN ('recorded','not-recorded','not-asked')),
|
|
1710
|
+
learn_why TEXT,
|
|
1711
|
+
recorded_at TEXT NOT NULL,
|
|
1712
|
+
CHECK ((learn_state = 'not-recorded') = (learn_why IS NOT NULL)),
|
|
1713
|
+
CHECK ((conditions_digest IS NULL) = (conditions_class IS NULL)),
|
|
1714
|
+
CHECK (conditions_digest IS NULL OR tree IS NOT NULL)
|
|
1715
|
+
);
|
|
1716
|
+
CREATE INDEX IF NOT EXISTS check_fact_by_repo ON check_fact (repo, recorded_at);
|
|
1717
|
+
CREATE INDEX IF NOT EXISTS check_fact_by_tree ON check_fact (tree, conditions_digest);
|
|
1718
|
+
CREATE TABLE IF NOT EXISTS check_result (
|
|
1719
|
+
run INTEGER NOT NULL REFERENCES check_fact(run),
|
|
1720
|
+
identity TEXT NOT NULL CHECK (length(identity) BETWEEN 1 AND 2600),
|
|
1721
|
+
state TEXT NOT NULL CHECK (state IN ('failed','passed','skipped','passed-on-retry')),
|
|
1722
|
+
source TEXT NOT NULL CHECK (source IN ('receipt','report','log','learn')),
|
|
1723
|
+
recorded_at TEXT NOT NULL,
|
|
1724
|
+
PRIMARY KEY (run, identity, source)
|
|
1725
|
+
);
|
|
1726
|
+
CREATE INDEX IF NOT EXISTS check_result_by_identity ON check_result (identity, run);
|
|
1727
|
+
CREATE TABLE IF NOT EXISTS local_observation (
|
|
1728
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1729
|
+
repo TEXT NOT NULL,
|
|
1730
|
+
identity TEXT NOT NULL,
|
|
1731
|
+
failed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1732
|
+
passed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1733
|
+
tree TEXT NOT NULL,
|
|
1734
|
+
conditions_digest TEXT NOT NULL,
|
|
1735
|
+
kind TEXT NOT NULL CHECK (kind IN ('regate','same-tree','in-check-retry')),
|
|
1736
|
+
observed_at TEXT NOT NULL,
|
|
1737
|
+
UNIQUE (identity, failed_run, passed_run),
|
|
1738
|
+
CHECK ((kind = 'in-check-retry') = (failed_run = passed_run))
|
|
1739
|
+
);
|
|
1740
|
+
CREATE INDEX IF NOT EXISTS local_observation_by_repo ON local_observation (repo, identity, observed_at);
|
|
1741
|
+
CREATE TABLE IF NOT EXISTS ci_flaky_observation (
|
|
1742
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1743
|
+
repo TEXT NOT NULL,
|
|
1744
|
+
provider TEXT NOT NULL CHECK (provider = 'github'),
|
|
1745
|
+
repository TEXT NOT NULL,
|
|
1746
|
+
workflow TEXT NOT NULL,
|
|
1747
|
+
lane_id TEXT NOT NULL,
|
|
1748
|
+
lane_name TEXT NOT NULL,
|
|
1749
|
+
workflow_run_id INTEGER NOT NULL CHECK (workflow_run_id >= 1),
|
|
1750
|
+
head_sha TEXT NOT NULL,
|
|
1751
|
+
failed_attempt INTEGER NOT NULL CHECK (failed_attempt >= 1),
|
|
1752
|
+
failed_job_id INTEGER NOT NULL,
|
|
1753
|
+
failed_conclusion TEXT NOT NULL,
|
|
1754
|
+
passed_attempt INTEGER NOT NULL,
|
|
1755
|
+
passed_job_id INTEGER NOT NULL,
|
|
1756
|
+
passed_conclusion TEXT NOT NULL,
|
|
1757
|
+
observed_at TEXT NOT NULL,
|
|
1758
|
+
UNIQUE (provider, repository, workflow_run_id, lane_id, failed_attempt, passed_attempt),
|
|
1759
|
+
CHECK (passed_attempt > failed_attempt)
|
|
1760
|
+
);
|
|
1761
|
+
CREATE INDEX IF NOT EXISTS ci_flaky_observation_by_repo ON ci_flaky_observation (repo, lane_id, observed_at);
|
|
1762
|
+
CREATE TABLE IF NOT EXISTS ci_pairing_rejected (
|
|
1763
|
+
provider TEXT NOT NULL CHECK (provider = 'github'),
|
|
1764
|
+
repository TEXT NOT NULL,
|
|
1765
|
+
workflow_run_id INTEGER NOT NULL CHECK (workflow_run_id >= 1),
|
|
1766
|
+
attempt INTEGER NOT NULL CHECK (attempt >= 1),
|
|
1767
|
+
lane_id TEXT,
|
|
1768
|
+
why TEXT NOT NULL,
|
|
1769
|
+
at TEXT NOT NULL,
|
|
1770
|
+
PRIMARY KEY (provider, repository, workflow_run_id, attempt, why)
|
|
1771
|
+
);
|
|
1772
|
+
CREATE TABLE IF NOT EXISTS learning_watch (
|
|
1773
|
+
repo TEXT NOT NULL,
|
|
1774
|
+
identity TEXT NOT NULL CHECK (length(identity) BETWEEN 1 AND 2600),
|
|
1775
|
+
reason TEXT NOT NULL CHECK (reason IN ('note','observation')),
|
|
1776
|
+
source_ref TEXT NOT NULL,
|
|
1777
|
+
since TEXT NOT NULL,
|
|
1778
|
+
until TEXT,
|
|
1779
|
+
PRIMARY KEY (repo, identity, reason, source_ref),
|
|
1780
|
+
CHECK (until IS NULL OR until >= since)
|
|
1781
|
+
);
|
|
1782
|
+
CREATE INDEX IF NOT EXISTS learning_watch_open ON learning_watch (repo, since) WHERE until IS NULL;
|
|
1783
|
+
CREATE TABLE IF NOT EXISTS lesson_note (
|
|
1784
|
+
proposal INTEGER PRIMARY KEY REFERENCES memory_proposal(id),
|
|
1785
|
+
repo TEXT NOT NULL,
|
|
1786
|
+
failed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1787
|
+
passing_run INTEGER NOT NULL REFERENCES run(id),
|
|
1788
|
+
repair_chain INTEGER REFERENCES repair_chain(id),
|
|
1789
|
+
revision_origin TEXT CHECK (revision_origin IS NULL OR revision_origin IN ('person','automatic-review','flow','lead','machine')),
|
|
1790
|
+
identities_json TEXT NOT NULL,
|
|
1791
|
+
changed_paths_json TEXT NOT NULL,
|
|
1792
|
+
test_files_changed_json TEXT NOT NULL,
|
|
1793
|
+
facts_digest TEXT NOT NULL,
|
|
1794
|
+
created_at TEXT NOT NULL,
|
|
1795
|
+
CHECK (failed_run <> passing_run)
|
|
1796
|
+
);
|
|
1797
|
+
CREATE INDEX IF NOT EXISTS lesson_note_by_repo ON lesson_note (repo, created_at);
|
|
1798
|
+
CREATE TABLE IF NOT EXISTS lesson_note_tally (
|
|
1799
|
+
note INTEGER NOT NULL REFERENCES lesson_note(proposal),
|
|
1800
|
+
month TEXT NOT NULL CHECK (month GLOB '[0-9][0-9][0-9][0-9]-[0-1][0-9]'),
|
|
1801
|
+
executions INTEGER NOT NULL DEFAULT 0 CHECK (executions >= 0),
|
|
1802
|
+
failures INTEGER NOT NULL DEFAULT 0 CHECK (failures >= 0 AND failures <= executions),
|
|
1803
|
+
mixed_failures INTEGER NOT NULL DEFAULT 0 CHECK (mixed_failures >= 0),
|
|
1804
|
+
received_executions INTEGER NOT NULL DEFAULT 0 CHECK (received_executions >= 0 AND received_executions <= executions),
|
|
1805
|
+
received_failures INTEGER NOT NULL DEFAULT 0 CHECK (received_failures >= 0 AND received_failures <= received_executions AND received_failures <= failures),
|
|
1806
|
+
unreported_checks INTEGER NOT NULL DEFAULT 0 CHECK (unreported_checks >= 0),
|
|
1807
|
+
sources_json TEXT NOT NULL DEFAULT '[]',
|
|
1808
|
+
partial INTEGER NOT NULL DEFAULT 0 CHECK (partial IN (0,1)),
|
|
1809
|
+
PRIMARY KEY (note, month)
|
|
1810
|
+
);
|
|
1811
|
+
CREATE TABLE IF NOT EXISTS memory_use (
|
|
1812
|
+
run INTEGER NOT NULL REFERENCES run(id),
|
|
1813
|
+
repo TEXT,
|
|
1814
|
+
kind TEXT NOT NULL CHECK (kind IN ('lesson','reference','decision')),
|
|
1815
|
+
item TEXT NOT NULL,
|
|
1816
|
+
version INTEGER,
|
|
1817
|
+
phase TEXT NOT NULL,
|
|
1818
|
+
included_at TEXT,
|
|
1819
|
+
PRIMARY KEY (run, kind, item)
|
|
1820
|
+
);
|
|
1821
|
+
CREATE INDEX IF NOT EXISTS memory_use_by_item ON memory_use (repo, kind, item, run);
|
|
1822
|
+
CREATE TABLE IF NOT EXISTS memory_tidy_proposal (
|
|
1823
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1824
|
+
repo TEXT NOT NULL,
|
|
1825
|
+
kind TEXT NOT NULL CHECK (kind IN ('retire','merge','contradiction','note-failing','restore','unmerge')),
|
|
1826
|
+
items_json TEXT NOT NULL,
|
|
1827
|
+
after_text TEXT,
|
|
1828
|
+
rationale TEXT NOT NULL,
|
|
1829
|
+
evidence_json TEXT NOT NULL,
|
|
1830
|
+
source TEXT NOT NULL CHECK (source IN ('usage','duplicate','model','note','person')),
|
|
1831
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','rejected','stale')),
|
|
1832
|
+
created_at TEXT NOT NULL,
|
|
1833
|
+
decided_by TEXT,
|
|
1834
|
+
decided_proof TEXT,
|
|
1835
|
+
decided_at TEXT,
|
|
1836
|
+
CHECK ((status = 'pending') = (decided_at IS NULL)),
|
|
1837
|
+
CHECK (status IN ('accepted','rejected') OR (decided_by IS NULL AND decided_proof IS NULL)),
|
|
1838
|
+
CHECK (status NOT IN ('accepted','rejected') OR decided_by IS NOT NULL)
|
|
1839
|
+
);
|
|
1840
|
+
CREATE INDEX IF NOT EXISTS memory_tidy_proposal_pending ON memory_tidy_proposal (repo, status, kind);
|
|
1841
|
+
CREATE TABLE IF NOT EXISTS decision_merge (
|
|
1842
|
+
merge INTEGER NOT NULL,
|
|
1843
|
+
successor INTEGER NOT NULL REFERENCES project_decision(id),
|
|
1844
|
+
successor_revision INTEGER NOT NULL CHECK (successor_revision >= 1),
|
|
1845
|
+
predecessor INTEGER NOT NULL REFERENCES project_decision(id),
|
|
1846
|
+
predecessor_revision INTEGER NOT NULL CHECK (predecessor_revision >= 1),
|
|
1847
|
+
at TEXT NOT NULL,
|
|
1848
|
+
by TEXT NOT NULL,
|
|
1849
|
+
undone_at TEXT,
|
|
1850
|
+
undone_by TEXT,
|
|
1851
|
+
PRIMARY KEY (merge, predecessor),
|
|
1852
|
+
CHECK (successor <> predecessor),
|
|
1853
|
+
CHECK ((undone_at IS NULL) = (undone_by IS NULL))
|
|
1854
|
+
);
|
|
1855
|
+
CREATE INDEX IF NOT EXISTS decision_merge_by_successor ON decision_merge (successor);
|
|
1856
|
+
CREATE INDEX IF NOT EXISTS decision_merge_by_predecessor ON decision_merge (predecessor);
|
|
1857
|
+
CREATE TRIGGER IF NOT EXISTS check_fact_no_update BEFORE UPDATE ON check_fact BEGIN SELECT RAISE(ABORT,'check facts are recorded once'); END;
|
|
1858
|
+
CREATE TRIGGER IF NOT EXISTS check_fact_no_delete BEFORE DELETE ON check_fact BEGIN SELECT RAISE(ABORT,'check facts are kept'); END;
|
|
1859
|
+
CREATE TRIGGER IF NOT EXISTS check_result_no_update BEFORE UPDATE ON check_result BEGIN SELECT RAISE(ABORT,'check results are recorded once'); END;
|
|
1860
|
+
CREATE TRIGGER IF NOT EXISTS local_observation_no_update BEFORE UPDATE ON local_observation BEGIN SELECT RAISE(ABORT,'observations are recorded once'); END;
|
|
1861
|
+
CREATE TRIGGER IF NOT EXISTS ci_flaky_observation_no_update BEFORE UPDATE ON ci_flaky_observation BEGIN SELECT RAISE(ABORT,'observations are recorded once'); END;
|
|
1862
|
+
CREATE TRIGGER IF NOT EXISTS ci_pairing_rejected_no_update BEFORE UPDATE ON ci_pairing_rejected BEGIN SELECT RAISE(ABORT,'rejected pairings are recorded once'); END;
|
|
1863
|
+
CREATE TRIGGER IF NOT EXISTS learning_watch_only_ends BEFORE UPDATE ON learning_watch
|
|
1864
|
+
WHEN NEW.repo IS NOT OLD.repo OR NEW.identity IS NOT OLD.identity OR NEW.reason IS NOT OLD.reason OR NEW.source_ref IS NOT OLD.source_ref
|
|
1865
|
+
OR NEW.since IS NOT OLD.since OR OLD.until IS NOT NULL OR NEW.until IS NULL
|
|
1866
|
+
BEGIN SELECT RAISE(ABORT,'a tracked identity only ends, once'); END;
|
|
1867
|
+
CREATE TRIGGER IF NOT EXISTS learning_watch_no_delete BEFORE DELETE ON learning_watch BEGIN SELECT RAISE(ABORT,'tracked identities are kept'); END;
|
|
1868
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_no_update BEFORE UPDATE ON lesson_note BEGIN SELECT RAISE(ABORT,'lesson note facts are recorded once'); END;
|
|
1869
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_no_delete BEFORE DELETE ON lesson_note BEGIN SELECT RAISE(ABORT,'lesson notes are kept'); END;
|
|
1870
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_tally_only_counts BEFORE UPDATE ON lesson_note_tally
|
|
1871
|
+
WHEN NEW.note IS NOT OLD.note OR NEW.month IS NOT OLD.month OR NEW.executions < OLD.executions OR NEW.failures < OLD.failures
|
|
1872
|
+
OR NEW.mixed_failures < OLD.mixed_failures OR NEW.received_executions < OLD.received_executions OR NEW.received_failures < OLD.received_failures
|
|
1873
|
+
OR NEW.unreported_checks < OLD.unreported_checks OR NEW.partial < OLD.partial
|
|
1874
|
+
BEGIN SELECT RAISE(ABORT,'a tally only counts up'); END;
|
|
1875
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_tally_no_delete BEFORE DELETE ON lesson_note_tally BEGIN SELECT RAISE(ABORT,'tallies are never pruned'); END;
|
|
1876
|
+
CREATE TRIGGER IF NOT EXISTS memory_use_no_update BEFORE UPDATE ON memory_use BEGIN SELECT RAISE(ABORT,'memory use is recorded once'); END;
|
|
1877
|
+
CREATE TRIGGER IF NOT EXISTS memory_use_no_delete BEFORE DELETE ON memory_use BEGIN SELECT RAISE(ABORT,'memory use is kept'); END;
|
|
1878
|
+
CREATE TRIGGER IF NOT EXISTS memory_tidy_proposal_only_decides BEFORE UPDATE ON memory_tidy_proposal
|
|
1879
|
+
WHEN NEW.id IS NOT OLD.id OR NEW.repo IS NOT OLD.repo OR NEW.kind IS NOT OLD.kind OR NEW.items_json IS NOT OLD.items_json
|
|
1880
|
+
OR NEW.after_text IS NOT OLD.after_text OR NEW.rationale IS NOT OLD.rationale OR NEW.evidence_json IS NOT OLD.evidence_json
|
|
1881
|
+
OR NEW.source IS NOT OLD.source OR NEW.created_at IS NOT OLD.created_at OR OLD.status <> 'pending'
|
|
1882
|
+
BEGIN SELECT RAISE(ABORT,'a tidy-up proposal is decided once'); END;
|
|
1883
|
+
CREATE TRIGGER IF NOT EXISTS memory_tidy_proposal_no_delete BEFORE DELETE ON memory_tidy_proposal BEGIN SELECT RAISE(ABORT,'tidy-up proposals are kept'); END;
|
|
1884
|
+
CREATE TRIGGER IF NOT EXISTS decision_merge_only_undone BEFORE UPDATE ON decision_merge
|
|
1885
|
+
WHEN NEW.merge IS NOT OLD.merge OR NEW.successor IS NOT OLD.successor OR NEW.successor_revision IS NOT OLD.successor_revision
|
|
1886
|
+
OR NEW.predecessor IS NOT OLD.predecessor OR NEW.predecessor_revision IS NOT OLD.predecessor_revision OR NEW.at IS NOT OLD.at
|
|
1887
|
+
OR NEW.by IS NOT OLD.by OR OLD.undone_at IS NOT NULL
|
|
1888
|
+
BEGIN SELECT RAISE(ABORT,'a merge is undone once, never rewritten'); END;
|
|
1889
|
+
CREATE TRIGGER IF NOT EXISTS decision_merge_no_delete BEFORE DELETE ON decision_merge BEGIN SELECT RAISE(ABORT,'merges are kept'); END;
|
|
1890
|
+
`;
|
|
1891
|
+
/** The tables V149_SCHEMA adds, in the order a test drops them to make the v148 shape (dependents first). */
|
|
1892
|
+
export const V149_TABLES = Object.freeze(["check_result", "check_fact", "local_observation", "ci_flaky_observation", "ci_pairing_rejected", "learning_watch",
|
|
1893
|
+
"lesson_note_tally", "lesson_note", "memory_use", "memory_tidy_proposal", "decision_merge"]);
|
|
1894
|
+
/** The v149 tables a whole-project deletion clears by repo (the rest hang off these or a deleted run or decision). */
|
|
1895
|
+
export const V149_REPO_TABLES = Object.freeze(["check_fact", "local_observation", "ci_flaky_observation", "learning_watch", "lesson_note", "memory_use", "memory_tidy_proposal"]);
|
|
1896
|
+
/** The v149 migration's steps after v148's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1897
|
+
export const V149_STEPS = Object.freeze(["learning-tables"]);
|
|
1898
|
+
/**
|
|
1899
|
+
* v150 (machine-identity.ts): which computer a process witness belongs to, by the machine's stable id rather than its
|
|
1900
|
+
* network hostname (which follows the network on a Mac with no HostName set). machine_host_alias records each
|
|
1901
|
+
* hostname this machine read while it knew its own id, written only by a process on that machine, beside the id it
|
|
1902
|
+
* read; a witness with no id (every saved one) is this machine's when its host is one of them. Never read for any
|
|
1903
|
+
* other machine's id.
|
|
1904
|
+
*/
|
|
1905
|
+
export const V150_SCHEMA = `
|
|
1906
|
+
CREATE TABLE IF NOT EXISTS machine_host_alias (
|
|
1907
|
+
machine_id TEXT NOT NULL CHECK (length(machine_id) = 32),
|
|
1908
|
+
host TEXT NOT NULL CHECK (host <> ''),
|
|
1909
|
+
first_seen_at TEXT NOT NULL,
|
|
1910
|
+
PRIMARY KEY (machine_id, host)
|
|
1911
|
+
);
|
|
1912
|
+
`;
|
|
1913
|
+
/** The tables V150_SCHEMA adds. */
|
|
1914
|
+
export const V150_TABLES = Object.freeze(["machine_host_alias"]);
|
|
1915
|
+
/** v150: a process witness's machine id (saved witnesses read NULL: matched by host name, as before). */
|
|
1916
|
+
export const V150_ADDED_COLUMNS = Object.freeze({
|
|
1917
|
+
run_process: { machine_id: { definition: "TEXT CHECK (machine_id IS NULL OR length(machine_id) = 32)", saved: "NULL" } },
|
|
1918
|
+
});
|
|
1919
|
+
/** The v150 migration's steps after v149's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1920
|
+
export const V150_STEPS = Object.freeze(["machine-aliases", "witness-machine"]);
|
|
1921
|
+
/** The v151 migration's steps after v150's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1922
|
+
export const V151_STEPS = Object.freeze(["checkout-idle"]);
|
|
1923
|
+
/** The v152 migration's steps after v151's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1924
|
+
export const V152_STEPS = Object.freeze(["code-map-tables", "code-map-switch", "code-map-watermark"]);
|
|
1925
|
+
/** v153 (plain-toolroll.md Phase 2): the tables each slice owns. */
|
|
1926
|
+
export const V153_SCHEMA = VANISHED_SCHEMA + CHECK_ONLY_SCHEMA + BUILDER_SERVICE_SCHEMA + FAST_PREVIEW_SCHEMA + ROUTE_CHANGE_SCHEMA;
|
|
1927
|
+
/** The v153 migration's steps after v152's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1928
|
+
export const V153_STEPS = Object.freeze(["plain-tables", "plain-columns"]);
|
|
1929
|
+
/** The v154 migration's steps after v153's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1930
|
+
export const V154_STEPS = Object.freeze(["check-stamps"]);
|
|
1653
1931
|
/**
|
|
1654
1932
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1655
1933
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -2156,10 +2434,10 @@ function lifecycleWords(value, cap) {
|
|
|
2156
2434
|
const LIFECYCLE_PHASE_WORDS = {
|
|
2157
2435
|
"agent-running": { subject: "agent working", body: "The agent is working in its own checkout. Nothing is finished yet." },
|
|
2158
2436
|
"validating-handoff": { subject: "checking the handoff", body: "The agent finished its turn; what it handed back is being checked." },
|
|
2159
|
-
"capturing-evidence": { subject: "
|
|
2437
|
+
"capturing-evidence": { subject: "saving its files", body: "The diff, checks and report are being saved." },
|
|
2160
2438
|
"committing": { subject: "committing", body: "The work is being committed on the task branch." },
|
|
2161
|
-
"verifying-proof": { subject: "
|
|
2162
|
-
"correcting-proof": { subject: "correcting the
|
|
2439
|
+
"verifying-proof": { subject: "checking the report", body: "The report is being checked against the approved requirements." },
|
|
2440
|
+
"correcting-proof": { subject: "correcting the report", body: "The report didn't match the saved result; one short correction turn is running." },
|
|
2163
2441
|
};
|
|
2164
2442
|
/** An operator's cancellation as one closed sentence: the reason, if any,
|
|
2165
2443
|
* scrubbed like every phone string and ended with a full stop unless it
|
|
@@ -2172,10 +2450,10 @@ function cancellationWords(reason) {
|
|
|
2172
2450
|
}
|
|
2173
2451
|
/** What an independent review concluded, in the words the saved result shows. */
|
|
2174
2452
|
const LIFECYCLE_REVIEW_WORDS = {
|
|
2175
|
-
verified: { subject: "checks verified", body: "Every
|
|
2176
|
-
refuted: { subject: "
|
|
2177
|
-
attested: { subject: "agent
|
|
2178
|
-
short: { subject: "
|
|
2453
|
+
verified: { subject: "checks verified", body: "Every requirement was checked against the saved result." },
|
|
2454
|
+
refuted: { subject: "the report doesn't match the result", body: "The reviewer found something that contradicts the saved result. It is not accepted as done." },
|
|
2455
|
+
attested: { subject: "only the agent's own checks", body: "The result rests on checks the agent reported, not checked independently." },
|
|
2456
|
+
short: { subject: "a requirement isn't shown", body: "Something a requirement needs is missing from the saved result." },
|
|
2179
2457
|
none: { subject: "comments only", body: "The reviewer left comments; no acceptance judgement was recorded." },
|
|
2180
2458
|
};
|
|
2181
2459
|
/** Why a task still waits after one hold lifts — the other owner, in words. */
|
|
@@ -4572,6 +4850,9 @@ function initializeStore(db, file, options = {}) {
|
|
|
4572
4850
|
// A current file is this build's exact shape: the checks above read, the connection gets its settings, and no DDL
|
|
4573
4851
|
// runs. Everything below runs once, on the way up from an older version (a fresh file is at v107 by now).
|
|
4574
4852
|
if (preflight === SCHEMA_VERSION) {
|
|
4853
|
+
const missing = codeMapShapeProblem(db);
|
|
4854
|
+
if (missing !== null)
|
|
4855
|
+
throw new Error(`${file}: refusing to open — ${missing}, though the schema says v${SCHEMA_VERSION}`);
|
|
4575
4856
|
db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
4576
4857
|
return new Store(db);
|
|
4577
4858
|
}
|
|
@@ -4941,6 +5222,38 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4941
5222
|
for (const [column, { definition }] of Object.entries(V148_ADDED_COLUMNS["project"]))
|
|
4942
5223
|
addColumn(db, "project", column, definition);
|
|
4943
5224
|
step("suggest-models");
|
|
5225
|
+
// v149: the learning-loops tables (new tables only; docs/plans/learning-loops.md). Idempotent, as v148's steps are.
|
|
5226
|
+
db.exec(V149_SCHEMA);
|
|
5227
|
+
step("learning-tables");
|
|
5228
|
+
// v150: this machine's stable id on each process witness and the hostnames it was seen under (V150_SCHEMA,
|
|
5229
|
+
// V150_ADDED_COLUMNS: saved witnesses read NULL). Idempotent, as v149's step is.
|
|
5230
|
+
db.exec(V150_SCHEMA);
|
|
5231
|
+
step("machine-aliases");
|
|
5232
|
+
for (const [column, { definition }] of Object.entries(V150_ADDED_COLUMNS["run_process"]))
|
|
5233
|
+
addColumn(db, "run_process", column, definition);
|
|
5234
|
+
step("witness-machine");
|
|
5235
|
+
// v151: the idle limit beside the cleanup setting (V151_ADDED_COLUMNS: a saved row reads NULL). Idempotent, as v150's steps are.
|
|
5236
|
+
for (const [column, { definition }] of Object.entries(V151_ADDED_COLUMNS["checkout_cleanup_setting"]))
|
|
5237
|
+
addColumn(db, "checkout_cleanup_setting", column, definition);
|
|
5238
|
+
step("checkout-idle");
|
|
5239
|
+
// v152: the code map's tables, a project's switch, and the run-id watermark, set once. Idempotent, as v151's step is.
|
|
5240
|
+
db.exec(V152_SCHEMA);
|
|
5241
|
+
step("code-map-tables");
|
|
5242
|
+
for (const [column, { definition }] of Object.entries(V152_ADDED_COLUMNS["project"]))
|
|
5243
|
+
addColumn(db, "project", column, definition);
|
|
5244
|
+
step("code-map-switch");
|
|
5245
|
+
db.exec("INSERT OR IGNORE INTO code_map_meta (id, run_watermark) SELECT 1, COALESCE(MAX(id), 0) FROM run");
|
|
5246
|
+
step("code-map-watermark");
|
|
5247
|
+
// v153: Phase 2's tables (new tables only). Idempotent, as v152's steps are.
|
|
5248
|
+
db.exec(V153_SCHEMA);
|
|
5249
|
+
step("plain-tables");
|
|
5250
|
+
for (const [table, columns] of Object.entries(V153_ADDED_COLUMNS))
|
|
5251
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
5252
|
+
addColumn(db, table, column, definition);
|
|
5253
|
+
step("plain-columns");
|
|
5254
|
+
// v154: each check launch's execution stamp (a new table). Idempotent, as v153's steps are.
|
|
5255
|
+
db.exec(V154_SCHEMA);
|
|
5256
|
+
step("check-stamps");
|
|
4944
5257
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4945
5258
|
// nothing to reclaim.
|
|
4946
5259
|
if (!fresh)
|
|
@@ -6152,6 +6465,8 @@ export class Store {
|
|
|
6152
6465
|
this.db = instrumentDatabase(trackWorkspaceWrites(db), this.telemetry);
|
|
6153
6466
|
// And on each instance: the sealed methods as its own locked properties, so nothing shadows one later.
|
|
6154
6467
|
lockStoreMoneyMethods(this);
|
|
6468
|
+
// This machine's recorded names (v150), for the custody checks that hold no store (machine-identity.ts).
|
|
6469
|
+
rememberStoredHostAliases(this.db);
|
|
6155
6470
|
}
|
|
6156
6471
|
/** TESTS ONLY: raw database access for migration fixtures. Production
|
|
6157
6472
|
* code never calls this — the typed methods are the API. */
|
|
@@ -6423,7 +6738,7 @@ export class Store {
|
|
|
6423
6738
|
this.noteLifecycle({
|
|
6424
6739
|
taskRef: ref.id, kind: "task-queued", identity: `t${ref.id}`,
|
|
6425
6740
|
subject: "Back in the queue",
|
|
6426
|
-
body: prior["state"] === "running" ? "Its
|
|
6741
|
+
body: prior["state"] === "running" ? "Its builder let it go without finishing; another attempt can start." : "Reopened for another attempt.",
|
|
6427
6742
|
link: chatControlHref("task", id),
|
|
6428
6743
|
}, now);
|
|
6429
6744
|
}
|
|
@@ -7741,6 +8056,11 @@ export class Store {
|
|
|
7741
8056
|
// from the live scope: a card that cut a word, or terms that changed since, seal nothing.
|
|
7742
8057
|
if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
|
|
7743
8058
|
return false;
|
|
8059
|
+
// THE IMPORT QUARANTINE (github-import.ts): a task filed from a GitHub issue or pull request, or a revision of
|
|
8060
|
+
// one, is outside text too. Its provenance is immutable and copied through revisions, so no automatic basis
|
|
8061
|
+
// ever seals it, here, whatever road it came by.
|
|
8062
|
+
if (basis.kind !== "chat" && issueImported(this, taskId))
|
|
8063
|
+
return false;
|
|
7744
8064
|
// CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
|
|
7745
8065
|
// all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
|
|
7746
8066
|
const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
|
|
@@ -7819,12 +8139,15 @@ export class Store {
|
|
|
7819
8139
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7820
8140
|
return false;
|
|
7821
8141
|
// The outside-request quarantine's chat road (above): the card showed every word of these live terms.
|
|
7822
|
-
if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
|
|
8142
|
+
if (basis?.kind === "chat" && (this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA || issueImported(this, taskId))
|
|
7823
8143
|
&& !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
|
|
7824
8144
|
return false;
|
|
7825
8145
|
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7826
8146
|
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7827
8147
|
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
8148
|
+
// A FAST START (fast-start.ts) stays a person's: no automatic release seal ever lands on one.
|
|
8149
|
+
if (automatic && this.db.prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND fast_start_at IS NOT NULL").get(BUILT_IN, taskId) !== undefined)
|
|
8150
|
+
return false;
|
|
7828
8151
|
if (automatic) {
|
|
7829
8152
|
if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
|
|
7830
8153
|
return false;
|
|
@@ -7834,6 +8157,9 @@ export class Store {
|
|
|
7834
8157
|
}
|
|
7835
8158
|
if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
|
|
7836
8159
|
return false;
|
|
8160
|
+
// Nor does a fast start ever anchor a release, whoever approves it.
|
|
8161
|
+
if (terms.terms !== null && this.db.prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND fast_start_at IS NOT NULL").get(BUILT_IN, taskId) !== undefined)
|
|
8162
|
+
return false;
|
|
7837
8163
|
if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
|
|
7838
8164
|
return false;
|
|
7839
8165
|
const changed = this.db
|
|
@@ -7863,8 +8189,12 @@ export class Store {
|
|
|
7863
8189
|
// (setup review): the approval is what was stale, and it is new.
|
|
7864
8190
|
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
|
|
7865
8191
|
if (ref !== undefined) {
|
|
7866
|
-
if (basis === undefined || basis.kind === "chat")
|
|
8192
|
+
if (basis === undefined || basis.kind === "chat") {
|
|
7867
8193
|
this.noteTaskAct(Number(ref["id"]), "approved", now);
|
|
8194
|
+
clearRetryEpisode(this, Number(ref["id"]));
|
|
8195
|
+
// An ordinary yes stands on its own: a fast start's window is over (its Accept backstop stays).
|
|
8196
|
+
this.db.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ? AND fast_start_until IS NOT NULL").run(Number(ref["id"]));
|
|
8197
|
+
}
|
|
7868
8198
|
this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`stale:${Number(ref["id"])}`);
|
|
7869
8199
|
// Results routing: this yes closes the task's open exposure (as proposed, or with the model it sealed),
|
|
7870
8200
|
// and lifts the hold a switched-off rule put on the approval it replaces.
|
|
@@ -7873,6 +8203,11 @@ export class Store {
|
|
|
7873
8203
|
this.db
|
|
7874
8204
|
.prepare("UPDATE notification SET resolved_at = ? WHERE dedupe_key LIKE ? AND resolved_at IS NULL")
|
|
7875
8205
|
.run(now.toISOString(), `stale-approval:${Number(ref["id"])}:%`);
|
|
8206
|
+
// One-tap release approval (trusted-release-reports.md A5): the yes, by any road, retires the task's
|
|
8207
|
+
// "ready for your approval" rows, so a push not delivered yet is never sent after it.
|
|
8208
|
+
this.db
|
|
8209
|
+
.prepare("UPDATE notification SET resolved_at = ? WHERE kind = 'release-ready' AND task_ref = ? AND resolved_at IS NULL")
|
|
8210
|
+
.run(now.toISOString(), Number(ref["id"]));
|
|
7876
8211
|
// The approval fact (Telegram task updates): a NEW yes on these
|
|
7877
8212
|
// exact bytes. Re-sealing an approval that already stands on
|
|
7878
8213
|
// the same digest is the same state and says nothing.
|
|
@@ -7880,20 +8215,20 @@ export class Store {
|
|
|
7880
8215
|
if (alreadyOn !== working["digest"]) {
|
|
7881
8216
|
this.noteLifecycle({
|
|
7882
8217
|
taskRef: Number(ref["id"]), kind: "scope-approved", identity: `t${Number(ref["id"])}`,
|
|
7883
|
-
subject: "
|
|
8218
|
+
subject: "Plan approved",
|
|
7884
8219
|
body: basis === undefined
|
|
7885
|
-
? `Approved by ${lifecycleWords(by, 40)}. A connected
|
|
8220
|
+
? `Approved by ${lifecycleWords(by, 40)}. A connected builder can take it next.`
|
|
7886
8221
|
: basis.kind === "chat"
|
|
7887
|
-
? `Approved by ${lifecycleWords(by, 40)} in chat. A connected
|
|
8222
|
+
? `Approved by ${lifecycleWords(by, 40)} in chat. A connected builder can take it next.`
|
|
7888
8223
|
: basis.kind === "grant"
|
|
7889
|
-
? "Approved automatically: its flow runs on its own. A connected
|
|
8224
|
+
? "Approved automatically: its flow runs on its own. A connected builder can take it next."
|
|
7890
8225
|
: basis.kind === "schedule"
|
|
7891
|
-
? "Approved by its standing order. A connected
|
|
8226
|
+
? "Approved by its standing order. A connected builder can take it next."
|
|
7892
8227
|
: basis.kind === "auto-check"
|
|
7893
8228
|
? "Checking automatically: it approves itself once CI and the release check pass."
|
|
7894
8229
|
: basis.kind === "auto-green"
|
|
7895
8230
|
? "Approved automatically: CI and the release check passed."
|
|
7896
|
-
: "Approved automatically under the operating mode. A connected
|
|
8231
|
+
: "Approved automatically under the operating mode. A connected builder can take it next.",
|
|
7897
8232
|
link: chatControlHref("task", taskId),
|
|
7898
8233
|
}, now);
|
|
7899
8234
|
}
|
|
@@ -7932,7 +8267,7 @@ export class Store {
|
|
|
7932
8267
|
JOIN task_ref ON task_ref.id = ? AND task_ref.backend = 'built-in' AND task_scope.task_id = task_ref.external_id
|
|
7933
8268
|
WHERE task_scope.approved_at IS NOT NULL
|
|
7934
8269
|
AND NOT ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
|
|
7935
|
-
.get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
|
|
8270
|
+
.get(taskRef, now.toISOString(), now.toISOString(), now.toISOString(), now.toISOString());
|
|
7936
8271
|
if (dead !== undefined)
|
|
7937
8272
|
return false;
|
|
7938
8273
|
// A release task (release-once.md): the whole live-release predicate, beyond what the SQL can say.
|
|
@@ -9472,10 +9807,7 @@ export class Store {
|
|
|
9472
9807
|
.get(taskRef);
|
|
9473
9808
|
if (ref === undefined || String(ref["backend"]) !== BUILT_IN)
|
|
9474
9809
|
continue;
|
|
9475
|
-
|
|
9476
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
9477
|
-
.run(now.toISOString(), String(ref["external_id"]));
|
|
9478
|
-
if (Number(changes) > 0)
|
|
9810
|
+
if (settleVanished(this, taskRef, now) === "queued")
|
|
9479
9811
|
requeued?.push(String(ref["external_id"]));
|
|
9480
9812
|
}
|
|
9481
9813
|
return held.map(row => String(row["lease_id"]));
|
|
@@ -10268,7 +10600,7 @@ export class Store {
|
|
|
10268
10600
|
this.noteLifecycle({
|
|
10269
10601
|
taskRef: run.taskRef, run: runId, kind: "review-requested", identity: `r${runId}`,
|
|
10270
10602
|
subject: "Independent review requested",
|
|
10271
|
-
body: `${automatic ? "Queued automatically" : `Requested by ${lifecycleWords(by, 40)}`} for
|
|
10603
|
+
body: `${automatic ? "Queued automatically" : `Requested by ${lifecycleWords(by, 40)}`} for build #${runId}'s saved result. A connected reviewer runs it next.`,
|
|
10272
10604
|
link: chatResultHref(reviewedTask, runId, "checks"),
|
|
10273
10605
|
}, now);
|
|
10274
10606
|
}
|
|
@@ -11572,6 +11904,10 @@ export class Store {
|
|
|
11572
11904
|
for (const incident of incidents)
|
|
11573
11905
|
this.resolveIncidentLocked(incident, by, now);
|
|
11574
11906
|
this.resetStrikes(taskRef);
|
|
11907
|
+
clearRetryEpisode(this, taskRef);
|
|
11908
|
+
// A person's Retry is a new person act: a fast start gets a new start window. A machine's never does.
|
|
11909
|
+
if (!machineActing())
|
|
11910
|
+
this.db.prepare("UPDATE task_ref SET fast_start_until = ? WHERE id = ? AND fast_start_until IS NOT NULL").run(new Date(now.getTime() + 30 * 60_000).toISOString(), taskRef);
|
|
11575
11911
|
this.db
|
|
11576
11912
|
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state IN ('failed','queued','done')")
|
|
11577
11913
|
.run(stamp, taskId);
|
|
@@ -11579,10 +11915,10 @@ export class Store {
|
|
|
11579
11915
|
this.resolveEpisode(`stalled:${taskRef}`, now);
|
|
11580
11916
|
this.noteLifecycle({
|
|
11581
11917
|
taskRef, kind: "task-requeued", identity: `t${taskRef}`,
|
|
11582
|
-
subject: "
|
|
11918
|
+
subject: "Queued again",
|
|
11583
11919
|
body: rejectedRun !== null
|
|
11584
|
-
? `Its last result (
|
|
11585
|
-
:
|
|
11920
|
+
? `Its last result (build #${rejectedRun}) was not accepted; the next attempt continues on the same branch under the current plan.`
|
|
11921
|
+
: `Its repeated failures are cleared; a builder can take it again.`,
|
|
11586
11922
|
link: chatControlHref("task", taskId),
|
|
11587
11923
|
}, now);
|
|
11588
11924
|
this.bumpWake();
|
|
@@ -13094,18 +13430,18 @@ export class Store {
|
|
|
13094
13430
|
const stopped = result.outcome === "failed" && result.reason === "interrupted" && this.stopOf(id) !== null;
|
|
13095
13431
|
if (role === "builder") {
|
|
13096
13432
|
if (stopped) {
|
|
13097
|
-
this.noteLifecycle({ taskRef, run: id, kind: "run-stopped", identity: `r${id}`, subject: `
|
|
13433
|
+
this.noteLifecycle({ taskRef, run: id, kind: "run-stopped", identity: `r${id}`, subject: `Build #${id} stopped`, body: "Its work is preserved. Resume this attempt from the task page when ready.", link: chatControlHref("recovery", taskId) }, now);
|
|
13098
13434
|
}
|
|
13099
13435
|
else if (result.outcome === "built") {
|
|
13100
13436
|
this.noteLifecycle({
|
|
13101
13437
|
taskRef, run: id, kind: "run-finished", identity: `r${id}`,
|
|
13102
|
-
subject: `
|
|
13438
|
+
subject: `Build #${id} built`,
|
|
13103
13439
|
body: result.committed === false ? "It finished without a commit; the handoff explains why." : "The result is saved locally. It is not yet independently reviewed or published.",
|
|
13104
13440
|
link: chatResultHref(taskId, id, "changes"),
|
|
13105
13441
|
}, now);
|
|
13106
13442
|
}
|
|
13107
13443
|
else if (result.outcome === "no-change") {
|
|
13108
|
-
this.noteLifecycle({ taskRef, run: id, kind: "run-finished", identity: `r${id}`, subject: `
|
|
13444
|
+
this.noteLifecycle({ taskRef, run: id, kind: "run-finished", identity: `r${id}`, subject: `Build #${id} finished with no changes`, body: "Nothing needed to change; the handoff explains why.", link: chatResultHref(taskId, id) }, now);
|
|
13109
13445
|
}
|
|
13110
13446
|
return;
|
|
13111
13447
|
}
|
|
@@ -13607,12 +13943,12 @@ export class Store {
|
|
|
13607
13943
|
if (road.road !== "repair" && road.road !== "correction" && !(role === "planner" && parent !== null)) {
|
|
13608
13944
|
const agent = lifecycleWords(`${provider ?? "claude"}${model === null ? "" : ` · ${model}`}`, 60);
|
|
13609
13945
|
const words = role === "reviewer"
|
|
13610
|
-
? { subject: `Independent review started (attempt ${reviewAttempt ?? 1})`, body: `Reviewing the saved result of
|
|
13946
|
+
? { subject: `Independent review started (attempt ${reviewAttempt ?? 1})`, body: `Reviewing the saved result of build #${run.parentRun}. Running on ${agent}.`, link: chatResultHref(taskId, run.parentRun, "checks") }
|
|
13611
13947
|
: role === "planner"
|
|
13612
|
-
? { subject:
|
|
13948
|
+
? { subject: "Planning started", body: `Drafting the plan on ${agent}.`, link: chatControlHref("planning", taskId) }
|
|
13613
13949
|
: role === "scout"
|
|
13614
|
-
? { subject:
|
|
13615
|
-
: { subject: road.road === "recovered" ? `
|
|
13950
|
+
? { subject: "Research started", body: `A read-only look on ${agent}; what comes back is a report.`, link: chatControlHref("task", taskId) }
|
|
13951
|
+
: { subject: road.road === "recovered" ? `Build #${id} resumed from its saved draft` : `Build #${id} started`, body: `Building on ${agent}.`, link: chatControlHref("task", taskId) };
|
|
13616
13952
|
this.noteLifecycle({ taskRef: run.taskRef, run: id, kind: "run-started", identity: `r${id}`, ...words }, run.now);
|
|
13617
13953
|
}
|
|
13618
13954
|
return id;
|
|
@@ -14601,7 +14937,7 @@ export class Store {
|
|
|
14601
14937
|
if (taskId === null)
|
|
14602
14938
|
return;
|
|
14603
14939
|
const words = LIFECYCLE_PHASE_WORDS[phase];
|
|
14604
|
-
this.noteLifecycle({ taskRef, run: id, kind: "run-phase", identity: `r${id}`, subject: `
|
|
14940
|
+
this.noteLifecycle({ taskRef, run: id, kind: "run-phase", identity: `r${id}`, subject: `Build #${id}: ${words.subject}`, body: words.body, link: chatControlHref("task", taskId) }, now);
|
|
14605
14941
|
});
|
|
14606
14942
|
}
|
|
14607
14943
|
/** The accepted head and the validated conclusion, once known. */
|
|
@@ -17825,6 +18161,7 @@ export class Store {
|
|
|
17825
18161
|
this.db.prepare(`INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail)
|
|
17826
18162
|
VALUES (?,?,?,NULL,NULL,'project remote recorded','recorded','access',?)`)
|
|
17827
18163
|
.run(now.toISOString(), actor, repo, JSON.stringify({ previousRemote: previous, remoteUrl }));
|
|
18164
|
+
codeMapRemoteChanged(this, repo);
|
|
17828
18165
|
}
|
|
17829
18166
|
/** The remote URL captured on admission or explicitly supplied by the owner; null when none was (or the project was removed). */
|
|
17830
18167
|
projectRemote(path) {
|
|
@@ -17833,7 +18170,10 @@ export class Store {
|
|
|
17833
18170
|
}
|
|
17834
18171
|
/** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
|
|
17835
18172
|
forgetProject(path) {
|
|
17836
|
-
this.
|
|
18173
|
+
this.transact(() => {
|
|
18174
|
+
forgetCodeMap(this, path);
|
|
18175
|
+
this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
|
|
18176
|
+
});
|
|
17837
18177
|
}
|
|
17838
18178
|
/** Most recently opened first — the opener page's order. */
|
|
17839
18179
|
listProjects() {
|
|
@@ -18524,10 +18864,7 @@ export class Store {
|
|
|
18524
18864
|
.get(taskRef, BUILT_IN);
|
|
18525
18865
|
if (ref === undefined)
|
|
18526
18866
|
return;
|
|
18527
|
-
|
|
18528
|
-
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
18529
|
-
.run(stamp, String(ref["external_id"]));
|
|
18530
|
-
if (Number(changes) > 0)
|
|
18867
|
+
if (settleVanished(this, taskRef, now) === "queued")
|
|
18531
18868
|
requeued.push(String(ref["external_id"]));
|
|
18532
18869
|
};
|
|
18533
18870
|
const open = this.db
|
|
@@ -18613,16 +18950,11 @@ export class Store {
|
|
|
18613
18950
|
this.db
|
|
18614
18951
|
.prepare("UPDATE claim SET released_at = ?, released_by = 'recovered' WHERE lease_id = ?")
|
|
18615
18952
|
.run(stamp, leaseId);
|
|
18616
|
-
this.db
|
|
18617
|
-
.prepare(`UPDATE task SET state = 'queued', updated_at = ?
|
|
18618
|
-
WHERE state = 'running' AND id = (
|
|
18619
|
-
SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
|
|
18620
|
-
)`)
|
|
18621
|
-
.run(stamp, taskRef, BUILT_IN);
|
|
18622
18953
|
this.db
|
|
18623
18954
|
.prepare(`UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ?
|
|
18624
18955
|
WHERE lease_id = ? AND outcome IS NULL`)
|
|
18625
18956
|
.run(stamp, leaseId);
|
|
18957
|
+
settleVanished(this, taskRef, now);
|
|
18626
18958
|
this.db
|
|
18627
18959
|
.prepare(`UPDATE worktree SET released_at = ?, verified = 0
|
|
18628
18960
|
WHERE runner = ? AND task_ref = ? AND released_at IS NULL`)
|
|
@@ -18644,14 +18976,7 @@ export class Store {
|
|
|
18644
18976
|
this.db
|
|
18645
18977
|
.prepare("UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ? WHERE id = ?")
|
|
18646
18978
|
.run(stamp, Number(row["run_id"]));
|
|
18647
|
-
|
|
18648
|
-
this.db
|
|
18649
|
-
.prepare(`UPDATE task SET state = 'queued', updated_at = ?
|
|
18650
|
-
WHERE state = 'running' AND id = (
|
|
18651
|
-
SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
|
|
18652
|
-
)`)
|
|
18653
|
-
.run(stamp, taskRef, BUILT_IN);
|
|
18654
|
-
}
|
|
18979
|
+
settleVanished(this, taskRef, now);
|
|
18655
18980
|
this.db
|
|
18656
18981
|
.prepare("UPDATE worktree SET released_at = ?, verified = 0 WHERE runner = ? AND task_ref = ? AND released_at IS NULL")
|
|
18657
18982
|
.run(stamp, runner, taskRef);
|
|
@@ -18785,7 +19110,7 @@ export class Store {
|
|
|
18785
19110
|
if (stoppedTask !== null) {
|
|
18786
19111
|
this.noteLifecycle(run.role === "reviewer"
|
|
18787
19112
|
? { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping review #${run.id}`, body: "The review is being stopped. The saved result is unchanged.", link: chatControlHref("recovery", stoppedTask) }
|
|
18788
|
-
: { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping
|
|
19113
|
+
: { taskRef: run.taskRef, run: run.id, kind: "run-stopping", identity: `r${run.id}`, subject: `Stopping build #${run.id}`, body: "Its processes are being stopped and its work is kept. The task stays paused until this build is resumed.", link: chatControlHref("recovery", stoppedTask) }, now);
|
|
18789
19114
|
}
|
|
18790
19115
|
this.bumpWake();
|
|
18791
19116
|
return { ok: true, stop: this.stopOf(args.runId), repeated: false };
|
|
@@ -18820,12 +19145,20 @@ export class Store {
|
|
|
18820
19145
|
* what kind of answer it is (needs-you.ts reads it): `alive` while
|
|
18821
19146
|
* something of the run may still be running, `elsewhere` when only
|
|
18822
19147
|
* another computer can tell, `unprovable` when nothing is known to run
|
|
18823
|
-
* but its exit was never recorded
|
|
18824
|
-
*
|
|
18825
|
-
*
|
|
18826
|
-
*
|
|
19148
|
+
* but its exit was never recorded (Toolroll's own look, stop-proof.ts,
|
|
19149
|
+
* settles it), `unknown` when this computer can't check, or that look
|
|
19150
|
+
* was blocked. `settling` already meets the automatic reconciliation
|
|
19151
|
+
* proof. Nobody is ever asked to confirm a build stopped. `cause` names
|
|
19152
|
+
* what stop-proof.ts can prove. */
|
|
18827
19153
|
stopQuiescenceFact(runId) {
|
|
18828
|
-
const fact = (run, kind, problem) =>
|
|
19154
|
+
const fact = (run, kind, problem, cause) => cause === undefined ? { run, kind, problem } : { run, kind, problem, cause };
|
|
19155
|
+
// What Toolroll can prove on its own (stop-proof.ts): until its own check of this computer's processes has run, the
|
|
19156
|
+
// answer is `unprovable` (it settles by itself); once that check could not look, or found a process it didn't start
|
|
19157
|
+
// in the build's folder, it is `unknown`, and the plain reason says why. Nobody is asked to vouch either way.
|
|
19158
|
+
const provable = (run, problem, cause) => {
|
|
19159
|
+
const verdict = stopProofVerdictOf(this, run);
|
|
19160
|
+
return verdict === null ? fact(run, "unprovable", problem, cause) : fact(run, "unknown", `run #${run} ${verdict}`, cause);
|
|
19161
|
+
};
|
|
18829
19162
|
const ids = this.ownedRunsOf(runId);
|
|
18830
19163
|
if (ids.length === 0)
|
|
18831
19164
|
return fact(runId, "open", `run #${runId} does not exist`);
|
|
@@ -18836,24 +19169,27 @@ export class Store {
|
|
|
18836
19169
|
if (ownedProcessCount(runOwnerTag(this, id)) > 0)
|
|
18837
19170
|
return fact(id, "alive", `run #${id} still has an owned subprocess`);
|
|
18838
19171
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
19172
|
+
const here = storeMachine(this.db);
|
|
18839
19173
|
let incompleteSpawn = false;
|
|
18840
19174
|
if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
|
|
18841
|
-
return
|
|
19175
|
+
return provable(id, `run #${id} may have spawned before its process witness was recorded; exit is unproven`, "no-witness");
|
|
18842
19176
|
}
|
|
18843
19177
|
for (const witness of witnesses) {
|
|
18844
19178
|
// A recorded group exit proves the whole group ended, not just its
|
|
18845
19179
|
// leader. Re-probing its number could mistake reuse for the old group.
|
|
18846
19180
|
if (witness["exited_at"] !== null)
|
|
18847
19181
|
continue;
|
|
18848
|
-
const host = String(witness["host"]);
|
|
18849
19182
|
const bootId = witness["boot_id"] === null || witness["boot_id"] === undefined ? null : String(witness["boot_id"]);
|
|
18850
|
-
//
|
|
19183
|
+
// This machine by its stable id, or by one of its own names for a witness from before v150 (machine-identity.ts):
|
|
19184
|
+
// a hostname that moved with the network never makes this machine's own witness another computer's.
|
|
19185
|
+
const local = witnessIsLocal(witness, here);
|
|
19186
|
+
// A VERIFIED boot change on this machine (v53): nothing of the old
|
|
18851
19187
|
// boot survives — an incomplete spawn, a reused pid and a native
|
|
18852
19188
|
// object alike are gone. Every other case keeps the conservative
|
|
18853
19189
|
// road below.
|
|
18854
|
-
if (provenDeadByBootChange({ host, bootId }))
|
|
19190
|
+
if (local && provenDeadByBootChange({ host: String(witness["host"]), bootId }, { host: String(witness["host"]), bootId: currentBootId() }))
|
|
18855
19191
|
continue;
|
|
18856
|
-
if (
|
|
19192
|
+
if (!local)
|
|
18857
19193
|
return fact(id, "elsewhere", `run #${id}'s process belongs to another host; exit is unproven here`);
|
|
18858
19194
|
const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
|
|
18859
19195
|
const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
|
|
@@ -18887,24 +19223,32 @@ export class Store {
|
|
|
18887
19223
|
return fact(id, "alive", `run #${id}'s workspace is still held by process ${occupied.by}`);
|
|
18888
19224
|
}
|
|
18889
19225
|
catch {
|
|
18890
|
-
// An unreadable workspace note is no proof either way;
|
|
18891
|
-
|
|
18892
|
-
|
|
19226
|
+
// An unreadable workspace note is no proof either way; Toolroll's own look at this computer's processes settles
|
|
19227
|
+
// it (stop-proof.ts), as an approver's recorded word did before.
|
|
19228
|
+
if (!this.workspaceConfirmedByApprover(id)) {
|
|
19229
|
+
const verdict = stopProofVerdictOf(this, id);
|
|
19230
|
+
return fact(id, "unknown", verdict === null ? `run #${id}'s workspace occupancy could not be established` : `run #${id} ${verdict}`, "workspace-unreadable");
|
|
19231
|
+
}
|
|
18893
19232
|
}
|
|
18894
19233
|
}
|
|
18895
|
-
if (incompleteSpawn)
|
|
18896
|
-
|
|
19234
|
+
if (incompleteSpawn) {
|
|
19235
|
+
if (this.settleableUnspawnedWitnesses(run, witnesses).length > 0)
|
|
19236
|
+
return fact(id, "settling", `run #${id} has an incomplete spawn witness; exit is unproven`);
|
|
19237
|
+
return provable(id, `run #${id} has an incomplete spawn witness; exit is unproven`, "incomplete-spawn");
|
|
19238
|
+
}
|
|
18897
19239
|
}
|
|
18898
19240
|
return null;
|
|
18899
19241
|
}
|
|
18900
|
-
/**
|
|
18901
|
-
*
|
|
19242
|
+
/** A run whose workspace note couldn't be read was proven free: by Toolroll's own look (stop-proof.ts), or, in
|
|
19243
|
+
* ledgers from before it, by an approver's recorded word (still read, never written now). */
|
|
18902
19244
|
workspaceConfirmedByApprover(runId) {
|
|
18903
|
-
|
|
19245
|
+
// An older approver's word still counts (ledger entries before stop-proof.ts); Toolroll's own proof counts the same.
|
|
19246
|
+
return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action IN ('workspace settled by approver', ?) LIMIT 1").get(runId, STOP_PROOF_WORKSPACE_FREE) !== undefined;
|
|
18904
19247
|
}
|
|
18905
19248
|
reserveRunProcess(runId, now, group = true) {
|
|
18906
|
-
|
|
18907
|
-
|
|
19249
|
+
recordMachineHostAliases(this.db, now);
|
|
19250
|
+
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,machine_id) VALUES (?,?,?,?,?,?)")
|
|
19251
|
+
.run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId(), currentMachineId()).lastInsertRowid);
|
|
18908
19252
|
}
|
|
18909
19253
|
/** The run's process witnesses not yet proved exited — after first saving whatever exit evidence is available. */
|
|
18910
19254
|
openRunProcessCount(runId, now) {
|
|
@@ -18924,12 +19268,13 @@ export class Store {
|
|
|
18924
19268
|
// check uses: ESRCH, or a process now holding the pid that was born after
|
|
18925
19269
|
// the run ended. An open run has no finish time, so only ESRCH counts.
|
|
18926
19270
|
const finishedAt = this.getRun(runId)?.finishedAt ?? null;
|
|
19271
|
+
const here = storeMachine(this.db);
|
|
18927
19272
|
let recorded = 0;
|
|
18928
19273
|
for (const row of rows) {
|
|
18929
19274
|
const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
|
|
18930
|
-
if (
|
|
19275
|
+
if (!witnessIsLocal(row, here) || typeof row["observed_at"] !== "string" || !Number.isFinite(Date.parse(row["observed_at"])) || Date.parse(row["observed_at"]) > now.getTime())
|
|
18931
19276
|
continue;
|
|
18932
|
-
let gone = provenDeadByBootChange({ host, bootId });
|
|
19277
|
+
let gone = provenDeadByBootChange({ host, bootId }, { host, bootId: currentBootId() });
|
|
18933
19278
|
if (!gone) {
|
|
18934
19279
|
const backend = row["containment"] == null ? null : String(row["containment"]);
|
|
18935
19280
|
const container = row["container"] == null ? null : String(row["container"]);
|
|
@@ -18940,11 +19285,12 @@ export class Store {
|
|
|
18940
19285
|
gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, finishedAt === null ? undefined : { observedAt: row["observed_at"], finishedAt });
|
|
18941
19286
|
}
|
|
18942
19287
|
}
|
|
19288
|
+
const sameMachine = witnessesCarryMachine(this.db);
|
|
18943
19289
|
if (gone)
|
|
18944
19290
|
recorded += Number(this.db.prepare(`UPDATE run_process SET exited_at = ?
|
|
18945
|
-
WHERE id = ? AND run = ? AND exited_at IS NULL AND pid IS ? AND host = ? AND process_group = ?
|
|
19291
|
+
WHERE id = ? AND run = ? AND exited_at IS NULL AND pid IS ? AND host = ?${sameMachine ? " AND machine_id IS ?" : ""} AND process_group = ?
|
|
18946
19292
|
AND observed_at = ? AND boot_id IS ? AND containment IS ? AND container IS ? AND container_identity IS ?`)
|
|
18947
|
-
.run(now.toISOString(), row["id"], runId, row["pid"], row["host"], row["process_group"], row["observed_at"], row["boot_id"], row["containment"], row["container"], row["container_identity"]).changes);
|
|
19293
|
+
.run(now.toISOString(), row["id"], runId, row["pid"], row["host"], ...(sameMachine ? [row["machine_id"] ?? null] : []), row["process_group"], row["observed_at"], row["boot_id"], row["containment"], row["container"], row["container_identity"]).changes);
|
|
18948
19294
|
}
|
|
18949
19295
|
return recorded;
|
|
18950
19296
|
}
|
|
@@ -18954,12 +19300,13 @@ export class Store {
|
|
|
18954
19300
|
* yet stays open and is passed over, never holding newer runs behind it:
|
|
18955
19301
|
* every finished run is visited, a batch at a time. Never signals. */
|
|
18956
19302
|
recordFinishedRunExits(now, batch = 200) {
|
|
19303
|
+
const local = localWitnessSql(this.db, "p");
|
|
18957
19304
|
const page = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
18958
|
-
WHERE p.exited_at IS NULL AND
|
|
19305
|
+
WHERE p.exited_at IS NULL AND ${local.sql} AND p.run > ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
|
|
18959
19306
|
ORDER BY p.run LIMIT ?`);
|
|
18960
19307
|
let recorded = 0;
|
|
18961
19308
|
for (let after = 0;;) {
|
|
18962
|
-
const runs = page.all(
|
|
19309
|
+
const runs = page.all(...local.params, after, batch).map(row => Number(row["run"]));
|
|
18963
19310
|
for (const run of runs)
|
|
18964
19311
|
recorded += this.recordRunProcessExits(run, now);
|
|
18965
19312
|
if (runs.length < batch)
|
|
@@ -18989,12 +19336,13 @@ export class Store {
|
|
|
18989
19336
|
* here, every other witness of the run has exited and none of the run's
|
|
18990
19337
|
* process groups is alive, it settles as never started, with a ledger
|
|
18991
19338
|
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
18992
|
-
*
|
|
19339
|
+
* Toolroll's own look (stop-proof.ts). Native witnesses keep their OS-only road. */
|
|
18993
19340
|
settleUnspawnedWitnesses(now, onlyRun) {
|
|
19341
|
+
const local = localWitnessSql(this.db, "p");
|
|
18994
19342
|
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
18995
19343
|
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
18996
|
-
AND
|
|
18997
|
-
AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(
|
|
19344
|
+
AND ${local.sql} AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
|
|
19345
|
+
AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(...local.params, onlyRun ?? null, onlyRun ?? null);
|
|
18998
19346
|
let settled = 0;
|
|
18999
19347
|
for (const candidate of candidates) {
|
|
19000
19348
|
const runId = Number(candidate["run"]);
|
|
@@ -19029,93 +19377,16 @@ export class Store {
|
|
|
19029
19377
|
settleableUnspawnedWitnesses(run, rows) {
|
|
19030
19378
|
if (run.outcome === null || run.finishedAt === null || ownedProcessCount(runOwnerTag(this, run.id)) > 0)
|
|
19031
19379
|
return [];
|
|
19032
|
-
const
|
|
19380
|
+
const here = storeMachine(this.db);
|
|
19381
|
+
const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && witnessIsLocal(row, here));
|
|
19033
19382
|
const others = rows.filter(row => !pidless.includes(row));
|
|
19034
19383
|
if (pidless.length === 0 || others.length === 0 || others.some(row => row["exited_at"] === null))
|
|
19035
19384
|
return [];
|
|
19036
|
-
const known = others.filter(row => row["pid"] !== null && row
|
|
19385
|
+
const known = others.filter(row => row["pid"] !== null && witnessIsLocal(row, here));
|
|
19037
19386
|
if (known.length === 0 || known.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] })))
|
|
19038
19387
|
return [];
|
|
19039
19388
|
return pidless;
|
|
19040
19389
|
}
|
|
19041
|
-
/** `run settle` (an approver's last resort): every witness of a finished
|
|
19042
|
-
* run that cannot be proven either way is recorded settled under the
|
|
19043
|
-
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
19044
|
-
* alive or could be checked only from another host. Never signals. */
|
|
19045
|
-
settleRunWitnessesByApprover(args, now) {
|
|
19046
|
-
return this.transact(() => {
|
|
19047
|
-
const run = this.getRun(args.runId);
|
|
19048
|
-
if (run === null)
|
|
19049
|
-
return { ok: false, reason: "unknown-run", detail: `no run #${args.runId}` };
|
|
19050
|
-
if (run.outcome === null)
|
|
19051
|
-
return { ok: false, reason: "still-running", detail: `run #${args.runId} is still open` };
|
|
19052
|
-
const alive = (detail) => ({ ok: false, reason: "alive", detail });
|
|
19053
|
-
if (ownedProcessCount(runOwnerTag(this, args.runId)) > 0)
|
|
19054
|
-
return alive(`run #${args.runId} still has an owned subprocess`);
|
|
19055
|
-
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(args.runId);
|
|
19056
|
-
const open = rows.filter(row => row["exited_at"] === null);
|
|
19057
|
-
for (const row of rows) {
|
|
19058
|
-
if (row["pid"] === null)
|
|
19059
|
-
continue;
|
|
19060
|
-
if (row["host"] !== hostname()) {
|
|
19061
|
-
if (row["exited_at"] === null)
|
|
19062
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
19063
|
-
continue;
|
|
19064
|
-
}
|
|
19065
|
-
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] }))
|
|
19066
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
19067
|
-
}
|
|
19068
|
-
for (const row of open) {
|
|
19069
|
-
if (row["pid"] === null && row["host"] !== hostname())
|
|
19070
|
-
return alive(`run #${args.runId} has a witness from ${String(row["host"])}; settle it there`);
|
|
19071
|
-
if (row["containment"] != null && row["container"] != null && row["container_empty_at"] == null &&
|
|
19072
|
-
containerEmptiness(String(row["containment"]), String(row["container"]), process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "populated") {
|
|
19073
|
-
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
19074
|
-
}
|
|
19075
|
-
}
|
|
19076
|
-
// A workspace still held by a live process refuses; one whose note can't
|
|
19077
|
-
// be read is settled by the approver's word, in the ledger.
|
|
19078
|
-
let workspace = false;
|
|
19079
|
-
if (run.worktree !== null) {
|
|
19080
|
-
try {
|
|
19081
|
-
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
19082
|
-
if (occupied.held)
|
|
19083
|
-
return alive(`run #${args.runId}'s workspace is still held by process ${occupied.by}`);
|
|
19084
|
-
}
|
|
19085
|
-
catch {
|
|
19086
|
-
workspace = !this.workspaceConfirmedByApprover(args.runId);
|
|
19087
|
-
}
|
|
19088
|
-
}
|
|
19089
|
-
const ref = this.refById(run.taskRef);
|
|
19090
|
-
if (workspace) {
|
|
19091
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19092
|
-
action: "workspace settled by approver", outcome: run.worktree ?? "", source: "request", detail: args.why });
|
|
19093
|
-
}
|
|
19094
|
-
// A run that may have spawned before any witness was written has none
|
|
19095
|
-
// to settle: the approver's word is recorded as one ended witness.
|
|
19096
|
-
if (rows.length === 0 && run.providerStartedAt !== null && !this.processesSummarized(args.runId)) {
|
|
19097
|
-
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
19098
|
-
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
19099
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19100
|
-
action: "process witness settled by approver", outcome: `witness ${id}`, source: "request", detail: args.why });
|
|
19101
|
-
this.settleQuiescentStops(now);
|
|
19102
|
-
return { ok: true, witnesses: [id], repeated: false };
|
|
19103
|
-
}
|
|
19104
|
-
if (open.length === 0) {
|
|
19105
|
-
if (workspace)
|
|
19106
|
-
this.settleQuiescentStops(now);
|
|
19107
|
-
return { ok: true, witnesses: [], repeated: !workspace };
|
|
19108
|
-
}
|
|
19109
|
-
const ids = open.map(row => Number(row["id"]));
|
|
19110
|
-
for (const id of ids)
|
|
19111
|
-
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
19112
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19113
|
-
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
19114
|
-
compactSettledRunProcesses(this.db, now, args.runId);
|
|
19115
|
-
this.settleQuiescentStops(now);
|
|
19116
|
-
return { ok: true, witnesses: ids, repeated: false };
|
|
19117
|
-
});
|
|
19118
|
-
}
|
|
19119
19390
|
recordRunProcess(runId, pid, now, group = true, witness) {
|
|
19120
19391
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
19121
19392
|
throw new Error("a process witness needs a valid spawned PID");
|
|
@@ -19126,8 +19397,9 @@ export class Store {
|
|
|
19126
19397
|
throw new Error("spawn witness custody changed");
|
|
19127
19398
|
return;
|
|
19128
19399
|
}
|
|
19129
|
-
this.db
|
|
19130
|
-
|
|
19400
|
+
recordMachineHostAliases(this.db, now);
|
|
19401
|
+
this.db.prepare("INSERT INTO run_process (run,pid,host,process_group,observed_at,boot_id,machine_id) VALUES (?,?,?,?,?,?,?)")
|
|
19402
|
+
.run(runId, pid, hostname(), group ? 1 : 0, now.toISOString(), currentBootId(), currentMachineId());
|
|
19131
19403
|
}
|
|
19132
19404
|
/** The native OS object a witnessed spawn ran inside (v53) — recorded
|
|
19133
19405
|
* with the spawn, before the target can execute. */
|
|
@@ -19147,11 +19419,12 @@ export class Store {
|
|
|
19147
19419
|
* must independently prove gone; this never grants authority to signal. */
|
|
19148
19420
|
runCustodyProvenGone(runId) {
|
|
19149
19421
|
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ?").all(runId);
|
|
19422
|
+
const here = storeMachine(this.db);
|
|
19150
19423
|
// A summarized run's witnesses all exited, each on recorded proof.
|
|
19151
19424
|
return (rows.length > 0 || this.processesSummarized(runId)) && rows.every(row => {
|
|
19152
|
-
if (
|
|
19425
|
+
if (!witnessIsLocal(row, here))
|
|
19153
19426
|
return false;
|
|
19154
|
-
if (provenDeadByBootChange({ host: String(row["host"]), bootId: row["boot_id"] == null ? null : String(row["boot_id"]) }))
|
|
19427
|
+
if (provenDeadByBootChange({ host: String(row["host"]), bootId: row["boot_id"] == null ? null : String(row["boot_id"]) }, { host: String(row["host"]), bootId: currentBootId() }))
|
|
19155
19428
|
return true;
|
|
19156
19429
|
if (row["containment"] == null || row["container"] == null)
|
|
19157
19430
|
return false;
|
|
@@ -19227,7 +19500,7 @@ export class Store {
|
|
|
19227
19500
|
this.addRunNote(args.runId, args.by, "Resumed. The next pass takes a fresh claim, re-proves the signed scope, and inherits this attempt's preserved draft with fresh proof.", now);
|
|
19228
19501
|
const resumedTask = this.externalIdFor(args.taskRef);
|
|
19229
19502
|
if (resumedTask !== null) {
|
|
19230
|
-
this.noteLifecycle({ taskRef: args.taskRef, run: args.runId, kind: "run-resumed", identity: `r${args.runId}`, subject: `
|
|
19503
|
+
this.noteLifecycle({ taskRef: args.taskRef, run: args.runId, kind: "run-resumed", identity: `r${args.runId}`, subject: `Build #${args.runId} resumed`, body: "The next attempt continues from the saved work.", link: chatControlHref("task", resumedTask) }, now);
|
|
19231
19504
|
}
|
|
19232
19505
|
this.bumpWake();
|
|
19233
19506
|
return { ok: true, stop: this.stopOf(args.runId) };
|
|
@@ -19987,12 +20260,14 @@ export class Store {
|
|
|
19987
20260
|
.prepare(`INSERT OR IGNORE INTO push_delivery (notification, subscription, created_at)
|
|
19988
20261
|
SELECT notification.id, push_subscription.id, ?
|
|
19989
20262
|
FROM notification, push_subscription
|
|
19990
|
-
WHERE
|
|
20263
|
+
WHERE notification.push_class IS NOT NULL
|
|
20264
|
+
-- A fact that needs nothing from a person (checks in progress) stays in the activity log: no phone ping.
|
|
20265
|
+
AND notification.kind NOT IN (SELECT value FROM json_each(?))
|
|
19991
20266
|
AND notification.resolved_at IS NULL
|
|
19992
20267
|
AND (notification.recipient IS NULL OR notification.recipient = push_subscription.approver)
|
|
19993
20268
|
AND push_subscription.retired_at IS NULL
|
|
19994
20269
|
AND notification.id > push_subscription.starts_after_notification`)
|
|
19995
|
-
.run(now.toISOString());
|
|
20270
|
+
.run(now.toISOString(), JSON.stringify([...ACTIVITY_ONLY_KINDS]));
|
|
19996
20271
|
// Pings follow responsibility: a pair this person must not be alerted about retires before it is ever sent.
|
|
19997
20272
|
if (Number(changes) > 0) {
|
|
19998
20273
|
const fresh = this.db.prepare(`SELECT p.id AS pair, s.approver AS approver, n.* FROM push_delivery p
|
|
@@ -20319,7 +20594,8 @@ export class Store {
|
|
|
20319
20594
|
return null;
|
|
20320
20595
|
}
|
|
20321
20596
|
}
|
|
20322
|
-
/** The level a run
|
|
20597
|
+
/** The level a later check of this run may run at, at most: missing or unreadable reads as sandboxed, the narrowest.
|
|
20598
|
+
* Never an account of how its checks DID run: that is runCheckStamp (check-stamp.ts). */
|
|
20323
20599
|
runCheckAccess(runId) {
|
|
20324
20600
|
return checkAccessOf(this.db.prepare("SELECT sandbox_access FROM run WHERE id = ?").get(runId)?.["sandbox_access"]);
|
|
20325
20601
|
}
|
|
@@ -21602,6 +21878,11 @@ export function carryReleaseApproval(store, w, now, recheck) {
|
|
|
21602
21878
|
const problem = recheck();
|
|
21603
21879
|
if (problem !== null)
|
|
21604
21880
|
return { ok: false, why: problem };
|
|
21881
|
+
// How the release's check ran, read again with the write: a launch since the preflight counts (H2).
|
|
21882
|
+
const ran = taskCheckStamp(store, a.repo, a.tipTask);
|
|
21883
|
+
const unshown = ran === null ? null : stampRefusal(ran.reading, checkAccessOf(a.accessDigest));
|
|
21884
|
+
if (unshown !== null)
|
|
21885
|
+
return { ok: false, why: `This release's approval can't carry: ${unshown}.` };
|
|
21605
21886
|
if (store.getTask(w.taskId) !== null)
|
|
21606
21887
|
return { ok: false, why: `A task ${w.taskId} already exists.` };
|
|
21607
21888
|
const anchorRow = store.handle.prepare("SELECT * FROM task_scope WHERE task_id = ?").get(a.anchorTask);
|
|
@@ -21637,7 +21918,7 @@ export function carryReleaseApproval(store, w, now, recheck) {
|
|
|
21637
21918
|
store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: a.repo, taskId: w.taskId, runId: null, action: "release carried", outcome: "carried", source: "work",
|
|
21638
21919
|
detail: `release ${a.id} · carry ${carryId} · ${a.tipSha} → ${w.to} · ${w.files} files · ${w.changedLines} lines · request ${w.requestId}` });
|
|
21639
21920
|
// One quiet task notification, no push.
|
|
21640
|
-
store.enqueueNotification({ dedupeKey: `release-carried:${a.id}:${carryId}`, kind: "release-carried", subject: `
|
|
21921
|
+
store.enqueueNotification({ dedupeKey: `release-carried:${a.id}:${carryId}`, kind: "release-carried", subject: `Release now includes ${w.to.slice(0, 7)}`,
|
|
21641
21922
|
body: `Release ${a.version} now ships ${w.to.slice(0, 7)} (${w.files} file${w.files === 1 ? "" : "s"}, ${w.changedLines} line${w.changedLines === 1 ? "" : "s"}) under ${anchor.approvedBy ?? "the"} release approval.`,
|
|
21642
21923
|
link: chatControlHref("task", w.taskId), source: { taskRef: ref.id } }, now);
|
|
21643
21924
|
return { ok: true, taskId: w.taskId, carry: releaseCarryTo(store, a.id, w.to) };
|
|
@@ -22458,7 +22739,8 @@ export function recordReleaseStep(store, entry) {
|
|
|
22458
22739
|
.run(entry.taskId, entry.tag, entry.version, entry.pr, entry.step, entry.state, at, ended ? at : null, detail, at);
|
|
22459
22740
|
}
|
|
22460
22741
|
/** A release's steps, for the release task: the newest tag recorded for it, its steps in the order they ran, and the
|
|
22461
|
-
* step it stopped at when the last thing recorded was a failure
|
|
22742
|
+
* step it stopped at when the last thing recorded was a failure and no build of the task has started since. Null: no
|
|
22743
|
+
* step recorded. */
|
|
22462
22744
|
export function releaseProgressFor(store, taskId) {
|
|
22463
22745
|
const newest = store.handle.prepare("SELECT tag FROM release_step WHERE task_id = ? ORDER BY updated_at DESC, rowid DESC LIMIT 1").get(taskId);
|
|
22464
22746
|
if (newest === undefined)
|
|
@@ -22468,9 +22750,13 @@ export function releaseProgressFor(store, taskId) {
|
|
|
22468
22750
|
const steps = rows.map(row => ({ step: String(row["step"]), state: String(row["state"]), startedAt: String(row["started_at"]),
|
|
22469
22751
|
endedAt: row["ended_at"] === null ? null : String(row["ended_at"]), detail: row["detail"] === null ? null : String(row["detail"]) }));
|
|
22470
22752
|
const last = [...rows].sort((a, b) => String(a["updated_at"]).localeCompare(String(b["updated_at"]))).at(-1);
|
|
22471
|
-
const
|
|
22753
|
+
const failed = String(last["state"]) === "failed";
|
|
22754
|
+
const live = !failed ? undefined : store.handle.prepare(`SELECT id, started_at FROM run WHERE task_ref IN (SELECT value FROM json_each(?)) AND role = 'builder'
|
|
22755
|
+
AND finished_at IS NULL AND outcome IS NULL AND started_at > ? ORDER BY id DESC LIMIT 1`).get(JSON.stringify(taskFamilyRefs(store, taskId).map(one => one.id)), String(last["updated_at"]));
|
|
22756
|
+
const building = live === undefined ? null : { run: Number(live["id"]), startedAt: String(live["started_at"]) };
|
|
22757
|
+
const stopped = failed && building === null ? { step: String(last["step"]), message: last["detail"] === null ? "" : String(last["detail"]), at: String(last["updated_at"]) } : null;
|
|
22472
22758
|
const pr = last["pr"] === null ? null : Number(last["pr"]);
|
|
22473
|
-
return { tag, version: String(last["version"]), pr, startedAt: steps[0].startedAt, updatedAt: String(last["updated_at"]), steps, stopped };
|
|
22759
|
+
return { tag, version: String(last["version"]), pr, startedAt: steps[0].startedAt, updatedAt: String(last["updated_at"]), steps, stopped, building };
|
|
22474
22760
|
}
|
|
22475
22761
|
/** The release task whose steps were recorded most recently (v146), or null when none was. */
|
|
22476
22762
|
export function latestReleaseStepTask(store) {
|
|
@@ -22661,3 +22947,166 @@ export function forgetProjectModels(store, repo, now) {
|
|
|
22661
22947
|
return rows + forgetProjectRoutes(store, repo);
|
|
22662
22948
|
});
|
|
22663
22949
|
}
|
|
22950
|
+
// ---- Toolroll's own stop proof (stop-proof.ts) ------------------------------------------------------------------
|
|
22951
|
+
/** The ledger words of Toolroll's own stop proof. Nobody is asked to vouch that a build stopped: Toolroll looks at
|
|
22952
|
+
* this computer's processes itself, and a person's older recorded word (`run settle` before this) is still read. */
|
|
22953
|
+
export const STOP_PROOF_SETTLED = "run stop proven automatically";
|
|
22954
|
+
export const STOP_PROOF_BLOCKED = "automatic stop check blocked";
|
|
22955
|
+
export const STOP_PROOF_WORKSPACE_FREE = "workspace proven free automatically";
|
|
22956
|
+
/** Why Toolroll's own stop check is blocked for a run, in words that follow "run #N " (the stop fact's problem), or
|
|
22957
|
+
* null when its newest word is not a block (never checked, or proven since). */
|
|
22958
|
+
export function stopProofVerdictOf(store, runId) {
|
|
22959
|
+
const row = store.handle.prepare("SELECT action, outcome FROM action_ledger WHERE run_id = ? AND action IN (?, ?) ORDER BY id DESC LIMIT 1")
|
|
22960
|
+
.get(runId, STOP_PROOF_BLOCKED, STOP_PROOF_SETTLED);
|
|
22961
|
+
return row === undefined || row["action"] !== STOP_PROOF_BLOCKED ? null : String(row["outcome"]);
|
|
22962
|
+
}
|
|
22963
|
+
/** Record that Toolroll's own stop check could not prove a run stopped (`words` follow "run #N "; `detail` is the
|
|
22964
|
+
* evidence). The same block twice in a row is recorded once. True when recorded. */
|
|
22965
|
+
export function recordStopProofBlocked(store, runId, words, detail, now) {
|
|
22966
|
+
return store.transact(() => {
|
|
22967
|
+
if (stopProofVerdictOf(store, runId) === words)
|
|
22968
|
+
return false;
|
|
22969
|
+
const run = store.getRun(runId);
|
|
22970
|
+
const ref = run === null ? null : store.refById(run.taskRef);
|
|
22971
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId,
|
|
22972
|
+
action: STOP_PROOF_BLOCKED, outcome: words, source: "work", detail });
|
|
22973
|
+
return true;
|
|
22974
|
+
});
|
|
22975
|
+
}
|
|
22976
|
+
/**
|
|
22977
|
+
* Settle a finished run's unproven stop on Toolroll's own observation of this computer (stop-proof.ts): the caller
|
|
22978
|
+
* looked at every process here and found none of the run's. Applies only while the run's stop is still unproven for
|
|
22979
|
+
* exactly `cause` (re-read in this transaction): a pid-less spawn witness is recorded ended, a run with no witness gets
|
|
22980
|
+
* one ended witness, an unreadable workspace note is recorded proven free. The evidence goes in the ledger. Never
|
|
22981
|
+
* signals. True when this call settled it.
|
|
22982
|
+
*/
|
|
22983
|
+
export function settleRunStopByObservation(store, args, now) {
|
|
22984
|
+
return store.transact(() => {
|
|
22985
|
+
const fact = store.stopQuiescenceFact(args.runId);
|
|
22986
|
+
if (fact === null || fact.run !== args.runId || fact.cause !== args.cause || (fact.kind !== "unprovable" && fact.kind !== "unknown"))
|
|
22987
|
+
return false;
|
|
22988
|
+
const run = store.getRun(args.runId);
|
|
22989
|
+
if (run === null || run.outcome === null || run.finishedAt === null)
|
|
22990
|
+
return false;
|
|
22991
|
+
const db = store.handle;
|
|
22992
|
+
const ref = store.refById(run.taskRef);
|
|
22993
|
+
const at = now.toISOString();
|
|
22994
|
+
let what;
|
|
22995
|
+
if (args.cause === "incomplete-spawn") {
|
|
22996
|
+
const local = localWitnessSql(db);
|
|
22997
|
+
const changed = db.prepare(`UPDATE run_process SET exited_at = ? WHERE run = ? AND exited_at IS NULL AND pid IS NULL
|
|
22998
|
+
AND containment IS NULL AND container IS NULL AND ${local.sql}`).run(at, args.runId, ...local.params).changes;
|
|
22999
|
+
if (Number(changed) === 0)
|
|
23000
|
+
return false;
|
|
23001
|
+
what = `${Number(changed)} process witness${Number(changed) === 1 ? "" : "es"} with no process ID recorded ended`;
|
|
23002
|
+
}
|
|
23003
|
+
else if (args.cause === "no-witness") {
|
|
23004
|
+
recordMachineHostAliases(db, now);
|
|
23005
|
+
db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at,machine_id) VALUES (?,?,0,?,?,?,?)")
|
|
23006
|
+
.run(args.runId, hostname(), at, currentBootId(), at, currentMachineId());
|
|
23007
|
+
what = "its missing process witness recorded ended";
|
|
23008
|
+
}
|
|
23009
|
+
else {
|
|
23010
|
+
store.recordAction({ at, actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
23011
|
+
action: STOP_PROOF_WORKSPACE_FREE, outcome: run.worktree ?? "", source: "work", detail: args.evidence });
|
|
23012
|
+
what = "its folder recorded free";
|
|
23013
|
+
}
|
|
23014
|
+
store.recordAction({ at, actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
23015
|
+
action: STOP_PROOF_SETTLED, outcome: what, source: "work", detail: args.evidence });
|
|
23016
|
+
compactSettledRunProcesses(db, now, args.runId);
|
|
23017
|
+
store.settleQuiescentStops(now);
|
|
23018
|
+
return true;
|
|
23019
|
+
});
|
|
23020
|
+
}
|
|
23021
|
+
/**
|
|
23022
|
+
* This machine as the store knows it (v150, machine-identity.ts): its stable id and every name it is known by, the
|
|
23023
|
+
* hostnames recorded beside its own id included. Read fresh each time, so a name recorded by another process on this
|
|
23024
|
+
* machine counts at once.
|
|
23025
|
+
*/
|
|
23026
|
+
export function storeMachine(db) {
|
|
23027
|
+
const id = currentMachineId();
|
|
23028
|
+
let recorded = [];
|
|
23029
|
+
if (id !== null) {
|
|
23030
|
+
try {
|
|
23031
|
+
recorded = db.prepare("SELECT host FROM machine_host_alias WHERE machine_id = ?").all(id).map(row => String(row["host"]));
|
|
23032
|
+
}
|
|
23033
|
+
catch {
|
|
23034
|
+
recorded = [];
|
|
23035
|
+
}
|
|
23036
|
+
rememberHostAliases(id, recorded);
|
|
23037
|
+
}
|
|
23038
|
+
return thisMachine(recorded);
|
|
23039
|
+
}
|
|
23040
|
+
/** Load this machine's recorded names for the checks that hold no store; a store that can't read them loads none. */
|
|
23041
|
+
export function rememberStoredHostAliases(db) {
|
|
23042
|
+
try {
|
|
23043
|
+
storeMachine(db);
|
|
23044
|
+
}
|
|
23045
|
+
catch { /* none recorded, or no table to read: only the live names count */ }
|
|
23046
|
+
}
|
|
23047
|
+
/** Record the names this machine is known by right now beside its own id, read here at this moment (never a name a
|
|
23048
|
+
* witness, a note or another computer supplied). Nothing when this machine's id is unknown. */
|
|
23049
|
+
export function recordMachineHostAliases(db, now) {
|
|
23050
|
+
const id = currentMachineId();
|
|
23051
|
+
if (id === null)
|
|
23052
|
+
return;
|
|
23053
|
+
const names = liveHostAliases();
|
|
23054
|
+
const insert = db.prepare("INSERT OR IGNORE INTO machine_host_alias (machine_id, host, first_seen_at) VALUES (?, ?, ?)");
|
|
23055
|
+
for (const host of names)
|
|
23056
|
+
insert.run(id, host, now.toISOString());
|
|
23057
|
+
rememberHostAliases(id, names);
|
|
23058
|
+
}
|
|
23059
|
+
/** Whether a run_process row is this machine's (machine-identity.ts isThisMachine). */
|
|
23060
|
+
export function witnessIsLocal(row, here) {
|
|
23061
|
+
return isThisMachine({ host: String(row["host"]), machineId: row["machine_id"] == null ? null : String(row["machine_id"]) }, here);
|
|
23062
|
+
}
|
|
23063
|
+
/** The same rule as witnessIsLocal, as SQL over run_process (optionally aliased): its id is ours, or it has none and
|
|
23064
|
+
* its host is one of our names. With our id unknown, `machine_id = NULL` matches nothing: an id-bearing witness is
|
|
23065
|
+
* never taken for ours. */
|
|
23066
|
+
export function localWitnessSql(db, alias) {
|
|
23067
|
+
const p = alias === undefined ? "" : `${alias}.`, here = storeMachine(db);
|
|
23068
|
+
if (!witnessesCarryMachine(db))
|
|
23069
|
+
return { sql: `${p}host IN (SELECT value FROM json_each(?))`, params: [JSON.stringify(here.hosts)] };
|
|
23070
|
+
return { sql: `(${p}machine_id = ? OR (${p}machine_id IS NULL AND ${p}host IN (SELECT value FROM json_each(?))))`, params: [here.id, JSON.stringify(here.hosts)] };
|
|
23071
|
+
}
|
|
23072
|
+
/** Whether this database has v150's run_process.machine_id. An update settles leftover records on the database it is
|
|
23073
|
+
* about to back up, still at the older schema (toolroll-update.ts drain), so no query it reaches may assume the column:
|
|
23074
|
+
* there every witness has no id, and the host-name rule above is the whole rule. */
|
|
23075
|
+
export function witnessesCarryMachine(db) {
|
|
23076
|
+
return db.prepare("SELECT 1 FROM pragma_table_info('run_process') WHERE name = 'machine_id'").get() !== undefined;
|
|
23077
|
+
}
|
|
23078
|
+
// ---- Idle cleanup (checkout-cleanup.ts) ----------------------------------------------------------------------------
|
|
23079
|
+
/** Settings → Storage: how long a task stays idle before its clean folder goes (the default until someone chooses). */
|
|
23080
|
+
export function checkoutIdle(store) {
|
|
23081
|
+
const row = store.handle.prepare("SELECT idle FROM checkout_cleanup_setting WHERE id = 1").get();
|
|
23082
|
+
return row === undefined || row["idle"] === null ? DEFAULT_IDLE : String(row["idle"]);
|
|
23083
|
+
}
|
|
23084
|
+
/** Choose how long a task stays idle before its clean folder goes; the ledger keeps before → after. */
|
|
23085
|
+
export function setCheckoutIdle(store, idle, by, now) {
|
|
23086
|
+
return store.transact(() => {
|
|
23087
|
+
const before = checkoutIdle(store);
|
|
23088
|
+
store.handle.prepare("INSERT INTO checkout_cleanup_setting (id, cleanup, idle, updated_by, updated_at) VALUES (1, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET idle = excluded.idle, updated_by = excluded.updated_by, updated_at = excluded.updated_at")
|
|
23089
|
+
.run(store.checkoutCleanup(), idle, by, now.toISOString());
|
|
23090
|
+
if (before !== idle)
|
|
23091
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo: null, taskId: null, runId: null, action: "idle cleanup changed", outcome: "changed", source: "policy", detail: `${idleWords(before)} → ${idleWords(idle)}` });
|
|
23092
|
+
return checkoutIdle(store);
|
|
23093
|
+
});
|
|
23094
|
+
}
|
|
23095
|
+
/** A check launch's execution stamp (check-stamp.ts), written by the code that launched it. One row per distinct launch. */
|
|
23096
|
+
export function recordCheckStamp(store, runId, stamp, now) {
|
|
23097
|
+
store.handle.prepare("INSERT OR IGNORE INTO check_stamp (run_id, digest, stamp, at) VALUES (?, ?, ?, ?)").run(runId, stampDigest(stamp), JSON.stringify(stamp), now.toISOString());
|
|
23098
|
+
}
|
|
23099
|
+
/** How a run's checks actually ran, by every launch it stamped: `unknown` when none is there or one can't be read. */
|
|
23100
|
+
export function runCheckStamp(store, runId) {
|
|
23101
|
+
return readStamps(store.handle.prepare("SELECT stamp FROM check_stamp WHERE run_id = ? ORDER BY id").all(runId).map(row => row["stamp"]));
|
|
23102
|
+
}
|
|
23103
|
+
/** The newest run of a task whose checks ran (it saved a check log), and how they ran; null when none has. */
|
|
23104
|
+
export function taskCheckStamp(store, repo, taskId) {
|
|
23105
|
+
const runs = store.handle.prepare(`SELECT r.id FROM run r JOIN task_ref t ON t.id = r.task_ref WHERE t.repo = ? AND t.external_id = ? ORDER BY r.id DESC`).all(repo, taskId);
|
|
23106
|
+
for (const row of runs) {
|
|
23107
|
+
const runId = Number(row["id"]);
|
|
23108
|
+
if (store.artifactsFor(runId).some(one => one.kind === "check-log"))
|
|
23109
|
+
return { runId, reading: runCheckStamp(store, runId) };
|
|
23110
|
+
}
|
|
23111
|
+
return null;
|
|
23112
|
+
}
|