@wenathlan/devthink 2.0.14 → 2.0.19

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 (444) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/run.ts DELETED
@@ -1,2135 +0,0 @@
1
- /**
2
- * The run module of the 1.1.90 consolidation: every correlated variation of the run logic interned in this one file, so the module family carries one surface without duplicate variations.
3
- * The correlation is the run lifecycle itself, ordered from the base outward: runstate holds the typed run state every other variation builds on (the keepalive heartbeat trail, the url history, the environment and turnaround provenance, the restart recovery, the zombie reaping, the storage level run lock, the integrity sealing and the quota pruning); resilience continues the same lifecycle at the runrecord level (the legal state machine, the offline queue with its replay, the checkpoints, the heartbeat staleness window, the zombie check beside the reap audit event and the rollback compensation the user chooses); runhistory keeps the per run url visits, the merged runtimeline stream, the tab isolation namespace and the session lock that serializes concurrent runs; runtimeline deepens the observation side of the same runs (console, error, rejection, long task and network failure capture with redaction, spam collapse, rotation and the console diff between two runs); runbudget guards the step and memory ceilings with the timeoutcancel bound; runresume, runreplay and runparallel optimize the identical lifecycle (the checkpoint resume with fingerprint revalidation, the audit walk replay of a sealed run and the parallel read lanes with carried selectors); and backgroundruns closes the family with the queue that keeps reviewed workflows executing with no open surface, holding the same keepalive signal of the base lifecycle.
4
- * No window, depth, retention or ceiling is ever hardcoded anywhere in the family: every interval, tolerance, budget and bound stays the user's choice, and no recovery, replay, rollback or queue path ever bypasses the human review.
5
- */
6
-
7
- /* ── Merged from runstate.ts ── */
8
-
9
- import type {
10
- environmentkind,
11
- environmentprovenance,
12
- runlock,
13
- runstaterecord,
14
- sealedrunstate,
15
- urlhistoryentry,
16
- } from "./types.js";
17
-
18
- /**
19
- * Run state logic of the 1.1.60 family.
20
- * Correlated rules for the typed run state live here: the keepalive lifecycle with its start and stop events, its heartbeat trail and its reattach after a service worker restart, the url history of every navigation inside the run, the environment and worker turnaround of every step, the restart recovery of the pending step, the zombie reaping of runs whose heartbeat fell silent, the storage level run lock that holds a session against concurrent runs, the serialization of steps that share one tab across parallel branches, the integrity sealing of persisted run state and the quota pruning under storage pressure.
21
- * No interval, window or lock expiry is ever hardcoded: the heartbeat interval, the zombie tolerance and the lock expiry stay user choices, the run state persists on every heartbeat, and no recovery path ever bypasses the human review.
22
- */
23
-
24
- /** Opens one run state: the keepalive port opens, the start event records and the run waits for its first heartbeat. */
25
- export function openrun(input: {
26
- runid: string;
27
- sessionid: string;
28
- planid: string;
29
- profileid: string;
30
- interval: number;
31
- now: number;
32
- }): runstaterecord {
33
- if (input.runid.trim() === "" || input.sessionid.trim() === "")
34
- throw new Error("The run state needs its run and session ids.");
35
- if (!Number.isFinite(input.interval) || input.interval <= 0)
36
- throw new Error("The keepalive heartbeat interval stays a positive user value in milliseconds.");
37
- return {
38
- runid: input.runid,
39
- sessionid: input.sessionid,
40
- planid: input.planid,
41
- profileid: input.profileid,
42
- state: "active",
43
- urlhistory: [],
44
- environments: {},
45
- turnarounds: {},
46
- keepalive: {
47
- runid: input.runid,
48
- sessionid: input.sessionid,
49
- state: "active",
50
- startedat: input.now,
51
- interval: input.interval,
52
- beats: 0,
53
- lastbeatat: input.now,
54
- portopen: true,
55
- events: [
56
- {
57
- kind: "start" as const,
58
- at: input.now,
59
- detail: `The keepalive port opens for the run ${input.runid} and beats every ${input.interval} milliseconds.`,
60
- },
61
- ],
62
- },
63
- updatedat: input.now,
64
- };
65
- }
66
-
67
- /** Emits one keepalive heartbeat: the beat counter grows, the last beat time moves and the event trail records the beat so the persisted state carries it into any restart recovery. */
68
- export function beatrun(state: runstaterecord, now: number): runstaterecord {
69
- if (state.keepalive.state !== "active")
70
- throw new Error(`The run ${state.runid} is ${state.keepalive.state}; a stopped run emits no heartbeat.`);
71
- return {
72
- ...state,
73
- keepalive: {
74
- ...state.keepalive,
75
- beats: state.keepalive.beats + 1,
76
- lastbeatat: now,
77
- events: [...state.keepalive.events, { kind: "heartbeat" as const, at: now }].slice(-200),
78
- },
79
- updatedat: now,
80
- };
81
- }
82
-
83
- /** Closes one run at its terminal state: the keepalive port closes, the stop event records and the run state freezes for the audit trail. */
84
- export function closerun(state: runstaterecord, now: number): runstaterecord {
85
- if (state.keepalive.state === "stopped")
86
- throw new Error(`The run ${state.runid} already stopped its keepalive port.`);
87
- return {
88
- ...state,
89
- state: "completed",
90
- keepalive: {
91
- ...state.keepalive,
92
- state: "stopped",
93
- stoppedat: now,
94
- portopen: false,
95
- events: [
96
- ...state.keepalive.events,
97
- { kind: "stop" as const, at: now, detail: "The run reached a terminal state and the keepalive port closed." },
98
- ].slice(-200),
99
- },
100
- updatedat: now,
101
- };
102
- }
103
-
104
- /** Reattaches the keepalive port after a service worker restart: the persisted state rehydrates, the reattach event records and the pending step stays exactly where the run stopped. */
105
- export function reattachrun(state: runstaterecord, now: number): runstaterecord {
106
- if (state.keepalive.state !== "active")
107
- throw new Error(`The run ${state.runid} is ${state.keepalive.state}; a stopped run never reattaches.`);
108
- return {
109
- ...state,
110
- state: "recovered",
111
- keepalive: {
112
- ...state.keepalive,
113
- portopen: true,
114
- events: [
115
- ...state.keepalive.events,
116
- {
117
- kind: "reattach" as const,
118
- at: now,
119
- detail: "The service worker restarted and the keepalive port reattached from the persisted run state.",
120
- },
121
- ].slice(-200),
122
- },
123
- updatedat: now,
124
- };
125
- }
126
-
127
- /** Records one navigation inside the run: every url the run visits lands its history entry so the audit reads the full route of the run. */
128
- export function recordurl(state: runstaterecord, entry: { url: string; stepid: string; now: number }): runstaterecord {
129
- if (entry.url.trim() === "") throw new Error("The url history entry needs its url.");
130
- const record: urlhistoryentry = { url: entry.url, stepid: entry.stepid, at: entry.now };
131
- return {
132
- ...state,
133
- urlhistory: [
134
- ...state.urlhistory.filter((item) => !(item.url === record.url && item.stepid === record.stepid)),
135
- record,
136
- ],
137
- updatedat: entry.now,
138
- };
139
- }
140
-
141
- /** Records the environment of one executed step with its provenance: the step id, the environment and the origin land in the run state beside the outcome. */
142
- export function recordenvironment(
143
- state: runstaterecord,
144
- input: { stepid: string; environment: environmentkind; origin: string; now: number },
145
- ): runstaterecord {
146
- if (input.stepid.trim() === "") throw new Error("The environment record needs its step id.");
147
- const provenance: environmentprovenance = {
148
- origin: input.origin,
149
- stepid: input.stepid,
150
- environment: input.environment,
151
- };
152
- return {
153
- ...state,
154
- environments: { ...state.environments, [input.stepid]: input.environment },
155
- lastprovenance: provenance,
156
- updatedat: input.now,
157
- };
158
- }
159
-
160
- /** Records the worker turnaround of one offloaded step: the milliseconds between the worker request and its answer land in the run state for later profiling. */
161
- export function recordturnaround(
162
- state: runstaterecord,
163
- input: { stepid: string; milliseconds: number; now: number },
164
- ): runstaterecord {
165
- if (input.stepid.trim() === "") throw new Error("The turnaround record needs its step id.");
166
- if (!Number.isFinite(input.milliseconds) || input.milliseconds < 0)
167
- throw new Error("The worker turnaround stays a non-negative duration in milliseconds.");
168
- return { ...state, turnarounds: { ...state.turnarounds, [input.stepid]: input.milliseconds }, updatedat: input.now };
169
- }
170
-
171
- /** Marks the pending step of one run for restart recovery: the executor resumes exactly this step after the service worker restart, and no other step ever moves. */
172
- export function markpending(state: runstaterecord, stepid: string | undefined, now: number): runstaterecord {
173
- return {
174
- ...state,
175
- ...(stepid !== undefined && stepid.trim() !== "" ? { pendingstepid: stepid } : {}),
176
- updatedat: now,
177
- };
178
- }
179
-
180
- /** Reads the recovery plan of one interrupted run: the pending step names the single step the executor resumes, and a run without a pending step starts over through the same review instead of guessing. */
181
- export function recoveryplan(state: runstaterecord): {
182
- runid: string;
183
- pendingstepid?: string;
184
- recoverable: boolean;
185
- reason: string;
186
- } {
187
- if (state.state === "completed")
188
- return {
189
- runid: state.runid,
190
- recoverable: false,
191
- reason: `The run ${state.runid} completed before the restart; nothing resumes.`,
192
- };
193
- if (state.state === "reaped")
194
- return {
195
- runid: state.runid,
196
- recoverable: false,
197
- reason: `The run ${state.runid} was reaped as a zombie; the user starts a fresh reviewed run.`,
198
- };
199
- if (state.pendingstepid === undefined || state.pendingstepid.trim() === "")
200
- return {
201
- runid: state.runid,
202
- recoverable: false,
203
- reason: `The run ${state.runid} carries no pending step; a fresh reviewed run starts over instead of guessing.`,
204
- };
205
- return {
206
- runid: state.runid,
207
- pendingstepid: state.pendingstepid,
208
- recoverable: true,
209
- reason: `The executor resumes the pending step ${state.pendingstepid} of the run ${state.runid} from the persisted run state.`,
210
- };
211
- }
212
-
213
- /** Sweeps the zombie runs whose heartbeat fell silent: a run whose last beat sits past the tolerated silent intervals reaps, while a live run keeps its state; the tolerance stays the user configured number of intervals. */
214
- export function zombiesweep(input: { states: runstaterecord[]; now: number; interval: number; missedlimit: number }): {
215
- states: runstaterecord[];
216
- reaped: string[];
217
- } {
218
- if (!Number.isFinite(input.interval) || input.interval <= 0)
219
- throw new Error("The zombie sweep needs its heartbeat interval as a positive user value.");
220
- if (!Number.isInteger(input.missedlimit) || input.missedlimit < 1)
221
- throw new Error("The zombie tolerance stays a positive whole number of silent intervals.");
222
- const silentfor = input.interval * input.missedlimit;
223
- const zombies = input.states.filter(
224
- (state) => state.keepalive.state === "active" && input.now - state.keepalive.lastbeatat > silentfor,
225
- );
226
- if (zombies.length === 0) return { states: input.states, reaped: [] };
227
- const ids = new Set(zombies.map((state) => state.runid));
228
- return {
229
- states: input.states.map((state) =>
230
- ids.has(state.runid)
231
- ? {
232
- ...state,
233
- state: "reaped" as const,
234
- keepalive: {
235
- ...state.keepalive,
236
- state: "stopped" as const,
237
- portopen: false,
238
- ...(state.keepalive.stoppedat === undefined ? { stoppedat: input.now } : {}),
239
- events: [
240
- ...state.keepalive.events,
241
- { kind: "stop" as const, at: input.now, detail: "The zombie reaper closed the silent run." },
242
- ],
243
- },
244
- }
245
- : state,
246
- ),
247
- reaped: [...ids],
248
- };
249
- }
250
-
251
- /** Acquires the storage level run lock of one session: a session with no live lock gains its run lock while a concurrent run of the same session refuses, so one session never carries two runs at once. */
252
- export function acquirerunlock(input: {
253
- locks: runlock[];
254
- sessionid: string;
255
- runid: string;
256
- holder: string;
257
- expiresat?: number;
258
- now: number;
259
- }): { locks: runlock[]; acquired: boolean; reason: string } {
260
- if (input.sessionid.trim() === "" || input.runid.trim() === "")
261
- throw new Error("The run lock needs its session and run ids.");
262
- const live = input.locks.filter(
263
- (lock) => lock.sessionid === input.sessionid && (lock.expiresat === undefined || lock.expiresat > input.now),
264
- );
265
- const held = live.find((lock) => lock.runid !== input.runid);
266
- if (held)
267
- return {
268
- locks: input.locks,
269
- acquired: false,
270
- reason: `The session ${input.sessionid} already holds the run ${held.runid}; a session never carries two concurrent runs.`,
271
- };
272
- const own = input.locks.find((lock) => lock.sessionid === input.sessionid && lock.runid === input.runid);
273
- if (own)
274
- return {
275
- locks: input.locks,
276
- acquired: true,
277
- reason: `The run ${input.runid} of the session ${input.sessionid} already holds its lock.`,
278
- };
279
- const lock: runlock = {
280
- sessionid: input.sessionid,
281
- runid: input.runid,
282
- holder: input.holder,
283
- acquiredat: input.now,
284
- ...(input.expiresat !== undefined ? { expiresat: input.expiresat } : {}),
285
- };
286
- return {
287
- locks: [...input.locks.filter((entry) => entry.sessionid !== input.sessionid), lock],
288
- acquired: true,
289
- reason: `The run ${input.runid} locked the session ${input.sessionid} against concurrent runs.`,
290
- };
291
- }
292
-
293
- /** Releases the storage level run lock of one session: the run that holds the lock frees it while another holder refuses. */
294
- export function releaserunlock(input: { locks: runlock[]; sessionid: string; runid: string; now: number }): {
295
- locks: runlock[];
296
- released: boolean;
297
- reason: string;
298
- } {
299
- const lock = input.locks.find((entry) => entry.sessionid === input.sessionid);
300
- if (!lock || lock.runid !== input.runid)
301
- return {
302
- locks: input.locks,
303
- released: false,
304
- reason: `The run ${input.runid} holds no lock of the session ${input.sessionid}.`,
305
- };
306
- return {
307
- locks: input.locks.filter((entry) => entry.sessionid !== input.sessionid),
308
- released: true,
309
- reason: `The run ${input.runid} released the run lock of the session ${input.sessionid}.`,
310
- };
311
- }
312
-
313
- /** Expires the run locks past their user configured expiry: an expired lock frees its session while a lock without an expiry keeps holding until its run releases it. */
314
- export function expirerunlocks(locks: runlock[], now: number): { locks: runlock[]; expired: string[] } {
315
- const expired = locks.filter((lock) => lock.expiresat !== undefined && now > lock.expiresat);
316
- if (expired.length === 0) return { locks, expired: [] };
317
- const ids = new Set(expired.map((lock) => lock.sessionid));
318
- return { locks: locks.filter((lock) => !ids.has(lock.sessionid)), expired: [...ids] };
319
- }
320
-
321
- /** Serializes the steps that share one tab across parallel branches: every branch keeps its inner order while the shared tab receives one deterministic lane, so two branches never touch one tab inside the same beat. */
322
- export function serializesteps(input: {
323
- branches: Array<{ branchid: string; steps: Array<{ stepid: string; tabid: number }> }>;
324
- }): Array<{ branchid: string; stepid: string; tabid: number; order: number }> {
325
- const shared = new Set<number>();
326
- const counts = new Map<number, number>();
327
- for (const branch of input.branches)
328
- for (const step of branch.steps) counts.set(step.tabid, (counts.get(step.tabid) ?? 0) + 1);
329
- for (const [tabid, count] of counts) if (count > 1) shared.add(tabid);
330
- const order: Array<{ branchid: string; stepid: string; tabid: number; order: number }> = [];
331
- let cursor = 0;
332
- for (const branch of input.branches) {
333
- for (const step of branch.steps) {
334
- if (shared.has(step.tabid)) {
335
- order.push({ branchid: branch.branchid, stepid: step.stepid, tabid: step.tabid, order: cursor });
336
- cursor += 1;
337
- }
338
- }
339
- }
340
- for (const branch of input.branches) {
341
- for (const step of branch.steps) {
342
- if (!shared.has(step.tabid)) {
343
- order.push({ branchid: branch.branchid, stepid: step.stepid, tabid: step.tabid, order: cursor });
344
- cursor += 1;
345
- }
346
- }
347
- }
348
- return order;
349
- }
350
-
351
- /** Seals one persisted run state with its sha-256 integrity digest: the payload and its digest travel together so a tampered run state at rest stays detectable before any recovery uses it. */
352
- export async function sealrunstate(state: runstaterecord): Promise<sealedrunstate> {
353
- const payload = JSON.stringify(state);
354
- const digest = await sha256(payload);
355
- return { payload, algorithm: "sha-256", digest, sealedat: state.updatedat };
356
- }
357
-
358
- /** Opens one sealed run state: the digest must match the payload before the recovery reads it, and a tampered or malformed seal refuses. */
359
- export async function openseal(sealed: sealedrunstate): Promise<runstaterecord> {
360
- const digest = await sha256(sealed.payload);
361
- if (digest !== sealed.digest)
362
- throw new Error(
363
- "The sealed run state fails its integrity digest; a tampered run state never reaches the recovery.",
364
- );
365
- const parsed = JSON.parse(sealed.payload) as runstaterecord;
366
- if (
367
- parsed === null ||
368
- typeof parsed !== "object" ||
369
- typeof parsed.runid !== "string" ||
370
- typeof parsed.sessionid !== "string"
371
- )
372
- throw new Error("The sealed run state carries no run record.");
373
- return parsed;
374
- }
375
-
376
- async function sha256(value: string): Promise<string> {
377
- const bytes = new TextEncoder().encode(value);
378
- const digest = await crypto.subtle.digest("SHA-256", bytes);
379
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
380
- }
381
-
382
- /** Prunes the stored run states under storage pressure: the oldest completed run states leave first while every active or recovered run keeps its state, and the pruned ids name what left. */
383
- export function prunerunstates(input: { states: runstaterecord[]; used: number; ceiling: number }): {
384
- states: runstaterecord[];
385
- pruned: string[];
386
- reason: string;
387
- } {
388
- if (!Number.isFinite(input.ceiling) || input.ceiling <= 0)
389
- return {
390
- states: input.states,
391
- pruned: [],
392
- reason: "No storage ceiling is configured, so the run states stay under the user choice alone.",
393
- };
394
- if (input.used <= input.ceiling)
395
- return {
396
- states: input.states,
397
- pruned: [],
398
- reason: `The run state storage sits at ${input.used} of the ${input.ceiling} bytes the user configured; no pressure exists.`,
399
- };
400
- const removable = input.states
401
- .filter((state) => state.state === "completed" || state.state === "reaped")
402
- .sort((one, two) => one.updatedat - two.updatedat);
403
- const states = [...input.states];
404
- const pruned: string[] = [];
405
- for (const candidate of removable) {
406
- pruned.push(candidate.runid);
407
- const index = states.findIndex((state) => state.runid === candidate.runid);
408
- if (index >= 0) states.splice(index, 1);
409
- if (pruned.length >= Math.max(1, Math.ceil(input.states.length / 2))) break;
410
- }
411
- return {
412
- states,
413
- pruned,
414
- reason: `The storage pressure at ${input.used} of ${input.ceiling} bytes pruned the ${pruned.length} oldest finished run state${pruned.length === 1 ? "" : "s"} while every active run keeps its state.`,
415
- };
416
- }
417
-
418
- /** Exports the run states as one single audit record: every run, its url history, its environments, its turnarounds and its keepalive trail in one envelope the auditor reads at once. */
419
- export function exportrunstate(
420
- states: runstaterecord[],
421
- now: number,
422
- ): { runs: number; urls: number; environments: number; offloaded: number; beats: number; exportedat: number } {
423
- return {
424
- runs: states.length,
425
- urls: states.reduce((total, state) => total + state.urlhistory.length, 0),
426
- environments: states.reduce((total, state) => total + Object.keys(state.environments).length, 0),
427
- offloaded: states.reduce((total, state) => total + Object.values(state.turnarounds).length, 0),
428
- beats: states.reduce((total, state) => total + state.keepalive.beats, 0),
429
- exportedat: now,
430
- };
431
- }
432
-
433
- /* ── Merged from resilience.ts ── */
434
-
435
- import { iscomplete } from "./progress.js";
436
- import type {
437
- agentplan,
438
- auditevent,
439
- checkpointrecord,
440
- heartbeatrecord,
441
- planprogress,
442
- rollbackitem,
443
- runrecord,
444
- runsettings,
445
- runstate,
446
- runtransition,
447
- queuedtask,
448
- toolstep,
449
- } from "./types.js";
450
-
451
- /**
452
- * Run lifecycle logic of the 1.1.70 family.
453
- * The runstatemachine tracks every legal transition of a run so a service worker restart or a lost connection never loses work: approved plans queue offline with monotonic sequences, steps carry deterministic idempotencykeys so replays deduplicate, checkpoints capture the completed steps with their page digest, heartbeats time the live runs, zombies reap through the audit trail, and failed runs roll back through compensating steps the user explicitly chooses.
454
- * No window, depth or retention is ever hardcoded: the heartbeat staleness window, the offline queue depth and the failed run retention stay user choices, and no recovery, replay or rollback path ever bypasses the human review.
455
- */
456
-
457
- /** The documented heartbeat staleness window the roadmap names while the configured window stays the user's choice with no code ceiling. */
458
- export const roadmapheartbeatwindow = 60_000;
459
-
460
- /** The legal transitions of each runstate: queued runs start or cancel, running runs pause, complete, fail or cancel, paused and awaitingapproval runs resume or stop, and only a failed or cancelled run rolls back. */
461
- export function runstatemachine(): Record<runstate, runstate[]> {
462
- return {
463
- queued: ["running", "awaitingapproval", "cancelled", "failed"],
464
- running: ["paused", "awaitingapproval", "completed", "failed", "cancelled"],
465
- paused: ["running", "cancelled", "failed"],
466
- awaitingapproval: ["running", "paused", "cancelled", "failed"],
467
- completed: [],
468
- failed: ["rolledback"],
469
- cancelled: ["rolledback"],
470
- rolledback: [],
471
- pending: ["running", "paused", "cancelled", "failed"],
472
- done: [],
473
- };
474
- }
475
-
476
- /** Reads whether one run transition stays legal inside the state machine: every state allows only the pairs its lifecycle defines. */
477
- export function transitionislegal(transition: runtransition): boolean {
478
- return runstatemachine()[transition.from].includes(transition.to);
479
- }
480
-
481
- /** Moves one runrecord through a single legal transition or throws: an illegal transition never silently mutates the run state. */
482
- export function advancerun(run: runrecord, to: runstate, now: number): runrecord {
483
- if (!transitionislegal({ from: run.state, to }))
484
- throw new Error(
485
- `The run ${run.runid} cannot move from ${run.state} to ${to}; the run lifecycle allows only its legal transitions.`,
486
- );
487
- return { ...run, state: to, updatedat: now };
488
- }
489
-
490
- /** Creates one runrecord in the queued state for an approved plan: the run waits in the offlinequeue or the executor before it ever runs. */
491
- export function initialrun(input: { planid: string; sessionid: string; now: number; runid?: string }): runrecord {
492
- if (input.planid.trim() === "") throw new Error("The run record needs its plan id.");
493
- if (input.sessionid.trim() === "") throw new Error("The run record needs its session id.");
494
- return {
495
- runid: input.runid ?? crypto.randomUUID(),
496
- planid: input.planid,
497
- sessionid: input.sessionid,
498
- state: "queued",
499
- createdat: input.now,
500
- updatedat: input.now,
501
- };
502
- }
503
-
504
- /** Derives the deterministic idempotencykey of one step from its plan and step identity so a replay of the same step deduplicates on the same key. */
505
- export function stepkey(input: { planid: string; stepid: string; kind: string }): string {
506
- if (input.planid.trim() === "" || input.stepid.trim() === "")
507
- throw new Error("The step key needs its plan and step ids.");
508
- if (input.kind.trim() === "") throw new Error("The step key needs its step kind.");
509
- return `${input.planid}:${input.stepid}:${input.kind}`;
510
- }
511
-
512
- /** Returns the count of tasks waiting in the offlinequeue. */
513
- export function queuesize(queue: queuedtask[]): number {
514
- return queue.length;
515
- }
516
-
517
- /** Appends one approved plan to the offlinequeue with a monotonic sequence number so the replay drains the tasks in their enqueue order. */
518
- export function enqueueoffline(
519
- queue: queuedtask[],
520
- input: { plan: agentplan; sessionid: string; now: number },
521
- ): queuedtask[] {
522
- if (input.plan.state !== "approved")
523
- throw new Error("Only an approved plan enters the offlinequeue; an unreviewed plan never queues.");
524
- const sequence = queue.reduce((highest, task) => Math.max(highest, task.sequence), 0) + 1;
525
- return [
526
- ...queue,
527
- {
528
- sequence,
529
- planid: input.plan.id,
530
- sessionid: input.sessionid,
531
- plan: input.plan,
532
- enqueuedat: input.now,
533
- expiresat: input.plan.expiresat,
534
- },
535
- ];
536
- }
537
-
538
- /** Returns the next queuedtask in sequence order and the queue without it; an empty queue returns no task. */
539
- export function dequeueoffline(queue: queuedtask[]): { queue: queuedtask[]; task?: queuedtask } {
540
- if (queue.length === 0) return { queue };
541
- const ordered = [...queue].sort((one, two) => one.sequence - two.sequence);
542
- const [task, ...rest] = ordered;
543
- if (task === undefined) return { queue: [...ordered] };
544
- return { queue: rest, task };
545
- }
546
-
547
- /** Reads the replay plan of the offlinequeue once connectivity returns: every task drains in sequence order while the tasks whose plan window passed offline expire and fail instead of running. */
548
- export function replayqueue(queue: queuedtask[], now: number): { ready: queuedtask[]; expired: queuedtask[] } {
549
- const ordered = [...queue].sort((one, two) => one.sequence - two.sequence);
550
- const ready = ordered.filter((task) => task.expiresat > now);
551
- const expired = ordered.filter((task) => task.expiresat <= now);
552
- return { ready, expired };
553
- }
554
-
555
- /** Captures one checkpoint of a run: the completed step ids, the step boundary, the page digest a resume revalidates and the timestamp. */
556
- export function makecheckpoint(input: {
557
- runid: string;
558
- stepid: string;
559
- completed: string[];
560
- digest: string;
561
- now: number;
562
- }): checkpointrecord {
563
- if (input.runid.trim() === "") throw new Error("The checkpoint needs its run id.");
564
- if (input.stepid.trim() === "") throw new Error("The checkpoint needs its step id.");
565
- if (input.digest.trim() === "")
566
- throw new Error("The checkpoint needs its page digest; a digestless resume never revalidates the page.");
567
- return {
568
- runid: input.runid,
569
- stepid: input.stepid,
570
- completed: [...input.completed],
571
- digest: input.digest,
572
- createdat: input.now,
573
- };
574
- }
575
-
576
- /** Returns the steps that follow the last stored checkpoint so the resume continues at the first open step instead of repeating completed work. */
577
- export function resumecheckpoint(input: { checkpoint: checkpointrecord | undefined; steps: string[] }): string[] {
578
- if (input.checkpoint === undefined) return [...input.steps];
579
- const done = new Set(input.checkpoint.completed);
580
- return input.steps.filter((step) => !done.has(step));
581
- }
582
-
583
- /** Verifies one checkpointrecord against its run and page digest: a checkpoint of another run or a page that changed since the capture never validates. */
584
- export function checkpointisvalid(input: { checkpoint: checkpointrecord; runid: string; digest: string }): boolean {
585
- return (
586
- input.checkpoint.runid === input.runid &&
587
- input.checkpoint.digest === input.digest &&
588
- input.checkpoint.digest.trim() !== ""
589
- );
590
- }
591
-
592
- /** Refreshes the heartbeatrecord of a running run: the beat counter grows and the last beat time moves so the zombiecheck sees the run alive. */
593
- export function runheartbeat(input: { runid: string; beat?: number; now: number }): heartbeatrecord {
594
- if (input.runid.trim() === "") throw new Error("The heartbeat needs its run id.");
595
- const previous = Number.isFinite(input.beat) ? (input.beat as number) : 0;
596
- return { runid: input.runid, beat: previous + 1, at: input.now };
597
- }
598
-
599
- /** Reads whether the last heartbeat of a run sits older than the configured window: a stale heartbeat turns a running run into a zombie candidate. */
600
- export function heartbeatisstale(input: {
601
- heartbeat: heartbeatrecord | undefined;
602
- now: number;
603
- window: number;
604
- }): boolean {
605
- if (input.heartbeat === undefined) return true;
606
- return input.now - input.heartbeat.at > input.window;
607
- }
608
-
609
- /** Loads the user configured heartbeat staleness window without a hard cap: an absent setting keeps the documented roadmap window while every configured value stays the user's choice. */
610
- export function heartbeatwindowof(settings: runsettings | undefined): number {
611
- return settings?.heartbeatwindow ?? roadmapheartbeatwindow;
612
- }
613
-
614
- /** Lists the running runs whose heartbeat is stale: every run past its staleness window names a zombie candidate the reaper may reap. */
615
- export function zombiecheck(input: {
616
- runs: runrecord[];
617
- heartbeats: Record<string, heartbeatrecord>;
618
- now: number;
619
- window: number;
620
- }): runrecord[] {
621
- return input.runs.filter(
622
- (run) =>
623
- run.state === "running" &&
624
- heartbeatisstale({ heartbeat: input.heartbeats[run.runid], now: input.now, window: input.window }),
625
- );
626
- }
627
-
628
- /** Transitions one zombie run to failed and records its reap audit event: the reaped run keeps its record for the audit trail while it never executes again. */
629
- export function reaprun(run: runrecord, now: number): { run: runrecord; event: auditevent } {
630
- if (run.state !== "running")
631
- throw new Error(`The run ${run.runid} is ${run.state}; only a running run with a silent heartbeat reaps.`);
632
- const reaped: runrecord = { ...run, state: "failed", updatedat: now };
633
- return {
634
- run: reaped,
635
- event: {
636
- id: crypto.randomUUID(),
637
- kind: "reap",
638
- at: now,
639
- summary: `The zombie reaper failed the run ${run.runid} whose heartbeat stayed silent past the user window; the user starts a fresh reviewed run instead of guessing.`,
640
- sessionid: run.sessionid,
641
- planid: run.planid,
642
- },
643
- };
644
- }
645
-
646
- /** Lists the mutating steps a run already executed: every executed step that is not read only names a side effect the rollback must compensate. */
647
- export function sideeffects(steps: toolstep[], executedstepids: string[]): toolstep[] {
648
- const executed = new Set(executedstepids);
649
- return steps.filter((step) => executed.has(step.id) && step.risk !== "read");
650
- }
651
-
652
- /** The compensating action of each mutating kind in plain language; the kinds outside the table report their outcome for the user review because their effect stays applied. */
653
- const compensationtable: Record<string, string> = {
654
- navigate: "navigate back to the origin the run started from",
655
- openlink: "close the opened link target and return to the origin the run started from",
656
- followlink: "navigate back to the origin the run started from",
657
- navlist: "close every tab the list navigation opened and return to the origin the run started from",
658
- batchopen: "close every tab the batch navigation opened and return to the origin the run started from",
659
- openprivate: "close the private window the navigation opened",
660
- reload: "the navigation history stays as the run left it; the compensation reports the route for the user review",
661
- reloadcache:
662
- "the navigation history stays as the run left it; the compensation reports the route for the user review",
663
- back: "the navigation history stays as the run left it; the compensation reports the route for the user review",
664
- forward: "the navigation history stays as the run left it; the compensation reports the route for the user review",
665
- type: "restore the field value the form carried before the typing",
666
- setvalue: "restore the field value the form carried before the change",
667
- appendtext: "restore the field value the form carried before the append",
668
- typeedit: "restore the field value the form carried before the edit",
669
- clear: "restore the cleared value from the session record",
670
- check: "restore the checkbox state the form carried before the check",
671
- uncheck: "restore the checkbox state the form carried before the uncheck",
672
- toggle: "restore the control state the form carried before the toggle",
673
- chooseradio: "restore the radio choice the form carried",
674
- setslider: "restore the slider position the form carried",
675
- setdate: "restore the date the form carried",
676
- setcolor: "restore the color the form carried",
677
- select: "restore the selection the form carried",
678
- selectmulti: "restore the selections the form carried",
679
- upload: "clear the file input of the uploaded file",
680
- attachfile: "clear the file input of the attached file",
681
- click: "the click stays executed; the compensation reports its outcome for the user review",
682
- clickdeep: "the click stays executed; the compensation reports its outcome for the user review",
683
- rightclick: "the click stays executed; the compensation reports its outcome for the user review",
684
- doubleclick: "the click stays executed; the compensation reports its outcome for the user review",
685
- submit: "the submission stays submitted; the compensation reports it for the user review",
686
- submitform: "the submission stays submitted; the compensation reports it for the user review",
687
- retryform: "the submission stays submitted; the compensation reports it for the user review",
688
- submitsearch: "the search submission stays submitted; the compensation reports it for the user review",
689
- writestorage: "remove the written storage key",
690
- setattribute: "restore the attribute value the page carried",
691
- removeattribute: "restore the removed attribute from the session record",
692
- setcookies: "delete the cookies the step set",
693
- clearcookies: "restore the cleared cookies from the session record",
694
- blockrequest: "remove the block rule the step applied",
695
- mockresponse: "remove the mock rule the step applied",
696
- rewriteheaders: "remove the header rewrite rule the step applied",
697
- routeproxy: "remove the proxy route the step applied",
698
- overridescript: "remove the script override the step applied",
699
- tabcreate: "close the created tab",
700
- tabclose: "reopen the closed tab from the session record",
701
- windowcreate: "close the created window",
702
- downloadfile: "delete the downloaded file from the user downloads",
703
- evaluate: "the evaluated script stays applied; the compensation reports its effect for the user review",
704
- };
705
-
706
- /** Builds the compensating steps of a failed run for its executed side effects; read only steps carry no compensation at all because they changed nothing. */
707
- export function rollbackrun(input: {
708
- runid: string;
709
- steps: toolstep[];
710
- executedstepids: string[];
711
- origin: string;
712
- now: number;
713
- }): rollbackitem[] {
714
- return sideeffects(input.steps, input.executedstepids).map((step) => ({
715
- runid: input.runid,
716
- stepid: step.id,
717
- kind: step.kind,
718
- origin: input.origin,
719
- compensation:
720
- compensationtable[step.kind] ??
721
- `the ${step.kind} step stays executed; the compensation reports its outcome for the user review`,
722
- at: input.now,
723
- }));
724
- }
725
-
726
- /** Moves one run to cancelled and returns the rollback choice the cancel pairs with: the cancel itself never executes a compensation without the explicit user choice. */
727
- export function cancelrunrecord(run: runrecord, now: number): runrecord {
728
- return advancerun(run, "cancelled", now);
729
- }
730
-
731
- /** Pairs one cancellation with its optional rollback execution: the choice rollback builds the compensating steps while the choice none stops the run with its state frozen for the audit trail. */
732
- export function cancelrollback(input: {
733
- run: runrecord;
734
- steps: toolstep[];
735
- executedstepids: string[];
736
- origin: string;
737
- choice: "rollback" | "none";
738
- now: number;
739
- }): { run: runrecord; compensations: rollbackitem[]; reason: string } {
740
- const cancelled = cancelrunrecord(input.run, input.now);
741
- if (input.choice === "none")
742
- return {
743
- run: cancelled,
744
- compensations: [],
745
- reason: `The run ${input.run.runid} cancelled without a rollback; the executed steps stay applied and the run state freezes for the audit trail.`,
746
- };
747
- const compensations = rollbackrun({
748
- runid: input.run.runid,
749
- steps: input.steps,
750
- executedstepids: input.executedstepids,
751
- origin: input.origin,
752
- now: input.now,
753
- });
754
- const rolledback = advancerun(cancelled, "rolledback", input.now);
755
- return {
756
- run: rolledback,
757
- compensations,
758
- reason: `The run ${input.run.runid} cancelled with its rollback: ${compensations.length} compensating step${compensations.length === 1 ? "" : "s"} run only after the explicit user choice.`,
759
- };
760
- }
761
-
762
- /** Transitions one run to failed and freezes its progress snapshot so the failure keeps exactly the state it failed at. */
763
- export function runfail(input: { run: runrecord; progress?: planprogress; now: number }): {
764
- run: runrecord;
765
- frozen?: planprogress;
766
- } {
767
- const failed = advancerun(input.run, "failed", input.now);
768
- if (input.progress === undefined) return { run: failed };
769
- return {
770
- run: failed,
771
- frozen: {
772
- ...input.progress,
773
- ...(input.progress.runid !== undefined ? { runid: input.progress.runid } : { runid: input.run.runid }),
774
- updatedat: input.now,
775
- },
776
- };
777
- }
778
-
779
- /** Transitions one run to completed only when iscomplete reports the plan finished; an unfinished run never completes. */
780
- export function runcomplete(input: {
781
- run: runrecord;
782
- plan: agentplan;
783
- progress?: planprogress;
784
- now: number;
785
- }): runrecord {
786
- if (!iscomplete(input.progress, input.plan))
787
- throw new Error(
788
- `The run ${input.run.runid} did not finish every step of its plan; an unfinished run never completes.`,
789
- );
790
- return advancerun(input.run, "completed", input.now);
791
- }
792
-
793
- /** Rebuilds one runrecord after a service worker restart: the persisted plan restores its run with its original timestamps while an expired or unapproved plan rebuilds as failed because it never resumes. */
794
- export function runfromplan(plan: agentplan, sessionid: string, now: number): runrecord {
795
- if (plan.state !== "approved" || plan.expiresat <= now)
796
- return { runid: plan.id, planid: plan.id, sessionid, state: "failed", createdat: plan.createdat, updatedat: now };
797
- return { runid: plan.id, planid: plan.id, sessionid, state: "running", createdat: plan.createdat, updatedat: now };
798
- }
799
-
800
- /** Returns the summary of one run: its state, its step counts against its plan, its checkpoint position and its last update. */
801
- export function runsummaryof(input: {
802
- run: runrecord;
803
- plan: agentplan;
804
- progress?: planprogress;
805
- checkpoint?: checkpointrecord;
806
- }): { runid: string; state: runstate; steps: number; completed: number; checkpointstep?: string; updatedat: number } {
807
- const completed = input.progress?.completedsteps.length ?? 0;
808
- return {
809
- runid: input.run.runid,
810
- state: input.run.state,
811
- steps: input.plan.steps.length,
812
- completed,
813
- ...(input.checkpoint !== undefined ? { checkpointstep: input.checkpoint.stepid } : {}),
814
- updatedat: input.run.updatedat,
815
- };
816
- }
817
-
818
- /* ── Merged from runhistory.ts ── */
819
-
820
- import type { agentsession, lockrecord, runtimelineevent, tabstate, urlvisit } from "./types.js";
821
-
822
- /**
823
- * Run history logic of the 1.1.71 state depth family.
824
- * Every run keeps its own urlhistory and timeline: urlvisit entries append per navigation with consecutive duplicates folded, the runtimeline merges step results, audit events and url visits into one ordered stream with phase buckets, tabisolate namespaces the memory keys per tab so runs never share state, and the sessionlock serializes concurrent runs on one session while it names its holder and expires its abandoned locks.
825
- * No window stays hardcoded: the lock expiry window reads the user setting with no code ceiling, urlhistory stays scoped per run and never merges runs, and no lock, visit or timeline path ever bypasses the review.
826
- */
827
-
828
- /** The documented lock expiry window the roadmap names while the configured window stays the user's choice with no code ceiling. */
829
- export const roadmaplockwindow = 120_000;
830
-
831
- /** Builds one urlvisit of a navigation inside a run: the url, the page title, the time and the run it belongs to. */
832
- export function visitof(input: { url: string; title?: string; runid: string; now: number }): urlvisit {
833
- if (input.url.trim() === "") throw new Error("The url visit needs its url.");
834
- if (input.runid.trim() === "") throw new Error("The url visit needs its run id.");
835
- return {
836
- url: input.url,
837
- ...(input.title !== undefined && input.title.trim() !== "" ? { title: input.title } : {}),
838
- at: input.now,
839
- runid: input.runid,
840
- };
841
- }
842
-
843
- /** Appends one urlvisit to the urlhistory of a run: consecutive visits to the same url fold into one entry so the history reads as the real route, and the visits of one run never merge with another run. */
844
- export function appendvisit(visits: urlvisit[], visit: urlvisit): urlvisit[] {
845
- const last = visits.at(-1);
846
- if (last !== undefined && last.url === visit.url && last.runid === visit.runid) {
847
- const folded: urlvisit = { ...last, at: visit.at };
848
- return [...visits.slice(0, -1), folded];
849
- }
850
- return [...visits, visit];
851
- }
852
-
853
- /** Builds one runtimeline event of a step result: the step identity, its outcome summary and its timestamp inside the run. */
854
- export function stepeventof(input: {
855
- runid: string;
856
- stepid: string;
857
- summary: string;
858
- ok: boolean;
859
- now: number;
860
- }): runtimelineevent {
861
- return {
862
- at: input.now,
863
- runid: input.runid,
864
- source: "step",
865
- summary: `${input.ok ? "completed" : "failed"}: ${input.summary}`,
866
- stepid: input.stepid,
867
- };
868
- }
869
-
870
- /** Builds one runtimeline event of an audit event so the timeline carries the audit trail beside the steps. */
871
- export function auditeventof(event: auditevent): runtimelineevent {
872
- return {
873
- at: event.at,
874
- runid: "",
875
- source: "audit",
876
- summary: event.summary,
877
- ...(event.stepid !== undefined ? { stepid: event.stepid } : {}),
878
- kind: event.kind,
879
- };
880
- }
881
-
882
- /** Builds one runtimeline event of a url visit so the timeline carries the route beside the steps. */
883
- export function visiteventof(visit: urlvisit): runtimelineevent {
884
- return {
885
- at: visit.at,
886
- runid: visit.runid,
887
- source: "visit",
888
- summary: `visited ${visit.url}${visit.title !== undefined ? ` (${visit.title})` : ""}`,
889
- };
890
- }
891
-
892
- /** Merges step results, audit events and url visits into one ordered runtimeline stream: every event sorts by its timestamp, the audit events carry their kind and the visits carry their route. */
893
- export function runtimelineof(input: {
894
- steps: runtimelineevent[];
895
- audits: runtimelineevent[];
896
- visits: urlvisit[];
897
- runid: string;
898
- }): runtimelineevent[] {
899
- const scoped = input.audits.filter((event) => event.runid === input.runid || event.runid === "");
900
- const merged = [
901
- ...input.steps.filter((event) => event.runid === input.runid),
902
- ...scoped,
903
- ...input.visits.filter((visit) => visit.runid === input.runid).map(visiteventof),
904
- ];
905
- return merged.sort((one, two) => one.at - two.at);
906
- }
907
-
908
- /** Buckets one runtimeline stream by phase for the timeline view: every event lands in the phase its timestamp names while the order inside a phase stays the stream order. */
909
- export function buckettimeline(events: runtimelineevent[]): Array<{ phase: string; events: runtimelineevent[] }> {
910
- const buckets = new Map<string, runtimelineevent[]>();
911
- for (const event of events) {
912
- const phase =
913
- event.source === "visit"
914
- ? "route"
915
- : event.source === "audit"
916
- ? "audit"
917
- : event.summary.startsWith("failed")
918
- ? "failures"
919
- : "steps";
920
- const bucket = buckets.get(phase) ?? [];
921
- bucket.push(event);
922
- buckets.set(phase, bucket);
923
- }
924
- return [...buckets.entries()].map(([phase, bucket]) => ({ phase, events: bucket }));
925
- }
926
-
927
- /** Derives the isolated namespace id of one tab: every memory key of the run lives under its tab namespace so runs never share state. */
928
- export function tabnamespace(tabid: number): string {
929
- return `tab:${tabid}`;
930
- }
931
-
932
- /** Names one memory key inside the isolated namespace of its tab; the shared key stays readable beside the namespace copy. */
933
- export function tabkey(tabid: number, key: string): string {
934
- if (key.trim() === "") throw new Error("The namespaced key needs its key.");
935
- return `${tabnamespace(tabid)}:${key}`;
936
- }
937
-
938
- /** Copies the shared config into the tab namespace on demand: the tabstate carries its namespace id, its isolated run id and the copied config snapshot. */
939
- export function tabisolate(input: {
940
- tabid: number;
941
- config: Record<string, unknown>;
942
- runid?: string;
943
- now: number;
944
- }): tabstate {
945
- if (!Number.isFinite(input.tabid)) throw new Error("The tab isolation needs its tab id.");
946
- return {
947
- tabid: input.tabid,
948
- namespace: tabnamespace(input.tabid),
949
- config: { ...input.config },
950
- ...(input.runid !== undefined && input.runid.trim() !== "" ? { runid: input.runid } : {}),
951
- at: input.now,
952
- };
953
- }
954
-
955
- /** Acquires one lockrecord for a run on a session: the holder names the run, the acquisition time stamps it and the expiry window stays the user choice. */
956
- export function acquiresessionlock(input: {
957
- runid: string;
958
- sessionid: string;
959
- window: number;
960
- now: number;
961
- }): lockrecord {
962
- if (input.runid.trim() === "") throw new Error("The lock needs its run id.");
963
- if (input.sessionid.trim() === "") throw new Error("The lock needs its session id.");
964
- return {
965
- holder: input.runid,
966
- runid: input.runid,
967
- sessionid: input.sessionid,
968
- acquiredat: input.now,
969
- expiresat: input.now + input.window,
970
- };
971
- }
972
-
973
- /** Reads whether one lock blocks a run: a lock of the same run never blocks itself, a live foreign lock blocks and names its holder while an expired lock blocks nothing because its window passed. */
974
- export function lockblocks(
975
- lock: lockrecord | undefined,
976
- runid: string,
977
- now: number,
978
- ): { blocked: boolean; reason: string } {
979
- if (lock === undefined)
980
- return {
981
- blocked: false,
982
- reason: "No sessionlock stands on the session; the run starts through the reviewed path.",
983
- };
984
- if (lock.runid === runid)
985
- return {
986
- blocked: false,
987
- reason: `The sessionlock already belongs to the run ${runid}; the run holds its own lock.`,
988
- };
989
- if (lock.expiresat <= now)
990
- return {
991
- blocked: false,
992
- reason: `The sessionlock of the run ${lock.holder} expired past its window; the stale lock leaves before a new run starts.`,
993
- };
994
- return {
995
- blocked: true,
996
- reason: `The sessionlock of the session ${lock.sessionid} stays held by the run ${lock.holder}; the concurrent run ${runid} waits because one session never runs two runs at once.`,
997
- };
998
- }
999
-
1000
- /** Reads whether one lock belongs to a run so the release path only clears its own lock. */
1001
- export function lockheldby(lock: lockrecord | undefined, runid: string): boolean {
1002
- return lock !== undefined && lock.runid === runid;
1003
- }
1004
-
1005
- /** Releases the lock of a completed, failed or cancelled run: the sessionlock clears so the next reviewed run may acquire it. */
1006
- export function releasesessionlock(lock: lockrecord | undefined, runid: string): boolean {
1007
- return lockheldby(lock, runid);
1008
- }
1009
-
1010
- /** Loads the user configured lock expiry window without a hard cap: an absent setting keeps the documented roadmap window while every configured value stays the user's choice. */
1011
- export function lockwindowof(settings: runsettings | undefined): number {
1012
- return settings?.lockwindow ?? roadmaplockwindow;
1013
- }
1014
-
1015
- /** Lists the locks whose expiry window passed: every abandoned lock names an expiry candidate the startup pass clears before the zombiecheck runs. */
1016
- export function stalelocks(locks: lockrecord[], now: number): lockrecord[] {
1017
- return locks.filter((lock) => lock.expiresat <= now);
1018
- }
1019
-
1020
- /** Attaches the acquired lockid to one agentsession so concurrent run protection travels with the session record. */
1021
- export function sessionwithlock(session: agentsession, lockid: string | undefined): agentsession {
1022
- if (lockid === undefined || lockid.trim() === "") return session;
1023
- return { ...session, lockid };
1024
- }
1025
-
1026
- /** Summarizes the urlhistory of one run: the visit count, its distinct origins and its first and last visit times; a run without visits carries none. */
1027
- export function visitsummary(visits: urlvisit[]): { count: number; origins: string[]; first?: number; last?: number } {
1028
- const scoped = visits.filter((visit) => visit.url.trim() !== "");
1029
- const origins = [
1030
- ...new Set(
1031
- scoped
1032
- .map((visit) => {
1033
- try {
1034
- return new URL(visit.url).origin;
1035
- } catch {
1036
- return "";
1037
- }
1038
- })
1039
- .filter((origin) => origin !== ""),
1040
- ),
1041
- ];
1042
- const first = scoped[0]?.at;
1043
- const last = scoped.at(-1)?.at;
1044
- return {
1045
- count: scoped.length,
1046
- origins,
1047
- ...(first !== undefined ? { first } : {}),
1048
- ...(last !== undefined ? { last } : {}),
1049
- };
1050
- }
1051
-
1052
- /** Reads the final url of a completed navigation step result so the urlhistory records the url the redirect chain landed on; a result without a final url reports the planned url unchanged. */
1053
- export function finalurlof(input: { plannedurl: string; finalurl?: string }): string {
1054
- if (input.finalurl !== undefined && input.finalurl.trim() !== "") return input.finalurl;
1055
- return input.plannedurl;
1056
- }
1057
-
1058
- /** Lists the navigation steps of a plan in their reviewed order: the urlhistory records one urlvisit for each completed navigation step. */
1059
- export function navigationsteps(steps: toolstep[]): toolstep[] {
1060
- return steps.filter((step) =>
1061
- [
1062
- "navigate",
1063
- "openlink",
1064
- "followlink",
1065
- "spanav",
1066
- "navlist",
1067
- "batchopen",
1068
- "openprivate",
1069
- "reload",
1070
- "reloadcache",
1071
- "back",
1072
- "forward",
1073
- ].includes(step.kind),
1074
- );
1075
- }
1076
-
1077
- /** Reads the runrecord of a run with its visits attached so the record carries its urlhistory beside its lifecycle state. */
1078
- export function runwithvisits(run: runrecord, visits: urlvisit[]): runrecord {
1079
- const scoped = visits.filter((visit) => visit.runid === run.runid);
1080
- if (scoped.length === 0) return run;
1081
- return { ...run, visits: scoped };
1082
- }
1083
-
1084
- /* ── Merged from runtimeline.ts ── */
1085
-
1086
- import type {
1087
- consolediff,
1088
- consoleentry,
1089
- errorrecord,
1090
- exchangerecord,
1091
- loglevel,
1092
- loglevelset,
1093
- longtaskentry,
1094
- netfailureentry,
1095
- rejectionrecord,
1096
- rotationrule,
1097
- spamrule,
1098
- stackframe,
1099
- timelineentry,
1100
- timelinesource,
1101
- } from "./types.js";
1102
-
1103
- /**
1104
- * Run timeline logics for the 1.1.45 debugging family.
1105
- * Every correlated rule for console capture with reviewed depth bounds and secret redaction, stack frame parsing, error, rejection and resource failure capture, long task attribution windows, the timeline binding to one run and its step ids, spam collapse with reviewed thresholds, log rotation without entry loss, level and source filters, the blocking duration per step window and the console diff between two runs lives in this file.
1106
- * Console, error and task watching derives from page-injected listeners installed through the scripting api and the performance buffers, so no debugger permission exists anywhere in the manifest; redaction runs before any entry leaves the page bridge and captured text never carries reviewed secret patterns.
1107
- */
1108
-
1109
- /** The debugging kinds, listed among the available capabilities of every proposal request. */
1110
- export const timelinekinds: string[] = ["watchconsole", "watcherrors", "watchtasks"];
1111
-
1112
- /** The reviewed level set, ordered from the most severe to the most verbose level. */
1113
- export const loglevels: loglevel[] = ["error", "warn", "info", "log", "debug", "trace"];
1114
-
1115
- /** The reviewed timeline source grammar: page console output, script errors, promise rejections, resource failures, long tasks, failed network requests and instrumented devtools protocol events. */
1116
- export const timelinesources: timelinesource[] = [
1117
- "console",
1118
- "error",
1119
- "rejection",
1120
- "resource",
1121
- "longtask",
1122
- "network",
1123
- "cdp",
1124
- ];
1125
-
1126
- /** Ranks one level inside the reviewed level set; lower ranks are more severe. */
1127
- export function levelrank(level: loglevel): number {
1128
- return loglevels.indexOf(level);
1129
- }
1130
-
1131
- /** Redacts reviewed secret patterns from console text before any entry leaves the page bridge; matched patterns never survive into a stored entry. */
1132
- export function redactconsoletext(text: string, patterns: string[]): string {
1133
- let redacted = text;
1134
- for (const pattern of patterns) {
1135
- if (!pattern) continue;
1136
- while (redacted.includes(pattern)) redacted = redacted.replace(pattern, "[redacted]");
1137
- }
1138
- return redacted;
1139
- }
1140
-
1141
- /** Classifies the kind tag of one console argument for the consoleentry argument kinds. */
1142
- export function argkind(value: unknown): string {
1143
- if (value === null) return "null";
1144
- if (Array.isArray(value)) return "array";
1145
- if (value instanceof Error) return "error";
1146
- switch (typeof value) {
1147
- case "string":
1148
- return "string";
1149
- case "number":
1150
- return "number";
1151
- case "boolean":
1152
- return "boolean";
1153
- case "bigint":
1154
- return "bigint";
1155
- case "symbol":
1156
- return "symbol";
1157
- case "function":
1158
- return "function";
1159
- case "undefined":
1160
- return "undefined";
1161
- default:
1162
- return "object";
1163
- }
1164
- }
1165
-
1166
- /** Serializes one console argument through the reviewed depth bound; deeper objects collapse to their constructor tag so the bound stays a user choice with no code ceiling. */
1167
- export function serializearg(value: unknown, depth: number): string {
1168
- const render = (item: unknown, remaining: number): string => {
1169
- if (item instanceof Error) return `${item.name}: ${item.message}`;
1170
- if (typeof item === "string") return item;
1171
- if (typeof item === "function") return `[function ${item.name || "anonymous"}]`;
1172
- if (typeof item === "bigint") return `${item}n`;
1173
- if (typeof item === "symbol") return item.toString();
1174
- if (item === null || item === undefined || typeof item !== "object") return String(item);
1175
- if (remaining <= 0) {
1176
- const tag = Array.isArray(item)
1177
- ? "Array"
1178
- : ((item as { constructor?: { name?: string } }).constructor?.name ?? "Object");
1179
- return `[${tag}]`;
1180
- }
1181
- if (Array.isArray(item)) return `[${item.map((entry) => render(entry, remaining - 1)).join(", ")}]`;
1182
- const record = item as Record<string, unknown>;
1183
- return `{${Object.keys(record)
1184
- .map((key) => `${key}: ${render(record[key], remaining - 1)}`)
1185
- .join(", ")}}`;
1186
- };
1187
- return render(value, Math.max(0, depth));
1188
- }
1189
-
1190
- /** Builds one captured console entry from a forwarded console call: the level, the redacted text of every serialized argument and the argument kinds, with redaction applied before the entry exists. */
1191
- export function consolecapture(input: {
1192
- level: loglevel;
1193
- args: unknown[];
1194
- depth: number;
1195
- redact: string[];
1196
- }): consoleentry {
1197
- const parts = input.args.map((arg) => serializearg(arg, input.depth));
1198
- return {
1199
- level: input.level,
1200
- text: redactconsoletext(parts.join(" "), input.redact),
1201
- argkinds: input.args.map((arg) => argkind(arg)),
1202
- repeat: 1,
1203
- };
1204
- }
1205
-
1206
- /** Parses one stack trace text into stack frames with function names, urls, lines and columns; unparseable lines are skipped instead of crashing capture. */
1207
- export function stackframes(stacktext: string): stackframe[] {
1208
- const frames: stackframe[] = [];
1209
- for (const row of stacktext.split("\n")) {
1210
- const trimmed = row.trim();
1211
- if (!trimmed.startsWith("at ")) continue;
1212
- const body = trimmed.slice(3).trim();
1213
- const location = body.match(/\(([^()]*:\d+:\d+)\)$/) ?? body.match(/^(.*:\d+:\d+)$/);
1214
- const located = location?.[1];
1215
- if (!located) continue;
1216
- const segments = located.split(":");
1217
- const column = Number.parseInt(segments.pop() ?? "", 10);
1218
- const lineno = Number.parseInt(segments.pop() ?? "", 10);
1219
- const url = segments.join(":");
1220
- if (!Number.isFinite(lineno) || lineno < 0) continue;
1221
- const name = body.endsWith(`(${located})`) ? body.slice(0, body.length - located.length - 2).trim() : "";
1222
- frames.push({
1223
- ...(name ? { functionname: name } : {}),
1224
- url,
1225
- line: lineno,
1226
- ...(Number.isFinite(column) ? { column } : {}),
1227
- });
1228
- }
1229
- return frames;
1230
- }
1231
-
1232
- /** Builds one captured javascript error record body from an error event: the redacted message, the parsed stack frames, the source url and the line. */
1233
- export function errorcapture(input: {
1234
- message: string;
1235
- sourceurl: string;
1236
- line: number;
1237
- stacktext?: string;
1238
- redact: string[];
1239
- }): Pick<errorrecord, "message" | "frames" | "sourceurl" | "line"> {
1240
- return {
1241
- message: redactconsoletext(input.message, input.redact),
1242
- frames: input.stacktext !== undefined ? stackframes(input.stacktext) : [],
1243
- sourceurl: input.sourceurl,
1244
- line: input.line,
1245
- };
1246
- }
1247
-
1248
- /** Builds one captured unhandled rejection record body from the rejection reason text and its stack frames, with the reason redacted before capture. */
1249
- export function rejectioncapture(input: {
1250
- reason: string;
1251
- stacktext?: string;
1252
- redact: string[];
1253
- }): Pick<rejectionrecord, "reason" | "frames"> {
1254
- return {
1255
- reason: redactconsoletext(input.reason, input.redact),
1256
- frames: input.stacktext !== undefined ? stackframes(input.stacktext) : [],
1257
- };
1258
- }
1259
-
1260
- /** Builds the long task entry bodies of one watch window from performance longtask entries with their attribution names; the reviewed threshold filters entries below the user configured duration. */
1261
- export function longtaskcapture(input: {
1262
- entries: Array<{ starttime: number; duration: number; attributions: string[] }>;
1263
- threshold: number;
1264
- }): Array<Pick<longtaskentry, "duration" | "starttime" | "attributions">> {
1265
- return input.entries
1266
- .filter((entry) => entry.duration >= input.threshold)
1267
- .map((entry) => ({
1268
- duration: Math.round(entry.duration),
1269
- starttime: Math.round(entry.starttime),
1270
- attributions: [...entry.attributions],
1271
- }));
1272
- }
1273
-
1274
- /** One run timeline bound to its run and step ids. */
1275
- export interface timeline {
1276
- runid: string;
1277
- origin: string;
1278
- stepids: string[];
1279
- attachedat: number;
1280
- entries: timelineentry[];
1281
- }
1282
-
1283
- /** Binds one timeline to the run and its step ids; capture stays scoped to that run and every entry carries the run correlation id of its step. */
1284
- export function attachtimeline(input: { runid: string; origin: string; stepids: string[]; now: number }): timeline {
1285
- return { runid: input.runid, origin: input.origin, stepids: [...input.stepids], attachedat: input.now, entries: [] };
1286
- }
1287
-
1288
- /** Applies the reviewed level set to timeline entries: per step level floors drop entries more verbose than the floor and source filters drop entries from unreviewed sources. */
1289
- export function filterentries(entries: timelineentry[], levelset: loglevelset): timelineentry[] {
1290
- return entries.filter((entry) => {
1291
- const floor = levelset.floors?.[entry.stepid] ?? levelset.floors?.["*"];
1292
- if (floor !== undefined && levelrank(entry.level) > levelrank(floor)) return false;
1293
- if (levelset.sources !== undefined && levelset.sources.length > 0 && !levelset.sources.includes(entry.source))
1294
- return false;
1295
- return true;
1296
- });
1297
- }
1298
-
1299
- /** Collapses repeated identical messages inside the reviewed spam window into counts and flags the patterns that exceed the reviewed collapse threshold. */
1300
- export function spamdetect(
1301
- entries: timelineentry[],
1302
- rule: spamrule,
1303
- ): { entries: Array<timelineentry & { repeat: number }>; flagged: Array<{ message: string; count: number }> } {
1304
- const collapsed: Array<timelineentry & { repeat: number }> = [];
1305
- const counts = new Map<string, number>();
1306
- for (const entry of entries) {
1307
- if (rule.pattern !== "" && !entry.message.includes(rule.pattern)) {
1308
- collapsed.push({ ...entry, repeat: 1 });
1309
- continue;
1310
- }
1311
- const key = `${entry.level}|${entry.source}|${entry.message}`;
1312
- const previous = collapsed[collapsed.length - 1];
1313
- if (
1314
- previous &&
1315
- previous.repeat !== undefined &&
1316
- `${previous.level}|${previous.source}|${previous.message}` === key &&
1317
- entry.time - previous.time <= rule.windowsize
1318
- ) {
1319
- previous.repeat += 1;
1320
- continue;
1321
- }
1322
- collapsed.push({ ...entry, repeat: 1 });
1323
- }
1324
- for (const entry of collapsed) {
1325
- if (entry.repeat > 1) counts.set(`${entry.level}|${entry.source}|${entry.message}`, entry.repeat);
1326
- }
1327
- const flagged = [...counts.entries()]
1328
- .filter(([, count]) => count > rule.collapse)
1329
- .map(([key, count]) => ({ message: key.split("|").slice(2).join("|"), count }));
1330
- return { entries: collapsed, flagged };
1331
- }
1332
-
1333
- /** Rotates the timeline of one run: the newest entries stay inside the reviewed max entries window while every overflow entry moves to the rotation target store without data loss. */
1334
- export function rotatelogs(
1335
- entries: timelineentry[],
1336
- rule: rotationrule,
1337
- ): { kept: timelineentry[]; overflow: timelineentry[] } {
1338
- if (entries.length <= rule.maxentries) return { kept: [...entries], overflow: [] };
1339
- const kept = entries.slice(entries.length - rule.maxentries);
1340
- const overflow = entries.slice(0, entries.length - rule.maxentries);
1341
- return { kept, overflow };
1342
- }
1343
-
1344
- /** Counts the timeline entries per level for the response envelope timeline block. */
1345
- export function timelinecounts(entries: timelineentry[]): Record<string, number> {
1346
- const counts: Record<string, number> = {};
1347
- for (const level of loglevels) counts[level] = 0;
1348
- for (const entry of entries) counts[entry.level] = (counts[entry.level] ?? 0) + 1;
1349
- return counts;
1350
- }
1351
-
1352
- /** Sums the blocking duration of long task entries inside one step window; the window stays a reviewed value with no code ceiling. */
1353
- export function blockingduration(
1354
- tasks: Array<{ starttime: number; duration: number }>,
1355
- stepid: string,
1356
- window: { startedat: number; endedat: number },
1357
- ): { stepid: string; blocking: number; tasks: number } {
1358
- const inside = tasks.filter((task) => task.starttime >= window.startedat && task.starttime <= window.endedat);
1359
- return { stepid, blocking: inside.reduce((total, task) => total + task.duration, 0), tasks: inside.length };
1360
- }
1361
-
1362
- /** Marks one failed request of the run in the timeline from its observed exchange: the url, status, error class and correlation id. */
1363
- export function netfailureentryof(input: { id: string; exchange: exchangerecord; at: number }): netfailureentry | null {
1364
- const exchange = input.exchange;
1365
- if (exchange.errorclass === undefined && exchange.status < 400) return null;
1366
- return {
1367
- id: input.id,
1368
- runid: exchange.runid,
1369
- stepid: exchange.stepid,
1370
- url: exchange.url,
1371
- status: exchange.status,
1372
- errorclass: exchange.errorclass ?? "httperror",
1373
- correlationid: exchange.correlationid,
1374
- at: input.at,
1375
- };
1376
- }
1377
-
1378
- /** Decides whether a watcher detached early because the run tab navigated inside its watch window: navigation timestamps inside the window detach the watcher at the navigation time. */
1379
- export function watcherdetached(input: { startedat: number; lifetime: number; navigations: number[] }): {
1380
- detached: boolean;
1381
- at?: number;
1382
- } {
1383
- for (const navigation of input.navigations) {
1384
- if (navigation >= input.startedat && navigation <= input.startedat + input.lifetime)
1385
- return { detached: true, at: navigation };
1386
- }
1387
- return { detached: false };
1388
- }
1389
-
1390
- /** Compares the console output of two runs and classifies every line as added, removed or repeated; repeated lines carry their repeat counts. */
1391
- export function consolediff(input: {
1392
- baseid: string;
1393
- targetid: string;
1394
- baselines: string[];
1395
- targetlines: string[];
1396
- now: number;
1397
- }): consolediff {
1398
- const base = input.baselines;
1399
- const target = input.targetlines;
1400
- const basemap = new Map<string, number>();
1401
- for (const line of base) basemap.set(line, (basemap.get(line) ?? 0) + 1);
1402
- const targetmap = new Map<string, number>();
1403
- for (const line of target) targetmap.set(line, (targetmap.get(line) ?? 0) + 1);
1404
- const lines = [];
1405
- const added: string[] = [];
1406
- const removed: string[] = [];
1407
- const repeated: string[] = [];
1408
- for (const [line, count] of targetmap) {
1409
- const basecount = basemap.get(line) ?? 0;
1410
- if (basecount === 0) {
1411
- for (let index = 0; index < count; index += 1) {
1412
- lines.push({ kind: "added" as const, text: line });
1413
- added.push(line);
1414
- }
1415
- continue;
1416
- }
1417
- const share = Math.min(basecount, count);
1418
- for (let index = 0; index < share; index += 1) {
1419
- lines.push({ kind: "repeated" as const, text: line, count: share });
1420
- repeated.push(line);
1421
- }
1422
- for (let index = share; index < count; index += 1) {
1423
- lines.push({ kind: "added" as const, text: line });
1424
- added.push(line);
1425
- }
1426
- }
1427
- for (const [line, count] of basemap) {
1428
- const targetcount = targetmap.get(line) ?? 0;
1429
- const missing = Math.max(0, count - targetcount);
1430
- for (let index = 0; index < missing; index += 1) {
1431
- lines.push({ kind: "removed" as const, text: line });
1432
- removed.push(line);
1433
- }
1434
- }
1435
- return {
1436
- base: input.baseid,
1437
- target: input.targetid,
1438
- lines,
1439
- added: added.length,
1440
- removed: removed.length,
1441
- repeated: repeated.length,
1442
- at: input.now,
1443
- };
1444
- }
1445
-
1446
- /* ── Merged from runbudget.ts ── */
1447
-
1448
- import type { budgetalert, runbudgetrecord, timeoutcancelevent, timeoutcancelpolicy } from "./types.js";
1449
- import { randomid } from "./memory.js";
1450
-
1451
- /**
1452
- * Runbudget logic of the 1.1.69 family.
1453
- * Every run tracks its step usage against the user chosen step budget and its memory pressure through the worker telemetry, the budgetalerts fire at the user chosen thresholds with their severity levels while the critical threshold pauses the run pending a user choice instead of refusing it, and the timeoutcancel aborts a slow step at the user chosen bound with the cancel event recorded in the immutable log beside the step outcome.
1454
- * Every budget, threshold and bound stays a user choice: the tracker measures and never caps, and the pause waits for the user instead of dropping work.
1455
- */
1456
-
1457
- /** Builds one runbudget record of a run: the step usage against the user chosen step budget beside the memory pressure the worker telemetry reported. */
1458
- export function runbudgetof(input: {
1459
- runid: string;
1460
- steps: number;
1461
- stepbudget?: number;
1462
- pressure: number;
1463
- memorybudget?: number;
1464
- now: number;
1465
- }): runbudgetrecord {
1466
- if (input.runid.trim() === "") throw new Error("The runbudget record needs its run id.");
1467
- const pressure = Number.isFinite(input.pressure) ? Math.min(Math.max(input.pressure, 0), 1) : 0;
1468
- return {
1469
- runid: input.runid,
1470
- steps: Math.max(input.steps, 0),
1471
- ...(input.stepbudget !== undefined ? { stepbudget: input.stepbudget } : {}),
1472
- pressure,
1473
- ...(input.memorybudget !== undefined ? { memorybudget: input.memorybudget } : {}),
1474
- at: input.now,
1475
- };
1476
- }
1477
-
1478
- /** Reads the budgetalerts of one runbudget record: the warning threshold fires its warning while the critical threshold fires its critical alert that pauses the run pending a user choice; an absent threshold keeps the alert informational only. */
1479
- export function budgetalertstate(input: {
1480
- budget: runbudgetrecord;
1481
- thresholds?: { warning?: number; critical?: number };
1482
- }): budgetalert[] {
1483
- const alerts: budgetalert[] = [];
1484
- const stepsbudget = input.budget.stepbudget;
1485
- if (stepsbudget !== undefined && stepsbudget > 0) {
1486
- const ratio = input.budget.steps / stepsbudget;
1487
- const alert = budgetalertof({
1488
- runid: input.budget.runid,
1489
- kind: "steps",
1490
- ratio,
1491
- ...(input.thresholds !== undefined ? { thresholds: input.thresholds } : {}),
1492
- at: input.budget.at,
1493
- });
1494
- if (alert !== undefined) alerts.push(alert);
1495
- }
1496
- const memorybudget = input.budget.memorybudget;
1497
- if (memorybudget !== undefined && memorybudget > 0) {
1498
- const ratio = input.budget.pressure / memorybudget;
1499
- const alert = budgetalertof({
1500
- runid: input.budget.runid,
1501
- kind: "memory",
1502
- ratio,
1503
- ...(input.thresholds !== undefined ? { thresholds: input.thresholds } : {}),
1504
- at: input.budget.at,
1505
- });
1506
- if (alert !== undefined) alerts.push(alert);
1507
- }
1508
- return alerts;
1509
- }
1510
-
1511
- /** Reads the single alert of one budget ratio at its reached severity level; the critical level pauses the run pending a user choice. */
1512
- function budgetalertof(input: {
1513
- runid: string;
1514
- kind: "steps" | "memory";
1515
- ratio: number;
1516
- thresholds?: { warning?: number; critical?: number };
1517
- at: number;
1518
- }): budgetalert | undefined {
1519
- const critical = input.thresholds?.critical;
1520
- const warning = input.thresholds?.warning;
1521
- if (critical !== undefined && critical > 0 && input.ratio >= critical)
1522
- return { runid: input.runid, level: "critical", kind: input.kind, ratio: input.ratio, paused: true, at: input.at };
1523
- if (warning !== undefined && warning > 0 && input.ratio >= warning)
1524
- return { runid: input.runid, level: "warning", kind: input.kind, ratio: input.ratio, paused: false, at: input.at };
1525
- return undefined;
1526
- }
1527
-
1528
- /** Reads whether one run pauses for its budgetalerts: only a critical alert pauses the run pending a user choice while every warning stays informational. */
1529
- export function runpausesforalerts(alerts: budgetalert[]): boolean {
1530
- return alerts.some((alert) => alert.paused);
1531
- }
1532
-
1533
- /** Reads the timeoutcancel decision of one step: the step aborts at the user chosen bound while an absent bound never aborts a step because the bound stays a user choice. */
1534
- export function timeoutcanceldecision(input: { elapsed: number; bound?: number }): { abort: boolean; reason: string } {
1535
- if (input.bound === undefined)
1536
- return {
1537
- abort: false,
1538
- reason:
1539
- "The user set no timeout bound and the engine sets none; the step runs to its end inside the reviewed plan.",
1540
- };
1541
- if (!Number.isFinite(input.elapsed))
1542
- return {
1543
- abort: false,
1544
- reason: "The step reported no finite elapsed time; the executor never aborts on a telemetry gap.",
1545
- };
1546
- if (input.elapsed <= input.bound)
1547
- return {
1548
- abort: false,
1549
- reason: `The step ran ${input.elapsed} milliseconds inside the user timeout bound of ${input.bound} milliseconds.`,
1550
- };
1551
- return {
1552
- abort: true,
1553
- reason: `The step ran ${input.elapsed} milliseconds past the user timeout bound of ${input.bound} milliseconds; the executor aborts it and records the cancel event beside the step outcome.`,
1554
- };
1555
- }
1556
-
1557
- /** Builds one timeoutcancel event: the aborted step, its bound, its elapsed time and the immutable log entry the cancel wrote beside the step outcome. */
1558
- export function timeoutcanceleventof(input: {
1559
- runid: string;
1560
- stepid: string;
1561
- bound: number;
1562
- elapsed: number;
1563
- now: number;
1564
- logged: boolean;
1565
- }): timeoutcancelevent {
1566
- if (input.runid.trim() === "") throw new Error("The timeoutcancel event needs its run id.");
1567
- if (input.stepid.trim() === "") throw new Error("The timeoutcancel event needs its step id.");
1568
- return {
1569
- id: randomid(),
1570
- runid: input.runid,
1571
- stepid: input.stepid,
1572
- bound: Math.max(input.bound, 0),
1573
- elapsed: Math.max(input.elapsed, 0),
1574
- at: input.now,
1575
- logged: input.logged,
1576
- };
1577
- }
1578
-
1579
- /** Reads the timeoutcancel policies of one plan: one policy per step carrying the user chosen bound, and an absent bound keeps every policy unbounded. */
1580
- export function timeoutcancelpolicies(input: { steps: string[]; bound?: number }): timeoutcancelpolicy[] {
1581
- return input.steps.map((stepid) => ({ stepid, ...(input.bound !== undefined ? { bound: input.bound } : {}) }));
1582
- }
1583
-
1584
- /* ── Merged from runresume.ts ── */
1585
-
1586
- import type { navdedupeentry, resumepoint, runcacheentry, stepprefetchhint } from "./types.js";
1587
-
1588
- /**
1589
- * Runresume logic of the 1.1.69 family.
1590
- * The efficientresume checkpoints every step boundary with its cursor and its page digest, skips the completed steps after a restart and revalidates the page fingerprint before it resumes, the navdedupe skips navigations to the already active url and folds repeated planned visits into one navigation, the runcache stores fetched resources per run keyed by their digest, serves the repeat fetches of the same run and clears at the run end unless the user pins the profile cache, and the stepprefetch derives its likely next pages and likely next selectors from the plan structure alone.
1591
- * The resume, the dedupe and the prefetch optimize the run only: they never skip a review and never fetch a page the reviewed plan never named.
1592
- */
1593
-
1594
- /** Builds one efficientresume checkpoint: the step boundary with its cursor and the digest of the page fingerprint the resume revalidates. */
1595
- export function resumepointof(input: {
1596
- runid: string;
1597
- stepid: string;
1598
- cursor: number;
1599
- digest: string;
1600
- now: number;
1601
- }): resumepoint {
1602
- if (input.runid.trim() === "") throw new Error("The resume checkpoint needs its run id.");
1603
- if (input.stepid.trim() === "") throw new Error("The resume checkpoint needs its step id.");
1604
- if (input.digest.trim() === "")
1605
- throw new Error("The resume checkpoint needs its page digest; a digestless resume never revalidates the page.");
1606
- return {
1607
- runid: input.runid,
1608
- stepid: input.stepid,
1609
- cursor: Math.max(input.cursor, 0),
1610
- digest: input.digest,
1611
- at: input.now,
1612
- };
1613
- }
1614
-
1615
- /** Reads the skip plan after a restart: the steps with a checkpoint skip while the resume continues at the first step without one. */
1616
- export function skipcompletedsteps(input: { checkpoints: resumepoint[]; steps: string[] }): {
1617
- skip: string[];
1618
- resumeat?: string;
1619
- } {
1620
- const done = new Set(input.checkpoints.map((checkpoint) => checkpoint.stepid));
1621
- const skip = input.steps.filter((step) => done.has(step));
1622
- const resumeat = input.steps.find((step) => !done.has(step));
1623
- return { skip, ...(resumeat !== undefined ? { resumeat } : {}) };
1624
- }
1625
-
1626
- /** Revalidates the page fingerprint before one resume: a matching digest resumes while a changed fingerprint refuses so the resume never continues against a different page. */
1627
- export function revalidatefingerprint(input: { checkpoint: resumepoint; fingerprint: string }): {
1628
- ok: boolean;
1629
- reason: string;
1630
- } {
1631
- if (input.checkpoint.digest === input.fingerprint)
1632
- return {
1633
- ok: true,
1634
- reason: `The page fingerprint matches the checkpoint digest of the step ${input.checkpoint.stepid}; the resume continues at cursor ${input.checkpoint.cursor}.`,
1635
- };
1636
- return {
1637
- ok: false,
1638
- reason: `The page fingerprint changed since the checkpoint of the step ${input.checkpoint.stepid}; the resume refuses so the run never continues against a different page.`,
1639
- };
1640
- }
1641
-
1642
- /** Reads the navdedupe verdict of one planned navigation: a navigation to the already active url skips while every other navigation runs. */
1643
- export function navdedupeverdict(input: { url: string; activeurl: string }): { skip: boolean; reason: string } {
1644
- if (input.url.trim() === "")
1645
- return { skip: false, reason: "The planned navigation carries no url; the navigation runs as reviewed." };
1646
- if (input.url === input.activeurl)
1647
- return {
1648
- skip: true,
1649
- reason: `The planned navigation to ${input.url} targets the already active url; the navdedupe skips the duplicate navigation while the step outcome records the skip.`,
1650
- };
1651
- return {
1652
- skip: false,
1653
- reason: `The planned navigation to ${input.url} targets a new url; the navigation runs as reviewed.`,
1654
- };
1655
- }
1656
-
1657
- /** Folds repeated planned visits into one navigation per url: every step that plans the same url merges into one navdedupe entry. */
1658
- export function foldplannedvisits(steps: Array<{ id: string; url?: string }>): navdedupeentry[] {
1659
- const folded = new Map<string, navdedupeentry>();
1660
- for (const step of steps) {
1661
- if (step.url === undefined || step.url.trim() === "") continue;
1662
- const entry = folded.get(step.url);
1663
- if (entry === undefined) {
1664
- folded.set(step.url, { url: step.url, stepids: [step.id] });
1665
- continue;
1666
- }
1667
- entry.stepids.push(step.id);
1668
- }
1669
- return [...folded.values()];
1670
- }
1671
-
1672
- /** Reads the digest key of one fetched resource within one run so the runcache stores and serves it by digest. */
1673
- export function runcachedigest(input: { runid: string; resource: string }): string {
1674
- if (input.runid.trim() === "") throw new Error("The runcache digest needs its run id.");
1675
- if (input.resource.trim() === "") throw new Error("The runcache digest needs its resource.");
1676
- return `${input.runid}:${input.resource}`;
1677
- }
1678
-
1679
- /** Serves one repeat fetch from the runcache: a stored entry of the same run and the same resource answers the fetch while no stored entry keeps the fetch running. */
1680
- export function runcachehit(
1681
- entries: runcacheentry[],
1682
- input: { runid: string; resource: string },
1683
- ): runcacheentry | undefined {
1684
- return entries.find((entry) => entry.runid === input.runid && entry.resource === input.resource);
1685
- }
1686
-
1687
- /** Reads the runcache sweep of one run end: the entries clear at the run end while the pinned entries of the user profile cache survive. */
1688
- export function runcachesweep(
1689
- entries: runcacheentry[],
1690
- input: { runid: string; pin: boolean },
1691
- ): { kept: runcacheentry[]; cleared: number } {
1692
- if (input.pin) return { kept: [...entries], cleared: 0 };
1693
- const kept = entries.filter((entry) => entry.runid !== input.runid || entry.pinned);
1694
- return { kept, cleared: entries.length - kept.length };
1695
- }
1696
-
1697
- /** Derives the stepprefetch hints from the plan structure alone: every step names its likely next page from the next planned navigation and its likely next selectors from the targets of the steps that follow, so the warming never reaches a page the reviewed plan never named. */
1698
- export function stepprefetchhints(plan: {
1699
- steps: Array<{ id: string; kind: string; value?: string; target?: string }>;
1700
- }): stepprefetchhint[] {
1701
- return plan.steps.map((step, index) => {
1702
- const rest = plan.steps.slice(index + 1);
1703
- const nav = rest.find(
1704
- (candidate) =>
1705
- ["navigate", "openlink", "followlink"].includes(candidate.kind) &&
1706
- candidate.value !== undefined &&
1707
- candidate.value.trim() !== "",
1708
- );
1709
- const selectors = rest
1710
- .map((candidate) => candidate.target)
1711
- .filter((target): target is string => target !== undefined && target.trim() !== "");
1712
- return {
1713
- stepid: step.id,
1714
- ...(nav?.value !== undefined ? { page: nav.value } : {}),
1715
- selectors: [...new Set(selectors)].slice(0, 5),
1716
- };
1717
- });
1718
- }
1719
-
1720
- /* ── Merged from runreplay.ts ── */
1721
-
1722
- import type { immutablelogentry, runreplaysession, runreplaystep } from "./types.js";
1723
-
1724
- /**
1725
- * Runreplay logic of the 1.1.66 family.
1726
- * The audit walk of one sealed run lives here: the replay opens only a sealed run whose chain verified, derives one replay step per recorded log entry with its gate resolutions and its restored observation and capture refs, and the viewer moves forward, backward and to any chosen step while every viewer action records in the audit trail.
1727
- * The replay never re-executes anything: it reads the immutable log and the stored captures only, so a replay of a run never touches the page.
1728
- */
1729
-
1730
- /** Derives the observation version and the capture id of one log entry from its summary provenance when the entry carries them; an entry without refs restores its text alone. */
1731
- function restoredrefsof(entry: immutablelogentry): { observationversion?: number; captureid?: string } {
1732
- const observation = /observation (\d+)/i.exec(entry.summary);
1733
- const capture = /capture ([a-z0-9-]+)/i.exec(entry.summary);
1734
- return {
1735
- ...(observation !== null ? { observationversion: Number(observation[1]) } : {}),
1736
- ...(capture !== null ? { captureid: capture[1] } : {}),
1737
- };
1738
- }
1739
-
1740
- /** Builds one replay step of a log entry with its gate resolutions: the step annotates the gate the run waited at when the entry carries one. */
1741
- export function replaystepof(
1742
- entry: immutablelogentry,
1743
- index: number,
1744
- gates: Array<{ gateid: string; kind: string; resolution: string; at: number }>,
1745
- ): runreplaystep {
1746
- const refs = restoredrefsof(entry);
1747
- return {
1748
- stepid: entry.stepid ?? entry.id,
1749
- index,
1750
- summary: entry.summary,
1751
- ...(refs.observationversion !== undefined ? { observationversion: refs.observationversion } : {}),
1752
- ...(refs.captureid !== undefined ? { captureid: refs.captureid } : {}),
1753
- gateresolutions: entry.stepid === undefined ? [] : gates.filter((gate) => gate.gateid === entry.stepid),
1754
- };
1755
- }
1756
-
1757
- /** Builds one runreplay session of a sealed run: one replay step per verified log entry in order, the cursor starts before the first step and the viewer actions start empty. */
1758
- export function runreplaysessionof(input: {
1759
- runid: string;
1760
- entries: immutablelogentry[];
1761
- gates?: Array<{ gateid: string; kind: string; resolution: string; at: number }>;
1762
- now: number;
1763
- }): runreplaysession {
1764
- if (input.runid.trim() === "") throw new Error("The runreplay session needs its recorded run id.");
1765
- if (input.entries.length === 0)
1766
- throw new Error("The runreplay session needs at least one verified log entry to walk.");
1767
- const steps = input.entries.map((entry, index) => replaystepof(entry, index, input.gates ?? []));
1768
- return {
1769
- id: `replay:${input.runid}:${input.now}`,
1770
- runid: input.runid,
1771
- cursor: 0,
1772
- playing: false,
1773
- steps,
1774
- openedat: input.now,
1775
- actions: [],
1776
- };
1777
- }
1778
-
1779
- /** Moves the replay cursor one step forward or backward; the cursor stays inside the step list because a replay walks its recorded chain only. */
1780
- export function replaymove(
1781
- session: runreplaysession,
1782
- direction: "forward" | "backward",
1783
- now: number,
1784
- ): runreplaysession {
1785
- const next =
1786
- direction === "forward" ? Math.min(session.steps.length - 1, session.cursor + 1) : Math.max(0, session.cursor - 1);
1787
- const step = session.steps[next];
1788
- return {
1789
- ...session,
1790
- cursor: next,
1791
- playing: false,
1792
- actions: [
1793
- ...session.actions,
1794
- { kind: "step" as const, at: now, ...(step !== undefined ? { stepid: step.stepid } : {}) },
1795
- ],
1796
- };
1797
- }
1798
-
1799
- /** Jumps the replay cursor to one chosen step of the recorded chain; a step outside the chain refuses the jump. */
1800
- export function replayjump(session: runreplaysession, stepid: string, now: number): runreplaysession {
1801
- const index = session.steps.findIndex((step) => step.stepid === stepid);
1802
- if (index === -1) throw new Error(`The runreplay knows no ${stepid} step in the recorded chain of ${session.runid}.`);
1803
- return {
1804
- ...session,
1805
- cursor: index,
1806
- playing: false,
1807
- actions: [...session.actions, { kind: "jump" as const, stepid, at: now }],
1808
- };
1809
- }
1810
-
1811
- /** Toggles the replay play state: playing advances the viewer through the recorded steps while the pause holds the cursor exactly where it stands. */
1812
- export function replayplay(session: runreplaysession, playing: boolean, now: number): runreplaysession {
1813
- return {
1814
- ...session,
1815
- playing,
1816
- actions: [...session.actions, { kind: playing ? ("play" as const) : ("pause" as const), at: now }],
1817
- };
1818
- }
1819
-
1820
- /** Records one viewer action of the replay session for the audit trail: every play, pause, step and jump keeps its time. */
1821
- export function replayviewaction(
1822
- session: runreplaysession,
1823
- action: { kind: "play" | "pause" | "step" | "jump"; stepid?: string; at: number },
1824
- ): runreplaysession {
1825
- return {
1826
- ...session,
1827
- actions: [
1828
- ...session.actions,
1829
- { kind: action.kind, ...(action.stepid !== undefined ? { stepid: action.stepid } : {}), at: action.at },
1830
- ],
1831
- };
1832
- }
1833
-
1834
- /** Reads the restored view of the replayed step: the log summary it stands on beside the observation and the capture the step restored from the stored captures of the run. */
1835
- export function replayrestoredview(step: runreplaystep): {
1836
- stepid: string;
1837
- index: number;
1838
- summary: string;
1839
- observationversion?: number;
1840
- captureid?: string;
1841
- gateresolutions: Array<{ gateid: string; kind: string; resolution: string; at: number }>;
1842
- } {
1843
- return {
1844
- stepid: step.stepid,
1845
- index: step.index,
1846
- summary: step.summary,
1847
- ...(step.observationversion !== undefined ? { observationversion: step.observationversion } : {}),
1848
- ...(step.captureid !== undefined ? { captureid: step.captureid } : {}),
1849
- gateresolutions: step.gateresolutions,
1850
- };
1851
- }
1852
-
1853
- /** Reads the replay cursor of one viewed run the memory keeps per run, so a reopened replay stands where the viewer left it. */
1854
- export function replaycursorof(session: runreplaysession): { runid: string; cursor: number; playing: boolean } {
1855
- return { runid: session.runid, cursor: session.cursor, playing: session.playing };
1856
- }
1857
-
1858
- /* ── Merged from runparallel.ts ── */
1859
-
1860
- import type { readparallelgroup, sessionreusegrant, warmselectorrecord } from "./types.js";
1861
-
1862
- /**
1863
- * Runparallel logic of the 1.1.69 family.
1864
- * The readparallel groups the independent page reads of one run into parallel lanes and joins the lanes before the dependent steps, the warmselectors carry the validated selectors between adjacent steps and revalidate on a fingerprint change only, and the sessionreuse attaches an authenticated profile to a run on its explicit per profile consent while it isolates the cookies per task through separate containers.
1865
- * The parallel lanes, the carried selectors and the reused sessions optimize the run only: every read, every resolution and every attached profile stays inside the reviewed plan and its consent gates.
1866
- */
1867
-
1868
- /** Groups the independent page reads of one run into parallel lanes: every read runs in its own lane while the user chosen lane ceiling keeps the waves bounded, and an absent ceiling keeps every lane open. */
1869
- export function readparallelgroupof(input: {
1870
- runid: string;
1871
- reads: Array<{ stepid: string; origin: string }>;
1872
- lanes?: number;
1873
- }): readparallelgroup {
1874
- if (input.runid.trim() === "") throw new Error("The readparallel group needs its run id.");
1875
- const lanes = input.lanes !== undefined && input.lanes > 0 ? input.reads.slice(0, input.lanes) : [...input.reads];
1876
- return {
1877
- runid: input.runid,
1878
- lanes: lanes.map((read) => ({ stepid: read.stepid, origin: read.origin })),
1879
- joined: false,
1880
- };
1881
- }
1882
-
1883
- /** Joins one readparallel group before its dependent steps: every lane completes before the dependent step runs. */
1884
- export function joinlanes(group: readparallelgroup): readparallelgroup {
1885
- return { ...group, joined: true };
1886
- }
1887
-
1888
- /** Reads the lanes of one readparallel group that still run, so the join waits on the open lanes alone. */
1889
- export function openlanes(group: readparallelgroup): Array<{ stepid: string; origin: string }> {
1890
- return group.lanes.filter((lane) => lane.stepid !== "");
1891
- }
1892
-
1893
- /** Carries one validated selector to the adjacent step: the record holds the resolution and the page fingerprint the resolution stays valid under. */
1894
- export function carrywarmselector(input: {
1895
- selector: string;
1896
- resolution: string;
1897
- fingerprint: string;
1898
- from: string;
1899
- }): warmselectorrecord {
1900
- if (input.selector.trim() === "") throw new Error("The warmselector record needs its selector.");
1901
- if (input.fingerprint.trim() === "")
1902
- throw new Error("The warmselector record needs its page fingerprint; a fingerprintless carry never revalidates.");
1903
- return {
1904
- selector: input.selector,
1905
- resolution: input.resolution,
1906
- fingerprint: input.fingerprint,
1907
- carriedfrom: input.from,
1908
- };
1909
- }
1910
-
1911
- /** Reads whether one carried selector stays valid: the carry holds while the page fingerprint stays unchanged, and only a fingerprint change revalidates the selector. */
1912
- export function warmselectorcarryvalid(input: { record: warmselectorrecord; fingerprint: string }): {
1913
- carry: boolean;
1914
- reason: string;
1915
- } {
1916
- if (input.record.fingerprint === input.fingerprint)
1917
- return {
1918
- carry: true,
1919
- reason: `The page fingerprint stayed unchanged since the step ${input.record.carriedfrom}; the carried resolution of ${input.record.selector} holds and needs no revalidation.`,
1920
- };
1921
- return {
1922
- carry: false,
1923
- reason: `The page fingerprint changed since the step ${input.record.carriedfrom}; the carried resolution of ${input.record.selector} revalidates before the dispatch.`,
1924
- };
1925
- }
1926
-
1927
- /** Builds one sessionreuse grant: the authenticated profile attaches to a run only through its explicit per profile consent prompt. */
1928
- export function sessionreusegrantof(input: {
1929
- profile: string;
1930
- container: string;
1931
- promptid: string;
1932
- consentedat: number;
1933
- runid?: string;
1934
- }): sessionreusegrant {
1935
- if (input.profile.trim() === "") throw new Error("The sessionreuse grant needs its profile.");
1936
- if (input.promptid.trim() === "")
1937
- throw new Error("The sessionreuse grant needs its consent prompt id; a promptless grant attaches no profile.");
1938
- return {
1939
- profile: input.profile,
1940
- container: input.container,
1941
- promptid: input.promptid,
1942
- consentedat: input.consentedat,
1943
- ...(input.runid !== undefined ? { runid: input.runid } : {}),
1944
- };
1945
- }
1946
-
1947
- /** Reads the container name of one task under one profile so the sessionreuse isolates the cookies per task through separate containers. */
1948
- export function sessioncontainerof(input: { profile: string; taskid: string }): string {
1949
- if (input.profile.trim() === "") throw new Error("The session container needs its profile.");
1950
- if (input.taskid.trim() === "") throw new Error("The session container needs its task id.");
1951
- return `${input.profile}:${input.taskid}`;
1952
- }
1953
-
1954
- /* ── Merged from backgroundruns.ts ── */
1955
-
1956
- import type { backgroundqueueentry } from "./types.js";
1957
- import { backgroundrungate } from "./policy.js";
1958
-
1959
- /**
1960
- * Background run queue logic of the 1.1.66 family.
1961
- * The queue that keeps workflows executing with no open surface lives here: each entry holds one reviewed workflow the user queued, the queue hands the entries to the executor one at a time in their queued order, a running entry holds the keepalive signal of 1.1.60 for its whole duration, the interrupted entries of a restart requeue for the recovery, and every state transition feeds the attentionfeed through the statusbadge.
1962
- * The queue never starts an unreviewed workflow: a queued entry needs its approved review state exactly like a foreground run.
1963
- */
1964
-
1965
- /** Builds one background queue entry: the workflow stays queued until the executor picks it while no keepalive signal is held yet. */
1966
- export function backgroundqueueentryof(input: {
1967
- workflowid: string;
1968
- summary: string;
1969
- now: number;
1970
- }): backgroundqueueentry {
1971
- if (input.workflowid.trim() === "") throw new Error("The background queue entry needs its workflow id.");
1972
- return {
1973
- id: `background:${input.workflowid}:${input.now}`,
1974
- workflowid: input.workflowid,
1975
- state: "queued",
1976
- keepaliveheld: false,
1977
- queuedat: input.now,
1978
- summary: input.summary,
1979
- };
1980
- }
1981
-
1982
- /** Enqueues one workflow for a background run: the queue keeps its order and one workflow may wait more than once. */
1983
- export function enqueuebackgroundrun(input: {
1984
- queue: backgroundqueueentry[];
1985
- workflowid: string;
1986
- summary: string;
1987
- now: number;
1988
- }): backgroundqueueentry[] {
1989
- return [
1990
- ...input.queue,
1991
- backgroundqueueentryof({ workflowid: input.workflowid, summary: input.summary, now: input.now }),
1992
- ];
1993
- }
1994
-
1995
- /** Reads the next queued entry of the queue in its queued order; an empty queue returns undefined. */
1996
- export function nextbackgroundrun(queue: backgroundqueueentry[]): backgroundqueueentry | undefined {
1997
- return [...queue].filter((entry) => entry.state === "queued").sort((a, b) => a.queuedat - b.queuedat)[0];
1998
- }
1999
-
2000
- /** Begins one background run: the entry moves to running and holds the keepalive signal for its whole duration behind the background run gate. */
2001
- export function beginbackgroundrun(
2002
- entry: backgroundqueueentry,
2003
- now: number,
2004
- reviewed: boolean,
2005
- ): { entry: backgroundqueueentry; gate: { allowed: boolean; reason: string } } {
2006
- const gate = backgroundrungate({ reviewed, keepaliveheld: true });
2007
- return {
2008
- entry: gate.allowed ? { ...entry, state: "running" as const, keepaliveheld: true, startedat: now } : entry,
2009
- gate: { allowed: gate.allowed, reason: gate.reason ?? "" },
2010
- };
2011
- }
2012
-
2013
- /** Finishes one background run: the entry reaches its terminal state and releases the keepalive hold it carried. */
2014
- export function finishbackgroundrun(
2015
- entry: backgroundqueueentry,
2016
- state: "done" | "failed",
2017
- now: number,
2018
- ): backgroundqueueentry {
2019
- return { ...entry, state, keepaliveheld: false, endedat: now };
2020
- }
2021
-
2022
- /** Reads the restart recovery state of the queue: every running entry of an interrupted worker requeues while the terminal entries keep their state for the audit. */
2023
- export function resumebackgroundqueue(
2024
- queue: backgroundqueueentry[],
2025
- now: number,
2026
- ): { queue: backgroundqueueentry[]; requeued: string[] } {
2027
- const requeued: string[] = [];
2028
- const next = queue.map((entry) => {
2029
- if (entry.state !== "running") return entry;
2030
- requeued.push(entry.id);
2031
- const { startedat, ...rest } = entry;
2032
- void startedat;
2033
- return { ...rest, state: "queued" as const, keepaliveheld: false };
2034
- });
2035
- void now;
2036
- return { queue: next, requeued };
2037
- }
2038
-
2039
- /** Reads the attention descriptor of one background queue entry: a failed entry feeds the attentionfeed while a queued entry defers until the executor turn frees. */
2040
- export function backgroundrunattention(
2041
- entry: backgroundqueueentry,
2042
- origin: string,
2043
- ): { cause: "failure" | "deferral"; runid: string; origin: string; summary: string } | undefined {
2044
- if (entry.state === "failed")
2045
- return {
2046
- cause: "failure",
2047
- runid: entry.id,
2048
- origin,
2049
- summary: `The background run of ${entry.workflowid} failed: ${entry.summary}`,
2050
- };
2051
- if (entry.state === "queued")
2052
- return {
2053
- cause: "deferral",
2054
- runid: entry.id,
2055
- origin,
2056
- summary: `The background run of ${entry.workflowid} waits for its executor turn.`,
2057
- };
2058
- return undefined;
2059
- }
2060
-
2061
- /** Reads the dashboard view of the background queue: one row per entry with its state and its progress by the workflow step count. */
2062
- export function backgroundrunsview(
2063
- queue: backgroundqueueentry[],
2064
- ): Array<{
2065
- id: string;
2066
- workflowid: string;
2067
- state: string;
2068
- keepaliveheld: boolean;
2069
- queuedat: number;
2070
- startedat?: number;
2071
- endedat?: number;
2072
- summary: string;
2073
- progress: string;
2074
- }> {
2075
- return queue.map((entry) => ({
2076
- id: entry.id,
2077
- workflowid: entry.workflowid,
2078
- state: entry.state,
2079
- keepaliveheld: entry.keepaliveheld,
2080
- queuedat: entry.queuedat,
2081
- ...(entry.startedat !== undefined ? { startedat: entry.startedat } : {}),
2082
- ...(entry.endedat !== undefined ? { endedat: entry.endedat } : {}),
2083
- summary: entry.summary,
2084
- progress:
2085
- entry.state === "running"
2086
- ? `Running with the keepalive signal held since ${entry.startedat ?? entry.queuedat}`
2087
- : entry.state === "queued"
2088
- ? "Queued for the next executor turn"
2089
- : entry.state === "done"
2090
- ? "Completed in the background"
2091
- : "Failed; the attentionfeed carries the cause",
2092
- }));
2093
- }
2094
-
2095
- /** Reads the recenttray rows of the finished background runs: the tray grows its background section from the terminal entries of the queue. */
2096
- export function backgroundtrayrows(
2097
- queue: backgroundqueueentry[],
2098
- origin: string,
2099
- ): Array<{
2100
- runid: string;
2101
- origin: string;
2102
- outcome: "running" | "completed" | "halted" | "failed";
2103
- title: string;
2104
- at: number;
2105
- resumable: boolean;
2106
- reopenable: boolean;
2107
- }> {
2108
- return queue
2109
- .filter((entry) => entry.state === "done" || entry.state === "failed")
2110
- .map((entry) => ({
2111
- runid: entry.id,
2112
- origin,
2113
- outcome: entry.state === "done" ? "completed" : "failed",
2114
- title: `Background run of ${entry.workflowid}`,
2115
- at: entry.endedat ?? entry.queuedat,
2116
- resumable: false,
2117
- reopenable: true,
2118
- }));
2119
- }
2120
-
2121
- /** Cancels one queued entry of the background queue: a running entry keeps its executor path and only a queued entry leaves through the queue. */
2122
- export function cancelbackgroundentry(
2123
- queue: backgroundqueueentry[],
2124
- id: string,
2125
- ): { queue: backgroundqueueentry[]; cancelled: string[] } {
2126
- const cancelled: string[] = [];
2127
- const next = queue.filter((entry) => {
2128
- if (entry.id === id && entry.state === "queued") {
2129
- cancelled.push(entry.id);
2130
- return false;
2131
- }
2132
- return true;
2133
- });
2134
- return { queue: next, cancelled };
2135
- }