@evermore.work/server 2026.511.0-canary.2 → 2026.513.0-canary.0

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 (160) 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/registry.d.ts.map +1 -1
  5. package/dist/adapters/registry.js +20 -2
  6. package/dist/adapters/registry.js.map +1 -1
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +1 -3
  9. package/dist/app.js.map +1 -1
  10. package/dist/home-paths.d.ts +2 -4
  11. package/dist/home-paths.d.ts.map +1 -1
  12. package/dist/home-paths.js +8 -35
  13. package/dist/home-paths.js.map +1 -1
  14. package/dist/routes/agents.d.ts.map +1 -1
  15. package/dist/routes/agents.js +8 -2
  16. package/dist/routes/agents.js.map +1 -1
  17. package/dist/routes/issues.d.ts.map +1 -1
  18. package/dist/routes/issues.js +172 -11
  19. package/dist/routes/issues.js.map +1 -1
  20. package/dist/routes/plugins.d.ts.map +1 -1
  21. package/dist/routes/plugins.js +7 -21
  22. package/dist/routes/plugins.js.map +1 -1
  23. package/dist/services/environment-execution-target.d.ts.map +1 -1
  24. package/dist/services/environment-execution-target.js +2 -0
  25. package/dist/services/environment-execution-target.js.map +1 -1
  26. package/dist/services/environment-runtime.d.ts.map +1 -1
  27. package/dist/services/environment-runtime.js +30 -4
  28. package/dist/services/environment-runtime.js.map +1 -1
  29. package/dist/services/heartbeat.d.ts.map +1 -1
  30. package/dist/services/heartbeat.js +8 -3
  31. package/dist/services/heartbeat.js.map +1 -1
  32. package/dist/services/index.d.ts +1 -0
  33. package/dist/services/index.d.ts.map +1 -1
  34. package/dist/services/index.js +1 -0
  35. package/dist/services/index.js.map +1 -1
  36. package/dist/services/issue-recovery-actions.d.ts +40 -0
  37. package/dist/services/issue-recovery-actions.d.ts.map +1 -0
  38. package/dist/services/issue-recovery-actions.js +204 -0
  39. package/dist/services/issue-recovery-actions.js.map +1 -0
  40. package/dist/services/issues.d.ts +20 -0
  41. package/dist/services/issues.d.ts.map +1 -1
  42. package/dist/services/issues.js +160 -10
  43. package/dist/services/issues.js.map +1 -1
  44. package/dist/services/plugin-config-validator.js +1 -1
  45. package/dist/services/plugin-config-validator.js.map +1 -1
  46. package/dist/services/plugin-dev-watcher.d.ts.map +1 -1
  47. package/dist/services/plugin-dev-watcher.js +7 -2
  48. package/dist/services/plugin-dev-watcher.js.map +1 -1
  49. package/dist/services/plugin-environment-driver.d.ts +2 -0
  50. package/dist/services/plugin-environment-driver.d.ts.map +1 -1
  51. package/dist/services/plugin-environment-driver.js +2 -0
  52. package/dist/services/plugin-environment-driver.js.map +1 -1
  53. package/dist/services/plugin-host-services.d.ts.map +1 -1
  54. package/dist/services/plugin-host-services.js +31 -4
  55. package/dist/services/plugin-host-services.js.map +1 -1
  56. package/dist/services/plugin-loader.d.ts +2 -2
  57. package/dist/services/plugin-loader.js +3 -3
  58. package/dist/services/plugin-loader.js.map +1 -1
  59. package/dist/services/plugin-local-folders.d.ts +1 -0
  60. package/dist/services/plugin-local-folders.d.ts.map +1 -1
  61. package/dist/services/plugin-local-folders.js +45 -0
  62. package/dist/services/plugin-local-folders.js.map +1 -1
  63. package/dist/services/plugin-managed-agents.d.ts.map +1 -1
  64. package/dist/services/plugin-managed-agents.js +52 -9
  65. package/dist/services/plugin-managed-agents.js.map +1 -1
  66. package/dist/services/plugin-managed-skills.d.ts +14 -0
  67. package/dist/services/plugin-managed-skills.d.ts.map +1 -0
  68. package/dist/services/plugin-managed-skills.js +264 -0
  69. package/dist/services/plugin-managed-skills.js.map +1 -0
  70. package/dist/services/plugin-registry.d.ts +1 -1
  71. package/dist/services/plugin-registry.js +1 -1
  72. package/dist/services/recovery/service.d.ts.map +1 -1
  73. package/dist/services/recovery/service.js +225 -63
  74. package/dist/services/recovery/service.js.map +1 -1
  75. package/dist/services/recovery/successful-run-handoff.d.ts +2 -0
  76. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  77. package/dist/services/recovery/successful-run-handoff.js +8 -1
  78. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  79. package/dist/services/recovery/successful-run-handoff.test.js +5 -2
  80. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  81. package/package.json +14 -13
  82. package/skills/evermore-converting-plans-to-tasks/SKILL.md +1 -1
  83. package/skills/evermore-create-plugin/SKILL.md +95 -42
  84. package/skills/terminal-bench-loop/SKILL.md +4 -4
  85. package/ui-dist/assets/{_basePickBy-CbmMN1RH.js → _basePickBy-k9wCTyNE.js} +1 -1
  86. package/ui-dist/assets/{_baseUniq-D5Spi0LU.js → _baseUniq-gqdJ9_M4.js} +1 -1
  87. package/ui-dist/assets/{arc-7n9-L5yV.js → arc-CZ5fTbi1.js} +1 -1
  88. package/ui-dist/assets/{architectureDiagram-VXUJARFQ-CaCkwTrk.js → architectureDiagram-VXUJARFQ-C1PmJabO.js} +1 -1
  89. package/ui-dist/assets/{blockDiagram-VD42YOAC-C_gMX5sE.js → blockDiagram-VD42YOAC-C31GBmZl.js} +1 -1
  90. package/ui-dist/assets/{c4Diagram-YG6GDRKO-DsIPHY3x.js → c4Diagram-YG6GDRKO-CXALeUm_.js} +1 -1
  91. package/ui-dist/assets/channel-BscBiA2B.js +1 -0
  92. package/ui-dist/assets/{chunk-4BX2VUAB-ConiONwq.js → chunk-4BX2VUAB-CI9ame_l.js} +1 -1
  93. package/ui-dist/assets/{chunk-55IACEB6-rN7J69fU.js → chunk-55IACEB6-X7CMs2bY.js} +1 -1
  94. package/ui-dist/assets/{chunk-B4BG7PRW-CWrv2W6l.js → chunk-B4BG7PRW-CRFGJGGj.js} +1 -1
  95. package/ui-dist/assets/{chunk-DI55MBZ5-DxMrfgRx.js → chunk-DI55MBZ5-pFj4tWf4.js} +1 -1
  96. package/ui-dist/assets/{chunk-FMBD7UC4-C3IEVrmU.js → chunk-FMBD7UC4-DwtEpJHS.js} +1 -1
  97. package/ui-dist/assets/{chunk-QN33PNHL-mY7dIxLL.js → chunk-QN33PNHL-O_GMkM_d.js} +1 -1
  98. package/ui-dist/assets/{chunk-QZHKN3VN-C57cpB6v.js → chunk-QZHKN3VN-DVQoxgmw.js} +1 -1
  99. package/ui-dist/assets/{chunk-TZMSLE5B-Bznk-429.js → chunk-TZMSLE5B-nYT9Oclu.js} +1 -1
  100. package/ui-dist/assets/classDiagram-2ON5EDUG-DRC1sToY.js +1 -0
  101. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-DRC1sToY.js +1 -0
  102. package/ui-dist/assets/clone-djKk5h1v.js +1 -0
  103. package/ui-dist/assets/{cose-bilkent-S5V4N54A-WVNEjOkH.js → cose-bilkent-S5V4N54A-DrymQvjx.js} +1 -1
  104. package/ui-dist/assets/{dagre-6UL2VRFP-B5zFofAq.js → dagre-6UL2VRFP-BSMS-awI.js} +1 -1
  105. package/ui-dist/assets/{diagram-PSM6KHXK-B5uBquxq.js → diagram-PSM6KHXK-CKA4xpzT.js} +1 -1
  106. package/ui-dist/assets/{diagram-QEK2KX5R--vjH56tj.js → diagram-QEK2KX5R-D224Hw3l.js} +1 -1
  107. package/ui-dist/assets/{diagram-S2PKOQOG-BrRUS4fb.js → diagram-S2PKOQOG-QSLRBXq3.js} +1 -1
  108. package/ui-dist/assets/{erDiagram-Q2GNP2WA-DEz4OctV.js → erDiagram-Q2GNP2WA-BMb1ltru.js} +1 -1
  109. package/ui-dist/assets/{flowDiagram-NV44I4VS-B15VynOf.js → flowDiagram-NV44I4VS-DFO86CtG.js} +1 -1
  110. package/ui-dist/assets/{ganttDiagram-JELNMOA3-DMZlAptH.js → ganttDiagram-JELNMOA3-DqGmEMSm.js} +1 -1
  111. package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-CcxPbN5-.js → gitGraphDiagram-V2S2FVAM-C58nf2i-.js} +1 -1
  112. package/ui-dist/assets/{graph-BZIdisjp.js → graph-DosSwXzr.js} +1 -1
  113. package/ui-dist/assets/{index-ZU3cQRJ_.js → index-3RqTP3Wv.js} +1 -1
  114. package/ui-dist/assets/{index-CMFXrEKF.js → index-4nkrIR74.js} +154 -153
  115. package/ui-dist/assets/index-BNTUqCLB.css +1 -0
  116. package/ui-dist/assets/{index-JK3bQafC.js → index-BWnvh-op.js} +1 -1
  117. package/ui-dist/assets/{index-C_B62ZrS.js → index-BbNMaRBx.js} +1 -1
  118. package/ui-dist/assets/{index-D9_Jej4N.js → index-BfcmN5ZJ.js} +1 -1
  119. package/ui-dist/assets/{index-CLpI7eai.js → index-BgdsvI-o.js} +1 -1
  120. package/ui-dist/assets/{index-DPcKcWrz.js → index-BrTEhQHs.js} +1 -1
  121. package/ui-dist/assets/{index-CguyMMMe.js → index-C37UJrtl.js} +1 -1
  122. package/ui-dist/assets/{index-B8AK13gN.js → index-Ci5TGFP3.js} +1 -1
  123. package/ui-dist/assets/{index-CBsefjme.js → index-CqyFdYQi.js} +1 -1
  124. package/ui-dist/assets/{index-jrAeI9QO.js → index-Cu1tIzzo.js} +1 -1
  125. package/ui-dist/assets/{index-QcuAFhJE.js → index-CvQSsdq1.js} +1 -1
  126. package/ui-dist/assets/{index-CtbPMroJ.js → index-D6e8ieSZ.js} +1 -1
  127. package/ui-dist/assets/{index-Caiv3tFo.js → index-D9fEHHhi.js} +1 -1
  128. package/ui-dist/assets/{index-Br9DlaVa.js → index-DU3gaUtN.js} +1 -1
  129. package/ui-dist/assets/{index-B9gUF7Qo.js → index-DozqwUGT.js} +1 -1
  130. package/ui-dist/assets/{index-B2TuAYBk.js → index-DsbA9pOP.js} +1 -1
  131. package/ui-dist/assets/{index-BEgNCEf4.js → index-Dz0v0MS4.js} +1 -1
  132. package/ui-dist/assets/{index-DChtU7Y7.js → index-Oyl53tyz.js} +1 -1
  133. package/ui-dist/assets/{index-BXCWvVzU.js → index-ae2EPSbU.js} +1 -1
  134. package/ui-dist/assets/{index-C0ActS50.js → index-cxYbeASY.js} +1 -1
  135. package/ui-dist/assets/{index-CuiPTihA.js → index-gB4esTR_.js} +1 -1
  136. package/ui-dist/assets/{index-DZAJwG4j.js → index-uX_QygSO.js} +1 -1
  137. package/ui-dist/assets/{infoDiagram-HS3SLOUP-C2wYobHy.js → infoDiagram-HS3SLOUP-fH1KZK8Y.js} +1 -1
  138. package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-BtfEQPBF.js → journeyDiagram-XKPGCS4Q-Bs6gBpT7.js} +1 -1
  139. package/ui-dist/assets/{kanban-definition-3W4ZIXB7-B4U_W3OQ.js → kanban-definition-3W4ZIXB7-CjXiZk7z.js} +1 -1
  140. package/ui-dist/assets/{layout-ZIWcjZTf.js → layout-BNsoNCJt.js} +1 -1
  141. package/ui-dist/assets/{linear-D7hp1mR1.js → linear-D4_nHGWF.js} +1 -1
  142. package/ui-dist/assets/{mermaid.core-npjWckTB.js → mermaid.core-QfFHliw3.js} +4 -4
  143. package/ui-dist/assets/{mindmap-definition-VGOIOE7T-Cno5l-kW.js → mindmap-definition-VGOIOE7T-B_dKQ4Qp.js} +1 -1
  144. package/ui-dist/assets/{pieDiagram-ADFJNKIX-Ce0R1e38.js → pieDiagram-ADFJNKIX-DflXWaiu.js} +1 -1
  145. package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-BNZ-4uth.js → quadrantDiagram-AYHSOK5B-Bt3RB7oE.js} +1 -1
  146. package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-BIdjI2CO.js → requirementDiagram-UZGBJVZJ-DxsHOEgt.js} +1 -1
  147. package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-hYF-IP2X.js → sankeyDiagram-TZEHDZUN-DilTmstc.js} +1 -1
  148. package/ui-dist/assets/{sequenceDiagram-WL72ISMW-CBreF6um.js → sequenceDiagram-WL72ISMW-BYRLNxvZ.js} +1 -1
  149. package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DOje8UoB.js → stateDiagram-FKZM4ZOC-D0jvEPoV.js} +1 -1
  150. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-D_kHbugj.js +1 -0
  151. package/ui-dist/assets/{timeline-definition-IT6M3QCI-Da_eKz_x.js → timeline-definition-IT6M3QCI-KyT1QpWq.js} +1 -1
  152. package/ui-dist/assets/{treemap-GDKQZRPO-BAFi4Uf3.js → treemap-GDKQZRPO-BEa3HnWS.js} +1 -1
  153. package/ui-dist/assets/{xychartDiagram-PRI3JC2R-Bc_vIoPh.js → xychartDiagram-PRI3JC2R-BUxqP2No.js} +1 -1
  154. package/ui-dist/index.html +2 -2
  155. package/ui-dist/assets/channel-zaN5fJQ5.js +0 -1
  156. package/ui-dist/assets/classDiagram-2ON5EDUG-CZ5AdJ2X.js +0 -1
  157. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CZ5AdJ2X.js +0 -1
  158. package/ui-dist/assets/clone-CgK10jEJ.js +0 -1
  159. package/ui-dist/assets/index-B7sbRkCH.css +0 -1
  160. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-BtMV8Rt1.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { and, asc, desc, eq, gt, inArray, isNull, notInArray, sql } from "drizzle-orm";
2
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, } from "@evermore.work/shared";
3
- import { agents, agentWakeupRequests, approvals, companies, heartbeatRunEvents, heartbeatRunWatchdogDecisions, heartbeatRuns, issueApprovals, issueRelations, issueThreadInteractions, issues, } from "@evermore.work/db";
3
+ import { agents, agentWakeupRequests, approvals, companies, issueComments, heartbeatRunEvents, heartbeatRunWatchdogDecisions, heartbeatRuns, issueApprovals, issueRecoveryActions, issueRelations, issueThreadInteractions, issues, } from "@evermore.work/db";
4
4
  import { parseObject, asBoolean, asNumber } from "../../adapters/utils.js";
5
5
  import { runningProcesses } from "../../adapters/index.js";
6
6
  import { forbidden, notFound } from "../../errors.js";
@@ -10,10 +10,11 @@ import { redactSensitiveText } from "../../redaction.js";
10
10
  import { logActivity } from "../activity-log.js";
11
11
  import { budgetService } from "../budgets.js";
12
12
  import { instanceSettingsService } from "../instance-settings.js";
13
+ import { issueRecoveryActionService } from "../issue-recovery-actions.js";
13
14
  import { issueTreeControlService } from "../issue-tree-control.js";
14
15
  import { issueService } from "../issues.js";
15
16
  import { getRunLogStore } from "../run-log-store.js";
16
- import { DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, SUCCESSFUL_RUN_MISSING_STATE_REASON, buildSuccessfulRunHandoffExhaustedNotice, } from "./successful-run-handoff.js";
17
+ import { DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS, FINISH_SUCCESSFUL_RUN_HANDOFF_REASON, SUCCESSFUL_RUN_MISSING_STATE_REASON, buildSuccessfulRunHandoffExhaustedNotice, noticeMetadataReferencesRecoveryAction, } from "./successful-run-handoff.js";
17
18
  import { RECOVERY_ORIGIN_KINDS, buildIssueGraphLivenessLeafKey, isStrandedIssueRecoveryOriginKind, parseIssueGraphLivenessIncidentKey, } from "./origins.js";
18
19
  import { classifyIssueGraphLiveness, } from "./issue-graph-liveness.js";
19
20
  import { recoveryAssigneeAdapterOverrides, withRecoveryModelProfileHint, } from "./model-profile-hint.js";
@@ -242,6 +243,7 @@ function buildLivenessOriginalIssueComment(finding, escalation) {
242
243
  }
243
244
  export function recoveryService(db, deps) {
244
245
  const issuesSvc = issueService(db);
246
+ const recoveryActionsSvc = issueRecoveryActionService(db);
245
247
  const treeControlSvc = issueTreeControlService(db);
246
248
  const budgets = budgetService(db);
247
249
  const instanceSettings = instanceSettingsService(db);
@@ -1228,6 +1230,111 @@ export function recoveryService(db, deps) {
1228
1230
  });
1229
1231
  return recovery;
1230
1232
  }
1233
+ function strandedRecoveryActionKind(cause) {
1234
+ return cause === SUCCESSFUL_RUN_MISSING_STATE_REASON
1235
+ ? "missing_disposition"
1236
+ : "stranded_assigned_issue";
1237
+ }
1238
+ function strandedRecoveryActionFingerprint(input) {
1239
+ return [
1240
+ "source_scoped_recovery",
1241
+ input.issue.companyId,
1242
+ input.issue.id,
1243
+ input.recoveryCause,
1244
+ ].join(":");
1245
+ }
1246
+ function buildStrandedRecoveryActionEvidence(input) {
1247
+ const context = parseObject(input.latestRun?.contextSnapshot);
1248
+ return {
1249
+ sourceIssueId: input.issue.id,
1250
+ sourceIdentifier: input.issue.identifier,
1251
+ previousStatus: input.previousStatus,
1252
+ latestIssueStatus: input.issue.status,
1253
+ latestRunId: input.latestRun?.id ?? null,
1254
+ latestRunStatus: input.latestRun?.status ?? null,
1255
+ latestRunErrorCode: input.latestRun?.errorCode ?? null,
1256
+ retryReason: readNonEmptyString(context.retryReason) ?? null,
1257
+ recoveryCause: input.recoveryCause,
1258
+ sourceRunId: input.successfulRunHandoffEvidence?.sourceRunId ?? null,
1259
+ correctiveRunId: input.successfulRunHandoffEvidence?.correctiveRunId ?? null,
1260
+ missingDisposition: input.successfulRunHandoffEvidence?.missingDisposition ?? null,
1261
+ handoffAttempt: input.successfulRunHandoffEvidence?.handoffAttempt ?? null,
1262
+ maxHandoffAttempts: input.successfulRunHandoffEvidence?.maxHandoffAttempts ?? null,
1263
+ };
1264
+ }
1265
+ async function ensureSourceScopedStrandedRecoveryAction(input) {
1266
+ const recoveryCause = input.recoveryCause ?? "stranded_assigned_issue";
1267
+ const ownerAgentId = await resolveStrandedIssueRecoveryOwnerAgentId(input.issue);
1268
+ const now = new Date();
1269
+ const action = await recoveryActionsSvc.upsertSourceScoped({
1270
+ companyId: input.issue.companyId,
1271
+ sourceIssueId: input.issue.id,
1272
+ kind: strandedRecoveryActionKind(recoveryCause),
1273
+ ownerType: ownerAgentId ? "agent" : "board",
1274
+ ownerAgentId,
1275
+ previousOwnerAgentId: input.issue.assigneeAgentId,
1276
+ returnOwnerAgentId: input.issue.assigneeAgentId,
1277
+ cause: recoveryCause,
1278
+ fingerprint: strandedRecoveryActionFingerprint({
1279
+ issue: input.issue,
1280
+ recoveryCause,
1281
+ }),
1282
+ evidence: buildStrandedRecoveryActionEvidence({
1283
+ issue: input.issue,
1284
+ latestRun: input.latestRun,
1285
+ previousStatus: input.previousStatus,
1286
+ recoveryCause,
1287
+ successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
1288
+ }),
1289
+ nextAction: recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON
1290
+ ? "Choose and record a valid issue disposition without copying transcript content."
1291
+ : "Restore a live execution path, fix the runtime/adapter failure, or record an intentional manual resolution.",
1292
+ wakePolicy: ownerAgentId
1293
+ ? {
1294
+ type: "wake_owner",
1295
+ reason: "source_scoped_recovery_action",
1296
+ ownerAgentId,
1297
+ }
1298
+ : {
1299
+ type: "board_escalation",
1300
+ reason: "no_invokable_recovery_owner",
1301
+ },
1302
+ monitorPolicy: null,
1303
+ maxAttempts: null,
1304
+ lastAttemptAt: now,
1305
+ });
1306
+ return action;
1307
+ }
1308
+ async function enqueueSourceScopedStrandedRecoveryWake(input) {
1309
+ if (!input.action.ownerAgentId)
1310
+ return;
1311
+ await deps.enqueueWakeup(input.action.ownerAgentId, {
1312
+ source: "assignment",
1313
+ triggerDetail: "system",
1314
+ reason: "source_scoped_recovery_action",
1315
+ idempotencyKey: `source_scoped_recovery_action:${input.action.id}:${input.action.attemptCount}`,
1316
+ payload: withRecoveryModelProfileHint({
1317
+ issueId: input.issue.id,
1318
+ sourceIssueId: input.issue.id,
1319
+ recoveryActionId: input.action.id,
1320
+ strandedRunId: input.latestRun?.id ?? null,
1321
+ recoveryCause: input.recoveryCause,
1322
+ }),
1323
+ requestedByActorType: "system",
1324
+ requestedByActorId: null,
1325
+ contextSnapshot: withRecoveryModelProfileHint({
1326
+ issueId: input.issue.id,
1327
+ taskId: input.issue.id,
1328
+ wakeReason: "source_scoped_recovery_action",
1329
+ skipIssueComment: true,
1330
+ source: "issue_recovery_action",
1331
+ recoveryActionId: input.action.id,
1332
+ sourceIssueId: input.issue.id,
1333
+ strandedRunId: input.latestRun?.id ?? null,
1334
+ recoveryCause: input.recoveryCause,
1335
+ }),
1336
+ });
1337
+ }
1231
1338
  function buildRecoveryIssueInPlaceEscalationComment(input) {
1232
1339
  const runLink = input.latestRun
1233
1340
  ? runUiLink({ id: input.latestRun.id, agentId: input.latestRun.agentId }, input.prefix)
@@ -1298,29 +1405,31 @@ export function recoveryService(db, deps) {
1298
1405
  .then((rows) => rows.map((row) => row.blockerIssueId));
1299
1406
  }
1300
1407
  async function escalateStrandedAssignedIssue(input) {
1301
- const nestedRecoverySuppressed = isStrandedIssueRecoveryIssue(input.issue);
1302
- let recoveryIssue = null;
1303
- if (!nestedRecoverySuppressed) {
1304
- recoveryIssue = await ensureStrandedIssueRecoveryIssue({
1408
+ if (isStrandedIssueRecoveryIssue(input.issue)) {
1409
+ return escalateStrandedRecoveryIssueInPlace({
1305
1410
  issue: input.issue,
1306
1411
  previousStatus: input.previousStatus,
1307
1412
  latestRun: input.latestRun,
1308
- recoveryCause: input.recoveryCause,
1309
- successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
1310
1413
  });
1311
1414
  }
1415
+ const recoveryCause = input.recoveryCause ?? "stranded_assigned_issue";
1416
+ const recoveryAction = await ensureSourceScopedStrandedRecoveryAction({
1417
+ issue: input.issue,
1418
+ previousStatus: input.previousStatus,
1419
+ latestRun: input.latestRun,
1420
+ recoveryCause,
1421
+ successfulRunHandoffEvidence: input.successfulRunHandoffEvidence,
1422
+ });
1312
1423
  const blockerIds = await existingUnresolvedBlockerIssueIds(input.issue.companyId, input.issue.id);
1313
- const nextBlockerIds = recoveryIssue
1314
- ? [...new Set([...blockerIds, recoveryIssue.id])]
1315
- : blockerIds;
1316
1424
  const updated = await issuesSvc.update(input.issue.id, {
1317
1425
  status: "blocked",
1318
- blockedByIssueIds: nextBlockerIds,
1426
+ blockedByIssueIds: blockerIds,
1427
+ assigneeAgentId: recoveryAction.ownerAgentId ?? input.issue.assigneeAgentId,
1319
1428
  });
1320
1429
  if (!updated)
1321
1430
  return null;
1322
1431
  const prefix = await getCompanyIssuePrefix(input.issue.companyId);
1323
- const recoveryOwner = recoveryIssue?.assigneeAgentId ? await getAgent(recoveryIssue.assigneeAgentId) : null;
1432
+ const recoveryOwner = recoveryAction.ownerAgentId ? await getAgent(recoveryAction.ownerAgentId) : null;
1324
1433
  const sourceAssignee = input.issue.assigneeAgentId ? await getAgent(input.issue.assigneeAgentId) : null;
1325
1434
  let notice = null;
1326
1435
  if (input.recoveryCause === SUCCESSFUL_RUN_MISSING_STATE_REASON && input.successfulRunHandoffEvidence) {
@@ -1331,41 +1440,51 @@ export function recoveryService(db, deps) {
1331
1440
  : null,
1332
1441
  correctiveRun: input.latestRun ? { id: input.latestRun.id, status: input.latestRun.status } : null,
1333
1442
  sourceAssignee,
1334
- recoveryIssue,
1443
+ recoveryIssue: null,
1444
+ recoveryActionId: recoveryAction.id,
1335
1445
  recoveryOwner,
1336
1446
  latestIssueStatus: input.issue.status,
1337
1447
  latestHandoffRunStatus: input.latestRun?.status ?? "unknown",
1338
1448
  missingDisposition: input.successfulRunHandoffEvidence.missingDisposition,
1339
1449
  });
1340
1450
  }
1341
- let recoveryLine;
1342
- if (nestedRecoverySuppressed) {
1343
- recoveryLine = await buildNestedStrandedRecoveryLine(input.issue, prefix);
1344
- }
1345
- else if (recoveryIssue) {
1346
- recoveryLine = [
1451
+ const recoveryLine = recoveryAction.ownerAgentId
1452
+ ? [
1347
1453
  "",
1348
- `- Recovery issue: ${issueUiLink({ identifier: recoveryIssue.identifier, id: recoveryIssue.id }, prefix)}`,
1454
+ `- Recovery action: \`${recoveryAction.id}\``,
1349
1455
  `- Recovery owner: ${agentUiLink(recoveryOwner, prefix)}`,
1350
- "- Next action: the recovery owner should either restore a live execution path or record the manual resolution, then mark the recovery issue done.",
1351
- ].join("\n");
1352
- }
1353
- else {
1354
- recoveryLine = [
1456
+ "- Next action: the recovery owner should either restore a live execution path or record the manual resolution on the source issue.",
1457
+ ].join("\n")
1458
+ : [
1355
1459
  "",
1356
- "- Recovery issue: none created because Evermore could not find an invokable manager, creator, or executive owner with budget available.",
1460
+ `- Recovery action: \`${recoveryAction.id}\``,
1461
+ "- Recovery owner: board escalation, because Evermore could not find an invokable manager, creator, or executive owner with budget available.",
1357
1462
  "- Next action: a board operator should assign an invokable recovery owner, fix the agent/runtime state, or record an intentional manual resolution.",
1358
1463
  ].join("\n");
1359
- }
1360
- if (notice) {
1361
- await issuesSvc.addComment(input.issue.id, notice.body, {}, {
1362
- authorType: "system",
1363
- presentation: notice.presentation,
1364
- metadata: notice.metadata,
1365
- });
1366
- }
1367
- else {
1368
- await issuesSvc.addComment(input.issue.id, `${input.comment ?? ""}${recoveryLine}`, {});
1464
+ if (recoveryAction.attemptCount === 1) {
1465
+ const escalationCommentMarker = `Recovery action: \`${recoveryAction.id}\``;
1466
+ const hasEscalationComment = await db
1467
+ .select({ id: issueComments.id, body: issueComments.body, metadata: issueComments.metadata })
1468
+ .from(issueComments)
1469
+ .where(and(eq(issueComments.issueId, input.issue.id), eq(issueComments.authorType, "system")))
1470
+ .orderBy(desc(issueComments.createdAt))
1471
+ .limit(50)
1472
+ .then((rows) => rows.some((row) => (row.body ?? "").includes(escalationCommentMarker) ||
1473
+ noticeMetadataReferencesRecoveryAction(row.metadata, recoveryAction.id)));
1474
+ if (!hasEscalationComment) {
1475
+ if (notice) {
1476
+ await issuesSvc.addComment(input.issue.id, notice.body, {}, {
1477
+ authorType: "system",
1478
+ presentation: notice.presentation,
1479
+ metadata: notice.metadata,
1480
+ });
1481
+ }
1482
+ else {
1483
+ await issuesSvc.addComment(input.issue.id, `${input.comment ?? ""}${recoveryLine}`, {}, {
1484
+ authorType: "system",
1485
+ });
1486
+ }
1487
+ }
1369
1488
  }
1370
1489
  await logActivity(db, {
1371
1490
  companyId: input.issue.companyId,
@@ -1389,11 +1508,40 @@ export function recoveryService(db, deps) {
1389
1508
  latestRunId: input.latestRun?.id ?? null,
1390
1509
  latestRunStatus: input.latestRun?.status ?? null,
1391
1510
  latestRunErrorCode: input.latestRun?.errorCode ?? null,
1392
- recoveryIssueId: recoveryIssue?.id ?? null,
1393
- nestedRecoverySuppressed,
1394
- blockerIssueIds: nextBlockerIds,
1511
+ recoveryActionId: recoveryAction.id,
1512
+ recoveryOwnerAgentId: recoveryAction.ownerAgentId,
1513
+ previousOwnerAgentId: recoveryAction.previousOwnerAgentId,
1514
+ returnOwnerAgentId: recoveryAction.returnOwnerAgentId,
1515
+ blockerIssueIds: blockerIds,
1395
1516
  },
1396
1517
  });
1518
+ await enqueueSourceScopedStrandedRecoveryWake({
1519
+ action: recoveryAction,
1520
+ issue: input.issue,
1521
+ latestRun: input.latestRun,
1522
+ recoveryCause,
1523
+ });
1524
+ if (recoveryAction.ownerAgentId && recoveryAction.ownerAgentId === input.issue.assigneeAgentId) {
1525
+ const [currentIssue] = await db
1526
+ .select({
1527
+ status: issues.status,
1528
+ assigneeAgentId: issues.assigneeAgentId,
1529
+ })
1530
+ .from(issues)
1531
+ .where(eq(issues.id, input.issue.id))
1532
+ .limit(1);
1533
+ if (currentIssue &&
1534
+ (currentIssue.status !== "blocked" ||
1535
+ currentIssue.assigneeAgentId !== recoveryAction.ownerAgentId)) {
1536
+ const reblocked = await issuesSvc.update(input.issue.id, {
1537
+ status: "blocked",
1538
+ blockedByIssueIds: blockerIds,
1539
+ assigneeAgentId: recoveryAction.ownerAgentId,
1540
+ });
1541
+ if (reblocked)
1542
+ return reblocked;
1543
+ }
1544
+ }
1397
1545
  return updated;
1398
1546
  }
1399
1547
  async function reconcileStrandedAssignedIssues() {
@@ -1629,28 +1777,29 @@ export function recoveryService(db, deps) {
1629
1777
  return result;
1630
1778
  }
1631
1779
  async function collectIssueGraphLivenessFindings() {
1632
- const [issueRows, relationRows, agentRows, activeRunRows, activeIssueRunRows, wakeRows, interactionRows, approvalRows, recoveryIssueRows,] = await Promise.all([
1633
- db
1634
- .select({
1635
- id: issues.id,
1636
- companyId: issues.companyId,
1637
- identifier: issues.identifier,
1638
- title: issues.title,
1639
- status: issues.status,
1640
- projectId: issues.projectId,
1641
- goalId: issues.goalId,
1642
- parentId: issues.parentId,
1643
- assigneeAgentId: issues.assigneeAgentId,
1644
- assigneeUserId: issues.assigneeUserId,
1645
- createdByAgentId: issues.createdByAgentId,
1646
- createdByUserId: issues.createdByUserId,
1647
- executionPolicy: issues.executionPolicy,
1648
- executionState: issues.executionState,
1649
- monitorNextCheckAt: issues.monitorNextCheckAt,
1650
- monitorAttemptCount: issues.monitorAttemptCount,
1651
- })
1652
- .from(issues)
1653
- .where(and(isNull(issues.hiddenAt), notInArray(issues.originKind, [RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation]))),
1780
+ const issueRowsPromise = Promise.resolve(db
1781
+ .select({
1782
+ id: issues.id,
1783
+ companyId: issues.companyId,
1784
+ identifier: issues.identifier,
1785
+ title: issues.title,
1786
+ status: issues.status,
1787
+ projectId: issues.projectId,
1788
+ goalId: issues.goalId,
1789
+ parentId: issues.parentId,
1790
+ assigneeAgentId: issues.assigneeAgentId,
1791
+ assigneeUserId: issues.assigneeUserId,
1792
+ createdByAgentId: issues.createdByAgentId,
1793
+ createdByUserId: issues.createdByUserId,
1794
+ executionPolicy: issues.executionPolicy,
1795
+ executionState: issues.executionState,
1796
+ monitorNextCheckAt: issues.monitorNextCheckAt,
1797
+ monitorAttemptCount: issues.monitorAttemptCount,
1798
+ })
1799
+ .from(issues)
1800
+ .where(and(isNull(issues.hiddenAt), notInArray(issues.originKind, [RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation]))));
1801
+ const [issueRows, relationRows, agentRows, activeRunRows, activeIssueRunRows, wakeRows, interactionRows, approvalRows, recoveryIssueRows, recoveryActionRows,] = await Promise.all([
1802
+ issueRowsPromise,
1654
1803
  db
1655
1804
  .select({
1656
1805
  companyId: issueRelations.companyId,
@@ -1728,6 +1877,19 @@ export function recoveryService(db, deps) {
1728
1877
  STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
1729
1878
  RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation,
1730
1879
  ]), notInArray(issues.status, ["done", "cancelled"]))),
1880
+ issueRowsPromise.then((rows) => {
1881
+ const issueIdsUnderAnalysis = rows.map((row) => row.id);
1882
+ return issueIdsUnderAnalysis.length === 0
1883
+ ? []
1884
+ : db
1885
+ .select({
1886
+ companyId: issueRecoveryActions.companyId,
1887
+ issueId: issueRecoveryActions.sourceIssueId,
1888
+ status: issueRecoveryActions.status,
1889
+ })
1890
+ .from(issueRecoveryActions)
1891
+ .where(and(inArray(issueRecoveryActions.status, ["active", "escalated"]), inArray(issueRecoveryActions.sourceIssueId, issueIdsUnderAnalysis)));
1892
+ }),
1731
1893
  ]);
1732
1894
  const openRecoveryIssues = recoveryIssueRows.flatMap((row) => {
1733
1895
  if (row.originKind === RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation) {
@@ -1781,7 +1943,7 @@ export function recoveryService(db, deps) {
1781
1943
  })),
1782
1944
  pendingInteractions: interactionRows,
1783
1945
  pendingApprovals: approvalRows,
1784
- openRecoveryIssues,
1946
+ openRecoveryIssues: openRecoveryIssues.concat(recoveryActionRows),
1785
1947
  now: new Date(),
1786
1948
  });
1787
1949
  }