toolroll 0.9.93 → 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 (328) hide show
  1. package/CHANGELOG.md +68 -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-ui.js +1 -1
  11. package/dist/assignment.js +5 -0
  12. package/dist/boot-identity.d.ts +2 -1
  13. package/dist/boot-identity.js +6 -4
  14. package/dist/bounded-spawn.d.ts +57 -0
  15. package/dist/bounded-spawn.js +220 -0
  16. package/dist/browser/terminal.js +42 -42
  17. package/dist/browser/workspace.css +1 -1
  18. package/dist/browser/workspace.js +32 -32
  19. package/dist/browser-shell.d.ts +1 -1
  20. package/dist/browser-shell.js +2 -2
  21. package/dist/browser-workspace.d.ts +45 -2
  22. package/dist/browser-workspace.js +3 -3
  23. package/dist/builder-restart.d.ts +21 -0
  24. package/dist/builder-restart.js +50 -0
  25. package/dist/builder.d.ts +4 -0
  26. package/dist/builder.js +53 -24
  27. package/dist/chat-acceptance.js +1 -1
  28. package/dist/chat-actions.d.ts +12 -2
  29. package/dist/chat-actions.js +25 -2
  30. package/dist/chat-channel.js +11 -0
  31. package/dist/chat-decide.d.ts +7 -2
  32. package/dist/chat-decide.js +37 -9
  33. package/dist/chat-quiet.js +3 -3
  34. package/dist/chat-voice.d.ts +1 -1
  35. package/dist/chat-voice.js +2 -2
  36. package/dist/check-facts.d.ts +3 -1
  37. package/dist/check-facts.js +57 -11
  38. package/dist/check-failure.d.ts +22 -3
  39. package/dist/check-failure.js +57 -13
  40. package/dist/check-levels-ui.d.ts +4 -0
  41. package/dist/check-levels-ui.js +10 -1
  42. package/dist/check-load.d.ts +85 -0
  43. package/dist/check-load.js +147 -0
  44. package/dist/check-only-standing.d.ts +36 -0
  45. package/dist/check-only-standing.js +39 -0
  46. package/dist/check-only.d.ts +15 -0
  47. package/dist/check-only.js +99 -0
  48. package/dist/check-report.d.ts +2 -0
  49. package/dist/check-report.js +1 -1
  50. package/dist/check-stamp.d.ts +46 -0
  51. package/dist/check-stamp.js +93 -0
  52. package/dist/checkout-cleanup.d.ts +36 -5
  53. package/dist/checkout-cleanup.js +166 -14
  54. package/dist/cli-help.d.ts +4 -1
  55. package/dist/cli-help.js +47 -30
  56. package/dist/cli.js +11 -1
  57. package/dist/code-extract.d.ts +44 -0
  58. package/dist/code-extract.js +88 -0
  59. package/dist/code-map-acts.d.ts +24 -0
  60. package/dist/code-map-acts.js +53 -0
  61. package/dist/code-map-admission.d.ts +52 -0
  62. package/dist/code-map-admission.js +295 -0
  63. package/dist/code-map-build.d.ts +63 -0
  64. package/dist/code-map-build.js +415 -0
  65. package/dist/code-map-cli.d.ts +153 -0
  66. package/dist/code-map-cli.js +103 -0
  67. package/dist/code-map-main.d.ts +1 -0
  68. package/dist/code-map-main.js +48 -0
  69. package/dist/code-map-read.d.ts +106 -0
  70. package/dist/code-map-read.js +171 -0
  71. package/dist/code-map-report.d.ts +37 -0
  72. package/dist/code-map-report.js +95 -0
  73. package/dist/code-map-shape.d.ts +172 -0
  74. package/dist/code-map-shape.js +327 -0
  75. package/dist/code-map-store.d.ts +136 -0
  76. package/dist/code-map-store.js +233 -0
  77. package/dist/coding-provider.d.ts +2 -0
  78. package/dist/coding-provider.js +3 -2
  79. package/dist/coding-update.js +3 -2
  80. package/dist/coding-workspace.js +4 -4
  81. package/dist/console-chrome.js +2 -2
  82. package/dist/containment.d.ts +5 -0
  83. package/dist/containment.js +14 -1
  84. package/dist/contracts/chat-actions.d.ts +9 -0
  85. package/dist/contracts/chat-actions.js +2 -0
  86. package/dist/contracts/cli.d.ts +52 -0
  87. package/dist/contracts/cli.js +4 -0
  88. package/dist/contracts/console-api.d.ts +10 -3
  89. package/dist/contracts/console-api.js +10 -3
  90. package/dist/contracts/gateway-tools.d.ts +17 -0
  91. package/dist/contracts/gateway-tools.js +3 -1
  92. package/dist/contracts/lead-tools.d.ts +61 -0
  93. package/dist/contracts/lead-tools.js +10 -1
  94. package/dist/contracts/project-knowledge.d.ts +38 -5
  95. package/dist/contracts/project-knowledge.js +11 -5
  96. package/dist/contracts/project-memory.d.ts +3 -1
  97. package/dist/contracts/project-memory.js +3 -2
  98. package/dist/contracts/storage.d.ts +14 -0
  99. package/dist/contracts/storage.js +3 -1
  100. package/dist/contracts/store-json.d.ts +2 -0
  101. package/dist/contracts/youiyoux-request.d.ts +1 -1
  102. package/dist/contracts/youiyoux-request.js +1 -1
  103. package/dist/desktop-access.js +6 -2
  104. package/dist/dispatch.js +18 -15
  105. package/dist/dispose.js +8 -2
  106. package/dist/easy-start.js +2 -3
  107. package/dist/evals.js +2 -1
  108. package/dist/fast-start.d.ts +136 -0
  109. package/dist/fast-start.js +431 -0
  110. package/dist/fast-terms.d.ts +9 -0
  111. package/dist/fast-terms.js +23 -0
  112. package/dist/fenced-check-coverage.d.ts +62 -0
  113. package/dist/fenced-check-coverage.js +54 -0
  114. package/dist/first-run.js +1 -1
  115. package/dist/flaky-ci.d.ts +31 -0
  116. package/dist/flaky-ci.js +149 -0
  117. package/dist/flaky-cli.d.ts +60 -0
  118. package/dist/flaky-cli.js +60 -0
  119. package/dist/flaky.d.ts +75 -9
  120. package/dist/flaky.js +255 -0
  121. package/dist/flow-card-task.js +3 -0
  122. package/dist/flow-triggers.d.ts +1 -1
  123. package/dist/flow-triggers.js +1 -1
  124. package/dist/github-import.d.ts +62 -0
  125. package/dist/github-import.js +146 -0
  126. package/dist/guides.js +15 -10
  127. package/dist/invoke.d.ts +5 -1
  128. package/dist/invoke.js +49 -14
  129. package/dist/job-object-helper.ps1 +10 -3
  130. package/dist/keymap.d.ts +41 -0
  131. package/dist/keymap.js +51 -0
  132. package/dist/knowledge-cli.js +1 -1
  133. package/dist/knowledge-ui.d.ts +21 -4
  134. package/dist/knowledge-ui.js +75 -7
  135. package/dist/laptop-background.d.ts +16 -0
  136. package/dist/laptop-background.js +108 -2
  137. package/dist/lead-context.js +1 -1
  138. package/dist/lead-contract.d.ts +1 -1
  139. package/dist/lead-contract.js +2 -2
  140. package/dist/lead-progress.js +3 -0
  141. package/dist/lead-status.d.ts +2 -0
  142. package/dist/lead-status.js +6 -2
  143. package/dist/lead-tools.js +51 -2
  144. package/dist/lead.js +1 -1
  145. package/dist/lesson-notes.d.ts +81 -0
  146. package/dist/lesson-notes.js +336 -0
  147. package/dist/limits-ui.js +1 -1
  148. package/dist/local-owner.d.ts +1 -1
  149. package/dist/local-owner.js +11 -0
  150. package/dist/machine-check.d.ts +16 -0
  151. package/dist/machine-check.js +26 -0
  152. package/dist/machine-identity.d.ts +75 -0
  153. package/dist/machine-identity.js +170 -0
  154. package/dist/mcp-core.js +11 -1
  155. package/dist/memory-cli.d.ts +300 -1
  156. package/dist/memory-cli.js +62 -9
  157. package/dist/memory-pass.d.ts +16 -0
  158. package/dist/memory-pass.js +67 -3
  159. package/dist/memory-tidy.d.ts +181 -0
  160. package/dist/memory-tidy.js +523 -0
  161. package/dist/memory-use.d.ts +35 -0
  162. package/dist/memory-use.js +109 -0
  163. package/dist/merge-checklist.d.ts +55 -0
  164. package/dist/merge-checklist.js +182 -0
  165. package/dist/needs-you.d.ts +27 -8
  166. package/dist/needs-you.js +96 -39
  167. package/dist/nightly-github.d.ts +2 -0
  168. package/dist/nightly-github.js +4 -4
  169. package/dist/operate-remote-arguments.js +6 -1
  170. package/dist/operate-remote.js +3 -1
  171. package/dist/operate.d.ts +6 -1
  172. package/dist/operate.js +402 -52
  173. package/dist/phase-routing.js +2 -2
  174. package/dist/plain-schema.d.ts +23 -0
  175. package/dist/plain-schema.js +94 -0
  176. package/dist/plain-words.d.ts +20 -0
  177. package/dist/plain-words.js +66 -0
  178. package/dist/planner.js +2 -1
  179. package/dist/process-liveness.d.ts +2 -0
  180. package/dist/process-liveness.js +61 -17
  181. package/dist/project-delete-ui.d.ts +7 -2
  182. package/dist/project-delete-ui.js +12 -1
  183. package/dist/project-delete.js +8 -1
  184. package/dist/project-knowledge.d.ts +18 -11
  185. package/dist/project-knowledge.js +39 -9
  186. package/dist/project-learning.d.ts +2 -1
  187. package/dist/project-learning.js +15 -45
  188. package/dist/project-memory.d.ts +5 -0
  189. package/dist/project-memory.js +22 -2
  190. package/dist/proof.js +30 -5
  191. package/dist/provider-auth.js +1 -1
  192. package/dist/publish.js +9 -1
  193. package/dist/pull-request-flow.js +5 -0
  194. package/dist/push.d.ts +18 -2
  195. package/dist/push.js +40 -16
  196. package/dist/release-adapters.d.ts +9 -0
  197. package/dist/release-adapters.js +76 -0
  198. package/dist/release-auto.js +13 -13
  199. package/dist/release-card.d.ts +1 -2
  200. package/dist/release-card.js +11 -0
  201. package/dist/release-carry.d.ts +7 -2
  202. package/dist/release-carry.js +39 -9
  203. package/dist/release-cli.d.ts +4 -0
  204. package/dist/release-cli.js +29 -22
  205. package/dist/release-coverage.js +1 -1
  206. package/dist/release-next.d.ts +48 -0
  207. package/dist/release-next.js +161 -0
  208. package/dist/release.d.ts +20 -1
  209. package/dist/release.js +76 -8
  210. package/dist/remote-destination.d.ts +36 -0
  211. package/dist/remote-destination.js +217 -0
  212. package/dist/remote-main.d.ts +48 -0
  213. package/dist/remote-main.js +84 -5
  214. package/dist/repository-context.d.ts +3 -2
  215. package/dist/repository-context.js +112 -70
  216. package/dist/restart-certification.d.ts +1 -0
  217. package/dist/restart-certification.js +8 -6
  218. package/dist/result-acts.d.ts +27 -19
  219. package/dist/result-acts.js +36 -25
  220. package/dist/result-follow-ups.d.ts +2 -2
  221. package/dist/result-follow-ups.js +18 -4
  222. package/dist/result-review.d.ts +1 -1
  223. package/dist/route-change-check.d.ts +4 -0
  224. package/dist/route-change-check.js +13 -0
  225. package/dist/route-change.d.ts +22 -0
  226. package/dist/route-change.js +89 -0
  227. package/dist/route-hooks.js +3 -2
  228. package/dist/route-results.d.ts +6 -1
  229. package/dist/route-results.js +14 -5
  230. package/dist/run-closeout.d.ts +1 -1
  231. package/dist/run-closeout.js +7 -5
  232. package/dist/runner-process.js +3 -2
  233. package/dist/runner.js +2 -1
  234. package/dist/scope.d.ts +6 -1
  235. package/dist/scope.js +22 -9
  236. package/dist/serve.d.ts +1 -1
  237. package/dist/serve.js +24 -18
  238. package/dist/server/chat.js +14 -12
  239. package/dist/server/chrome.d.ts +1 -1
  240. package/dist/server/chrome.js +26 -35
  241. package/dist/server/console.css +23 -0
  242. package/dist/server/guards.js +3 -0
  243. package/dist/server/home.d.ts +6 -0
  244. package/dist/server/home.js +21 -13
  245. package/dist/server/http.js +1 -1
  246. package/dist/server/kit.js +1 -1
  247. package/dist/server/merge-ready-view.d.ts +10 -0
  248. package/dist/server/merge-ready-view.js +30 -0
  249. package/dist/server/pages-assets.d.ts +1 -0
  250. package/dist/server/pages-assets.js +8 -5
  251. package/dist/server/pages.js +103 -82
  252. package/dist/server/render-chat.js +2 -2
  253. package/dist/server/render-code-map.d.ts +30 -0
  254. package/dist/server/render-code-map.js +137 -0
  255. package/dist/server/render-flaky.d.ts +20 -0
  256. package/dist/server/render-flaky.js +59 -0
  257. package/dist/server/render-history.js +1 -1
  258. package/dist/server/render-new-task.d.ts +26 -1
  259. package/dist/server/render-new-task.js +30 -5
  260. package/dist/server/render-pages.d.ts +1 -175
  261. package/dist/server/render-pages.js +38 -313
  262. package/dist/server/render-projects.d.ts +2 -1
  263. package/dist/server/render-projects.js +9 -7
  264. package/dist/server/render-release.d.ts +2 -0
  265. package/dist/server/render-release.js +3 -3
  266. package/dist/server/render-results.d.ts +2 -0
  267. package/dist/server/render-results.js +67 -67
  268. package/dist/server/render-tasks.d.ts +13 -2
  269. package/dist/server/render-tasks.js +153 -136
  270. package/dist/server/retired-routes.d.ts +17 -0
  271. package/dist/server/retired-routes.js +53 -0
  272. package/dist/server/route-table.d.ts +3 -0
  273. package/dist/server/route-table.js +45 -12
  274. package/dist/server/settings.js +129 -22
  275. package/dist/server/task-views.js +61 -26
  276. package/dist/server/tasks.d.ts +2 -6
  277. package/dist/server/tasks.js +174 -321
  278. package/dist/server/welcome.js +1 -1
  279. package/dist/settings-groups-ui.js +1 -1
  280. package/dist/setup-guide.d.ts +2 -2
  281. package/dist/stop-proof.js +6 -4
  282. package/dist/storage-sweep-process.js +1 -1
  283. package/dist/storage-sweep.d.ts +5 -3
  284. package/dist/storage-sweep.js +30 -9
  285. package/dist/storage-ui.js +28 -17
  286. package/dist/storage.d.ts +27 -2
  287. package/dist/storage.js +39 -2
  288. package/dist/store.d.ts +80 -5
  289. package/dist/store.js +296 -87
  290. package/dist/surface.js +45 -4
  291. package/dist/sweep-quarantine.d.ts +1 -0
  292. package/dist/sweep-quarantine.js +6 -4
  293. package/dist/tamper-scan.d.ts +4 -0
  294. package/dist/tamper-scan.js +90 -0
  295. package/dist/task-aliases.d.ts +5 -0
  296. package/dist/task-aliases.js +9 -0
  297. package/dist/task-control.js +4 -1
  298. package/dist/task-outcome-cli.d.ts +2 -0
  299. package/dist/task-outcome-cli.js +17 -17
  300. package/dist/task-sizing.d.ts +5 -0
  301. package/dist/task-sizing.js +2 -0
  302. package/dist/task-source.d.ts +7 -0
  303. package/dist/task-source.js +14 -0
  304. package/dist/task-status.d.ts +9 -0
  305. package/dist/task-status.js +40 -49
  306. package/dist/teardown-git.d.ts +16 -0
  307. package/dist/teardown-git.js +63 -5
  308. package/dist/telegram-progress.js +5 -3
  309. package/dist/telegram-status.js +6 -4
  310. package/dist/test-infra.d.ts +8 -0
  311. package/dist/test-infra.js +18 -0
  312. package/dist/tidy.js +1 -1
  313. package/dist/toolroll-update.js +17 -5
  314. package/dist/tui-contract.d.ts +24 -0
  315. package/dist/tui-source.js +67 -8
  316. package/dist/tui.js +51 -51
  317. package/dist/vanished-retry.d.ts +30 -0
  318. package/dist/vanished-retry.js +174 -0
  319. package/dist/work-index.d.ts +2 -0
  320. package/dist/work-index.js +76 -62
  321. package/dist/work-summary.js +2 -0
  322. package/dist/workspace-ui.d.ts +4 -2
  323. package/dist/workspace-ui.js +28 -22
  324. package/dist/worktree.d.ts +12 -3
  325. package/dist/worktree.js +68 -13
  326. package/package.json +1 -1
  327. package/dist/server/render-next.d.ts +0 -63
  328. 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";
@@ -1117,7 +1125,18 @@ const REGISTERED_AT_FILING = "again, as it was filed";
1117
1125
  // v149 adds, and changes no saved row (docs/plans/learning-loops.md): every learning-loops table (V149_SCHEMA, new
1118
1126
  // tables only) — each check's recorded conditions and results, mixed-outcome observations, tracked identities, lesson
1119
1127
  // notes and their tallies, memory use, tidy-up proposals and decision merges.
1120
- export const SCHEMA_VERSION = 149;
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;
1121
1140
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
1122
1141
  export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
1123
1142
  /** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
@@ -1250,7 +1269,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
1250
1269
  * unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
1251
1270
  * (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
1252
1271
  */
1253
- 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]);
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]);
1254
1273
  /** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
1255
1274
  export const V119_ADDED_COLUMNS = Object.freeze({
1256
1275
  task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
@@ -1876,6 +1895,39 @@ export const V149_TABLES = Object.freeze(["check_result", "check_fact", "local_o
1876
1895
  export const V149_REPO_TABLES = Object.freeze(["check_fact", "local_observation", "ci_flaky_observation", "learning_watch", "lesson_note", "memory_use", "memory_tidy_proposal"]);
1877
1896
  /** The v149 migration's steps after v148's, in order (OpenOptions.migrationStep may stop after each). */
1878
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"]);
1879
1931
  /**
1880
1932
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
1881
1933
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -2382,10 +2434,10 @@ function lifecycleWords(value, cap) {
2382
2434
  const LIFECYCLE_PHASE_WORDS = {
2383
2435
  "agent-running": { subject: "agent working", body: "The agent is working in its own checkout. Nothing is finished yet." },
2384
2436
  "validating-handoff": { subject: "checking the handoff", body: "The agent finished its turn; what it handed back is being checked." },
2385
- "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." },
2386
2438
  "committing": { subject: "committing", body: "The work is being committed on the task branch." },
2387
- "verifying-proof": { subject: "verifying the proof", body: "The proof is being checked against the approved acceptance criteria." },
2388
- "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." },
2389
2441
  };
2390
2442
  /** An operator's cancellation as one closed sentence: the reason, if any,
2391
2443
  * scrubbed like every phone string and ended with a full stop unless it
@@ -2398,10 +2450,10 @@ function cancellationWords(reason) {
2398
2450
  }
2399
2451
  /** What an independent review concluded, in the words the saved result shows. */
2400
2452
  const LIFECYCLE_REVIEW_WORDS = {
2401
- verified: { subject: "checks verified", body: "Every acceptance check was verified against the saved result." },
2402
- refuted: { subject: "evidence conflicts with the result", body: "The reviewer found evidence that contradicts the saved result. It is not accepted as done." },
2403
- attested: { subject: "agent-reported evidence only", body: "The result rests on agent-reported evidence that was not independently verified." },
2404
- 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." },
2405
2457
  none: { subject: "comments only", body: "The reviewer left comments; no acceptance judgement was recorded." },
2406
2458
  };
2407
2459
  /** Why a task still waits after one hold lifts — the other owner, in words. */
@@ -4798,6 +4850,9 @@ function initializeStore(db, file, options = {}) {
4798
4850
  // A current file is this build's exact shape: the checks above read, the connection gets its settings, and no DDL
4799
4851
  // runs. Everything below runs once, on the way up from an older version (a fresh file is at v107 by now).
4800
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}`);
4801
4856
  db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
4802
4857
  return new Store(db);
4803
4858
  }
@@ -5170,6 +5225,35 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
5170
5225
  // v149: the learning-loops tables (new tables only; docs/plans/learning-loops.md). Idempotent, as v148's steps are.
5171
5226
  db.exec(V149_SCHEMA);
5172
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");
5173
5257
  // Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
5174
5258
  // nothing to reclaim.
5175
5259
  if (!fresh)
@@ -6381,6 +6465,8 @@ export class Store {
6381
6465
  this.db = instrumentDatabase(trackWorkspaceWrites(db), this.telemetry);
6382
6466
  // And on each instance: the sealed methods as its own locked properties, so nothing shadows one later.
6383
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);
6384
6470
  }
6385
6471
  /** TESTS ONLY: raw database access for migration fixtures. Production
6386
6472
  * code never calls this — the typed methods are the API. */
@@ -6652,7 +6738,7 @@ export class Store {
6652
6738
  this.noteLifecycle({
6653
6739
  taskRef: ref.id, kind: "task-queued", identity: `t${ref.id}`,
6654
6740
  subject: "Back in the queue",
6655
- 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.",
6656
6742
  link: chatControlHref("task", id),
6657
6743
  }, now);
6658
6744
  }
@@ -7970,6 +8056,11 @@ export class Store {
7970
8056
  // from the live scope: a card that cut a word, or terms that changed since, seal nothing.
7971
8057
  if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
7972
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;
7973
8064
  // CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
7974
8065
  // all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
7975
8066
  const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
@@ -8048,12 +8139,15 @@ export class Store {
8048
8139
  if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
8049
8140
  return false;
8050
8141
  // The outside-request quarantine's chat road (above): the card showed every word of these live terms.
8051
- if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
8142
+ if (basis?.kind === "chat" && (this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA || issueImported(this, taskId))
8052
8143
  && !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
8053
8144
  return false;
8054
8145
  // AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
8055
8146
  // in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
8056
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;
8057
8151
  if (automatic) {
8058
8152
  if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
8059
8153
  return false;
@@ -8063,6 +8157,9 @@ export class Store {
8063
8157
  }
8064
8158
  if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
8065
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;
8066
8163
  if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
8067
8164
  return false;
8068
8165
  const changed = this.db
@@ -8092,8 +8189,12 @@ export class Store {
8092
8189
  // (setup review): the approval is what was stale, and it is new.
8093
8190
  const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
8094
8191
  if (ref !== undefined) {
8095
- if (basis === undefined || basis.kind === "chat")
8192
+ if (basis === undefined || basis.kind === "chat") {
8096
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
+ }
8097
8198
  this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`stale:${Number(ref["id"])}`);
8098
8199
  // Results routing: this yes closes the task's open exposure (as proposed, or with the model it sealed),
8099
8200
  // and lifts the hold a switched-off rule put on the approval it replaces.
@@ -8114,20 +8215,20 @@ export class Store {
8114
8215
  if (alreadyOn !== working["digest"]) {
8115
8216
  this.noteLifecycle({
8116
8217
  taskRef: Number(ref["id"]), kind: "scope-approved", identity: `t${Number(ref["id"])}`,
8117
- subject: "Scope approved",
8218
+ subject: "Plan approved",
8118
8219
  body: basis === undefined
8119
- ? `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.`
8120
8221
  : basis.kind === "chat"
8121
- ? `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.`
8122
8223
  : basis.kind === "grant"
8123
- ? "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."
8124
8225
  : basis.kind === "schedule"
8125
- ? "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."
8126
8227
  : basis.kind === "auto-check"
8127
8228
  ? "Checking automatically: it approves itself once CI and the release check pass."
8128
8229
  : basis.kind === "auto-green"
8129
8230
  ? "Approved automatically: CI and the release check passed."
8130
- : "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.",
8131
8232
  link: chatControlHref("task", taskId),
8132
8233
  }, now);
8133
8234
  }
@@ -8166,7 +8267,7 @@ export class Store {
8166
8267
  JOIN task_ref ON task_ref.id = ? AND task_ref.backend = 'built-in' AND task_scope.task_id = task_ref.external_id
8167
8268
  WHERE task_scope.approved_at IS NOT NULL
8168
8269
  AND NOT ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
8169
- .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
8270
+ .get(taskRef, now.toISOString(), now.toISOString(), now.toISOString(), now.toISOString());
8170
8271
  if (dead !== undefined)
8171
8272
  return false;
8172
8273
  // A release task (release-once.md): the whole live-release predicate, beyond what the SQL can say.
@@ -9706,10 +9807,7 @@ export class Store {
9706
9807
  .get(taskRef);
9707
9808
  if (ref === undefined || String(ref["backend"]) !== BUILT_IN)
9708
9809
  continue;
9709
- const { changes } = this.db
9710
- .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
9711
- .run(now.toISOString(), String(ref["external_id"]));
9712
- if (Number(changes) > 0)
9810
+ if (settleVanished(this, taskRef, now) === "queued")
9713
9811
  requeued?.push(String(ref["external_id"]));
9714
9812
  }
9715
9813
  return held.map(row => String(row["lease_id"]));
@@ -10502,7 +10600,7 @@ export class Store {
10502
10600
  this.noteLifecycle({
10503
10601
  taskRef: run.taskRef, run: runId, kind: "review-requested", identity: `r${runId}`,
10504
10602
  subject: "Independent review requested",
10505
- 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.`,
10506
10604
  link: chatResultHref(reviewedTask, runId, "checks"),
10507
10605
  }, now);
10508
10606
  }
@@ -11806,6 +11904,10 @@ export class Store {
11806
11904
  for (const incident of incidents)
11807
11905
  this.resolveIncidentLocked(incident, by, now);
11808
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);
11809
11911
  this.db
11810
11912
  .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state IN ('failed','queued','done')")
11811
11913
  .run(stamp, taskId);
@@ -11813,10 +11915,10 @@ export class Store {
11813
11915
  this.resolveEpisode(`stalled:${taskRef}`, now);
11814
11916
  this.noteLifecycle({
11815
11917
  taskRef, kind: "task-requeued", identity: `t${taskRef}`,
11816
- subject: "Requeued",
11918
+ subject: "Queued again",
11817
11919
  body: rejectedRun !== null
11818
- ? `Its last result (attempt #${rejectedRun}) was not accepted; the next attempt continues on the same branch under the current scope.`
11819
- : `${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.`,
11820
11922
  link: chatControlHref("task", taskId),
11821
11923
  }, now);
11822
11924
  this.bumpWake();
@@ -13328,18 +13430,18 @@ export class Store {
13328
13430
  const stopped = result.outcome === "failed" && result.reason === "interrupted" && this.stopOf(id) !== null;
13329
13431
  if (role === "builder") {
13330
13432
  if (stopped) {
13331
- 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);
13332
13434
  }
13333
13435
  else if (result.outcome === "built") {
13334
13436
  this.noteLifecycle({
13335
13437
  taskRef, run: id, kind: "run-finished", identity: `r${id}`,
13336
- subject: `Attempt #${id} built`,
13438
+ subject: `Build #${id} built`,
13337
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.",
13338
13440
  link: chatResultHref(taskId, id, "changes"),
13339
13441
  }, now);
13340
13442
  }
13341
13443
  else if (result.outcome === "no-change") {
13342
- 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);
13343
13445
  }
13344
13446
  return;
13345
13447
  }
@@ -13841,12 +13943,12 @@ export class Store {
13841
13943
  if (road.road !== "repair" && road.road !== "correction" && !(role === "planner" && parent !== null)) {
13842
13944
  const agent = lifecycleWords(`${provider ?? "claude"}${model === null ? "" : ` · ${model}`}`, 60);
13843
13945
  const words = role === "reviewer"
13844
- ? { 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") }
13845
13947
  : role === "planner"
13846
- ? { 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) }
13847
13949
  : role === "scout"
13848
- ? { subject: `Scouting started (run #${id})`, body: `A read-only look on ${agent}; what comes back is a report.`, link: chatControlHref("task", taskId) }
13849
- : { 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) };
13850
13952
  this.noteLifecycle({ taskRef: run.taskRef, run: id, kind: "run-started", identity: `r${id}`, ...words }, run.now);
13851
13953
  }
13852
13954
  return id;
@@ -14835,7 +14937,7 @@ export class Store {
14835
14937
  if (taskId === null)
14836
14938
  return;
14837
14939
  const words = LIFECYCLE_PHASE_WORDS[phase];
14838
- 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);
14839
14941
  });
14840
14942
  }
14841
14943
  /** The accepted head and the validated conclusion, once known. */
@@ -18059,6 +18161,7 @@ export class Store {
18059
18161
  this.db.prepare(`INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail)
18060
18162
  VALUES (?,?,?,NULL,NULL,'project remote recorded','recorded','access',?)`)
18061
18163
  .run(now.toISOString(), actor, repo, JSON.stringify({ previousRemote: previous, remoteUrl }));
18164
+ codeMapRemoteChanged(this, repo);
18062
18165
  }
18063
18166
  /** The remote URL captured on admission or explicitly supplied by the owner; null when none was (or the project was removed). */
18064
18167
  projectRemote(path) {
@@ -18067,7 +18170,10 @@ export class Store {
18067
18170
  }
18068
18171
  /** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
18069
18172
  forgetProject(path) {
18070
- 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
+ });
18071
18177
  }
18072
18178
  /** Most recently opened first — the opener page's order. */
18073
18179
  listProjects() {
@@ -18758,10 +18864,7 @@ export class Store {
18758
18864
  .get(taskRef, BUILT_IN);
18759
18865
  if (ref === undefined)
18760
18866
  return;
18761
- const { changes } = this.db
18762
- .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
18763
- .run(stamp, String(ref["external_id"]));
18764
- if (Number(changes) > 0)
18867
+ if (settleVanished(this, taskRef, now) === "queued")
18765
18868
  requeued.push(String(ref["external_id"]));
18766
18869
  };
18767
18870
  const open = this.db
@@ -18847,16 +18950,11 @@ export class Store {
18847
18950
  this.db
18848
18951
  .prepare("UPDATE claim SET released_at = ?, released_by = 'recovered' WHERE lease_id = ?")
18849
18952
  .run(stamp, leaseId);
18850
- this.db
18851
- .prepare(`UPDATE task SET state = 'queued', updated_at = ?
18852
- WHERE state = 'running' AND id = (
18853
- SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
18854
- )`)
18855
- .run(stamp, taskRef, BUILT_IN);
18856
18953
  this.db
18857
18954
  .prepare(`UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ?
18858
18955
  WHERE lease_id = ? AND outcome IS NULL`)
18859
18956
  .run(stamp, leaseId);
18957
+ settleVanished(this, taskRef, now);
18860
18958
  this.db
18861
18959
  .prepare(`UPDATE worktree SET released_at = ?, verified = 0
18862
18960
  WHERE runner = ? AND task_ref = ? AND released_at IS NULL`)
@@ -18878,14 +18976,7 @@ export class Store {
18878
18976
  this.db
18879
18977
  .prepare("UPDATE run SET outcome = 'failed', reason = 'interrupted', finished_at = ? WHERE id = ?")
18880
18978
  .run(stamp, Number(row["run_id"]));
18881
- if (this.currentLiveLease(taskRef, now) === null) {
18882
- this.db
18883
- .prepare(`UPDATE task SET state = 'queued', updated_at = ?
18884
- WHERE state = 'running' AND id = (
18885
- SELECT external_id FROM task_ref WHERE id = ? AND backend = ?
18886
- )`)
18887
- .run(stamp, taskRef, BUILT_IN);
18888
- }
18979
+ settleVanished(this, taskRef, now);
18889
18980
  this.db
18890
18981
  .prepare("UPDATE worktree SET released_at = ?, verified = 0 WHERE runner = ? AND task_ref = ? AND released_at IS NULL")
18891
18982
  .run(stamp, runner, taskRef);
@@ -19019,7 +19110,7 @@ export class Store {
19019
19110
  if (stoppedTask !== null) {
19020
19111
  this.noteLifecycle(run.role === "reviewer"
19021
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) }
19022
- : { 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);
19023
19114
  }
19024
19115
  this.bumpWake();
19025
19116
  return { ok: true, stop: this.stopOf(args.runId), repeated: false };
@@ -19078,6 +19169,7 @@ export class Store {
19078
19169
  if (ownedProcessCount(runOwnerTag(this, id)) > 0)
19079
19170
  return fact(id, "alive", `run #${id} still has an owned subprocess`);
19080
19171
  const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
19172
+ const here = storeMachine(this.db);
19081
19173
  let incompleteSpawn = false;
19082
19174
  if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
19083
19175
  return provable(id, `run #${id} may have spawned before its process witness was recorded; exit is unproven`, "no-witness");
@@ -19087,15 +19179,17 @@ export class Store {
19087
19179
  // leader. Re-probing its number could mistake reuse for the old group.
19088
19180
  if (witness["exited_at"] !== null)
19089
19181
  continue;
19090
- const host = String(witness["host"]);
19091
19182
  const bootId = witness["boot_id"] === null || witness["boot_id"] === undefined ? null : String(witness["boot_id"]);
19092
- // 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
19093
19187
  // boot survives — an incomplete spawn, a reused pid and a native
19094
19188
  // object alike are gone. Every other case keeps the conservative
19095
19189
  // road below.
19096
- if (provenDeadByBootChange({ host, bootId }))
19190
+ if (local && provenDeadByBootChange({ host: String(witness["host"]), bootId }, { host: String(witness["host"]), bootId: currentBootId() }))
19097
19191
  continue;
19098
- if (host !== hostname())
19192
+ if (!local)
19099
19193
  return fact(id, "elsewhere", `run #${id}'s process belongs to another host; exit is unproven here`);
19100
19194
  const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
19101
19195
  const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
@@ -19152,8 +19246,9 @@ export class Store {
19152
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;
19153
19247
  }
19154
19248
  reserveRunProcess(runId, now, group = true) {
19155
- return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
19156
- .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);
19157
19252
  }
19158
19253
  /** The run's process witnesses not yet proved exited — after first saving whatever exit evidence is available. */
19159
19254
  openRunProcessCount(runId, now) {
@@ -19173,12 +19268,13 @@ export class Store {
19173
19268
  // check uses: ESRCH, or a process now holding the pid that was born after
19174
19269
  // the run ended. An open run has no finish time, so only ESRCH counts.
19175
19270
  const finishedAt = this.getRun(runId)?.finishedAt ?? null;
19271
+ const here = storeMachine(this.db);
19176
19272
  let recorded = 0;
19177
19273
  for (const row of rows) {
19178
19274
  const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
19179
- 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())
19180
19276
  continue;
19181
- let gone = provenDeadByBootChange({ host, bootId });
19277
+ let gone = provenDeadByBootChange({ host, bootId }, { host, bootId: currentBootId() });
19182
19278
  if (!gone) {
19183
19279
  const backend = row["containment"] == null ? null : String(row["containment"]);
19184
19280
  const container = row["container"] == null ? null : String(row["container"]);
@@ -19189,11 +19285,12 @@ export class Store {
19189
19285
  gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, finishedAt === null ? undefined : { observedAt: row["observed_at"], finishedAt });
19190
19286
  }
19191
19287
  }
19288
+ const sameMachine = witnessesCarryMachine(this.db);
19192
19289
  if (gone)
19193
19290
  recorded += Number(this.db.prepare(`UPDATE run_process SET exited_at = ?
19194
- 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 = ?
19195
19292
  AND observed_at = ? AND boot_id IS ? AND containment IS ? AND container IS ? AND container_identity IS ?`)
19196
- .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);
19197
19294
  }
19198
19295
  return recorded;
19199
19296
  }
@@ -19203,12 +19300,13 @@ export class Store {
19203
19300
  * yet stays open and is passed over, never holding newer runs behind it:
19204
19301
  * every finished run is visited, a batch at a time. Never signals. */
19205
19302
  recordFinishedRunExits(now, batch = 200) {
19303
+ const local = localWitnessSql(this.db, "p");
19206
19304
  const page = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
19207
- 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
19208
19306
  ORDER BY p.run LIMIT ?`);
19209
19307
  let recorded = 0;
19210
19308
  for (let after = 0;;) {
19211
- 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"]));
19212
19310
  for (const run of runs)
19213
19311
  recorded += this.recordRunProcessExits(run, now);
19214
19312
  if (runs.length < batch)
@@ -19240,10 +19338,11 @@ export class Store {
19240
19338
  * entry. A sole pid-less witness has no group to prove gone and stays for
19241
19339
  * Toolroll's own look (stop-proof.ts). Native witnesses keep their OS-only road. */
19242
19340
  settleUnspawnedWitnesses(now, onlyRun) {
19341
+ const local = localWitnessSql(this.db, "p");
19243
19342
  const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
19244
19343
  WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
19245
- AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
19246
- 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);
19247
19346
  let settled = 0;
19248
19347
  for (const candidate of candidates) {
19249
19348
  const runId = Number(candidate["run"]);
@@ -19278,11 +19377,12 @@ export class Store {
19278
19377
  settleableUnspawnedWitnesses(run, rows) {
19279
19378
  if (run.outcome === null || run.finishedAt === null || ownedProcessCount(runOwnerTag(this, run.id)) > 0)
19280
19379
  return [];
19281
- 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));
19282
19382
  const others = rows.filter(row => !pidless.includes(row));
19283
19383
  if (pidless.length === 0 || others.length === 0 || others.some(row => row["exited_at"] === null))
19284
19384
  return [];
19285
- const known = others.filter(row => row["pid"] !== null && row["host"] === hostname());
19385
+ const known = others.filter(row => row["pid"] !== null && witnessIsLocal(row, here));
19286
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"] })))
19287
19387
  return [];
19288
19388
  return pidless;
@@ -19297,8 +19397,9 @@ export class Store {
19297
19397
  throw new Error("spawn witness custody changed");
19298
19398
  return;
19299
19399
  }
19300
- this.db.prepare("INSERT INTO run_process (run,pid,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?,?)")
19301
- .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());
19302
19403
  }
19303
19404
  /** The native OS object a witnessed spawn ran inside (v53) — recorded
19304
19405
  * with the spawn, before the target can execute. */
@@ -19318,11 +19419,12 @@ export class Store {
19318
19419
  * must independently prove gone; this never grants authority to signal. */
19319
19420
  runCustodyProvenGone(runId) {
19320
19421
  const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ?").all(runId);
19422
+ const here = storeMachine(this.db);
19321
19423
  // A summarized run's witnesses all exited, each on recorded proof.
19322
19424
  return (rows.length > 0 || this.processesSummarized(runId)) && rows.every(row => {
19323
- if (String(row["host"]) !== hostname())
19425
+ if (!witnessIsLocal(row, here))
19324
19426
  return false;
19325
- 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() }))
19326
19428
  return true;
19327
19429
  if (row["containment"] == null || row["container"] == null)
19328
19430
  return false;
@@ -19398,7 +19500,7 @@ export class Store {
19398
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);
19399
19501
  const resumedTask = this.externalIdFor(args.taskRef);
19400
19502
  if (resumedTask !== null) {
19401
- 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);
19402
19504
  }
19403
19505
  this.bumpWake();
19404
19506
  return { ok: true, stop: this.stopOf(args.runId) };
@@ -20158,12 +20260,14 @@ export class Store {
20158
20260
  .prepare(`INSERT OR IGNORE INTO push_delivery (notification, subscription, created_at)
20159
20261
  SELECT notification.id, push_subscription.id, ?
20160
20262
  FROM notification, push_subscription
20161
- 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(?))
20162
20266
  AND notification.resolved_at IS NULL
20163
20267
  AND (notification.recipient IS NULL OR notification.recipient = push_subscription.approver)
20164
20268
  AND push_subscription.retired_at IS NULL
20165
20269
  AND notification.id > push_subscription.starts_after_notification`)
20166
- .run(now.toISOString());
20270
+ .run(now.toISOString(), JSON.stringify([...ACTIVITY_ONLY_KINDS]));
20167
20271
  // Pings follow responsibility: a pair this person must not be alerted about retires before it is ever sent.
20168
20272
  if (Number(changes) > 0) {
20169
20273
  const fresh = this.db.prepare(`SELECT p.id AS pair, s.approver AS approver, n.* FROM push_delivery p
@@ -20490,7 +20594,8 @@ export class Store {
20490
20594
  return null;
20491
20595
  }
20492
20596
  }
20493
- /** 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). */
20494
20599
  runCheckAccess(runId) {
20495
20600
  return checkAccessOf(this.db.prepare("SELECT sandbox_access FROM run WHERE id = ?").get(runId)?.["sandbox_access"]);
20496
20601
  }
@@ -21773,6 +21878,11 @@ export function carryReleaseApproval(store, w, now, recheck) {
21773
21878
  const problem = recheck();
21774
21879
  if (problem !== null)
21775
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}.` };
21776
21886
  if (store.getTask(w.taskId) !== null)
21777
21887
  return { ok: false, why: `A task ${w.taskId} already exists.` };
21778
21888
  const anchorRow = store.handle.prepare("SELECT * FROM task_scope WHERE task_id = ?").get(a.anchorTask);
@@ -21808,7 +21918,7 @@ export function carryReleaseApproval(store, w, now, recheck) {
21808
21918
  store.recordAction({ at: now.toISOString(), actor: "toolroll", repo: a.repo, taskId: w.taskId, runId: null, action: "release carried", outcome: "carried", source: "work",
21809
21919
  detail: `release ${a.id} · carry ${carryId} · ${a.tipSha} → ${w.to} · ${w.files} files · ${w.changedLines} lines · request ${w.requestId}` });
21810
21920
  // One quiet task notification, no push.
21811
- 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)}`,
21812
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.`,
21813
21923
  link: chatControlHref("task", w.taskId), source: { taskRef: ref.id } }, now);
21814
21924
  return { ok: true, taskId: w.taskId, carry: releaseCarryTo(store, a.id, w.to) };
@@ -22629,7 +22739,8 @@ export function recordReleaseStep(store, entry) {
22629
22739
  .run(entry.taskId, entry.tag, entry.version, entry.pr, entry.step, entry.state, at, ended ? at : null, detail, at);
22630
22740
  }
22631
22741
  /** A release's steps, for the release task: the newest tag recorded for it, its steps in the order they ran, and the
22632
- * 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. */
22633
22744
  export function releaseProgressFor(store, taskId) {
22634
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);
22635
22746
  if (newest === undefined)
@@ -22639,9 +22750,13 @@ export function releaseProgressFor(store, taskId) {
22639
22750
  const steps = rows.map(row => ({ step: String(row["step"]), state: String(row["state"]), startedAt: String(row["started_at"]),
22640
22751
  endedAt: row["ended_at"] === null ? null : String(row["ended_at"]), detail: row["detail"] === null ? null : String(row["detail"]) }));
22641
22752
  const last = [...rows].sort((a, b) => String(a["updated_at"]).localeCompare(String(b["updated_at"]))).at(-1);
22642
- 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;
22643
22758
  const pr = last["pr"] === null ? null : Number(last["pr"]);
22644
- 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 };
22645
22760
  }
22646
22761
  /** The release task whose steps were recorded most recently (v146), or null when none was. */
22647
22762
  export function latestReleaseStepTask(store) {
@@ -22878,15 +22993,17 @@ export function settleRunStopByObservation(store, args, now) {
22878
22993
  const at = now.toISOString();
22879
22994
  let what;
22880
22995
  if (args.cause === "incomplete-spawn") {
22996
+ const local = localWitnessSql(db);
22881
22997
  const changed = db.prepare(`UPDATE run_process SET exited_at = ? WHERE run = ? AND exited_at IS NULL AND pid IS NULL
22882
- AND containment IS NULL AND container IS NULL AND host = ?`).run(at, args.runId, hostname()).changes;
22998
+ AND containment IS NULL AND container IS NULL AND ${local.sql}`).run(at, args.runId, ...local.params).changes;
22883
22999
  if (Number(changed) === 0)
22884
23000
  return false;
22885
23001
  what = `${Number(changed)} process witness${Number(changed) === 1 ? "" : "es"} with no process ID recorded ended`;
22886
23002
  }
22887
23003
  else if (args.cause === "no-witness") {
22888
- db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
22889
- .run(args.runId, hostname(), at, currentBootId(), at);
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());
22890
23007
  what = "its missing process witness recorded ended";
22891
23008
  }
22892
23009
  else {
@@ -22901,3 +23018,95 @@ export function settleRunStopByObservation(store, args, now) {
22901
23018
  return true;
22902
23019
  });
22903
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
+ }