@paperclipai/server 2026.824.0 → 2026.825.0-beta.1

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 (519) hide show
  1. package/dist/adapters/builtin-adapter-types.d.ts.map +1 -1
  2. package/dist/adapters/builtin-adapter-types.js +1 -0
  3. package/dist/adapters/builtin-adapter-types.js.map +1 -1
  4. package/dist/adapters/plugin-loader.d.ts +1 -0
  5. package/dist/adapters/plugin-loader.d.ts.map +1 -1
  6. package/dist/adapters/plugin-loader.js +12 -1
  7. package/dist/adapters/plugin-loader.js.map +1 -1
  8. package/dist/adapters/plugin-loader.test.d.ts +2 -0
  9. package/dist/adapters/plugin-loader.test.d.ts.map +1 -0
  10. package/dist/adapters/plugin-loader.test.js +37 -0
  11. package/dist/adapters/plugin-loader.test.js.map +1 -0
  12. package/dist/adapters/registry.d.ts.map +1 -1
  13. package/dist/adapters/registry.js +68 -5
  14. package/dist/adapters/registry.js.map +1 -1
  15. package/dist/adapters/registry.test.d.ts +2 -0
  16. package/dist/adapters/registry.test.d.ts.map +1 -0
  17. package/dist/adapters/registry.test.js +35 -0
  18. package/dist/adapters/registry.test.js.map +1 -0
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js +5 -7
  21. package/dist/app.js.map +1 -1
  22. package/dist/auth/better-auth.d.ts +10 -0
  23. package/dist/auth/better-auth.d.ts.map +1 -1
  24. package/dist/auth/better-auth.js +47 -0
  25. package/dist/auth/better-auth.js.map +1 -1
  26. package/dist/auth/workspace-login-handoff-exchange.d.ts +75 -0
  27. package/dist/auth/workspace-login-handoff-exchange.d.ts.map +1 -0
  28. package/dist/auth/workspace-login-handoff-exchange.js +73 -0
  29. package/dist/auth/workspace-login-handoff-exchange.js.map +1 -0
  30. package/dist/auth/workspace-login-handoff-plugin.d.ts +40 -0
  31. package/dist/auth/workspace-login-handoff-plugin.d.ts.map +1 -0
  32. package/dist/auth/workspace-login-handoff-plugin.js +140 -0
  33. package/dist/auth/workspace-login-handoff-plugin.js.map +1 -0
  34. package/dist/auth/workspace-login-handoff.d.ts +189 -0
  35. package/dist/auth/workspace-login-handoff.d.ts.map +1 -0
  36. package/dist/auth/workspace-login-handoff.js +319 -0
  37. package/dist/auth/workspace-login-handoff.js.map +1 -0
  38. package/dist/build-info.json +3 -0
  39. package/dist/config.d.ts +1 -0
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js +14 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/dev-runner-worktree.d.ts.map +1 -1
  44. package/dist/dev-runner-worktree.js +5 -0
  45. package/dist/dev-runner-worktree.js.map +1 -1
  46. package/dist/duplex-aggregate-ceiling-env.d.ts +21 -0
  47. package/dist/duplex-aggregate-ceiling-env.d.ts.map +1 -0
  48. package/dist/duplex-aggregate-ceiling-env.js +23 -0
  49. package/dist/duplex-aggregate-ceiling-env.js.map +1 -0
  50. package/dist/embedded-postgres-supervisor.d.ts +27 -0
  51. package/dist/embedded-postgres-supervisor.d.ts.map +1 -0
  52. package/dist/embedded-postgres-supervisor.js +72 -0
  53. package/dist/embedded-postgres-supervisor.js.map +1 -0
  54. package/dist/errors.d.ts +2 -0
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +6 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +115 -7
  60. package/dist/index.js.map +1 -1
  61. package/dist/instrumentation.d.ts +33 -0
  62. package/dist/instrumentation.d.ts.map +1 -1
  63. package/dist/instrumentation.js +68 -1
  64. package/dist/instrumentation.js.map +1 -1
  65. package/dist/middleware/auth.d.ts.map +1 -1
  66. package/dist/middleware/auth.js +35 -10
  67. package/dist/middleware/auth.js.map +1 -1
  68. package/dist/middleware/error-handler.d.ts.map +1 -1
  69. package/dist/middleware/error-handler.js +8 -1
  70. package/dist/middleware/error-handler.js.map +1 -1
  71. package/dist/middleware/logger.d.ts.map +1 -1
  72. package/dist/middleware/logger.js +5 -2
  73. package/dist/middleware/logger.js.map +1 -1
  74. package/dist/middleware/redact-sensitive.d.ts.map +1 -1
  75. package/dist/middleware/redact-sensitive.js +4 -0
  76. package/dist/middleware/redact-sensitive.js.map +1 -1
  77. package/dist/middleware/validate.d.ts +3 -1
  78. package/dist/middleware/validate.d.ts.map +1 -1
  79. package/dist/middleware/validate.js +37 -0
  80. package/dist/middleware/validate.js.map +1 -1
  81. package/dist/routes/access.d.ts.map +1 -1
  82. package/dist/routes/access.js +20 -0
  83. package/dist/routes/access.js.map +1 -1
  84. package/dist/routes/activity.js +4 -4
  85. package/dist/routes/activity.js.map +1 -1
  86. package/dist/routes/adapter-login-route-spine.d.ts +81 -0
  87. package/dist/routes/adapter-login-route-spine.d.ts.map +1 -0
  88. package/dist/routes/adapter-login-route-spine.js +40 -0
  89. package/dist/routes/adapter-login-route-spine.js.map +1 -0
  90. package/dist/routes/adapters.d.ts +33 -0
  91. package/dist/routes/adapters.d.ts.map +1 -1
  92. package/dist/routes/adapters.js +35 -1
  93. package/dist/routes/adapters.js.map +1 -1
  94. package/dist/routes/adapters.test.d.ts +2 -0
  95. package/dist/routes/adapters.test.d.ts.map +1 -0
  96. package/dist/routes/adapters.test.js +58 -0
  97. package/dist/routes/adapters.test.js.map +1 -0
  98. package/dist/routes/agents.d.ts.map +1 -1
  99. package/dist/routes/agents.js +281 -91
  100. package/dist/routes/agents.js.map +1 -1
  101. package/dist/routes/cases.d.ts +11 -22
  102. package/dist/routes/cases.d.ts.map +1 -1
  103. package/dist/routes/cases.js +13 -13
  104. package/dist/routes/companies.js +3 -3
  105. package/dist/routes/decision-training.js +6 -6
  106. package/dist/routes/decisions.js +4 -4
  107. package/dist/routes/decisions.js.map +1 -1
  108. package/dist/routes/environments.d.ts.map +1 -1
  109. package/dist/routes/environments.js +79 -6
  110. package/dist/routes/environments.js.map +1 -1
  111. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  112. package/dist/routes/execution-workspaces.js +384 -16
  113. package/dist/routes/execution-workspaces.js.map +1 -1
  114. package/dist/routes/file-resources.js +2 -2
  115. package/dist/routes/health.d.ts.map +1 -1
  116. package/dist/routes/health.js +58 -4
  117. package/dist/routes/health.js.map +1 -1
  118. package/dist/routes/instance-settings.d.ts.map +1 -1
  119. package/dist/routes/instance-settings.js +40 -0
  120. package/dist/routes/instance-settings.js.map +1 -1
  121. package/dist/routes/issues.d.ts +10 -0
  122. package/dist/routes/issues.d.ts.map +1 -1
  123. package/dist/routes/issues.js +495 -65
  124. package/dist/routes/issues.js.map +1 -1
  125. package/dist/routes/openapi.d.ts.map +1 -1
  126. package/dist/routes/openapi.js +241 -122
  127. package/dist/routes/openapi.js.map +1 -1
  128. package/dist/routes/pipelines.js +19 -19
  129. package/dist/routes/plugins.d.ts.map +1 -1
  130. package/dist/routes/plugins.js +22 -0
  131. package/dist/routes/plugins.js.map +1 -1
  132. package/dist/routes/projects.d.ts.map +1 -1
  133. package/dist/routes/projects.js +2 -0
  134. package/dist/routes/projects.js.map +1 -1
  135. package/dist/routes/routines.js +2 -2
  136. package/dist/routes/routines.js.map +1 -1
  137. package/dist/routes/secrets.d.ts +1 -1
  138. package/dist/routes/secrets.d.ts.map +1 -1
  139. package/dist/routes/secrets.js +39 -50
  140. package/dist/routes/secrets.js.map +1 -1
  141. package/dist/routes/setup-token-route.test.js +50 -3
  142. package/dist/routes/setup-token-route.test.js.map +1 -1
  143. package/dist/services/adapter-login-lease.d.ts +50 -0
  144. package/dist/services/adapter-login-lease.d.ts.map +1 -0
  145. package/dist/services/adapter-login-lease.js +26 -0
  146. package/dist/services/adapter-login-lease.js.map +1 -0
  147. package/dist/services/adapter-login-lease.test.d.ts +2 -0
  148. package/dist/services/adapter-login-lease.test.d.ts.map +1 -0
  149. package/dist/services/adapter-login-lease.test.js +68 -0
  150. package/dist/services/adapter-login-lease.test.js.map +1 -0
  151. package/dist/services/agent-action-audit.js +1 -1
  152. package/dist/services/agent-secret-bindings.d.ts +15 -0
  153. package/dist/services/agent-secret-bindings.d.ts.map +1 -1
  154. package/dist/services/agent-secret-bindings.js +2 -2
  155. package/dist/services/agent-secret-bindings.js.map +1 -1
  156. package/dist/services/agents.d.ts +13 -13
  157. package/dist/services/agents.d.ts.map +1 -1
  158. package/dist/services/agents.js +46 -5
  159. package/dist/services/agents.js.map +1 -1
  160. package/dist/services/artifact-review-documents.d.ts +34 -0
  161. package/dist/services/artifact-review-documents.d.ts.map +1 -0
  162. package/dist/services/artifact-review-documents.js +196 -0
  163. package/dist/services/artifact-review-documents.js.map +1 -0
  164. package/dist/services/assets.d.ts +1 -1
  165. package/dist/services/codex-device-login-credential-read.d.ts +58 -0
  166. package/dist/services/codex-device-login-credential-read.d.ts.map +1 -0
  167. package/dist/services/codex-device-login-credential-read.js +118 -0
  168. package/dist/services/codex-device-login-credential-read.js.map +1 -0
  169. package/dist/services/codex-device-login-reaper.d.ts.map +1 -1
  170. package/dist/services/codex-device-login-reaper.js +8 -5
  171. package/dist/services/codex-device-login-reaper.js.map +1 -1
  172. package/dist/services/codex-device-login-service.d.ts +127 -36
  173. package/dist/services/codex-device-login-service.d.ts.map +1 -1
  174. package/dist/services/codex-device-login-service.js +204 -84
  175. package/dist/services/codex-device-login-service.js.map +1 -1
  176. package/dist/services/company-portability.d.ts +3 -1
  177. package/dist/services/company-portability.d.ts.map +1 -1
  178. package/dist/services/company-portability.js +76 -20
  179. package/dist/services/company-portability.js.map +1 -1
  180. package/dist/services/company-skill-policy.d.ts +1 -1
  181. package/dist/services/costs.d.ts +1 -1
  182. package/dist/services/decisions.d.ts +6 -6
  183. package/dist/services/duplex-observability-recorder.d.ts +62 -0
  184. package/dist/services/duplex-observability-recorder.d.ts.map +1 -0
  185. package/dist/services/duplex-observability-recorder.js +54 -0
  186. package/dist/services/duplex-observability-recorder.js.map +1 -0
  187. package/dist/services/environment-config.js +5 -5
  188. package/dist/services/environment-config.js.map +1 -1
  189. package/dist/services/environment-custom-image-runtime.d.ts +116 -0
  190. package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
  191. package/dist/services/environment-custom-image-runtime.js +255 -1
  192. package/dist/services/environment-custom-image-runtime.js.map +1 -1
  193. package/dist/services/environment-custom-images.d.ts +39 -0
  194. package/dist/services/environment-custom-images.d.ts.map +1 -1
  195. package/dist/services/environment-custom-images.js +219 -17
  196. package/dist/services/environment-custom-images.js.map +1 -1
  197. package/dist/services/environment-driver-traits.d.ts +109 -0
  198. package/dist/services/environment-driver-traits.d.ts.map +1 -0
  199. package/dist/services/environment-driver-traits.js +102 -0
  200. package/dist/services/environment-driver-traits.js.map +1 -0
  201. package/dist/services/environment-execution-target.d.ts +4 -0
  202. package/dist/services/environment-execution-target.d.ts.map +1 -1
  203. package/dist/services/environment-execution-target.js +63 -7
  204. package/dist/services/environment-execution-target.js.map +1 -1
  205. package/dist/services/environment-run-orchestrator.d.ts +27 -1
  206. package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
  207. package/dist/services/environment-run-orchestrator.js +24 -4
  208. package/dist/services/environment-run-orchestrator.js.map +1 -1
  209. package/dist/services/environment-runtime.d.ts +118 -20
  210. package/dist/services/environment-runtime.d.ts.map +1 -1
  211. package/dist/services/environment-runtime.js +436 -91
  212. package/dist/services/environment-runtime.js.map +1 -1
  213. package/dist/services/environments.d.ts.map +1 -1
  214. package/dist/services/environments.js +20 -2
  215. package/dist/services/environments.js.map +1 -1
  216. package/dist/services/execution-workspace-branch-ownership.d.ts +4 -0
  217. package/dist/services/execution-workspace-branch-ownership.d.ts.map +1 -0
  218. package/dist/services/execution-workspace-branch-ownership.js +9 -0
  219. package/dist/services/execution-workspace-branch-ownership.js.map +1 -0
  220. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  221. package/dist/services/execution-workspace-policy.js +3 -0
  222. package/dist/services/execution-workspace-policy.js.map +1 -1
  223. package/dist/services/execution-workspaces.d.ts +2 -0
  224. package/dist/services/execution-workspaces.d.ts.map +1 -1
  225. package/dist/services/execution-workspaces.js +90 -2
  226. package/dist/services/execution-workspaces.js.map +1 -1
  227. package/dist/services/external-objects.d.ts +19 -19
  228. package/dist/services/finance.d.ts +1 -1
  229. package/dist/services/goals.d.ts +2 -2
  230. package/dist/services/heartbeat-policy.d.ts +3 -0
  231. package/dist/services/heartbeat-policy.d.ts.map +1 -0
  232. package/dist/services/heartbeat-policy.js +10 -0
  233. package/dist/services/heartbeat-policy.js.map +1 -0
  234. package/dist/services/heartbeat-run-runtime-status.test.js +1 -1
  235. package/dist/services/heartbeat-run-runtime-status.test.js.map +1 -1
  236. package/dist/services/heartbeat.d.ts +23 -1
  237. package/dist/services/heartbeat.d.ts.map +1 -1
  238. package/dist/services/heartbeat.js +192 -11
  239. package/dist/services/heartbeat.js.map +1 -1
  240. package/dist/services/index.d.ts +1 -0
  241. package/dist/services/index.d.ts.map +1 -1
  242. package/dist/services/index.js +1 -0
  243. package/dist/services/index.js.map +1 -1
  244. package/dist/services/instance-settings.d.ts.map +1 -1
  245. package/dist/services/instance-settings.js +2 -0
  246. package/dist/services/instance-settings.js.map +1 -1
  247. package/dist/services/issue-dependency-wakeups.d.ts +57 -9
  248. package/dist/services/issue-dependency-wakeups.d.ts.map +1 -1
  249. package/dist/services/issue-dependency-wakeups.js +146 -14
  250. package/dist/services/issue-dependency-wakeups.js.map +1 -1
  251. package/dist/services/issue-dependency-wakeups.test.d.ts +2 -0
  252. package/dist/services/issue-dependency-wakeups.test.d.ts.map +1 -0
  253. package/dist/services/issue-dependency-wakeups.test.js +183 -0
  254. package/dist/services/issue-dependency-wakeups.test.js.map +1 -0
  255. package/dist/services/issue-recovery-actions.d.ts +6 -1
  256. package/dist/services/issue-recovery-actions.d.ts.map +1 -1
  257. package/dist/services/issue-recovery-actions.js +123 -44
  258. package/dist/services/issue-recovery-actions.js.map +1 -1
  259. package/dist/services/issue-thread-interactions.d.ts +8 -0
  260. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  261. package/dist/services/issue-thread-interactions.js +220 -10
  262. package/dist/services/issue-thread-interactions.js.map +1 -1
  263. package/dist/services/issues.d.ts +8 -7
  264. package/dist/services/issues.d.ts.map +1 -1
  265. package/dist/services/issues.js +84 -12
  266. package/dist/services/issues.js.map +1 -1
  267. package/dist/services/local-service-supervisor.d.ts +61 -1
  268. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  269. package/dist/services/local-service-supervisor.js +221 -25
  270. package/dist/services/local-service-supervisor.js.map +1 -1
  271. package/dist/services/login-command.d.ts +46 -0
  272. package/dist/services/login-command.d.ts.map +1 -0
  273. package/dist/services/login-command.js +87 -0
  274. package/dist/services/login-command.js.map +1 -0
  275. package/dist/services/login-command.test.d.ts +2 -0
  276. package/dist/services/login-command.test.d.ts.map +1 -0
  277. package/dist/services/login-command.test.js +71 -0
  278. package/dist/services/login-command.test.js.map +1 -0
  279. package/dist/services/managed-workspace-identity.d.ts +140 -0
  280. package/dist/services/managed-workspace-identity.d.ts.map +1 -0
  281. package/dist/services/managed-workspace-identity.js +277 -0
  282. package/dist/services/managed-workspace-identity.js.map +1 -0
  283. package/dist/services/plugin-environment-driver.d.ts +15 -1
  284. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  285. package/dist/services/plugin-environment-driver.js +23 -1
  286. package/dist/services/plugin-environment-driver.js.map +1 -1
  287. package/dist/services/plugin-host-services.d.ts +3 -2
  288. package/dist/services/plugin-host-services.d.ts.map +1 -1
  289. package/dist/services/plugin-host-services.js +11 -2
  290. package/dist/services/plugin-host-services.js.map +1 -1
  291. package/dist/services/plugin-loader.d.ts.map +1 -1
  292. package/dist/services/plugin-loader.js +39 -8
  293. package/dist/services/plugin-loader.js.map +1 -1
  294. package/dist/services/plugin-managed-agents.d.ts.map +1 -1
  295. package/dist/services/plugin-managed-agents.js +50 -5
  296. package/dist/services/plugin-managed-agents.js.map +1 -1
  297. package/dist/services/plugin-manifest-validator.d.ts.map +1 -1
  298. package/dist/services/plugin-manifest-validator.js +5 -2
  299. package/dist/services/plugin-manifest-validator.js.map +1 -1
  300. package/dist/services/plugin-registry.d.ts +5 -5
  301. package/dist/services/plugin-worker-manager.d.ts +158 -11
  302. package/dist/services/plugin-worker-manager.d.ts.map +1 -1
  303. package/dist/services/plugin-worker-manager.js +1139 -79
  304. package/dist/services/plugin-worker-manager.js.map +1 -1
  305. package/dist/services/projects.d.ts +1 -1
  306. package/dist/services/recovery/disposition-repair.d.ts +24 -0
  307. package/dist/services/recovery/disposition-repair.d.ts.map +1 -0
  308. package/dist/services/recovery/disposition-repair.js +125 -0
  309. package/dist/services/recovery/disposition-repair.js.map +1 -0
  310. package/dist/services/recovery/service.d.ts +6 -6
  311. package/dist/services/recovery/service.d.ts.map +1 -1
  312. package/dist/services/recovery/service.js +856 -384
  313. package/dist/services/recovery/service.js.map +1 -1
  314. package/dist/services/recovery/stranded-notice.js +4 -4
  315. package/dist/services/recovery/stranded-notice.js.map +1 -1
  316. package/dist/services/recovery/stranded-notice.test.js +1 -1
  317. package/dist/services/recovery/stranded-notice.test.js.map +1 -1
  318. package/dist/services/recovery/successful-run-handoff.d.ts +1 -1
  319. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  320. package/dist/services/recovery/successful-run-handoff.js +6 -4
  321. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  322. package/dist/services/recovery/successful-run-handoff.test.js +1 -1
  323. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  324. package/dist/services/recovery-observability.d.ts +1 -1
  325. package/dist/services/recovery-observability.d.ts.map +1 -1
  326. package/dist/services/recovery-observability.js +4 -1
  327. package/dist/services/recovery-observability.js.map +1 -1
  328. package/dist/services/routines.d.ts.map +1 -1
  329. package/dist/services/routines.js +86 -1
  330. package/dist/services/routines.js.map +1 -1
  331. package/dist/services/runtime-exposure/loopback-listener.js +1 -1
  332. package/dist/services/runtime-exposure/loopback-listener.js.map +1 -1
  333. package/dist/services/runtime-exposure/loopback-listener.test.js +1 -1
  334. package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -1
  335. package/dist/services/scripts/codex-auth-read.cjs +93 -0
  336. package/dist/services/secret-proposal-authorization.d.ts +15 -0
  337. package/dist/services/secret-proposal-authorization.d.ts.map +1 -0
  338. package/dist/services/secret-proposal-authorization.js +29 -0
  339. package/dist/services/secret-proposal-authorization.js.map +1 -0
  340. package/dist/services/secret-proposal-notifications.d.ts +18 -0
  341. package/dist/services/secret-proposal-notifications.d.ts.map +1 -0
  342. package/dist/services/secret-proposal-notifications.js +37 -0
  343. package/dist/services/secret-proposal-notifications.js.map +1 -0
  344. package/dist/services/secret-proposals.d.ts +20 -10
  345. package/dist/services/secret-proposals.d.ts.map +1 -1
  346. package/dist/services/secret-proposals.js +205 -14
  347. package/dist/services/secret-proposals.js.map +1 -1
  348. package/dist/services/secrets.d.ts +3 -3
  349. package/dist/services/settings-visibility.d.ts +17 -0
  350. package/dist/services/settings-visibility.d.ts.map +1 -0
  351. package/dist/services/settings-visibility.js +29 -0
  352. package/dist/services/settings-visibility.js.map +1 -0
  353. package/dist/services/setup-token-reaper.d.ts +36 -0
  354. package/dist/services/setup-token-reaper.d.ts.map +1 -0
  355. package/dist/services/setup-token-reaper.js +38 -0
  356. package/dist/services/setup-token-reaper.js.map +1 -0
  357. package/dist/services/setup-token-reaper.test.d.ts +2 -0
  358. package/dist/services/setup-token-reaper.test.d.ts.map +1 -0
  359. package/dist/services/setup-token-reaper.test.js +123 -0
  360. package/dist/services/setup-token-reaper.test.js.map +1 -0
  361. package/dist/services/setup-token-session.d.ts +57 -45
  362. package/dist/services/setup-token-session.d.ts.map +1 -1
  363. package/dist/services/setup-token-session.js +118 -110
  364. package/dist/services/setup-token-session.js.map +1 -1
  365. package/dist/services/setup-token-session.test.js +88 -47
  366. package/dist/services/setup-token-session.test.js.map +1 -1
  367. package/dist/services/setup-token-transport-binding.d.ts +20 -12
  368. package/dist/services/setup-token-transport-binding.d.ts.map +1 -1
  369. package/dist/services/setup-token-transport-binding.js +59 -32
  370. package/dist/services/setup-token-transport-binding.js.map +1 -1
  371. package/dist/services/setup-token-transport-binding.test.js +50 -17
  372. package/dist/services/setup-token-transport-binding.test.js.map +1 -1
  373. package/dist/services/smoke-lab.d.ts +1 -1
  374. package/dist/services/stalled-review-decisions.d.ts +1 -1
  375. package/dist/services/status-cards.d.ts +168 -168
  376. package/dist/services/tool-access-policy.d.ts +2 -2
  377. package/dist/services/tool-gateway.d.ts +1 -1
  378. package/dist/services/work-products.d.ts +19 -0
  379. package/dist/services/work-products.d.ts.map +1 -1
  380. package/dist/services/work-products.js +50 -3
  381. package/dist/services/work-products.js.map +1 -1
  382. package/dist/services/workspace-git-operation-scheduler.test.js +10 -3
  383. package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -1
  384. package/dist/services/workspace-instance-cleanup.d.ts +9 -0
  385. package/dist/services/workspace-instance-cleanup.d.ts.map +1 -1
  386. package/dist/services/workspace-instance-cleanup.js +29 -3
  387. package/dist/services/workspace-instance-cleanup.js.map +1 -1
  388. package/dist/services/workspace-login-handoff-issuer.d.ts +95 -0
  389. package/dist/services/workspace-login-handoff-issuer.d.ts.map +1 -0
  390. package/dist/services/workspace-login-handoff-issuer.js +147 -0
  391. package/dist/services/workspace-login-handoff-issuer.js.map +1 -0
  392. package/dist/services/workspace-operations.d.ts +4 -1
  393. package/dist/services/workspace-operations.d.ts.map +1 -1
  394. package/dist/services/workspace-operations.js +29 -15
  395. package/dist/services/workspace-operations.js.map +1 -1
  396. package/dist/services/workspace-readiness.d.ts +56 -0
  397. package/dist/services/workspace-readiness.d.ts.map +1 -0
  398. package/dist/services/workspace-readiness.js +250 -0
  399. package/dist/services/workspace-readiness.js.map +1 -0
  400. package/dist/services/workspace-realization.d.ts.map +1 -1
  401. package/dist/services/workspace-realization.js +5 -33
  402. package/dist/services/workspace-realization.js.map +1 -1
  403. package/dist/services/workspace-runtime-exposure.test.js +321 -12
  404. package/dist/services/workspace-runtime-exposure.test.js.map +1 -1
  405. package/dist/services/workspace-runtime-leases.d.ts.map +1 -1
  406. package/dist/services/workspace-runtime-leases.js +1 -1
  407. package/dist/services/workspace-runtime-leases.js.map +1 -1
  408. package/dist/services/workspace-runtime-read-model.d.ts +8 -8
  409. package/dist/services/workspace-runtime-read-model.test.js +20 -0
  410. package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
  411. package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
  412. package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
  413. package/dist/services/workspace-runtime.d.ts +77 -4
  414. package/dist/services/workspace-runtime.d.ts.map +1 -1
  415. package/dist/services/workspace-runtime.js +1020 -126
  416. package/dist/services/workspace-runtime.js.map +1 -1
  417. package/dist/worktree-config.d.ts.map +1 -1
  418. package/dist/worktree-config.js +3 -76
  419. package/dist/worktree-config.js.map +1 -1
  420. package/dist/worktree-seed-manifest.d.ts +8 -0
  421. package/dist/worktree-seed-manifest.d.ts.map +1 -0
  422. package/dist/worktree-seed-manifest.js +47 -0
  423. package/dist/worktree-seed-manifest.js.map +1 -0
  424. package/package.json +34 -30
  425. package/skills/paperclip/SKILL.md +2 -0
  426. package/skills/paperclip/references/api-reference.md +44 -2
  427. package/ui-dist/assets/CompanyExport-DnEsO-St.js +4 -0
  428. package/ui-dist/assets/{abnfDiagram-N423BO3Z-BvgoV-u5.js → abnfDiagram-N423BO3Z-2ff8TsQ0.js} +1 -1
  429. package/ui-dist/assets/{arc-BInnUBUU.js → arc-B2AXsH34.js} +1 -1
  430. package/ui-dist/assets/architectureDiagram-T3A2C74G-BDxtKMxk.js +36 -0
  431. package/ui-dist/assets/{blockDiagram-VBNYF7ZC-D9kzAkLN.js → blockDiagram-VBNYF7ZC-BB8T5Mj5.js} +1 -1
  432. package/ui-dist/assets/{c4Diagram-5PPSVZJV-BwzsgV_0.js → c4Diagram-5PPSVZJV-lMo8qk-x.js} +1 -1
  433. package/ui-dist/assets/channel-DCTVZpyq.js +1 -0
  434. package/ui-dist/assets/{chunk-2GRJ4B5K-D2EQ9vrl.js → chunk-2GRJ4B5K-CsS7EB3G.js} +1 -1
  435. package/ui-dist/assets/{chunk-2Q5K7J3B-DOA_8fSd.js → chunk-2Q5K7J3B-DFyQPjoR.js} +1 -1
  436. package/ui-dist/assets/{chunk-5RXB4S5H-CT5ukRiT.js → chunk-5RXB4S5H-No2SHOQR.js} +1 -1
  437. package/ui-dist/assets/{chunk-5VM5RSS4-D2gOlkyB.js → chunk-5VM5RSS4-DdxQO7V9.js} +1 -1
  438. package/ui-dist/assets/{chunk-6Q2QTUOP-DUb6aG8c.js → chunk-6Q2QTUOP-gbHjh-aK.js} +1 -1
  439. package/ui-dist/assets/{chunk-GF5L2VYU-BPabG3Cx.js → chunk-GF5L2VYU-h-BBl6NY.js} +1 -1
  440. package/ui-dist/assets/{chunk-JWPE2WC7-scNfhlZz.js → chunk-JWPE2WC7-Q3pJdTz5.js} +1 -1
  441. package/ui-dist/assets/{chunk-KBJHAD2P-BcRAzRpq.js → chunk-KBJHAD2P-Ba8LGLSx.js} +1 -1
  442. package/ui-dist/assets/{chunk-RYQCIY6F-BiZ0-EGh.js → chunk-RYQCIY6F-9VcvykBo.js} +1 -1
  443. package/ui-dist/assets/{chunk-XXDRQBXY-lJULxFIE.js → chunk-XXDRQBXY-NVbjDH4W.js} +1 -1
  444. package/ui-dist/assets/classDiagram-JCYQIIEL-CzJ88u6h.js +1 -0
  445. package/ui-dist/assets/classDiagram-v2-OCEON4UE-CzJ88u6h.js +1 -0
  446. package/ui-dist/assets/cose-bilkent-JH36ORCC-Cdz4wtUh.js +1 -0
  447. package/ui-dist/assets/{cynefin-VYW2F7L2-B34jJY1x.js → cynefin-VYW2F7L2-MpzJkW5s.js} +1 -1
  448. package/ui-dist/assets/{cynefinDiagram-MW4NZA55-DQ8QK8JK.js → cynefinDiagram-MW4NZA55-_Nx8av7i.js} +1 -1
  449. package/ui-dist/assets/{dagre-VZM6K2ZE-B7lSKgxH.js → dagre-VZM6K2ZE-DcZTQ4Oh.js} +1 -1
  450. package/ui-dist/assets/{diagram-7IWD3JNH-B3hLAFGE.js → diagram-7IWD3JNH-UyLi2mee.js} +1 -1
  451. package/ui-dist/assets/{diagram-B4RE2ZJO-CH9Ro9e0.js → diagram-B4RE2ZJO-B6p0Ivqg.js} +1 -1
  452. package/ui-dist/assets/{diagram-LBJQPF4R-DdPVMii7.js → diagram-LBJQPF4R-C8u6pa97.js} +1 -1
  453. package/ui-dist/assets/{diagram-Q27KOJAE-f5VXswjc.js → diagram-Q27KOJAE-DbwyagW0.js} +1 -1
  454. package/ui-dist/assets/{diagram-UB23O5K3-DGqxixsR.js → diagram-UB23O5K3-Cn5BMEH7.js} +1 -1
  455. package/ui-dist/assets/{ebnfDiagram-BXEA7PRR-C-1klg_w.js → ebnfDiagram-BXEA7PRR-vE0IZma9.js} +1 -1
  456. package/ui-dist/assets/{erDiagram-JOGREHBK-Dt4qK4gv.js → erDiagram-JOGREHBK-3OZVIlVG.js} +1 -1
  457. package/ui-dist/assets/{flowDiagram-UKHOOZJN-9ZlrTuET.js → flowDiagram-UKHOOZJN-C94FiPyr.js} +1 -1
  458. package/ui-dist/assets/ganttDiagram-PKOTCBZU-DMVaPmF-.js +292 -0
  459. package/ui-dist/assets/{gitGraphDiagram-DS77QQ5N-HH2fSgad.js → gitGraphDiagram-DS77QQ5N-yeFtJ689.js} +1 -1
  460. package/ui-dist/assets/{index-Cm6iEYPt.js → index-B4ZF3vY8.js} +288 -224
  461. package/ui-dist/assets/{index-CHXG4GPL.js → index-Bi_vqkcQ.js} +1 -1
  462. package/ui-dist/assets/{index-Bc9g6q63.js → index-BjDGQg9-.js} +1 -1
  463. package/ui-dist/assets/{index-FHzrJ7iQ.js → index-BnpyUMTb.js} +1 -1
  464. package/ui-dist/assets/{index-C1mpWCcn.js → index-BoqQTVMA.js} +1 -1
  465. package/ui-dist/assets/index-Bq-Mr-4x.js +1 -0
  466. package/ui-dist/assets/{index-DceuCrr8.js → index-Brb2U7N4.js} +1 -1
  467. package/ui-dist/assets/{index-DQIrrn2L.js → index-BsBybVwH.js} +1 -1
  468. package/ui-dist/assets/{index-CQhaM6qd.js → index-C2iv4Wgu.js} +1 -1
  469. package/ui-dist/assets/{index-DV2FOgyD.js → index-CKxIu7uO.js} +1 -1
  470. package/ui-dist/assets/{index-DU4U1fuv.js → index-Cb4VGIwp.js} +1 -1
  471. package/ui-dist/assets/{index-B61ORkIb.js → index-ChpszwcO.js} +1 -1
  472. package/ui-dist/assets/{index-DlVBBF8o.js → index-CoQFdT8N.js} +1 -1
  473. package/ui-dist/assets/index-D6P8_f0W.css +1 -0
  474. package/ui-dist/assets/index-DKdmghUG.js +1 -0
  475. package/ui-dist/assets/{index-CVTL3aAL.js → index-DaBci7Bx.js} +1 -1
  476. package/ui-dist/assets/{index-diFs0CNC.js → index-DjYaNiXZ.js} +1 -1
  477. package/ui-dist/assets/{index-Bf7tyGxE.js → index-DxTBJl4h.js} +1 -1
  478. package/ui-dist/assets/{index-DsdVneqe.js → index-Dz3E8nn0.js} +1 -1
  479. package/ui-dist/assets/{index-DvFUXI-Q.js → index-J7dz_hap.js} +1 -1
  480. package/ui-dist/assets/{index-CLu3Tfpv.js → index-JMp2Gpnz.js} +1 -1
  481. package/ui-dist/assets/{index-CcRTJ93J.js → index-Kp6wavQt.js} +1 -1
  482. package/ui-dist/assets/{index-BvFx-4KW.js → index-LBhYZ0L4.js} +1 -1
  483. package/ui-dist/assets/{index-BbAhzCq8.js → index-c3C0P6l8.js} +4 -4
  484. package/ui-dist/assets/{index-BY5ITKmG.js → index-pBEPheTW.js} +1 -1
  485. package/ui-dist/assets/{infoDiagram-6WML65LV-2jai2fw-.js → infoDiagram-6WML65LV-BiL7jKLA.js} +1 -1
  486. package/ui-dist/assets/{ishikawaDiagram-WSZJBQD7-CUmGhmEb.js → ishikawaDiagram-WSZJBQD7-DYcQy41D.js} +1 -1
  487. package/ui-dist/assets/{journeyDiagram-NVQOT4AX-CGQuPDO1.js → journeyDiagram-NVQOT4AX-Bxva6JgA.js} +1 -1
  488. package/ui-dist/assets/{kanban-definition-27J2QSJJ-zgEPcbHl.js → kanban-definition-27J2QSJJ-B2HpN34Q.js} +1 -1
  489. package/ui-dist/assets/{linear-BVf5VtLS.js → linear-Dq8qSESw.js} +1 -1
  490. package/ui-dist/assets/{mermaid.core-D2TFkGfI.js → mermaid.core-Ikay8Cpl.js} +35 -35
  491. package/ui-dist/assets/{mindmap-definition-FAOFIHXS-xoAQ0Lb_.js → mindmap-definition-FAOFIHXS-C-SIdYSY.js} +1 -1
  492. package/ui-dist/assets/{pegDiagram-VL7TDLO6-CBR1PWLr.js → pegDiagram-VL7TDLO6-C7rIJ_13.js} +1 -1
  493. package/ui-dist/assets/{pieDiagram-7S7Q4E2Y-BMPnzK_e.js → pieDiagram-7S7Q4E2Y-3gx0a0ve.js} +1 -1
  494. package/ui-dist/assets/{quadrantDiagram-CIZ2JOQS-CPtVUPkR.js → quadrantDiagram-CIZ2JOQS-BiwM9QGv.js} +1 -1
  495. package/ui-dist/assets/{railroadDiagram-AXF67PYL-DC0lsZV1.js → railroadDiagram-AXF67PYL-rFS8XcFA.js} +1 -1
  496. package/ui-dist/assets/{requirementDiagram-LRYGKXZP-BF6k05SB.js → requirementDiagram-LRYGKXZP-CEAUDUTQ.js} +1 -1
  497. package/ui-dist/assets/{sankeyDiagram-W5VNT64P-CRN0ibiP.js → sankeyDiagram-W5VNT64P-B7anpjlM.js} +1 -1
  498. package/ui-dist/assets/{sequenceDiagram-SI44F4Z6-CQtTiPWm.js → sequenceDiagram-SI44F4Z6-CJEdJvm_.js} +1 -1
  499. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-BdjuGZd1.js → sizeCapture-X5ZJPWSS-CU1Vm6c1.js} +1 -1
  500. package/ui-dist/assets/{stateDiagram-OKZ733FA-B99onxyh.js → stateDiagram-OKZ733FA-wxVV9WIi.js} +1 -1
  501. package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-BIFLqlqR.js +1 -0
  502. package/ui-dist/assets/{swimlanes-SLNWSIFB-BIByPbrL.js → swimlanes-SLNWSIFB-Cd-C1Z_k.js} +2 -2
  503. package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-CoBROkkd.js +8 -0
  504. package/ui-dist/assets/{timeline-definition-Z64GVDOM-I1NgOcn9.js → timeline-definition-Z64GVDOM-SWngk4aR.js} +1 -1
  505. package/ui-dist/assets/{vennDiagram-T6HMQDX7-D6t2Njcl.js → vennDiagram-T6HMQDX7-DFdPUr36.js} +1 -1
  506. package/ui-dist/assets/{wardleyDiagram-T6FBY63Y-BiYKm9gh.js → wardleyDiagram-T6FBY63Y-C6IlrZ7t.js} +1 -1
  507. package/ui-dist/assets/{xychartDiagram-ELKLHX3M-BhwXKWIa.js → xychartDiagram-ELKLHX3M-BNm6hHGl.js} +1 -1
  508. package/ui-dist/index.html +2 -2
  509. package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +0 -36
  510. package/ui-dist/assets/channel-qoSYbCn0.js +0 -1
  511. package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +0 -1
  512. package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +0 -1
  513. package/ui-dist/assets/cose-bilkent-JH36ORCC-Do8pe8JB.js +0 -1
  514. package/ui-dist/assets/ganttDiagram-PKOTCBZU-CE3lyvFq.js +0 -292
  515. package/ui-dist/assets/index-BHiUiToL.css +0 -1
  516. package/ui-dist/assets/index-hkanj4t6.js +0 -1
  517. package/ui-dist/assets/index-yRJnh4vx.js +0 -1
  518. package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +0 -1
  519. package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-DgdkBnuK.js +0 -8
@@ -1,5 +1,5 @@
1
1
  import { and, asc, desc, eq, gt, gte, inArray, isNull, notInArray, or, sql } from "drizzle-orm";
2
- import { DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, MIN_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, PROVIDER_QUOTA_MONITOR_SERVICE_NAME, } from "@paperclipai/shared";
2
+ import { DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, MIN_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, PROVIDER_QUOTA_MONITOR_SERVICE_NAME, ISSUE_DISPOSITION_REPAIR_RETRY_REASON, } from "@paperclipai/shared";
3
3
  import { agents, agentWakeupRequests, approvals, activityLog, companies, heartbeatRunEvents, heartbeatRunWatchdogDecisions, heartbeatRuns, issueAttachments, issueComments, issueApprovals, issueRecoveryActions, issueRelations, issueThreadInteractions, issues, } from "@paperclipai/db";
4
4
  import { parseObject, asBoolean, asNumber } from "../../adapters/utils.js";
5
5
  import { runningProcesses } from "../../adapters/index.js";
@@ -9,6 +9,7 @@ import { logger } from "../../middleware/logger.js";
9
9
  import { isPidAlive, isProcessGroupAlive, terminateLocalService } from "../local-service-supervisor.js";
10
10
  import { redactCurrentUserText } from "../../log-redaction.js";
11
11
  import { redactSensitiveText } from "../../redaction.js";
12
+ import { isUniqueViolation } from "../../db-errors.js";
12
13
  import { logActivity } from "../activity-log.js";
13
14
  import { budgetService } from "../budgets.js";
14
15
  import { instanceSettingsService } from "../instance-settings.js";
@@ -16,8 +17,9 @@ import { issueRecoveryActionService } from "../issue-recovery-actions.js";
16
17
  import { issueTreeControlService } from "../issue-tree-control.js";
17
18
  import { TERMINAL_HEARTBEAT_RUN_STATUSES, issueService } from "../issues.js";
18
19
  import { applyIssueMonitorPolicyTransition, normalizeIssueExecutionPolicy, parseIssueExecutionState, } from "../issue-execution-policy.js";
19
- import { ISSUE_BLOCKERS_RESOLVED_WAKE_REASON, buildIssueBlockersResolvedWakeIdempotencyKey, findExistingIssueBlockersResolvedWakeForAnyKey, } from "../issue-dependency-wakeups.js";
20
+ import { ISSUE_BLOCKERS_RESOLVED_WAKE_REASON, buildIssueBlockersResolvedWakeStateKey, findExistingIssueBlockersResolvedWakeForReadyState, } from "../issue-dependency-wakeups.js";
20
21
  import { evaluateAgentInvokabilityFromDb } from "../agent-invokability.js";
22
+ import { isHeartbeatWakeOnDemandEnabled } from "../heartbeat-policy.js";
21
23
  import { getRunLogStore } from "../run-log-store.js";
22
24
  import { DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, SUCCESSFUL_RUN_MISSING_STATE_REASON, buildSuccessfulRunHandoffExhaustedNotice, isPluginManagedIssueLifecycle, noticeMetadataReferencesRecoveryAction, } from "./successful-run-handoff.js";
23
25
  import { buildExecutionReviewParticipantRecoveryNoticeSeed, buildExecutionReviewParticipantUnavailableNoticeSeed, buildStrandedRecoveryEscalationNotice, } from "./stranded-notice.js";
@@ -25,6 +27,7 @@ import { RECOVERY_ORIGIN_KINDS, buildIssueGraphLivenessLeafKey, isStrandedIssueR
25
27
  import { classifyIssueGraphLiveness, } from "./issue-graph-liveness.js";
26
28
  import { recoveryAssigneeAdapterOverrides, withRecoveryModelProfileHint, } from "./model-profile-hint.js";
27
29
  import { isAutomaticRecoverySuppressedByPauseHold } from "./pause-hold-guard.js";
30
+ import { collectDispositionRepairSourceState, dispositionRepairDelayMs, DISPOSITION_REPAIR_MAX_ATTEMPTS, } from "./disposition-repair.js";
28
31
  const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"];
29
32
  const UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES = ["interrupted", "failed", "cancelled", "timed_out"];
30
33
  export const ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS = 60 * 60 * 1000;
@@ -36,6 +39,8 @@ const STRANDED_ISSUE_RECOVERY_ORIGIN_KIND = RECOVERY_ORIGIN_KINDS.strandedIssueR
36
39
  const STALE_ACTIVE_RUN_EVALUATION_ORIGIN_KIND = RECOVERY_ORIGIN_KINDS.staleActiveRunEvaluation;
37
40
  const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
38
41
  const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON = "execution_review_participant_recovery";
42
+ const STRANDED_BOARD_ESCALATION_POLICY = "board_escalation_no_takeover_v1";
43
+ const DISPOSITION_REPAIR_IDEMPOTENCY_INDEX = "agent_wakeup_requests_disposition_repair_idempotency_uq";
39
44
  const RESOLVED_DEPENDENCY_WAKE_BACKSTOP_CANDIDATE_LIMIT = 500;
40
45
  const SESSIONED_LOCAL_ADAPTERS = new Set([
41
46
  "claude_local",
@@ -43,6 +48,7 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
43
48
  "cursor",
44
49
  "gemini_local",
45
50
  "hermes_local",
51
+ "kimi_local",
46
52
  "opencode_local",
47
53
  "pi_local",
48
54
  ]);
@@ -147,6 +153,10 @@ function readWorkspaceValidationFingerprint(latestRun) {
147
153
  const payload = readWorkspaceValidationPayload(latestRun);
148
154
  return readNonEmptyString(payload?.fingerprint);
149
155
  }
156
+ function readConfigurationIncompleteFingerprint(latestRun) {
157
+ const payload = parseObject(parseObject(latestRun?.resultJson).configurationIncomplete);
158
+ return readNonEmptyString(payload?.fingerprint);
159
+ }
150
160
  function readNonEmptyString(value) {
151
161
  return typeof value === "string" && value.trim().length > 0 ? value : null;
152
162
  }
@@ -287,6 +297,14 @@ export function classifyAdapterFailureForRecovery(latestRun, now = new Date()) {
287
297
  }
288
298
  export function classifyContinuationFailure(latestRun) {
289
299
  const errorCode = readNonEmptyString(latestRun?.errorCode);
300
+ if (errorCode === CONTINUATION_WAITING_ON_REVIEW_ERROR_CODE) {
301
+ return {
302
+ kind: "deliberate_wait_without_target",
303
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
304
+ baseBackoffMs: CONTINUATION_RECOVERY_TRANSIENT_BASE_BACKOFF_MS,
305
+ errorCode,
306
+ };
307
+ }
290
308
  if (errorCode && NON_RETRYABLE_CONTINUATION_ERROR_CODES.has(errorCode)) {
291
309
  return { kind: "non_retryable", maxAttempts: 0, baseBackoffMs: 0, errorCode };
292
310
  }
@@ -1307,8 +1325,11 @@ export function recoveryService(db, deps) {
1307
1325
  issueId: input.sourceIssue?.id ?? null,
1308
1326
  projectId: input.sourceIssue?.projectId ?? null,
1309
1327
  });
1310
- if ((await isAgentInvokable(candidate)) && !budgetBlock)
1328
+ if ((await isAgentInvokable(candidate)) &&
1329
+ isHeartbeatWakeOnDemandEnabled(candidate) &&
1330
+ !budgetBlock) {
1311
1331
  return candidate.id;
1332
+ }
1312
1333
  }
1313
1334
  return null;
1314
1335
  }
@@ -1423,15 +1444,6 @@ export function recoveryService(db, deps) {
1423
1444
  maybe.constraint_name === "issues_active_stale_run_evaluation_uq" ||
1424
1445
  typeof maybe.message === "string" && maybe.message.includes("issues_active_stale_run_evaluation_uq"));
1425
1446
  }
1426
- function isUniqueStrandedIssueRecoveryConflict(error) {
1427
- const maybe = unwrapDatabaseConflictError(error);
1428
- if (!maybe)
1429
- return false;
1430
- return maybe.code === "23505" &&
1431
- (maybe.constraint === "issues_active_stranded_issue_recovery_uq" ||
1432
- maybe.constraint_name === "issues_active_stranded_issue_recovery_uq" ||
1433
- typeof maybe.message === "string" && maybe.message.includes("issues_active_stranded_issue_recovery_uq"));
1434
- }
1435
1447
  async function ensureSourceIssueCommentedForStaleEvaluation(input) {
1436
1448
  if (!input.sourceIssue || ["done", "cancelled"].includes(input.sourceIssue.status))
1437
1449
  return false;
@@ -1856,15 +1868,6 @@ export function recoveryService(db, deps) {
1856
1868
  });
1857
1869
  return row;
1858
1870
  }
1859
- async function findOpenStrandedIssueRecoveryIssue(companyId, sourceIssueId) {
1860
- return db
1861
- .select()
1862
- .from(issues)
1863
- .where(and(eq(issues.companyId, companyId), eq(issues.originKind, STRANDED_ISSUE_RECOVERY_ORIGIN_KIND), eq(issues.originId, sourceIssueId), visibleIssueCondition(), notInArray(issues.status, ["done", "cancelled"])))
1864
- .orderBy(desc(issues.createdAt))
1865
- .limit(1)
1866
- .then((rows) => rows[0] ?? null);
1867
- }
1868
1871
  function isStrandedIssueRecoveryIssue(issue) {
1869
1872
  return issue.originKind === STRANDED_ISSUE_RECOVERY_ORIGIN_KIND;
1870
1873
  }
@@ -1889,260 +1892,22 @@ export function recoveryService(db, deps) {
1889
1892
  "- Next action: the assigned recovery owner or board operator should fix the runtime/adapter problem, resolve or reassign the original source issue, then mark this recovery issue done or cancelled.",
1890
1893
  ].join("\n");
1891
1894
  }
1892
- async function resolveStrandedIssueRecoveryOwnerAgentId(issue, preferredOwnerAgentId) {
1893
- const candidateIds = [];
1894
- if (preferredOwnerAgentId)
1895
- candidateIds.push(preferredOwnerAgentId);
1896
- if (issue.assigneeAgentId) {
1897
- const assignee = await getAgent(issue.assigneeAgentId);
1898
- if (assignee?.reportsTo)
1899
- candidateIds.push(assignee.reportsTo);
1900
- }
1901
- if (issue.createdByAgentId) {
1902
- const creator = await getAgent(issue.createdByAgentId);
1903
- if (creator?.reportsTo)
1904
- candidateIds.push(creator.reportsTo);
1905
- candidateIds.push(issue.createdByAgentId);
1906
- }
1907
- const roleCandidates = await db
1908
- .select()
1909
- .from(agents)
1910
- .where(and(eq(agents.companyId, issue.companyId), inArray(agents.role, ["cto", "ceo"])))
1911
- .orderBy(sql `case when ${agents.role} = 'cto' then 0 else 1 end`, asc(agents.createdAt));
1912
- candidateIds.push(...roleCandidates.map((agent) => agent.id));
1913
- if (issue.assigneeAgentId)
1914
- candidateIds.push(issue.assigneeAgentId);
1915
- const seen = new Set();
1916
- for (const agentId of candidateIds) {
1917
- if (seen.has(agentId))
1918
- continue;
1919
- seen.add(agentId);
1920
- const candidate = await getAgent(agentId);
1921
- if (!candidate || candidate.companyId !== issue.companyId)
1922
- continue;
1923
- const budgetBlock = await budgets.getInvocationBlock(issue.companyId, candidate.id, {
1924
- issueId: issue.id,
1925
- projectId: issue.projectId,
1926
- });
1927
- if ((await isAgentInvokable(candidate)) && !budgetBlock)
1928
- return candidate.id;
1929
- }
1930
- return null;
1931
- }
1932
- async function resolveInvokableRecoveryAgentId(issue, agentId) {
1933
- if (!agentId)
1934
- return null;
1935
- const candidate = await getAgent(agentId);
1936
- if (!candidate || candidate.companyId !== issue.companyId)
1937
- return null;
1938
- const budgetBlock = await budgets.getInvocationBlock(issue.companyId, candidate.id, {
1939
- issueId: issue.id,
1940
- projectId: issue.projectId,
1941
- });
1942
- return (await isAgentInvokable(candidate)) && !budgetBlock ? candidate.id : null;
1943
- }
1944
- async function resolveStrandedRecoveryRouting(input) {
1945
- const originalAgentId = input.latestRun?.agentId ?? input.issue.assigneeAgentId;
1946
- const returnOwnerAgentId = input.issue.assigneeAgentId ?? originalAgentId;
1947
- const routeToOriginal = input.recoveryCause === "process_lost" ||
1948
- input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON ||
1949
- input.recoveryCause === "codex_output_inactivity_monitor";
1950
- if (input.recoveryCause === "provider_quota") {
1951
- const retryAgentId = await resolveInvokableRecoveryAgentId(input.issue, originalAgentId);
1952
- if (!retryAgentId) {
1953
- return {
1954
- ownerAgentId: await resolveStrandedIssueRecoveryOwnerAgentId(input.issue),
1955
- returnOwnerAgentId: originalAgentId,
1956
- routingFallbackReason: "The original assignee is not invokable; quota recovery fell through to the manager ladder.",
1957
- };
1958
- }
1959
- return {
1960
- ownerAgentId: null,
1961
- returnOwnerAgentId: retryAgentId,
1962
- routingFallbackReason: null,
1963
- };
1964
- }
1965
- if (routeToOriginal) {
1966
- const ownerAgentId = await resolveInvokableRecoveryAgentId(input.issue, originalAgentId);
1967
- if (ownerAgentId) {
1968
- return { ownerAgentId, returnOwnerAgentId: originalAgentId, routingFallbackReason: null };
1969
- }
1970
- return {
1971
- ownerAgentId: await resolveStrandedIssueRecoveryOwnerAgentId(input.issue),
1972
- returnOwnerAgentId: originalAgentId,
1973
- routingFallbackReason: "The original assignee is not invokable; recovery fell through to the manager ladder.",
1974
- };
1975
- }
1895
+ function resolveStrandedRecoveryRouting(input) {
1896
+ const originalAgentId = input.issue.assigneeAgentId ?? input.latestRun?.agentId ?? null;
1976
1897
  return {
1977
- ownerAgentId: await resolveStrandedIssueRecoveryOwnerAgentId(input.issue, input.preferredOwnerAgentId),
1978
- returnOwnerAgentId,
1979
- routingFallbackReason: null,
1898
+ returnOwnerAgentId: originalAgentId,
1980
1899
  };
1981
1900
  }
1982
- function buildStrandedIssueRecoveryDescription(input) {
1983
- const sourceIssue = issueUiLink({ identifier: input.issue.identifier, id: input.issue.id }, input.prefix);
1984
- const runLink = input.latestRun
1985
- ? `[\`${input.latestRun.id}\`](/${input.prefix}/agents/${input.latestRun.agentId}/runs/${input.latestRun.id})`
1986
- : "none";
1987
- if (input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON) {
1988
- const sourceRunId = input.successfulRunHandoffEvidence?.sourceRunId;
1989
- const sourceRunLink = sourceRunId && input.latestRun
1990
- ? `[\`${sourceRunId}\`](/${input.prefix}/agents/${input.latestRun.agentId}/runs/${sourceRunId})`
1991
- : "unknown";
1992
- const missingDisposition = input.successfulRunHandoffEvidence?.missingDisposition ?? "clear_next_step";
1993
- return [
1994
- "Paperclip exhausted the bounded corrective handoff for a successful run that still has no valid issue disposition.",
1995
- "",
1996
- "This is not a runtime/adapter crash report. The source run succeeded; the remaining problem is the missing `done`, `in_review`, `blocked`, delegated follow-up, or explicit continuation path.",
1997
- "",
1998
- "## Safe Evidence",
1999
- "",
2000
- `- Source issue: ${sourceIssue}`,
2001
- `- Source run: ${sourceRunLink}`,
2002
- `- Corrective handoff run: ${runLink}`,
2003
- `- Source assignee: ${agentUiLink(input.sourceAssignee ?? null, input.prefix)}`,
2004
- `- Latest issue status: \`${input.issue.status}\``,
2005
- `- Latest handoff run status: \`${input.latestRun?.status ?? "unknown"}\``,
2006
- `- Normalized cause: \`${SUCCESSFUL_RUN_MISSING_STATE_REASON}\``,
2007
- `- Missing disposition: \`${missingDisposition}\``,
2008
- "- Suggested manager action: choose and record a valid issue disposition without copying transcript content.",
2009
- "",
2010
- "## Required Action",
2011
- "",
2012
- "- Inspect the source issue and run metadata, not raw transcript excerpts.",
2013
- "- Choose a valid issue disposition: `done`/`cancelled`, `in_review` with an owner, `blocked` with first-class blockers, delegated follow-up work, or an explicit continuation path.",
2014
- "- When the source issue has a clear owner and disposition, mark this recovery issue done.",
2015
- ].join("\n");
2016
- }
2017
- const retryReason = readNonEmptyString(parseObject(input.latestRun?.contextSnapshot)?.retryReason) ?? "unknown";
2018
- const failureSummary = summarizeRunFailureForIssueComment(input.latestRun);
2019
- const isReviewParticipantRecovery = input.recoveryCause === "execution_review_participant_recovery";
2020
- const detectedInvariant = isReviewParticipantRecovery
2021
- ? "execution_review_participant_recovery"
2022
- : "stranded_assigned_issue";
2023
- const requiredAction = isReviewParticipantRecovery
2024
- ? [
2025
- "- Inspect the latest reviewer run and the pending execution-review stage.",
2026
- "- Fix the reviewer runtime, restore the source issue to `in_review` with a live participant, or record an intentional manual resolution.",
2027
- "- When the source issue has a live review path or has been intentionally resolved, mark this recovery issue done.",
2028
- ]
2029
- : [
2030
- "- Inspect the latest run and source issue state.",
2031
- "- Fix the runtime/adapter problem, reassign the source issue, or convert the source issue into a clear manual-review state.",
2032
- "- When the source issue has a live execution path or has been intentionally resolved, mark this recovery issue done.",
2033
- ];
2034
- return [
2035
- isReviewParticipantRecovery
2036
- ? "Paperclip exhausted automatic recovery for a pending execution-review participant and created this explicit recovery task."
2037
- : "Paperclip exhausted automatic recovery for an assigned issue and created this explicit recovery task.",
2038
- "",
2039
- "## Source",
2040
- "",
2041
- `- Source issue: ${sourceIssue}`,
2042
- `- Previous source status: \`${input.previousStatus}\``,
2043
- `- Latest retry run: ${runLink}`,
2044
- `- Latest retry status: \`${input.latestRun?.status ?? "unknown"}\``,
2045
- `- Detected invariant: \`${detectedInvariant}\``,
2046
- `- Retry reason: \`${retryReason}\``,
2047
- failureSummary ? `- Failure: ${failureSummary.trim()}` : "- Failure: none recorded",
2048
- "",
2049
- "## Ownership",
2050
- "",
2051
- "- Selected owner: the first invokable manager/creator/executive candidate with budget available.",
2052
- "",
2053
- "## Required Action",
2054
- "",
2055
- ...requiredAction,
2056
- ].join("\n");
2057
- }
2058
- async function ensureStrandedIssueRecoveryIssue(input) {
2059
- if (isStrandedIssueRecoveryIssue(input.issue))
2060
- return null;
2061
- const existing = await findOpenStrandedIssueRecoveryIssue(input.issue.companyId, input.issue.id);
2062
- if (existing)
2063
- return existing;
2064
- const ownerAgentId = await resolveStrandedIssueRecoveryOwnerAgentId(input.issue);
2065
- if (!ownerAgentId)
2066
- return null;
2067
- const prefix = await getCompanyIssuePrefix(input.issue.companyId);
2068
- const sourceAssignee = input.issue.assigneeAgentId ? await getAgent(input.issue.assigneeAgentId) : null;
2069
- const recoveryCause = input.recoveryCause ?? "stranded_assigned_issue";
2070
- let recovery;
2071
- try {
2072
- recovery = await issuesSvc.create(input.issue.companyId, {
2073
- title: recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON
2074
- ? `Recover missing next step ${input.issue.identifier ?? input.issue.title}`
2075
- : `Recover stalled issue ${input.issue.identifier ?? input.issue.title}`,
2076
- description: buildStrandedIssueRecoveryDescription({
2077
- issue: input.issue,
2078
- latestRun: input.latestRun,
2079
- previousStatus: input.previousStatus,
2080
- prefix,
2081
- recoveryCause,
2082
- successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
2083
- sourceAssignee,
2084
- }),
2085
- status: "todo",
2086
- priority: input.issue.priority,
2087
- parentId: input.issue.id,
2088
- projectId: input.issue.projectId,
2089
- goalId: input.issue.goalId,
2090
- assigneeAgentId: ownerAgentId,
2091
- assigneeAdapterOverrides: recoveryAssigneeAdapterOverrides("status_only"),
2092
- originKind: STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
2093
- originId: input.issue.id,
2094
- originRunId: input.latestRun?.id ?? null,
2095
- originFingerprint: [
2096
- STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
2097
- input.issue.companyId,
2098
- input.issue.id,
2099
- recoveryCause,
2100
- input.latestRun?.id ?? "no-run",
2101
- ].join(":"),
2102
- billingCode: input.issue.billingCode,
2103
- inheritExecutionWorkspaceFromIssueId: input.issue.id,
2104
- });
2105
- }
2106
- catch (error) {
2107
- if (!isUniqueStrandedIssueRecoveryConflict(error))
2108
- throw error;
2109
- const raced = await findOpenStrandedIssueRecoveryIssue(input.issue.companyId, input.issue.id);
2110
- if (!raced)
2111
- throw error;
2112
- return raced;
2113
- }
2114
- await deps.enqueueWakeup(ownerAgentId, {
2115
- source: "assignment",
2116
- triggerDetail: "system",
2117
- reason: "issue_assigned",
2118
- payload: withRecoveryModelProfileHint({
2119
- issueId: recovery.id,
2120
- sourceIssueId: input.issue.id,
2121
- strandedRunId: input.latestRun?.id ?? null,
2122
- recoveryCause,
2123
- }, "status_only"),
2124
- requestedByActorType: "system",
2125
- requestedByActorId: null,
2126
- contextSnapshot: withRecoveryModelProfileHint({
2127
- issueId: recovery.id,
2128
- taskId: recovery.id,
2129
- wakeReason: "issue_assigned",
2130
- source: STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
2131
- sourceIssueId: input.issue.id,
2132
- strandedRunId: input.latestRun?.id ?? null,
2133
- recoveryCause,
2134
- }, "status_only"),
2135
- });
2136
- return recovery;
2137
- }
2138
1901
  function strandedRecoveryActionKind(cause) {
2139
1902
  return cause === SUCCESSFUL_RUN_MISSING_STATE_REASON
2140
1903
  ? "missing_disposition"
2141
- : cause === "workspace_validation_failed"
2142
- ? "workspace_validation"
2143
- : cause === "configuration_incomplete"
2144
- ? "configuration_validation"
2145
- : "stranded_assigned_issue";
1904
+ : cause === "deliberate_wait_without_target"
1905
+ ? "deliberate_wait_without_target"
1906
+ : cause === "workspace_validation_failed"
1907
+ ? "workspace_validation"
1908
+ : cause === "configuration_incomplete"
1909
+ ? "configuration_validation"
1910
+ : "stranded_assigned_issue";
2146
1911
  }
2147
1912
  function strandedRecoveryActionFingerprint(input) {
2148
1913
  if (input.recoveryCause === "workspace_validation_failed") {
@@ -2157,6 +1922,23 @@ export function recoveryService(db, deps) {
2157
1922
  ].join(":");
2158
1923
  }
2159
1924
  }
1925
+ // A configuration-incomplete failure that carries a stable identity (for
1926
+ // example an unresolved workspace base ref) dedupes per that identity, so a
1927
+ // different requested ref makes a new recovery action while the same ref
1928
+ // reuses one. Configuration gaps with no fingerprint fall back to the
1929
+ // issue-and-cause scope below.
1930
+ if (input.recoveryCause === "configuration_incomplete") {
1931
+ const configurationFingerprint = readConfigurationIncompleteFingerprint(input.latestRun);
1932
+ if (configurationFingerprint) {
1933
+ return [
1934
+ "source_scoped_recovery",
1935
+ input.issue.companyId,
1936
+ input.issue.id,
1937
+ input.recoveryCause,
1938
+ configurationFingerprint,
1939
+ ].join(":");
1940
+ }
1941
+ }
2160
1942
  return [
2161
1943
  "source_scoped_recovery",
2162
1944
  input.issue.companyId,
@@ -2189,20 +1971,25 @@ export function recoveryService(db, deps) {
2189
1971
  }
2190
1972
  async function ensureSourceScopedStrandedRecoveryAction(input) {
2191
1973
  const recoveryCause = resolveStrandedRecoveryCause(input.latestRun, input.recoveryCause);
2192
- const routing = await resolveStrandedRecoveryRouting({
1974
+ const routing = resolveStrandedRecoveryRouting({
2193
1975
  issue: input.issue,
2194
1976
  latestRun: input.latestRun,
2195
- recoveryCause,
2196
- preferredOwnerAgentId: input.recoveryOwnerAgentId,
2197
1977
  });
2198
- const ownerAgentId = routing.ownerAgentId;
1978
+ const isProviderQuotaWait = recoveryCause === "provider_quota";
2199
1979
  const now = new Date();
2200
1980
  const action = await recoveryActionsSvc.upsertSourceScoped({
2201
1981
  companyId: input.issue.companyId,
2202
1982
  sourceIssueId: input.issue.id,
1983
+ // A configuration-incomplete failure carries a per-identity fingerprint
1984
+ // (for example the unresolved workspace base ref). A different ref is a
1985
+ // distinct blocker, so it must get a new recovery action and notify the
1986
+ // operator, not overwrite the active action of the prior ref.
1987
+ supersedeOnIdentityChange: recoveryCause === "configuration_incomplete",
1988
+ preserveExistingOwner: true,
2203
1989
  kind: strandedRecoveryActionKind(recoveryCause),
2204
- ownerType: recoveryCause === "provider_quota" && !ownerAgentId ? "system" : ownerAgentId ? "agent" : "board",
2205
- ownerAgentId,
1990
+ ownerType: isProviderQuotaWait ? "system" : "board",
1991
+ ownerAgentId: null,
1992
+ ownerUserId: null,
2206
1993
  previousOwnerAgentId: input.issue.assigneeAgentId,
2207
1994
  returnOwnerAgentId: routing.returnOwnerAgentId,
2208
1995
  cause: recoveryCause,
@@ -2220,49 +2007,40 @@ export function recoveryService(db, deps) {
2220
2007
  successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
2221
2008
  }),
2222
2009
  failureSummary: summarizeRunFailureForIssueComment(input.latestRun)?.trim() ?? null,
2223
- routingFallbackReason: routing.routingFallbackReason,
2224
2010
  },
2011
+ evidenceOnCreate: isProviderQuotaWait
2012
+ ? {}
2013
+ : { routingPolicy: STRANDED_BOARD_ESCALATION_POLICY },
2225
2014
  nextAction: recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON
2226
- ? "Choose and record a valid issue disposition without copying transcript content."
2015
+ ? "Board operator: inspect the run evidence, then explicitly choose a valid issue disposition, retry the original owner, reassign, or intentionally resolve the task."
2227
2016
  : recoveryCause === "process_lost"
2228
- ? "Retry the original assignee from durable progress without redoing completed steps."
2017
+ ? "Board operator: inspect the retry history, then explicitly retry the original owner, reassign, or intentionally resolve the task."
2229
2018
  : recoveryCause === "provider_quota"
2230
2019
  ? "Wait for provider quota recovery, then retry the original assignee; do not wake a takeover owner."
2231
2020
  : recoveryCause === "codex_output_inactivity_monitor"
2232
- ? "Retry the same agent from durable progress after the output-inactivity termination."
2021
+ ? "Board operator: inspect the inactivity evidence, then explicitly retry the original owner, reassign, or intentionally resolve the task."
2233
2022
  : recoveryCause === "workspace_validation_failed"
2234
2023
  ? readWorkspaceValidationPayload(input.latestRun)?.reason === "git_worktree_branch_incoherence"
2235
- ? "Repair the source issue git worktree branch incoherence, or choose a new execution workspace, before resuming adapter execution."
2024
+ ? "Board operator: repair the source task git worktree branch incoherence or choose a new execution workspace, then explicitly retry or reassign."
2236
2025
  : readWorkspaceValidationPayload(input.latestRun)?.reason === "git_worktree_base_materialization_failed"
2237
- ? "Repair the project workspace repository URL or clone access, or configure a local checkout cwd, before resuming adapter execution."
2238
- : "Repair the source issue workspace link, project workspace cwd, or git checkout before resuming adapter execution."
2026
+ ? "Board operator: repair the project workspace repository URL or clone access, or configure a local checkout cwd, then explicitly retry or reassign."
2027
+ : "Board operator: repair the source task workspace link, project workspace cwd, or git checkout, then explicitly retry or reassign."
2239
2028
  : recoveryCause === "configuration_incomplete"
2240
- ? "Bind the missing secret(s) named in the run failure to the agent/project/routine env before resuming adapter execution."
2029
+ ? "Board operator: bind the missing secret(s) named in the run failure, then explicitly retry the original owner or reassign."
2241
2030
  : recoveryCause === "execution_review_participant_recovery"
2242
- ? "Repair the failed review participant path, restore the source issue to in_review with a live reviewer, or record an intentional manual resolution."
2243
- : "Restore a live execution path, fix the runtime/adapter failure, or record an intentional manual resolution.",
2244
- wakePolicy: recoveryCause === "provider_quota" && !ownerAgentId
2031
+ ? "Board operator: repair the failed review participant path, restore a live reviewer, explicitly reassign, or record an intentional resolution."
2032
+ : "Board operator: inspect the evidence, repair the runtime if appropriate, then explicitly retry the original owner, reassign, or intentionally resolve the task.",
2033
+ wakePolicy: isProviderQuotaWait
2245
2034
  ? {
2246
2035
  type: "monitor_only",
2247
2036
  reason: recoveryCause,
2248
2037
  }
2249
- : recoveryCause === "configuration_incomplete"
2250
- ? {
2251
- type: "manual_repair_required",
2252
- reason: recoveryCause,
2253
- ownerAgentId,
2254
- }
2255
- : ownerAgentId
2256
- ? {
2257
- type: "wake_owner",
2258
- reason: "source_scoped_recovery_action",
2259
- ownerAgentId,
2260
- }
2261
- : {
2262
- type: "board_escalation",
2263
- reason: "no_invokable_recovery_owner",
2264
- },
2265
- monitorPolicy: recoveryCause === "provider_quota" && !ownerAgentId
2038
+ : {
2039
+ type: "board_escalation",
2040
+ reason: recoveryCause,
2041
+ preservesSourceAssignee: true,
2042
+ },
2043
+ monitorPolicy: isProviderQuotaWait
2266
2044
  ? { type: "wait_recovery", retryAgentId: routing.returnOwnerAgentId }
2267
2045
  : null,
2268
2046
  maxAttempts: null,
@@ -2270,40 +2048,6 @@ export function recoveryService(db, deps) {
2270
2048
  });
2271
2049
  return action;
2272
2050
  }
2273
- async function enqueueSourceScopedStrandedRecoveryWake(input) {
2274
- if (input.recoveryCause === "provider_quota" && !input.action.ownerAgentId)
2275
- return;
2276
- if (input.recoveryCause === "configuration_incomplete")
2277
- return;
2278
- if (!input.action.ownerAgentId)
2279
- return;
2280
- await deps.enqueueWakeup(input.action.ownerAgentId, {
2281
- source: "assignment",
2282
- triggerDetail: "system",
2283
- reason: "source_scoped_recovery_action",
2284
- idempotencyKey: `source_scoped_recovery_action:${input.action.id}:${input.action.attemptCount}`,
2285
- payload: withRecoveryModelProfileHint({
2286
- issueId: input.issue.id,
2287
- sourceIssueId: input.issue.id,
2288
- recoveryActionId: input.action.id,
2289
- strandedRunId: input.latestRun?.id ?? null,
2290
- recoveryCause: input.recoveryCause,
2291
- }, "status_only"),
2292
- requestedByActorType: "system",
2293
- requestedByActorId: null,
2294
- contextSnapshot: withRecoveryModelProfileHint({
2295
- issueId: input.issue.id,
2296
- taskId: input.issue.id,
2297
- wakeReason: "source_scoped_recovery_action",
2298
- skipIssueComment: true,
2299
- source: "issue_recovery_action",
2300
- recoveryActionId: input.action.id,
2301
- sourceIssueId: input.issue.id,
2302
- strandedRunId: input.latestRun?.id ?? null,
2303
- recoveryCause: input.recoveryCause,
2304
- }, "status_only"),
2305
- });
2306
- }
2307
2051
  function readProviderQuotaRetryAt(latestRun, now) {
2308
2052
  const result = parseObject(latestRun?.resultJson);
2309
2053
  const context = parseObject(latestRun?.contextSnapshot);
@@ -2491,12 +2235,31 @@ export function recoveryService(db, deps) {
2491
2235
  async function existingUnresolvedBlockerIssueIds(companyId, issueId) {
2492
2236
  return existingUnresolvedBlockerIssues(companyId, issueId).then((rows) => rows.map((row) => row.id));
2493
2237
  }
2494
- async function resolveContinuationWaitingOnReview(issue) {
2495
- const existingBlockers = await existingUnresolvedBlockerIssues(issue.companyId, issue.id);
2496
- const openChildren = await db
2238
+ async function openChildIssues(issue) {
2239
+ return db
2497
2240
  .select({ id: issues.id, identifier: issues.identifier })
2498
2241
  .from(issues)
2499
2242
  .where(and(eq(issues.companyId, issue.companyId), eq(issues.parentId, issue.id), visibleIssueCondition(), notInArray(issues.status, ["done", "cancelled"])));
2243
+ }
2244
+ async function healthyOpenChildIssues(issue) {
2245
+ const childCandidates = await db
2246
+ .select()
2247
+ .from(issues)
2248
+ .where(and(eq(issues.companyId, issue.companyId), eq(issues.parentId, issue.id), visibleIssueCondition(), notInArray(issues.status, ["done", "cancelled"])));
2249
+ const openChildren = [];
2250
+ for (const child of childCandidates) {
2251
+ const childState = await collectDispositionRepairSourceState(db, { issue: child });
2252
+ if (childState.hasActiveExecutionPath || childState.hasDurableWaitingPath) {
2253
+ openChildren.push({ id: child.id, identifier: child.identifier });
2254
+ }
2255
+ }
2256
+ return openChildren;
2257
+ }
2258
+ async function resolveContinuationWaitingOnReview(issue) {
2259
+ const [existingBlockers, openChildren] = await Promise.all([
2260
+ existingUnresolvedBlockerIssues(issue.companyId, issue.id),
2261
+ openChildIssues(issue),
2262
+ ]);
2500
2263
  const blockedByIssueIds = [...new Set([...existingBlockers.map((row) => row.id), ...openChildren.map((row) => row.id)])];
2501
2264
  if (blockedByIssueIds.length === 0)
2502
2265
  return null;
@@ -2545,6 +2308,578 @@ export function recoveryService(db, deps) {
2545
2308
  });
2546
2309
  return updated;
2547
2310
  }
2311
+ function readDispositionRepairAttempt(latestRun) {
2312
+ if (!latestRun)
2313
+ return null;
2314
+ const context = parseObject(latestRun.contextSnapshot);
2315
+ if (readNonEmptyString(context.retryReason) !== ISSUE_DISPOSITION_REPAIR_RETRY_REASON)
2316
+ return null;
2317
+ return {
2318
+ attempt: Math.max(1, Math.floor(asNumber(context.dispositionRepairAttempt, 1))),
2319
+ fingerprint: readNonEmptyString(context.dispositionRepairFingerprint),
2320
+ };
2321
+ }
2322
+ async function resolveDispositionRepairActionAsCovered(issue, reason) {
2323
+ const active = await recoveryActionsSvc.getActiveForIssue(issue.companyId, issue.id);
2324
+ if (!active || active.kind !== "deliberate_wait_without_target")
2325
+ return;
2326
+ await recoveryActionsSvc.resolveActiveForIssue({
2327
+ companyId: issue.companyId,
2328
+ sourceIssueId: issue.id,
2329
+ actionId: active.id,
2330
+ status: "resolved",
2331
+ outcome: "restored",
2332
+ resolutionNote: reason,
2333
+ });
2334
+ await logActivity(db, {
2335
+ companyId: issue.companyId,
2336
+ actorType: "system",
2337
+ actorId: "recovery",
2338
+ agentId: null,
2339
+ runId: null,
2340
+ action: "issue.disposition_repair_resolved",
2341
+ entityType: "issue_recovery_action",
2342
+ entityId: active.id,
2343
+ details: {
2344
+ sourceIssueId: issue.id,
2345
+ sourceIdentifier: issue.identifier,
2346
+ reason,
2347
+ },
2348
+ });
2349
+ }
2350
+ async function ensureDispositionRepairAction(input) {
2351
+ let active = await recoveryActionsSvc.getActiveForIssue(input.issue.companyId, input.issue.id);
2352
+ if (active && (active.kind !== "deliberate_wait_without_target" ||
2353
+ active.fingerprint !== input.fingerprint)) {
2354
+ await recoveryActionsSvc.resolveActiveForIssue({
2355
+ companyId: input.issue.companyId,
2356
+ sourceIssueId: input.issue.id,
2357
+ actionId: active.id,
2358
+ status: "cancelled",
2359
+ outcome: "cancelled",
2360
+ resolutionNote: "source_state_changed",
2361
+ });
2362
+ await logActivity(db, {
2363
+ companyId: input.issue.companyId,
2364
+ actorType: "system",
2365
+ actorId: "recovery",
2366
+ agentId: null,
2367
+ runId: input.latestRun?.id ?? null,
2368
+ action: "issue.disposition_repair_fingerprint_reset",
2369
+ entityType: "issue_recovery_action",
2370
+ entityId: active.id,
2371
+ details: {
2372
+ sourceIssueId: input.issue.id,
2373
+ previousFingerprint: active.fingerprint,
2374
+ nextFingerprint: input.fingerprint,
2375
+ terminalReason: "source_state_changed",
2376
+ },
2377
+ });
2378
+ active = null;
2379
+ }
2380
+ if (active && active.attemptCount >= input.attemptCount)
2381
+ return active;
2382
+ return recoveryActionsSvc.upsertSourceScoped({
2383
+ companyId: input.issue.companyId,
2384
+ sourceIssueId: input.issue.id,
2385
+ kind: "deliberate_wait_without_target",
2386
+ ownerType: "agent",
2387
+ ownerAgentId: input.issue.assigneeAgentId,
2388
+ previousOwnerAgentId: input.issue.assigneeAgentId,
2389
+ returnOwnerAgentId: input.issue.assigneeAgentId,
2390
+ cause: "deliberate_wait_without_target",
2391
+ fingerprint: input.fingerprint,
2392
+ evidence: {
2393
+ sourceIssueId: input.issue.id,
2394
+ sourceIdentifier: input.issue.identifier,
2395
+ latestRunId: input.latestRun?.id ?? null,
2396
+ latestRunStatus: input.latestRun?.status ?? null,
2397
+ latestRunErrorCode: input.latestRun?.errorCode ?? null,
2398
+ sourceStateFingerprint: input.fingerprint,
2399
+ terminalReason: null,
2400
+ },
2401
+ nextAction: "The original owner must replace the parked summary with a terminal, live, blocked, monitored, or typed waiting disposition.",
2402
+ wakePolicy: {
2403
+ type: "bounded_owner_disposition_repair",
2404
+ retryAgentId: input.issue.assigneeAgentId,
2405
+ attempt: input.attemptCount,
2406
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2407
+ },
2408
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2409
+ attemptCount: input.attemptCount,
2410
+ lastAttemptAt: new Date(),
2411
+ });
2412
+ }
2413
+ async function scheduleDispositionRepairAttempt(input) {
2414
+ const agentId = input.issue.assigneeAgentId;
2415
+ if (!agentId)
2416
+ return null;
2417
+ const timing = dispositionRepairDelayMs(input.attempt, input.fingerprint);
2418
+ const now = new Date();
2419
+ const retryAt = new Date(now.getTime() + timing.delayMs);
2420
+ const idempotencyKey = `issue_disposition_repair:${input.issue.id}:${input.fingerprint}:${input.attempt}`;
2421
+ const context = withRecoveryModelProfileHint({
2422
+ issueId: input.issue.id,
2423
+ taskId: input.issue.id,
2424
+ wakeReason: ISSUE_DISPOSITION_REPAIR_RETRY_REASON,
2425
+ retryReason: ISSUE_DISPOSITION_REPAIR_RETRY_REASON,
2426
+ source: "issue.deliberate_wait_disposition_repair",
2427
+ retryOfRunId: input.latestRun?.id ?? null,
2428
+ recoveryActionId: input.action.id,
2429
+ dispositionRepairFingerprint: input.fingerprint,
2430
+ dispositionRepairAttempt: input.attempt,
2431
+ dispositionRepairMaxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2432
+ bypassContinuationSummaryPark: true,
2433
+ dispositionRepairInstruction: "Revalidate the issue and replace the invalid parked summary with a durable disposition. Continue productive work when appropriate.",
2434
+ }, "normal_model");
2435
+ const findScheduledRun = () => db
2436
+ .select({ run: heartbeatRuns })
2437
+ .from(agentWakeupRequests)
2438
+ .innerJoin(heartbeatRuns, eq(heartbeatRuns.id, agentWakeupRequests.runId))
2439
+ .where(and(eq(agentWakeupRequests.companyId, input.issue.companyId), eq(agentWakeupRequests.idempotencyKey, idempotencyKey), sql `${agentWakeupRequests.status} <> 'skipped'`))
2440
+ .limit(1)
2441
+ .then((rows) => rows[0]?.run ?? null);
2442
+ let scheduledRun = await findScheduledRun();
2443
+ let created = false;
2444
+ if (!scheduledRun) {
2445
+ try {
2446
+ if (timing.delayMs === 0) {
2447
+ const enqueuedRun = await deps.enqueueWakeup(agentId, {
2448
+ source: "automation",
2449
+ triggerDetail: "system",
2450
+ reason: ISSUE_DISPOSITION_REPAIR_RETRY_REASON,
2451
+ idempotencyKey,
2452
+ payload: withRecoveryModelProfileHint({
2453
+ issueId: input.issue.id,
2454
+ retryOfRunId: input.latestRun?.id ?? null,
2455
+ recoveryActionId: input.action.id,
2456
+ dispositionRepairFingerprint: input.fingerprint,
2457
+ dispositionRepairAttempt: input.attempt,
2458
+ bypassContinuationSummaryPark: true,
2459
+ }, "normal_model"),
2460
+ requestedByActorType: "system",
2461
+ requestedByActorId: null,
2462
+ contextSnapshot: context,
2463
+ });
2464
+ scheduledRun = enqueuedRun ?? (await findScheduledRun());
2465
+ created = Boolean(enqueuedRun);
2466
+ }
2467
+ else {
2468
+ scheduledRun = await db.transaction(async (tx) => {
2469
+ const wakeup = await tx
2470
+ .insert(agentWakeupRequests)
2471
+ .values({
2472
+ companyId: input.issue.companyId,
2473
+ agentId,
2474
+ source: "automation",
2475
+ triggerDetail: "system",
2476
+ reason: ISSUE_DISPOSITION_REPAIR_RETRY_REASON,
2477
+ payload: withRecoveryModelProfileHint({
2478
+ issueId: input.issue.id,
2479
+ retryOfRunId: input.latestRun?.id ?? null,
2480
+ recoveryActionId: input.action.id,
2481
+ dispositionRepairFingerprint: input.fingerprint,
2482
+ dispositionRepairAttempt: input.attempt,
2483
+ bypassContinuationSummaryPark: true,
2484
+ }, "normal_model"),
2485
+ status: "queued",
2486
+ requestedByActorType: "system",
2487
+ requestedByActorId: null,
2488
+ idempotencyKey,
2489
+ updatedAt: now,
2490
+ })
2491
+ .returning()
2492
+ .then((rows) => rows[0]);
2493
+ const run = await tx
2494
+ .insert(heartbeatRuns)
2495
+ .values({
2496
+ companyId: input.issue.companyId,
2497
+ agentId,
2498
+ invocationSource: "automation",
2499
+ triggerDetail: "system",
2500
+ status: "scheduled_retry",
2501
+ wakeupRequestId: wakeup.id,
2502
+ retryOfRunId: input.latestRun?.id ?? null,
2503
+ scheduledRetryAt: retryAt,
2504
+ scheduledRetryAttempt: input.attempt,
2505
+ scheduledRetryReason: ISSUE_DISPOSITION_REPAIR_RETRY_REASON,
2506
+ contextSnapshot: context,
2507
+ updatedAt: now,
2508
+ })
2509
+ .returning()
2510
+ .then((rows) => rows[0]);
2511
+ await tx
2512
+ .update(agentWakeupRequests)
2513
+ .set({ runId: run.id, updatedAt: now })
2514
+ .where(eq(agentWakeupRequests.id, wakeup.id));
2515
+ return run;
2516
+ });
2517
+ created = true;
2518
+ }
2519
+ }
2520
+ catch (error) {
2521
+ if (!isUniqueViolation(error, DISPOSITION_REPAIR_IDEMPOTENCY_INDEX))
2522
+ throw error;
2523
+ const winningRun = await findScheduledRun();
2524
+ if (!winningRun)
2525
+ throw error;
2526
+ scheduledRun = winningRun;
2527
+ }
2528
+ }
2529
+ if (!scheduledRun)
2530
+ return null;
2531
+ await db
2532
+ .update(issueRecoveryActions)
2533
+ .set({
2534
+ attemptCount: input.attempt,
2535
+ wakePolicy: {
2536
+ type: "bounded_owner_disposition_repair",
2537
+ retryAgentId: agentId,
2538
+ attempt: input.attempt,
2539
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2540
+ baseBackoffMs: timing.baseDelayMs,
2541
+ jitterMs: timing.jitterMs,
2542
+ retryAt: retryAt.toISOString(),
2543
+ scheduledRunId: scheduledRun.id,
2544
+ },
2545
+ timeoutAt: retryAt,
2546
+ lastAttemptAt: now,
2547
+ updatedAt: now,
2548
+ })
2549
+ .where(and(eq(issueRecoveryActions.id, input.action.id), eq(issueRecoveryActions.companyId, input.issue.companyId)));
2550
+ if (created) {
2551
+ await logActivity(db, {
2552
+ companyId: input.issue.companyId,
2553
+ actorType: "system",
2554
+ actorId: "recovery",
2555
+ agentId: null,
2556
+ runId: input.latestRun?.id ?? null,
2557
+ action: "issue.disposition_repair_scheduled",
2558
+ entityType: "issue_recovery_action",
2559
+ entityId: input.action.id,
2560
+ details: {
2561
+ sourceIssueId: input.issue.id,
2562
+ sourceIdentifier: input.issue.identifier,
2563
+ ownerAgentId: agentId,
2564
+ sourceStateFingerprint: input.fingerprint,
2565
+ attempt: input.attempt,
2566
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2567
+ baseBackoffMs: timing.baseDelayMs,
2568
+ jitterMs: timing.jitterMs,
2569
+ retryAt: retryAt.toISOString(),
2570
+ scheduledRunId: scheduledRun.id,
2571
+ },
2572
+ });
2573
+ }
2574
+ return scheduledRun;
2575
+ }
2576
+ async function latestRecoveryActionRun(action) {
2577
+ return db
2578
+ .select()
2579
+ .from(heartbeatRuns)
2580
+ .where(and(eq(heartbeatRuns.companyId, action.companyId), sql `${heartbeatRuns.contextSnapshot} ->> 'recoveryActionId' = ${action.id}`))
2581
+ .orderBy(desc(heartbeatRuns.createdAt), desc(heartbeatRuns.id))
2582
+ .limit(1)
2583
+ .then((rows) => rows[0] ?? null);
2584
+ }
2585
+ async function sourceHasNewPathOutsideRecoveryAction(action) {
2586
+ const [run, wake] = await Promise.all([
2587
+ db
2588
+ .select({ id: heartbeatRuns.id })
2589
+ .from(heartbeatRuns)
2590
+ .where(and(eq(heartbeatRuns.companyId, action.companyId), inArray(heartbeatRuns.status, [...EXECUTION_PATH_HEARTBEAT_RUN_STATUSES]), sql `coalesce(${heartbeatRuns.contextSnapshot} ->> 'issueId', ${heartbeatRuns.contextSnapshot} ->> 'taskId') = ${action.sourceIssueId}`, sql `coalesce(${heartbeatRuns.contextSnapshot} ->> 'recoveryActionId', '') <> ${action.id}`))
2591
+ .limit(1)
2592
+ .then((rows) => rows[0] ?? null),
2593
+ db
2594
+ .select({ id: agentWakeupRequests.id })
2595
+ .from(agentWakeupRequests)
2596
+ .where(and(eq(agentWakeupRequests.companyId, action.companyId), inArray(agentWakeupRequests.status, ["queued", "claimed", "deferred_issue_execution"]), sql `coalesce(${agentWakeupRequests.payload} ->> 'issueId', ${agentWakeupRequests.payload} ->> 'taskId') = ${action.sourceIssueId}`, sql `coalesce(${agentWakeupRequests.payload} ->> 'recoveryActionId', '') <> ${action.id}`))
2597
+ .limit(1)
2598
+ .then((rows) => rows[0] ?? null),
2599
+ ]);
2600
+ return Boolean(run || wake);
2601
+ }
2602
+ async function reconcileActiveRecoveryActions() {
2603
+ const rows = await db
2604
+ .select({ action: issueRecoveryActions, issue: issues })
2605
+ .from(issueRecoveryActions)
2606
+ .innerJoin(issues, and(eq(issues.id, issueRecoveryActions.sourceIssueId), eq(issues.companyId, issueRecoveryActions.companyId)))
2607
+ .where(inArray(issueRecoveryActions.status, ["active", "escalated"]));
2608
+ const result = { requeued: 0, escalated: 0, resolved: 0, skipped: 0, issueIds: [] };
2609
+ for (const { action, issue } of rows) {
2610
+ const wakePolicy = parseObject(action.wakePolicy);
2611
+ const wakePolicyType = readNonEmptyString(wakePolicy.type);
2612
+ if (wakePolicyType !== "bounded_recovery_owner" &&
2613
+ wakePolicyType !== "bounded_owner_disposition_repair" &&
2614
+ action.ownerType !== "board") {
2615
+ continue;
2616
+ }
2617
+ if (issue.status === "done" || issue.status === "cancelled") {
2618
+ const resolved = await recoveryActionsSvc.resolveActiveForIssue({
2619
+ companyId: action.companyId,
2620
+ sourceIssueId: action.sourceIssueId,
2621
+ actionId: action.id,
2622
+ status: "resolved",
2623
+ outcome: "restored",
2624
+ resolutionNote: "source_terminal",
2625
+ });
2626
+ if (resolved) {
2627
+ result.resolved += 1;
2628
+ result.issueIds.push(issue.id);
2629
+ }
2630
+ continue;
2631
+ }
2632
+ const [sourceState, healthyChildren, hasNewSourcePath] = await Promise.all([
2633
+ collectDispositionRepairSourceState(db, { issue }),
2634
+ healthyOpenChildIssues(issue),
2635
+ sourceHasNewPathOutsideRecoveryAction(action),
2636
+ ]);
2637
+ const durablePathRestored = action.ownerType !== "board" && sourceState.hasDurableWaitingPath;
2638
+ if (durablePathRestored || healthyChildren.length > 0 || hasNewSourcePath) {
2639
+ if (healthyChildren.length > 0 && !sourceState.hasDurableWaitingPath) {
2640
+ const blockerIds = await existingUnresolvedBlockerIssueIds(issue.companyId, issue.id);
2641
+ await issuesSvc.update(issue.id, {
2642
+ status: "blocked",
2643
+ blockedByIssueIds: [...new Set([
2644
+ ...blockerIds,
2645
+ ...healthyChildren.map((child) => child.id),
2646
+ ])],
2647
+ });
2648
+ }
2649
+ const resolved = await recoveryActionsSvc.resolveActiveForIssue({
2650
+ companyId: action.companyId,
2651
+ sourceIssueId: action.sourceIssueId,
2652
+ actionId: action.id,
2653
+ status: "resolved",
2654
+ outcome: "restored",
2655
+ resolutionNote: durablePathRestored
2656
+ ? `durable_path_restored:${sourceState.durablePathReason ?? "unknown"}`
2657
+ : healthyChildren.length > 0
2658
+ ? "durable_path_restored:healthy_child"
2659
+ : "new_source_execution_path",
2660
+ });
2661
+ if (resolved) {
2662
+ result.resolved += 1;
2663
+ result.issueIds.push(issue.id);
2664
+ }
2665
+ continue;
2666
+ }
2667
+ if (wakePolicyType === "bounded_owner_disposition_repair") {
2668
+ if (await isAutomaticRecoverySuppressedByPauseHold(db, issue.companyId, issue.id, treeControlSvc)) {
2669
+ result.skipped += 1;
2670
+ continue;
2671
+ }
2672
+ const latestRun = await latestRecoveryActionRun(action);
2673
+ const persistedAttempt = Math.max(action.attemptCount, Math.max(0, Math.floor(asNumber(wakePolicy.attempt, action.attemptCount))));
2674
+ const outcome = await reconcileDispositionRepair(issue, latestRun, {
2675
+ historicalAttemptCount: persistedAttempt,
2676
+ });
2677
+ if (outcome === "queued") {
2678
+ result.requeued += 1;
2679
+ result.issueIds.push(issue.id);
2680
+ }
2681
+ else if (outcome === "escalated") {
2682
+ result.escalated += 1;
2683
+ result.issueIds.push(issue.id);
2684
+ }
2685
+ else {
2686
+ result.skipped += 1;
2687
+ }
2688
+ continue;
2689
+ }
2690
+ if (action.ownerType === "board")
2691
+ continue;
2692
+ // Legacy takeover actions remain readable and resolvable, but recovery no
2693
+ // longer schedules another agent-owned wake for them.
2694
+ result.skipped += 1;
2695
+ }
2696
+ return result;
2697
+ }
2698
+ async function escalateDispositionRepair(input) {
2699
+ const action = await ensureDispositionRepairAction({
2700
+ issue: input.issue,
2701
+ latestRun: input.latestRun,
2702
+ fingerprint: input.fingerprint,
2703
+ attemptCount: input.attemptCount,
2704
+ });
2705
+ const now = new Date();
2706
+ await db
2707
+ .update(issueRecoveryActions)
2708
+ .set({
2709
+ status: "active",
2710
+ ownerType: "board",
2711
+ ownerAgentId: null,
2712
+ ownerUserId: null,
2713
+ maxAttempts: null,
2714
+ evidence: {
2715
+ ...action.evidence,
2716
+ latestRunId: input.latestRun?.id ?? null,
2717
+ latestRunStatus: input.latestRun?.status ?? null,
2718
+ latestRunErrorCode: input.latestRun?.errorCode ?? null,
2719
+ terminalReason: input.terminalReason,
2720
+ sourceAttemptCount: input.attemptCount,
2721
+ sourceMaxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2722
+ routingPolicy: STRANDED_BOARD_ESCALATION_POLICY,
2723
+ },
2724
+ nextAction: "Inspect the evidence and choose whether to repair, retry the original owner, explicitly reassign, or resolve the source issue.",
2725
+ wakePolicy: {
2726
+ type: "board_escalation",
2727
+ reason: input.terminalReason,
2728
+ preservesSourceAssignee: true,
2729
+ },
2730
+ timeoutAt: null,
2731
+ resolutionNote: input.terminalReason,
2732
+ updatedAt: now,
2733
+ })
2734
+ .where(and(eq(issueRecoveryActions.id, action.id), eq(issueRecoveryActions.companyId, input.issue.companyId)));
2735
+ const updated = await issuesSvc.update(input.issue.id, {
2736
+ status: "blocked",
2737
+ });
2738
+ if (!updated)
2739
+ return null;
2740
+ const sourceAssigneePreserved = updated.assigneeAgentId === input.issue.assigneeAgentId &&
2741
+ updated.assigneeUserId === input.issue.assigneeUserId;
2742
+ await issuesSvc.addComment(input.issue.id, [
2743
+ "Paperclip exhausted the bounded original-owner disposition repair without a durable source-state change.",
2744
+ "",
2745
+ `- Attempts: ${input.attemptCount}/${DISPOSITION_REPAIR_MAX_ATTEMPTS}`,
2746
+ `- Terminal reason: \`${input.terminalReason}\``,
2747
+ "- Recovery owner: board",
2748
+ "- Source ownership: unchanged; reassignment requires an explicit decision or a policy-defined serious failure.",
2749
+ "",
2750
+ "Next action: repair the liveness disposition or request an explicit source-owner decision.",
2751
+ ].join("\n"), {}, {
2752
+ authorType: "system",
2753
+ presentation: compactRecoveryPresentation("Recovery: disposition repair escalated — source owner preserved"),
2754
+ metadata: recoveryNoticeMetadata({
2755
+ cause: "deliberate_wait_without_target",
2756
+ latestRun: input.latestRun,
2757
+ recoveryActionId: action.id,
2758
+ previousStatus: input.issue.status,
2759
+ recoveryOwner: null,
2760
+ }),
2761
+ });
2762
+ await logActivity(db, {
2763
+ companyId: input.issue.companyId,
2764
+ actorType: "system",
2765
+ actorId: "recovery",
2766
+ agentId: null,
2767
+ runId: input.latestRun?.id ?? null,
2768
+ action: "issue.disposition_repair_escalated",
2769
+ entityType: "issue",
2770
+ entityId: input.issue.id,
2771
+ details: {
2772
+ identifier: input.issue.identifier,
2773
+ status: "blocked",
2774
+ previousStatus: input.issue.status,
2775
+ sourceStateFingerprint: input.fingerprint,
2776
+ attemptCount: input.attemptCount,
2777
+ maxAttempts: DISPOSITION_REPAIR_MAX_ATTEMPTS,
2778
+ terminalReason: input.terminalReason,
2779
+ recoveryActionId: action.id,
2780
+ recoveryOwnerAgentId: null,
2781
+ recoveryOwnerType: "board",
2782
+ routingPolicy: STRANDED_BOARD_ESCALATION_POLICY,
2783
+ sourceAssigneeBefore: {
2784
+ agentId: input.issue.assigneeAgentId,
2785
+ userId: input.issue.assigneeUserId,
2786
+ },
2787
+ sourceAssigneeAfter: {
2788
+ agentId: updated.assigneeAgentId,
2789
+ userId: updated.assigneeUserId,
2790
+ },
2791
+ sourceAssigneePreserved,
2792
+ },
2793
+ });
2794
+ if (!sourceAssigneePreserved) {
2795
+ logger.error({
2796
+ issueId: input.issue.id,
2797
+ beforeAssigneeAgentId: input.issue.assigneeAgentId,
2798
+ afterAssigneeAgentId: updated.assigneeAgentId,
2799
+ beforeAssigneeUserId: input.issue.assigneeUserId,
2800
+ afterAssigneeUserId: updated.assigneeUserId,
2801
+ }, "automatic disposition recovery observed a concurrent source-owner change");
2802
+ }
2803
+ return updated;
2804
+ }
2805
+ async function reconcileDispositionRepair(issue, latestRun, options = {}) {
2806
+ const current = await db
2807
+ .select()
2808
+ .from(issues)
2809
+ .where(and(eq(issues.companyId, issue.companyId), eq(issues.id, issue.id)))
2810
+ .limit(1)
2811
+ .then((rows) => rows[0] ?? null);
2812
+ if (!current || current.status === "done" || current.status === "cancelled")
2813
+ return "skipped";
2814
+ const dependencyWait = await resolveContinuationWaitingOnReview(current);
2815
+ if (dependencyWait) {
2816
+ await resolveDispositionRepairActionAsCovered(current, "dependency_wait_created");
2817
+ return "covered";
2818
+ }
2819
+ const state = await collectDispositionRepairSourceState(db, { issue: current });
2820
+ if (state.hasActiveExecutionPath)
2821
+ return "skipped";
2822
+ if (state.hasDurableWaitingPath) {
2823
+ await resolveDispositionRepairActionAsCovered(current, `durable_path_restored:${state.durablePathReason ?? "unknown"}`);
2824
+ return "covered";
2825
+ }
2826
+ const ownerAgentId = current.assigneeAgentId;
2827
+ const ownerAgent = ownerAgentId ? await getAgent(ownerAgentId) : null;
2828
+ const ownerInvokable = ownerAgent && ownerAgent.companyId === current.companyId
2829
+ ? (await isAgentInvokable(ownerAgent)) && isHeartbeatWakeOnDemandEnabled(ownerAgent)
2830
+ : false;
2831
+ const budgetBlocked = ownerAgentId ? await isInvocationBudgetBlocked(current, ownerAgentId) : true;
2832
+ const previousAttempt = readDispositionRepairAttempt(latestRun);
2833
+ const activeRepairAction = await recoveryActionsSvc.getActiveForIssue(current.companyId, current.id);
2834
+ const runAttempt = previousAttempt?.fingerprint === state.fingerprint
2835
+ ? previousAttempt.attempt
2836
+ : 0;
2837
+ const persistedAttempt = activeRepairAction?.kind === "deliberate_wait_without_target" &&
2838
+ activeRepairAction.fingerprint === state.fingerprint
2839
+ ? activeRepairAction.attemptCount
2840
+ : 0;
2841
+ // Upgrade compatibility: pre-fingerprint continuation parks already spent
2842
+ // attempts against this unchanged source state. Seed the durable counter
2843
+ // from that consecutive legacy history instead of granting five fresh
2844
+ // attempts merely because the recovery-action row did not exist yet.
2845
+ const historicalAttempt = Math.min(DISPOSITION_REPAIR_MAX_ATTEMPTS, Math.max(0, Math.floor(options.historicalAttemptCount ?? 0)));
2846
+ const sameFingerprintAttempt = Math.max(runAttempt, persistedAttempt, historicalAttempt);
2847
+ if (!ownerInvokable || budgetBlocked) {
2848
+ const escalated = await escalateDispositionRepair({
2849
+ issue: current,
2850
+ latestRun,
2851
+ fingerprint: state.fingerprint,
2852
+ attemptCount: sameFingerprintAttempt,
2853
+ terminalReason: !ownerInvokable ? "owner_not_invokable" : "owner_budget_blocked",
2854
+ });
2855
+ return escalated ? "escalated" : "skipped";
2856
+ }
2857
+ if (sameFingerprintAttempt >= DISPOSITION_REPAIR_MAX_ATTEMPTS) {
2858
+ const escalated = await escalateDispositionRepair({
2859
+ issue: current,
2860
+ latestRun,
2861
+ fingerprint: state.fingerprint,
2862
+ attemptCount: sameFingerprintAttempt,
2863
+ terminalReason: "unchanged_source_state_exhausted",
2864
+ });
2865
+ return escalated ? "escalated" : "skipped";
2866
+ }
2867
+ const nextAttempt = sameFingerprintAttempt + 1;
2868
+ const action = await ensureDispositionRepairAction({
2869
+ issue: current,
2870
+ latestRun,
2871
+ fingerprint: state.fingerprint,
2872
+ attemptCount: sameFingerprintAttempt,
2873
+ });
2874
+ const scheduled = await scheduleDispositionRepairAttempt({
2875
+ issue: current,
2876
+ latestRun,
2877
+ action,
2878
+ fingerprint: state.fingerprint,
2879
+ attempt: nextAttempt,
2880
+ });
2881
+ return scheduled ? "queued" : "skipped";
2882
+ }
2548
2883
  async function escalateStrandedAssignedIssue(input) {
2549
2884
  if (isStrandedIssueRecoveryIssue(input.issue)) {
2550
2885
  return escalateStrandedRecoveryIssueInPlace({
@@ -2559,7 +2894,6 @@ export function recoveryService(db, deps) {
2559
2894
  previousStatus: input.previousStatus,
2560
2895
  latestRun: input.latestRun,
2561
2896
  recoveryCause,
2562
- recoveryOwnerAgentId: input.recoveryOwnerAgentId,
2563
2897
  successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
2564
2898
  });
2565
2899
  const isProviderQuotaWait = recoveryCause === "provider_quota" &&
@@ -2577,12 +2911,13 @@ export function recoveryService(db, deps) {
2577
2911
  const updated = await issuesSvc.update(input.issue.id, {
2578
2912
  status: "blocked",
2579
2913
  blockedByIssueIds: blockerIds,
2580
- assigneeAgentId: recoveryAction.ownerAgentId ?? input.issue.assigneeAgentId,
2581
2914
  });
2582
2915
  if (!updated)
2583
2916
  return null;
2584
2917
  if (isProviderQuotaWait)
2585
2918
  return updated;
2919
+ const sourceAssigneePreserved = updated.assigneeAgentId === input.issue.assigneeAgentId &&
2920
+ updated.assigneeUserId === input.issue.assigneeUserId;
2586
2921
  const recoveryOwner = recoveryAction.ownerAgentId ? await getAgent(recoveryAction.ownerAgentId) : null;
2587
2922
  const sourceAssignee = input.issue.assigneeAgentId ? await getAgent(input.issue.assigneeAgentId) : null;
2588
2923
  let notice = null;
@@ -2690,38 +3025,31 @@ export function recoveryService(db, deps) {
2690
3025
  latestRunStatus: input.latestRun?.status ?? null,
2691
3026
  latestRunErrorCode: input.latestRun?.errorCode ?? null,
2692
3027
  recoveryActionId: recoveryAction.id,
3028
+ recoveryOwnerType: recoveryAction.ownerType,
2693
3029
  recoveryOwnerAgentId: recoveryAction.ownerAgentId,
2694
3030
  previousOwnerAgentId: recoveryAction.previousOwnerAgentId,
2695
3031
  returnOwnerAgentId: recoveryAction.returnOwnerAgentId,
3032
+ routingPolicy: parseObject(recoveryAction.evidence).routingPolicy ?? null,
3033
+ sourceAssigneeBefore: {
3034
+ agentId: input.issue.assigneeAgentId,
3035
+ userId: input.issue.assigneeUserId,
3036
+ },
3037
+ sourceAssigneeAfter: {
3038
+ agentId: updated.assigneeAgentId,
3039
+ userId: updated.assigneeUserId,
3040
+ },
3041
+ sourceAssigneePreserved,
2696
3042
  blockerIssueIds: blockerIds,
2697
3043
  },
2698
3044
  });
2699
- await enqueueSourceScopedStrandedRecoveryWake({
2700
- action: recoveryAction,
2701
- issue: input.issue,
2702
- latestRun: input.latestRun,
2703
- recoveryCause,
2704
- });
2705
- if (recoveryAction.ownerAgentId && recoveryAction.ownerAgentId === input.issue.assigneeAgentId) {
2706
- const [currentIssue] = await db
2707
- .select({
2708
- status: issues.status,
2709
- assigneeAgentId: issues.assigneeAgentId,
2710
- })
2711
- .from(issues)
2712
- .where(eq(issues.id, input.issue.id))
2713
- .limit(1);
2714
- if (currentIssue &&
2715
- (currentIssue.status !== "blocked" ||
2716
- currentIssue.assigneeAgentId !== recoveryAction.ownerAgentId)) {
2717
- const reblocked = await issuesSvc.update(input.issue.id, {
2718
- status: "blocked",
2719
- blockedByIssueIds: blockerIds,
2720
- assigneeAgentId: recoveryAction.ownerAgentId,
2721
- });
2722
- if (reblocked)
2723
- return reblocked;
2724
- }
3045
+ if (!sourceAssigneePreserved) {
3046
+ logger.error({
3047
+ issueId: input.issue.id,
3048
+ beforeAssigneeAgentId: input.issue.assigneeAgentId,
3049
+ afterAssigneeAgentId: updated.assigneeAgentId,
3050
+ beforeAssigneeUserId: input.issue.assigneeUserId,
3051
+ afterAssigneeUserId: updated.assigneeUserId,
3052
+ }, "automatic stranded recovery observed a concurrent source-owner change");
2725
3053
  }
2726
3054
  return updated;
2727
3055
  }
@@ -2845,6 +3173,7 @@ export function recoveryService(db, deps) {
2845
3173
  assignmentDispatched: 0,
2846
3174
  dispatchRequeued: 0,
2847
3175
  continuationRequeued: 0,
3176
+ dispositionRepairRequeued: 0,
2848
3177
  productiveContinuationObserved: 0,
2849
3178
  successfulContinuationObserved: 0,
2850
3179
  orphanBlockersAssigned: 0,
@@ -2874,12 +3203,40 @@ export function recoveryService(db, deps) {
2874
3203
  result.skipped += 1;
2875
3204
  continue;
2876
3205
  }
3206
+ let latestRun = await getLatestIssueRun(issue.companyId, issue.id);
2877
3207
  const agent = await getAgent(agentId);
2878
3208
  const agentInvokable = agent && agent.companyId === issue.companyId
2879
3209
  ? await isAgentInvokable(agent)
2880
3210
  : false;
2881
3211
  if (issue.status !== "in_review" && !agentInvokable) {
2882
- result.skipped += 1;
3212
+ const classification = classifyContinuationFailure(latestRun);
3213
+ if (classification.kind === "deliberate_wait_without_target" ||
3214
+ readDispositionRepairAttempt(latestRun)) {
3215
+ const outcome = await reconcileDispositionRepair(issue, latestRun);
3216
+ if (outcome === "escalated") {
3217
+ result.escalated += 1;
3218
+ result.issueIds.push(issue.id);
3219
+ }
3220
+ else {
3221
+ result.skipped += 1;
3222
+ }
3223
+ }
3224
+ else {
3225
+ const updated = await escalateStrandedAssignedIssue({
3226
+ issue,
3227
+ previousStatus: issue.status,
3228
+ latestRun,
3229
+ comment: "Paperclip cannot safely continue automatic recovery because the original assignee is not invokable. " +
3230
+ "The source assignment is unchanged and the board must choose the next action.",
3231
+ });
3232
+ if (updated) {
3233
+ result.escalated += 1;
3234
+ result.issueIds.push(issue.id);
3235
+ }
3236
+ else {
3237
+ result.skipped += 1;
3238
+ }
3239
+ }
2883
3240
  continue;
2884
3241
  }
2885
3242
  if (await hasActiveExecutionPath(issue.companyId, issue.id, issue.status === "in_review" ? agentId : null)) {
@@ -2894,11 +3251,44 @@ export function recoveryService(db, deps) {
2894
3251
  result.skipped += 1;
2895
3252
  continue;
2896
3253
  }
2897
- let latestRun = await getLatestIssueRun(issue.companyId, issue.id);
2898
3254
  if (isOperatorCancelledRun(latestRun)) {
2899
3255
  result.operatorCancelExempted += 1;
2900
3256
  continue;
2901
3257
  }
3258
+ if (await isInvocationBudgetBlocked(issue, agentId)) {
3259
+ const classification = classifyContinuationFailure(latestRun);
3260
+ if (classification.kind === "deliberate_wait_without_target" ||
3261
+ readDispositionRepairAttempt(latestRun)) {
3262
+ const outcome = await reconcileDispositionRepair(issue, latestRun);
3263
+ if (outcome === "escalated") {
3264
+ result.escalated += 1;
3265
+ result.issueIds.push(issue.id);
3266
+ }
3267
+ else {
3268
+ result.skipped += 1;
3269
+ }
3270
+ }
3271
+ else {
3272
+ const updated = await escalateStrandedAssignedIssue({
3273
+ issue,
3274
+ previousStatus: issue.status,
3275
+ latestRun,
3276
+ recoveryCause: issue.status === "in_review"
3277
+ ? EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON
3278
+ : undefined,
3279
+ comment: "Paperclip cannot safely continue automatic recovery because the original recovery target is over budget. " +
3280
+ "The source assignment is unchanged and the board must choose the next action.",
3281
+ });
3282
+ if (updated) {
3283
+ result.escalated += 1;
3284
+ result.issueIds.push(issue.id);
3285
+ }
3286
+ else {
3287
+ result.skipped += 1;
3288
+ }
3289
+ }
3290
+ continue;
3291
+ }
2902
3292
  if (latestRun?.status === "succeeded" && await hasPersistedDurableWaitPath(issue)) {
2903
3293
  result.skipped += 1;
2904
3294
  continue;
@@ -2978,6 +3368,7 @@ export function recoveryService(db, deps) {
2978
3368
  ? acceptedContinuationInteraction.resolvedAt ?? acceptedContinuationInteraction.updatedAt
2979
3369
  : null;
2980
3370
  if (acceptedContinuationInteraction && acceptedInteractionResolvedAt && !pendingExecutionState) {
3371
+ const legacyReviewParkAttempts = await summarizeRecentContinuationRetries(issue.companyId, issue.id, agentId, CONTINUATION_WAITING_ON_REVIEW_ERROR_CODE, acceptedInteractionResolvedAt);
2981
3372
  const successfulRunSinceResolution = await hasSuccessfulIssueRunSince(issue.companyId, issue.id, agentId, acceptedInteractionResolvedAt, acceptedContinuationInteraction.id);
2982
3373
  if (!successfulRunSinceResolution) {
2983
3374
  if (!agentInvokable) {
@@ -2993,7 +3384,32 @@ export function recoveryService(db, deps) {
2993
3384
  continue;
2994
3385
  }
2995
3386
  const latestPostResolutionRun = await getLatestIssueRunSince(issue.companyId, issue.id, agentId, acceptedInteractionResolvedAt);
2996
- const { consecutive } = await summarizeRecentContinuationRetries(issue.companyId, issue.id, agentId, CONTINUATION_WAITING_ON_REVIEW_ERROR_CODE, acceptedInteractionResolvedAt);
3387
+ if (classifyContinuationFailure(latestPostResolutionRun).kind ===
3388
+ "deliberate_wait_without_target") {
3389
+ const resolved = await resolveContinuationWaitingOnReview(issue);
3390
+ if (resolved) {
3391
+ result.waitingOnReviewResolved += 1;
3392
+ result.issueIds.push(issue.id);
3393
+ continue;
3394
+ }
3395
+ const outcome = await reconcileDispositionRepair(issue, latestPostResolutionRun, {
3396
+ historicalAttemptCount: legacyReviewParkAttempts.consecutive,
3397
+ });
3398
+ if (outcome === "queued") {
3399
+ result.continuationRequeued += 1;
3400
+ result.dispositionRepairRequeued += 1;
3401
+ result.issueIds.push(issue.id);
3402
+ }
3403
+ else if (outcome === "escalated") {
3404
+ result.escalated += 1;
3405
+ result.issueIds.push(issue.id);
3406
+ }
3407
+ else {
3408
+ result.skipped += 1;
3409
+ }
3410
+ continue;
3411
+ }
3412
+ const { consecutive } = legacyReviewParkAttempts;
2997
3413
  if (consecutive >= INTERACTION_CONTINUATION_REQUEUE_MAX_ATTEMPTS && latestPostResolutionRun) {
2998
3414
  const resolved = await resolveContinuationWaitingOnReview(issue);
2999
3415
  if (resolved) {
@@ -3058,7 +3474,6 @@ export function recoveryService(db, deps) {
3058
3474
  latestRun: participantLatestRun,
3059
3475
  notice: buildExecutionReviewParticipantUnavailableNoticeSeed(),
3060
3476
  recoveryCause: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON,
3061
- recoveryOwnerAgentId: participantAgentId,
3062
3477
  });
3063
3478
  if (updated) {
3064
3479
  result.escalated += 1;
@@ -3098,7 +3513,6 @@ export function recoveryService(db, deps) {
3098
3513
  previousStatus: "in_review",
3099
3514
  latestRun: participantLatestRun,
3100
3515
  recoveryCause: "configuration_incomplete",
3101
- recoveryOwnerAgentId: participantAgentId,
3102
3516
  comment: "Paperclip classified the active review participant's latest adapter failure as " +
3103
3517
  "`configuration_incomplete`. Moving the issue to `blocked` with the configuration fix " +
3104
3518
  "recorded instead of repeatedly requeueing the reviewer.",
@@ -3120,7 +3534,6 @@ export function recoveryService(db, deps) {
3120
3534
  latestRun: participantLatestRun,
3121
3535
  notice: buildExecutionReviewParticipantUnavailableNoticeSeed(),
3122
3536
  recoveryCause: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON,
3123
- recoveryOwnerAgentId: participantAgentId,
3124
3537
  });
3125
3538
  if (updated) {
3126
3539
  result.escalated += 1;
@@ -3138,7 +3551,6 @@ export function recoveryService(db, deps) {
3138
3551
  latestRun: participantLatestRun,
3139
3552
  notice: buildExecutionReviewParticipantRecoveryNoticeSeed(),
3140
3553
  recoveryCause: EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON,
3141
- recoveryOwnerAgentId: participantAgentId,
3142
3554
  });
3143
3555
  if (updated) {
3144
3556
  result.escalated += 1;
@@ -3251,6 +3663,22 @@ export function recoveryService(db, deps) {
3251
3663
  result.skipped += 1;
3252
3664
  continue;
3253
3665
  }
3666
+ if (readDispositionRepairAttempt(latestRun)) {
3667
+ const outcome = await reconcileDispositionRepair(issue, latestRun);
3668
+ if (outcome === "queued") {
3669
+ result.continuationRequeued += 1;
3670
+ result.dispositionRepairRequeued += 1;
3671
+ result.issueIds.push(issue.id);
3672
+ }
3673
+ else if (outcome === "escalated") {
3674
+ result.escalated += 1;
3675
+ result.issueIds.push(issue.id);
3676
+ }
3677
+ else {
3678
+ result.skipped += 1;
3679
+ }
3680
+ continue;
3681
+ }
3254
3682
  const handoffEvidence = isExhaustedSuccessfulRunHandoff(latestRun);
3255
3683
  if (handoffEvidence) {
3256
3684
  if (isPluginManagedIssueLifecycle(issue)) {
@@ -3339,6 +3767,20 @@ export function recoveryService(db, deps) {
3339
3767
  result.issueIds.push(issue.id);
3340
3768
  continue;
3341
3769
  }
3770
+ const outcome = await reconcileDispositionRepair(issue, latestRun);
3771
+ if (outcome === "queued") {
3772
+ result.continuationRequeued += 1;
3773
+ result.dispositionRepairRequeued += 1;
3774
+ result.issueIds.push(issue.id);
3775
+ }
3776
+ else if (outcome === "escalated") {
3777
+ result.escalated += 1;
3778
+ result.issueIds.push(issue.id);
3779
+ }
3780
+ else {
3781
+ result.skipped += 1;
3782
+ }
3783
+ continue;
3342
3784
  }
3343
3785
  if (classification.kind === "non_retryable") {
3344
3786
  const updated = await escalateStrandedAssignedIssue({
@@ -3422,6 +3864,12 @@ export function recoveryService(db, deps) {
3422
3864
  result.orphanBlockersAssigned = orphanBlockerRecovery.assigned;
3423
3865
  result.skipped += orphanBlockerRecovery.skipped;
3424
3866
  result.issueIds.push(...orphanBlockerRecovery.issueIds);
3867
+ const activeRecovery = await reconcileActiveRecoveryActions();
3868
+ result.continuationRequeued += activeRecovery.requeued;
3869
+ result.escalated += activeRecovery.escalated;
3870
+ result.skipped += activeRecovery.skipped;
3871
+ result.issueIds.push(...activeRecovery.issueIds);
3872
+ result.issueIds = [...new Set(result.issueIds)];
3425
3873
  return result;
3426
3874
  }
3427
3875
  async function collectIssueGraphLivenessFindings() {
@@ -3531,9 +3979,13 @@ export function recoveryService(db, deps) {
3531
3979
  ? []
3532
3980
  : db
3533
3981
  .select({
3982
+ id: issueRecoveryActions.id,
3534
3983
  companyId: issueRecoveryActions.companyId,
3535
3984
  issueId: issueRecoveryActions.sourceIssueId,
3536
3985
  status: issueRecoveryActions.status,
3986
+ ownerType: issueRecoveryActions.ownerType,
3987
+ ownerAgentId: issueRecoveryActions.ownerAgentId,
3988
+ ownerUserId: issueRecoveryActions.ownerUserId,
3537
3989
  })
3538
3990
  .from(issueRecoveryActions)
3539
3991
  .where(and(inArray(issueRecoveryActions.status, ["active", "escalated"]), inArray(issueRecoveryActions.sourceIssueId, issueIdsUnderAnalysis)));
@@ -3566,6 +4018,20 @@ export function recoveryService(db, deps) {
3566
4018
  status: row.status,
3567
4019
  }];
3568
4020
  });
4021
+ const liveRecoveryActionIds = new Set();
4022
+ for (const row of activeRunRows) {
4023
+ const recoveryActionId = readNonEmptyString(parseObject(row.contextSnapshot).recoveryActionId);
4024
+ if (recoveryActionId)
4025
+ liveRecoveryActionIds.add(recoveryActionId);
4026
+ }
4027
+ for (const row of wakeRows) {
4028
+ const recoveryActionId = readNonEmptyString(parseObject(row.payload).recoveryActionId);
4029
+ if (recoveryActionId)
4030
+ liveRecoveryActionIds.add(recoveryActionId);
4031
+ }
4032
+ const healthyRecoveryActions = recoveryActionRows.filter((row) => (row.status === "escalated" && row.ownerType === "board") ||
4033
+ Boolean(row.ownerUserId) ||
4034
+ (Boolean(row.ownerAgentId) && liveRecoveryActionIds.has(row.id)));
3569
4035
  return classifyIssueGraphLiveness({
3570
4036
  issues: issueRows,
3571
4037
  relations: relationRows,
@@ -3589,7 +4055,7 @@ export function recoveryService(db, deps) {
3589
4055
  })),
3590
4056
  pendingInteractions: interactionRows,
3591
4057
  pendingApprovals: approvalRows,
3592
- openRecoveryIssues: openRecoveryIssues.concat(recoveryActionRows),
4058
+ openRecoveryIssues: openRecoveryIssues.concat(healthyRecoveryActions),
3593
4059
  now: new Date(),
3594
4060
  });
3595
4061
  }
@@ -4096,6 +4562,7 @@ export function recoveryService(db, deps) {
4096
4562
  companyId: issues.companyId,
4097
4563
  identifier: issues.identifier,
4098
4564
  assigneeAgentId: issues.assigneeAgentId,
4565
+ blockedTransitionAt: issues.blockedTransitionAt,
4099
4566
  totalCount: sql `count(*) over()::int`,
4100
4567
  })
4101
4568
  .from(issueRelations)
@@ -4110,6 +4577,7 @@ export function recoveryService(db, deps) {
4110
4577
  companyId: issues.companyId,
4111
4578
  identifier: issues.identifier,
4112
4579
  assigneeAgentId: issues.assigneeAgentId,
4580
+ blockedTransitionAt: issues.blockedTransitionAt,
4113
4581
  totalCount: sql `count(*) over()::int`,
4114
4582
  })
4115
4583
  .from(issues)
@@ -4162,17 +4630,21 @@ export function recoveryService(db, deps) {
4162
4630
  result.notReadySkipped += 1;
4163
4631
  continue;
4164
4632
  }
4165
- const idempotencyKeys = readiness.blockerIssueIds.map((blockerIssueId) => buildIssueBlockersResolvedWakeIdempotencyKey({
4166
- dependentIssueId: candidate.id,
4167
- resolvedBlockerIssueId: blockerIssueId,
4168
- }));
4169
- const idempotencyKey = buildIssueBlockersResolvedWakeIdempotencyKey({
4633
+ // Level-triggered dedup: key on the full blocker set (the current ready
4634
+ // state), not on any single resolved edge. An older completed per-edge
4635
+ // wake for an earlier partial resolution has a different key, so it does
4636
+ // not suppress this wake. The shared helper still suppresses a duplicate
4637
+ // wake for the SAME ready state, which bounds reconciliation.
4638
+ const idempotencyKey = buildIssueBlockersResolvedWakeStateKey({
4170
4639
  dependentIssueId: candidate.id,
4171
- resolvedBlockerIssueId,
4640
+ blockerIssueIds: readiness.blockerIssueIds,
4641
+ blockedTransitionAt: candidate.blockedTransitionAt,
4172
4642
  });
4173
- const existingWake = await findExistingIssueBlockersResolvedWakeForAnyKey(db, {
4643
+ const existingWake = await findExistingIssueBlockersResolvedWakeForReadyState(db, {
4174
4644
  companyId,
4175
- idempotencyKeys,
4645
+ dependentIssueId: candidate.id,
4646
+ blockerIssueIds: readiness.blockerIssueIds,
4647
+ blockedTransitionAt: candidate.blockedTransitionAt,
4176
4648
  });
4177
4649
  if (existingWake) {
4178
4650
  result.existingWakeSkipped += 1;