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.
Files changed (341) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +4 -0
  5. package/dist/agent-fence.js +4 -1
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. 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
- export const SCHEMA_VERSION = 148;
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: "capturing evidence", body: "The diff, checks and proof are being captured." },
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: "verifying the proof", body: "The proof is being checked against the approved acceptance criteria." },
2162
- "correcting-proof": { subject: "correcting the proof", body: "The proof did not match the sealed result; a bounded correction turn is running." },
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 acceptance check was verified against the saved result." },
2176
- refuted: { subject: "evidence conflicts with the result", body: "The reviewer found evidence that contradicts the saved result. It is not accepted as done." },
2177
- attested: { subject: "agent-reported evidence only", body: "The result rests on agent-reported evidence that was not independently verified." },
2178
- short: { subject: "required evidence is missing", body: "Required evidence is missing from the saved result." },
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 worker released it without finishing; another attempt can start." : "Reopened for another attempt.",
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: "Scope approved",
8218
+ subject: "Plan approved",
7884
8219
  body: basis === undefined
7885
- ? `Approved by ${lifecycleWords(by, 40)}. A connected worker can take it next.`
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 worker can take it next.`
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 worker can take it next."
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 worker can take it next."
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 worker can take it next.",
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
- const { changes } = this.db
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 attempt #${runId}'s saved result. A connected reviewer runs it next.`,
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: "Requeued",
11918
+ subject: "Queued again",
11583
11919
  body: rejectedRun !== null
11584
- ? `Its last result (attempt #${rejectedRun}) was not accepted; the next attempt continues on the same branch under the current scope.`
11585
- : `${incidents.length > 0 ? "Its incident is resolved and its" : "Its"} failure streak is cleared; a worker can take it again.`,
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: `Attempt #${id} stopped`, body: "Its work is preserved. Resume this attempt from the task page when ready.", link: chatControlHref("recovery", taskId) }, now);
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: `Attempt #${id} built`,
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: `Attempt #${id} finished with no changes`, body: "Nothing needed to change; the handoff explains why.", link: chatResultHref(taskId, id) }, now);
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 attempt #${run.parentRun}. Running on ${agent}.`, link: chatResultHref(taskId, run.parentRun, "checks") }
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: `Planning started (run #${id})`, body: `Drafting the scope and plan on ${agent}.`, link: chatControlHref("planning", taskId) }
13948
+ ? { subject: "Planning started", body: `Drafting the plan on ${agent}.`, link: chatControlHref("planning", taskId) }
13613
13949
  : role === "scout"
13614
- ? { subject: `Scouting started (run #${id})`, body: `A read-only look on ${agent}; what comes back is a report.`, link: chatControlHref("task", taskId) }
13615
- : { subject: road.road === "recovered" ? `Attempt #${id} resumed from its preserved draft` : `Attempt #${id} started`, body: `Building on ${agent}.`, link: chatControlHref("task", taskId) };
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: `Attempt #${id}: ${words.subject}`, body: words.body, link: chatControlHref("task", taskId) }, now);
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.db.prepare("DELETE FROM project WHERE path = ?").run(path);
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
- const { changes } = this.db
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
- if (this.currentLiveLease(taskRef, now) === null) {
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 attempt #${run.id}`, body: "Its processes are being stopped and its work is preserved. The task stays paused until this attempt is resumed.", link: chatControlHref("recovery", stoppedTask) }, now);
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, `unknown` when this computer can't
18824
- * check at all (both are what `run settle` resolves; only the first may
18825
- * be called not running). `settling` already meets the automatic
18826
- * reconciliation proof, so no confirmation is needed. */
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) => ({ 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 fact(id, "unprovable", `run #${id} may have spawned before its process witness was recorded; exit is unproven`);
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
- // A VERIFIED boot change on this host (v53): nothing of the old
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 (host !== hostname())
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; an approver's recorded word settles it.
18891
- if (!this.workspaceConfirmedByApprover(id))
18892
- return fact(id, "unknown", `run #${id}'s workspace occupancy could not be established`);
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
- return fact(id, this.settleableUnspawnedWitnesses(run, witnesses).length > 0 ? "settling" : "unprovable", `run #${id} has an incomplete spawn witness; exit is unproven`);
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
- /** `run settle` recorded an approver's word that a run whose workspace
18901
- * note couldn't be read has stopped. */
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
- return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action = 'workspace settled by approver' LIMIT 1").get(runId) !== undefined;
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
- return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
18907
- .run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
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 (host !== hostname() || typeof row["observed_at"] !== "string" || !Number.isFinite(Date.parse(row["observed_at"])) || Date.parse(row["observed_at"]) > now.getTime())
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 p.host = ? AND p.run > ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
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(hostname(), after, batch).map(row => Number(row["run"]));
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
- * `run settle`. Native witnesses keep their OS-only road. */
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 p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
18997
- AND (? IS NULL OR p.run = ?) ORDER BY p.run`).all(hostname(), onlyRun ?? null, onlyRun ?? null);
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 pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && row["host"] === hostname());
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["host"] === hostname());
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.prepare("INSERT INTO run_process (run,pid,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?,?)")
19130
- .run(runId, pid, hostname(), group ? 1 : 0, now.toISOString(), currentBootId());
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 (String(row["host"]) !== hostname())
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: `Attempt #${args.runId} resumed`, body: "The next pass takes a fresh claim and continues from the preserved work.", link: chatControlHref("task", resumedTask) }, now);
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 (notification.push_class IS NOT NULL OR notification.kind = 'check-progress')
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's checks ran at: missing or unreadable reads as sandboxed. */
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: `Carried to ${w.to.slice(0, 7)}`,
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. Null: no step recorded. */
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 stopped = String(last["state"]) === "failed" ? { step: String(last["step"]), message: last["detail"] === null ? "" : String(last["detail"]), at: String(last["updated_at"]) } : null;
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
+ }