@penclipai/server 2026.426.0 → 2026.505.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.
- package/dist/adapters/builtin-adapter-types.d.ts.map +1 -1
- package/dist/adapters/builtin-adapter-types.js +3 -0
- package/dist/adapters/builtin-adapter-types.js.map +1 -1
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/registry.d.ts +2 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +76 -6
- package/dist/adapters/registry.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/utils.d.ts.map +1 -1
- package/dist/adapters/utils.js +2 -1
- package/dist/adapters/utils.js.map +1 -1
- package/dist/attachment-types.d.ts +1 -16
- package/dist/attachment-types.d.ts.map +1 -1
- package/dist/attachment-types.js +7 -0
- package/dist/attachment-types.js.map +1 -1
- package/dist/auth/better-auth.d.ts +3 -1
- package/dist/auth/better-auth.d.ts.map +1 -1
- package/dist/auth/better-auth.js +8 -2
- package/dist/auth/better-auth.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -13
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +143 -2
- package/dist/middleware/auth.js.map +1 -1
- package/dist/onboarding-assets/ceo/AGENTS.md +1 -1
- package/dist/onboarding-assets/ceo/HEARTBEAT.md +5 -5
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +30 -12
- package/dist/redaction.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +10 -0
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +4 -2
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/adapters.d.ts.map +1 -1
- package/dist/routes/adapters.js +1 -0
- package/dist/routes/adapters.js.map +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +317 -56
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/costs.d.ts.map +1 -1
- package/dist/routes/costs.js +21 -2
- package/dist/routes/costs.js.map +1 -1
- package/dist/routes/instance-settings.d.ts.map +1 -1
- package/dist/routes/instance-settings.js +37 -2
- package/dist/routes/instance-settings.js.map +1 -1
- package/dist/routes/issue-tree-control.d.ts.map +1 -1
- package/dist/routes/issue-tree-control.js +3 -1
- package/dist/routes/issue-tree-control.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +257 -32
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +10 -3
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/routines.d.ts.map +1 -1
- package/dist/routes/routines.js +6 -1
- package/dist/routes/routines.js.map +1 -1
- package/dist/routes/workspace-command-authz.d.ts +1 -1
- package/dist/routes/workspace-command-authz.d.ts.map +1 -1
- package/dist/routes/workspace-command-authz.js +2 -2
- package/dist/routes/workspace-command-authz.js.map +1 -1
- package/dist/runtime-api.d.ts +4 -0
- package/dist/runtime-api.d.ts.map +1 -1
- package/dist/runtime-api.js +38 -10
- package/dist/runtime-api.js.map +1 -1
- package/dist/services/companies.d.ts +6 -0
- package/dist/services/companies.d.ts.map +1 -1
- package/dist/services/companies.js +1 -0
- package/dist/services/companies.js.map +1 -1
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +16 -15
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/costs.d.ts +9 -0
- package/dist/services/costs.d.ts.map +1 -1
- package/dist/services/costs.js +45 -1
- package/dist/services/costs.js.map +1 -1
- package/dist/services/environment-execution-target.d.ts.map +1 -1
- package/dist/services/environment-execution-target.js +7 -13
- package/dist/services/environment-execution-target.js.map +1 -1
- package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
- package/dist/services/environment-run-orchestrator.js +56 -0
- package/dist/services/environment-run-orchestrator.js.map +1 -1
- package/dist/services/environment-runtime.d.ts +2 -0
- package/dist/services/environment-runtime.d.ts.map +1 -1
- package/dist/services/environment-runtime.js +80 -39
- package/dist/services/environment-runtime.js.map +1 -1
- package/dist/services/heartbeat-stop-metadata.d.ts +2 -1
- package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
- package/dist/services/heartbeat-stop-metadata.js +10 -1
- package/dist/services/heartbeat-stop-metadata.js.map +1 -1
- package/dist/services/heartbeat-stop-metadata.test.js +24 -0
- package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
- package/dist/services/heartbeat.d.ts +156 -5
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +1384 -112
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +4 -1
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-execution-policy.d.ts +56 -1
- package/dist/services/issue-execution-policy.d.ts.map +1 -1
- package/dist/services/issue-execution-policy.js +400 -2
- package/dist/services/issue-execution-policy.js.map +1 -1
- package/dist/services/issue-thread-interactions.d.ts +5 -1
- package/dist/services/issue-thread-interactions.d.ts.map +1 -1
- package/dist/services/issue-thread-interactions.js +44 -1
- package/dist/services/issue-thread-interactions.js.map +1 -1
- package/dist/services/issue-tree-control.d.ts +1 -0
- package/dist/services/issue-tree-control.d.ts.map +1 -1
- package/dist/services/issue-tree-control.js +84 -4
- package/dist/services/issue-tree-control.js.map +1 -1
- package/dist/services/issues.d.ts +10 -1
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +452 -48
- package/dist/services/issues.js.map +1 -1
- package/dist/services/plugin-environment-driver.d.ts +4 -0
- package/dist/services/plugin-environment-driver.d.ts.map +1 -1
- package/dist/services/plugin-environment-driver.js +18 -1
- package/dist/services/plugin-environment-driver.js.map +1 -1
- package/dist/services/productivity-review.d.ts +83 -0
- package/dist/services/productivity-review.d.ts.map +1 -0
- package/dist/services/productivity-review.js +650 -0
- package/dist/services/productivity-review.js.map +1 -0
- package/dist/services/recovery/index.d.ts +1 -1
- package/dist/services/recovery/index.d.ts.map +1 -1
- package/dist/services/recovery/index.js +1 -1
- package/dist/services/recovery/index.js.map +1 -1
- package/dist/services/recovery/issue-graph-liveness.d.ts +13 -1
- package/dist/services/recovery/issue-graph-liveness.d.ts.map +1 -1
- package/dist/services/recovery/issue-graph-liveness.js +212 -92
- package/dist/services/recovery/issue-graph-liveness.js.map +1 -1
- package/dist/services/recovery/origins.d.ts +2 -0
- package/dist/services/recovery/origins.d.ts.map +1 -1
- package/dist/services/recovery/origins.js +4 -0
- package/dist/services/recovery/origins.js.map +1 -1
- package/dist/services/recovery/run-liveness-continuations.d.ts.map +1 -1
- package/dist/services/recovery/run-liveness-continuations.js.map +1 -1
- package/dist/services/recovery/service.d.ts +20 -2
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +405 -63
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/routines.d.ts +5 -2
- package/dist/services/routines.d.ts.map +1 -1
- package/dist/services/routines.js +47 -3
- package/dist/services/routines.js.map +1 -1
- package/dist/worktree-config.d.ts.map +1 -1
- package/dist/worktree-config.js +2 -5
- package/dist/worktree-config.js.map +1 -1
- package/package.json +16 -15
- package/skills/diagnose-why-work-stopped/SKILL.md +161 -0
- package/skills/paperclip/SKILL.md +37 -26
- package/skills/paperclip/references/api-reference.md +6 -2
- package/skills/paperclip-converting-plans-to-tasks/SKILL.md +42 -0
- package/skills/paperclip-create-agent/SKILL.md +3 -2
- package/skills/paperclip-create-agent/references/agent-instruction-templates.md +1 -1
- package/skills/paperclip-create-agent/references/api-reference.md +7 -2
- package/skills/paperclip-create-agent/references/baseline-role-guide.md +1 -1
- package/skills/paperclip-create-agent/references/draft-review-checklist.md +2 -2
- package/skills/paperclip-dev/SKILL.md +267 -0
- package/skills/terminal-bench-loop/SKILL.md +236 -0
- package/ui-dist/assets/{_basePickBy-BRqa7PJ5.js → _basePickBy-BS0Fg_DB.js} +1 -1
- package/ui-dist/assets/{_baseUniq-DhE2yrXC.js → _baseUniq-Dtnt_4SE.js} +1 -1
- package/ui-dist/assets/{arc-7qnikTQ3.js → arc-BCoOPxh5.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-VXUJARFQ-CH0wVUOM.js → architectureDiagram-VXUJARFQ-C6eX2QUo.js} +1 -1
- package/ui-dist/assets/{blockDiagram-VD42YOAC-CeeRyJQX.js → blockDiagram-VD42YOAC-aUueUD4B.js} +1 -1
- package/ui-dist/assets/browser-ponyfill-BlAfsWm_.js +2 -0
- package/ui-dist/assets/{c4Diagram-YG6GDRKO-C_cV0CGo.js → c4Diagram-YG6GDRKO-CfPWRlOF.js} +1 -1
- package/ui-dist/assets/channel-ChNSCFJf.js +1 -0
- package/ui-dist/assets/{chunk-4BX2VUAB-DQ6pxPVT.js → chunk-4BX2VUAB-BTD1apA4.js} +1 -1
- package/ui-dist/assets/{chunk-55IACEB6-L8pS0IoX.js → chunk-55IACEB6-BXXF_ClN.js} +1 -1
- package/ui-dist/assets/{chunk-B4BG7PRW-BZKGE88E.js → chunk-B4BG7PRW-hAZeWGP8.js} +1 -1
- package/ui-dist/assets/{chunk-DI55MBZ5-CefSoZ_K.js → chunk-DI55MBZ5-cOH3UoEl.js} +1 -1
- package/ui-dist/assets/{chunk-FMBD7UC4-Bc3qTTHB.js → chunk-FMBD7UC4-Cu2yZOcl.js} +1 -1
- package/ui-dist/assets/{chunk-QN33PNHL-CjWBr5bI.js → chunk-QN33PNHL-0DNN5aRU.js} +1 -1
- package/ui-dist/assets/{chunk-QZHKN3VN-C0JUdmmz.js → chunk-QZHKN3VN-B9_bhK2n.js} +1 -1
- package/ui-dist/assets/{chunk-TZMSLE5B-D4d4I82z.js → chunk-TZMSLE5B-Cr5xwxio.js} +1 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-4aK1QZU3.js +1 -0
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-4aK1QZU3.js +1 -0
- package/ui-dist/assets/clone-C8lk5Qbc.js +1 -0
- package/ui-dist/assets/{cose-bilkent-S5V4N54A-B09h9XGZ.js → cose-bilkent-S5V4N54A-6_Dw6gpQ.js} +1 -1
- package/ui-dist/assets/{dagre-6UL2VRFP-CA02PXuX.js → dagre-6UL2VRFP-CFBhlh5H.js} +1 -1
- package/ui-dist/assets/{diagram-PSM6KHXK-DaT9cnrY.js → diagram-PSM6KHXK-C88ftcah.js} +1 -1
- package/ui-dist/assets/{diagram-QEK2KX5R-Drwc3gBw.js → diagram-QEK2KX5R-9EUupcuH.js} +1 -1
- package/ui-dist/assets/{diagram-S2PKOQOG-CpsGCaT6.js → diagram-S2PKOQOG-Dsml0wWh.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q2GNP2WA-CVkBh9TY.js → erDiagram-Q2GNP2WA-sM-XdfHS.js} +1 -1
- package/ui-dist/assets/{flowDiagram-NV44I4VS-De9sXvPR.js → flowDiagram-NV44I4VS-qll7oaoW.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-JELNMOA3-CSFa0gXS.js → ganttDiagram-JELNMOA3-VWnJMcjC.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-DEJaChxa.js → gitGraphDiagram-V2S2FVAM-DFnocrfl.js} +1 -1
- package/ui-dist/assets/{graph-D2R4DCtu.js → graph-nq3Qye4Z.js} +1 -1
- package/ui-dist/assets/{index-DEG-9CFs.js → index-3Owzaheh.js} +1 -1
- package/ui-dist/assets/{index-DHnKx9xX.js → index-B2A-a635.js} +1 -1
- package/ui-dist/assets/{index-C1I0SGDm.js → index-BGFrRiqa.js} +1 -1
- package/ui-dist/assets/{index-B44EtLRv.js → index-BVC5UhRK.js} +1 -1
- package/ui-dist/assets/{index-C_dAXwxT.js → index-BrP1U_Hy.js} +1 -1
- package/ui-dist/assets/{index-flZjKn_n.js → index-CXXHGqM8.js} +1 -1
- package/ui-dist/assets/{index-ssM_UKPW.js → index-CgyPAauR.js} +1 -1
- package/ui-dist/assets/{index-Ct1AraKR.js → index-CksQ4Ytv.js} +1 -1
- package/ui-dist/assets/{index-DQ6I_vpd.js → index-CrNzj2vZ.js} +1 -1
- package/ui-dist/assets/{index-DzZID5RY.js → index-CxbZBH3M.js} +1 -1
- package/ui-dist/assets/{index-Cn6_RRY5.js → index-D-dSSrf-.js} +1 -1
- package/ui-dist/assets/{index-CVa2OHgx.js → index-D6uZ_7Vh.js} +1 -1
- package/ui-dist/assets/{index-BzjWQd50.js → index-D7JGmxas.js} +1 -1
- package/ui-dist/assets/{index-CnT1_9UF.js → index-DDqO9GAq.js} +1 -1
- package/ui-dist/assets/index-DEUtmlPm.js +513 -0
- package/ui-dist/assets/{index-D2fEhyQg.js → index-DF5RDSoK.js} +1 -1
- package/ui-dist/assets/{index-CZGNe8K3.js → index-DfI92epU.js} +1 -1
- package/ui-dist/assets/{index-ByamXtyB.js → index-Dukb9MDQ.js} +1 -1
- package/ui-dist/assets/index-HP73_6Vr.css +1 -0
- package/ui-dist/assets/{index-BJS4rvUh.js → index-NXDTW2n4.js} +1 -1
- package/ui-dist/assets/{index-Bad5Hy7e.js → index-SxPPG9ig.js} +1 -1
- package/ui-dist/assets/{index-CC51mhhA.js → index-lC4Yz3Gw.js} +1 -1
- package/ui-dist/assets/{index-BFzkl36p.js → index-q2RXGI2V.js} +1 -1
- package/ui-dist/assets/{index-40icqWwg.js → index-qjfdrS96.js} +1 -1
- package/ui-dist/assets/{infoDiagram-HS3SLOUP-CJcjzWkM.js → infoDiagram-HS3SLOUP-CTrK5xoS.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-ByITI00s.js → journeyDiagram-XKPGCS4Q-YFC7FykG.js} +1 -1
- package/ui-dist/assets/{kanban-definition-3W4ZIXB7-DvEjKke-.js → kanban-definition-3W4ZIXB7-B3dlyva0.js} +1 -1
- package/ui-dist/assets/{layout-CZcd66hi.js → layout-DefunPTK.js} +1 -1
- package/ui-dist/assets/{linear-jTUy3iHu.js → linear-CIPvzeMv.js} +1 -1
- package/ui-dist/assets/{mermaid.core-DECSZPbJ.js → mermaid.core-zKYhmnnR.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-VGOIOE7T-Twtu17_c.js → mindmap-definition-VGOIOE7T-BlU-ebRa.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ADFJNKIX-DlbgZ010.js → pieDiagram-ADFJNKIX-Ceto4LXH.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-CMAa3qAT.js → quadrantDiagram-AYHSOK5B-C6M6hkuE.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-CXRTfJOe.js → requirementDiagram-UZGBJVZJ-B-bcG938.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-DeyO4fer.js → sankeyDiagram-TZEHDZUN-CIqty6Qi.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-WL72ISMW-Ch8wlJIL.js → sequenceDiagram-WL72ISMW-CIt2R5tk.js} +1 -1
- package/ui-dist/assets/{stateDiagram-FKZM4ZOC-BgL_AAl9.js → stateDiagram-FKZM4ZOC-BC1RFlfg.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-Iy6tYSSw.js +1 -0
- package/ui-dist/assets/{timeline-definition-IT6M3QCI-D1QWd7TQ.js → timeline-definition-IT6M3QCI-DZqvoU94.js} +1 -1
- package/ui-dist/assets/{treemap-GDKQZRPO-B5RkmUv8.js → treemap-GDKQZRPO-CSeKauwA.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-PRI3JC2R-WtDhjZfk.js → xychartDiagram-PRI3JC2R-Ut3mCiEd.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/locales/en/common.json +137 -1
- package/ui-dist/locales/zh-CN/common.json +111 -1
- package/ui-dist/assets/browser-ponyfill-Ct3hGqsr.js +0 -2
- package/ui-dist/assets/channel-pHFjGZL-.js +0 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-X4ZksqXl.js +0 -1
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-X4ZksqXl.js +0 -1
- package/ui-dist/assets/clone-DZzimpfG.js +0 -1
- package/ui-dist/assets/index-C1oE3J7o.css +0 -1
- package/ui-dist/assets/index-fSIlEIHr.js +0 -510
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-gnLzrhSv.js +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { and, asc, desc, eq, gt, inArray, isNull, notInArray, sql } from "drizzle-orm";
|
|
2
|
-
import {
|
|
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 "@penclipai/shared";
|
|
3
|
+
import { agents, agentWakeupRequests, approvals, companies, heartbeatRunEvents, heartbeatRunWatchdogDecisions, heartbeatRuns, issueApprovals, issueRelations, issueThreadInteractions, issues, } from "@penclipai/db";
|
|
3
4
|
import { parseObject, asBoolean, asNumber } from "../../adapters/utils.js";
|
|
4
5
|
import { runningProcesses } from "../../adapters/index.js";
|
|
5
6
|
import { forbidden, notFound } from "../../errors.js";
|
|
@@ -12,12 +13,11 @@ import { instanceSettingsService } from "../instance-settings.js";
|
|
|
12
13
|
import { issueTreeControlService } from "../issue-tree-control.js";
|
|
13
14
|
import { issueService } from "../issues.js";
|
|
14
15
|
import { getRunLogStore } from "../run-log-store.js";
|
|
15
|
-
import { RECOVERY_ORIGIN_KINDS, buildIssueGraphLivenessLeafKey, parseIssueGraphLivenessIncidentKey, } from "./origins.js";
|
|
16
|
+
import { RECOVERY_ORIGIN_KINDS, buildIssueGraphLivenessLeafKey, isStrandedIssueRecoveryOriginKind, parseIssueGraphLivenessIncidentKey, } from "./origins.js";
|
|
16
17
|
import { classifyIssueGraphLiveness, } from "./issue-graph-liveness.js";
|
|
17
18
|
import { isAutomaticRecoverySuppressedByPauseHold } from "./pause-hold-guard.js";
|
|
18
19
|
const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"];
|
|
19
20
|
const UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES = ["failed", "cancelled", "timed_out"];
|
|
20
|
-
const ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_MIN_STALE_MS = 24 * 60 * 60 * 1000;
|
|
21
21
|
export const ACTIVE_RUN_OUTPUT_SUSPICION_THRESHOLD_MS = 60 * 60 * 1000;
|
|
22
22
|
export const ACTIVE_RUN_OUTPUT_CRITICAL_THRESHOLD_MS = 4 * 60 * 60 * 1000;
|
|
23
23
|
export const ACTIVE_RUN_OUTPUT_CONTINUE_REARM_MS = 30 * 60 * 1000;
|
|
@@ -31,23 +31,9 @@ function readNonEmptyString(value) {
|
|
|
31
31
|
function summarizeRunFailureForIssueComment(run) {
|
|
32
32
|
if (!run)
|
|
33
33
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const firstLine = rawError
|
|
38
|
-
?.split(/\r?\n/)
|
|
39
|
-
.map((line) => line.trim())
|
|
40
|
-
.find(Boolean) ?? null;
|
|
41
|
-
const summarySource = apiMessageMatch?.[1] ?? firstLine;
|
|
42
|
-
const summary = summarySource && summarySource.length > 240
|
|
43
|
-
? `${summarySource.slice(0, 237)}...`
|
|
44
|
-
: summarySource;
|
|
45
|
-
if (errorCode && summary)
|
|
46
|
-
return ` Latest retry failure: \`${errorCode}\` - ${summary}.`;
|
|
47
|
-
if (errorCode)
|
|
48
|
-
return ` Latest retry failure: \`${errorCode}\`.`;
|
|
49
|
-
if (summary)
|
|
50
|
-
return ` Latest retry failure: ${summary}.`;
|
|
34
|
+
if (readNonEmptyString(run.error) || readNonEmptyString(run.errorCode)) {
|
|
35
|
+
return " Latest retry failure details were withheld from the issue thread; inspect the linked run for evidence.";
|
|
36
|
+
}
|
|
51
37
|
return null;
|
|
52
38
|
}
|
|
53
39
|
function didAutomaticRecoveryFail(latestRun, expectedRetryReason) {
|
|
@@ -105,6 +91,29 @@ function formatIssueLinksForComment(relations) {
|
|
|
105
91
|
function isAgentInvokable(agent) {
|
|
106
92
|
return Boolean(agent && !["paused", "terminated", "pending_approval"].includes(agent.status));
|
|
107
93
|
}
|
|
94
|
+
function isStrandedIssueRecoveryIssue(issue) {
|
|
95
|
+
return isStrandedIssueRecoveryOriginKind(issue.originKind);
|
|
96
|
+
}
|
|
97
|
+
function isUnsuccessfulTerminalIssueRun(latestRun) {
|
|
98
|
+
return Boolean(latestRun &&
|
|
99
|
+
UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES.includes(latestRun.status));
|
|
100
|
+
}
|
|
101
|
+
function isSuccessfulInProgressContinuationRun(latestRun) {
|
|
102
|
+
return latestRun?.status === "succeeded";
|
|
103
|
+
}
|
|
104
|
+
function isProductiveContinuationRun(latestRun) {
|
|
105
|
+
return latestRun?.status === "succeeded" &&
|
|
106
|
+
(latestRun.livenessState === "advanced" ||
|
|
107
|
+
latestRun.livenessState === "completed" ||
|
|
108
|
+
latestRun.livenessState === "blocked" ||
|
|
109
|
+
latestRun.livenessState === "needs_followup");
|
|
110
|
+
}
|
|
111
|
+
function isRepeatedProductiveContinuationRecovery(latestRun) {
|
|
112
|
+
const latestContext = parseObject(latestRun.contextSnapshot);
|
|
113
|
+
return readNonEmptyString(latestContext.retryReason) === "issue_continuation_needed" &&
|
|
114
|
+
readNonEmptyString(latestContext.source) === "issue.productive_terminal_continuation_recovery" &&
|
|
115
|
+
isProductiveContinuationRun(latestRun);
|
|
116
|
+
}
|
|
108
117
|
function parseLivenessIncidentKey(incidentKey) {
|
|
109
118
|
if (!incidentKey)
|
|
110
119
|
return null;
|
|
@@ -202,6 +211,7 @@ export function recoveryService(db, deps) {
|
|
|
202
211
|
error: heartbeatRuns.error,
|
|
203
212
|
errorCode: heartbeatRuns.errorCode,
|
|
204
213
|
contextSnapshot: heartbeatRuns.contextSnapshot,
|
|
214
|
+
livenessState: heartbeatRuns.livenessState,
|
|
205
215
|
})
|
|
206
216
|
.from(heartbeatRuns)
|
|
207
217
|
.where(and(eq(heartbeatRuns.companyId, companyId), sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}`))
|
|
@@ -226,6 +236,14 @@ export function recoveryService(db, deps) {
|
|
|
226
236
|
]);
|
|
227
237
|
return Boolean(run || deferredWake);
|
|
228
238
|
}
|
|
239
|
+
async function hasQueuedIssueWake(companyId, issueId) {
|
|
240
|
+
return db
|
|
241
|
+
.select({ id: agentWakeupRequests.id })
|
|
242
|
+
.from(agentWakeupRequests)
|
|
243
|
+
.where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.status, "queued"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issueId}`))
|
|
244
|
+
.limit(1)
|
|
245
|
+
.then((rows) => Boolean(rows[0]));
|
|
246
|
+
}
|
|
229
247
|
async function enqueueStrandedIssueRecovery(input) {
|
|
230
248
|
const queued = await deps.enqueueWakeup(input.agentId, {
|
|
231
249
|
source: "automation",
|
|
@@ -259,6 +277,32 @@ export function recoveryService(db, deps) {
|
|
|
259
277
|
}
|
|
260
278
|
return queued;
|
|
261
279
|
}
|
|
280
|
+
async function enqueueInitialAssignedTodoDispatch(issue, agentId) {
|
|
281
|
+
return deps.enqueueWakeup(agentId, {
|
|
282
|
+
source: "assignment",
|
|
283
|
+
triggerDetail: "system",
|
|
284
|
+
reason: "issue_assigned",
|
|
285
|
+
payload: {
|
|
286
|
+
issueId: issue.id,
|
|
287
|
+
mutation: "assigned_todo_liveness_dispatch",
|
|
288
|
+
},
|
|
289
|
+
requestedByActorType: "system",
|
|
290
|
+
requestedByActorId: null,
|
|
291
|
+
contextSnapshot: {
|
|
292
|
+
issueId: issue.id,
|
|
293
|
+
taskId: issue.id,
|
|
294
|
+
wakeReason: "issue_assigned",
|
|
295
|
+
source: "issue.assigned_todo_liveness_dispatch",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function isInvocationBudgetBlocked(issue, agentId) {
|
|
300
|
+
const budgetBlock = await budgets.getInvocationBlock(issue.companyId, agentId, {
|
|
301
|
+
issueId: issue.id,
|
|
302
|
+
projectId: issue.projectId,
|
|
303
|
+
});
|
|
304
|
+
return Boolean(budgetBlock);
|
|
305
|
+
}
|
|
262
306
|
async function reconcileUnassignedBlockingIssues() {
|
|
263
307
|
const candidates = await db
|
|
264
308
|
.select({
|
|
@@ -603,6 +647,14 @@ export function recoveryService(db, deps) {
|
|
|
603
647
|
(maybe.constraint === "issues_active_stale_run_evaluation_uq" ||
|
|
604
648
|
typeof maybe.message === "string" && maybe.message.includes("issues_active_stale_run_evaluation_uq"));
|
|
605
649
|
}
|
|
650
|
+
function isUniqueStrandedIssueRecoveryConflict(error) {
|
|
651
|
+
if (!error || typeof error !== "object")
|
|
652
|
+
return false;
|
|
653
|
+
const maybe = error;
|
|
654
|
+
return maybe.code === "23505" &&
|
|
655
|
+
(maybe.constraint === "issues_active_stranded_issue_recovery_uq" ||
|
|
656
|
+
typeof maybe.message === "string" && maybe.message.includes("issues_active_stranded_issue_recovery_uq"));
|
|
657
|
+
}
|
|
606
658
|
async function ensureSourceIssueBlockedByStaleEvaluation(input) {
|
|
607
659
|
if (!input.sourceIssue || ["done", "cancelled"].includes(input.sourceIssue.status))
|
|
608
660
|
return false;
|
|
@@ -990,6 +1042,8 @@ export function recoveryService(db, deps) {
|
|
|
990
1042
|
].join("\n");
|
|
991
1043
|
}
|
|
992
1044
|
async function ensureStrandedIssueRecoveryIssue(input) {
|
|
1045
|
+
if (isStrandedIssueRecoveryIssue(input.issue))
|
|
1046
|
+
return null;
|
|
993
1047
|
const existing = await findOpenStrandedIssueRecoveryIssue(input.issue.companyId, input.issue.id);
|
|
994
1048
|
if (existing)
|
|
995
1049
|
return existing;
|
|
@@ -997,32 +1051,43 @@ export function recoveryService(db, deps) {
|
|
|
997
1051
|
if (!ownerAgentId)
|
|
998
1052
|
return null;
|
|
999
1053
|
const prefix = await getCompanyIssuePrefix(input.issue.companyId);
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1054
|
+
let recovery;
|
|
1055
|
+
try {
|
|
1056
|
+
recovery = await issuesSvc.create(input.issue.companyId, {
|
|
1057
|
+
title: `Recover stalled issue ${input.issue.identifier ?? input.issue.title}`,
|
|
1058
|
+
description: buildStrandedIssueRecoveryDescription({
|
|
1059
|
+
issue: input.issue,
|
|
1060
|
+
latestRun: input.latestRun,
|
|
1061
|
+
previousStatus: input.previousStatus,
|
|
1062
|
+
prefix,
|
|
1063
|
+
}),
|
|
1064
|
+
status: "todo",
|
|
1065
|
+
priority: input.issue.priority,
|
|
1066
|
+
parentId: input.issue.id,
|
|
1067
|
+
projectId: input.issue.projectId,
|
|
1068
|
+
goalId: input.issue.goalId,
|
|
1069
|
+
assigneeAgentId: ownerAgentId,
|
|
1070
|
+
originKind: STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
|
|
1071
|
+
originId: input.issue.id,
|
|
1072
|
+
originRunId: input.latestRun?.id ?? null,
|
|
1073
|
+
originFingerprint: [
|
|
1074
|
+
STRANDED_ISSUE_RECOVERY_ORIGIN_KIND,
|
|
1075
|
+
input.issue.companyId,
|
|
1076
|
+
input.issue.id,
|
|
1077
|
+
input.latestRun?.id ?? "no-run",
|
|
1078
|
+
].join(":"),
|
|
1079
|
+
billingCode: input.issue.billingCode,
|
|
1080
|
+
inheritExecutionWorkspaceFromIssueId: input.issue.id,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
if (!isUniqueStrandedIssueRecoveryConflict(error))
|
|
1085
|
+
throw error;
|
|
1086
|
+
const raced = await findOpenStrandedIssueRecoveryIssue(input.issue.companyId, input.issue.id);
|
|
1087
|
+
if (!raced)
|
|
1088
|
+
throw error;
|
|
1089
|
+
return raced;
|
|
1090
|
+
}
|
|
1026
1091
|
await deps.enqueueWakeup(ownerAgentId, {
|
|
1027
1092
|
source: "assignment",
|
|
1028
1093
|
triggerDetail: "system",
|
|
@@ -1045,6 +1110,60 @@ export function recoveryService(db, deps) {
|
|
|
1045
1110
|
});
|
|
1046
1111
|
return recovery;
|
|
1047
1112
|
}
|
|
1113
|
+
function buildRecoveryIssueInPlaceEscalationComment(input) {
|
|
1114
|
+
const runLink = input.latestRun
|
|
1115
|
+
? runUiLink({ id: input.latestRun.id, agentId: input.latestRun.agentId }, input.prefix)
|
|
1116
|
+
: "none";
|
|
1117
|
+
const retryReason = readNonEmptyString(parseObject(input.latestRun?.contextSnapshot)?.retryReason) ?? "none";
|
|
1118
|
+
const failureSummary = summarizeRunFailureForIssueComment(input.latestRun);
|
|
1119
|
+
return [
|
|
1120
|
+
"Paperclip stopped automatic stranded-work recovery for this recovery issue.",
|
|
1121
|
+
"",
|
|
1122
|
+
`- Recovery issue: ${issueUiLink({ identifier: input.issue.identifier, id: input.issue.id }, input.prefix)}`,
|
|
1123
|
+
`- Previous status: \`${input.previousStatus}\``,
|
|
1124
|
+
`- Latest run: ${runLink}`,
|
|
1125
|
+
`- Latest run status: \`${input.latestRun?.status ?? "unknown"}\``,
|
|
1126
|
+
`- Retry reason: \`${retryReason}\``,
|
|
1127
|
+
failureSummary ? `- Failure: ${failureSummary.trim()}` : "- Failure: none recorded",
|
|
1128
|
+
"- Guard: recovery issues do not create nested `stranded_issue_recovery` issues.",
|
|
1129
|
+
"",
|
|
1130
|
+
"Next action: the current recovery owner should inspect the failed run evidence, restore a live execution path or record the manual resolution, then move this recovery issue out of `blocked`.",
|
|
1131
|
+
].join("\n");
|
|
1132
|
+
}
|
|
1133
|
+
async function escalateStrandedRecoveryIssueInPlace(input) {
|
|
1134
|
+
const updated = await issuesSvc.update(input.issue.id, { status: "blocked" });
|
|
1135
|
+
if (!updated)
|
|
1136
|
+
return null;
|
|
1137
|
+
const prefix = await getCompanyIssuePrefix(input.issue.companyId);
|
|
1138
|
+
await issuesSvc.addComment(input.issue.id, buildRecoveryIssueInPlaceEscalationComment({
|
|
1139
|
+
issue: input.issue,
|
|
1140
|
+
previousStatus: input.previousStatus,
|
|
1141
|
+
latestRun: input.latestRun,
|
|
1142
|
+
prefix,
|
|
1143
|
+
}), {});
|
|
1144
|
+
await logActivity(db, {
|
|
1145
|
+
companyId: input.issue.companyId,
|
|
1146
|
+
actorType: "system",
|
|
1147
|
+
actorId: "system",
|
|
1148
|
+
agentId: null,
|
|
1149
|
+
runId: null,
|
|
1150
|
+
action: "issue.updated",
|
|
1151
|
+
entityType: "issue",
|
|
1152
|
+
entityId: input.issue.id,
|
|
1153
|
+
details: {
|
|
1154
|
+
identifier: input.issue.identifier,
|
|
1155
|
+
status: "blocked",
|
|
1156
|
+
previousStatus: input.previousStatus,
|
|
1157
|
+
source: "recovery.reconcile_stranded_recovery_issue",
|
|
1158
|
+
latestRunId: input.latestRun?.id ?? null,
|
|
1159
|
+
latestRunStatus: input.latestRun?.status ?? null,
|
|
1160
|
+
latestRunErrorCode: input.latestRun?.errorCode ?? null,
|
|
1161
|
+
originKind: input.issue.originKind,
|
|
1162
|
+
originId: input.issue.originId,
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
return updated;
|
|
1166
|
+
}
|
|
1048
1167
|
async function existingBlockerIssueIds(companyId, issueId) {
|
|
1049
1168
|
return db
|
|
1050
1169
|
.select({ blockerIssueId: issueRelations.issueId })
|
|
@@ -1061,6 +1180,13 @@ export function recoveryService(db, deps) {
|
|
|
1061
1180
|
.then((rows) => rows.map((row) => row.blockerIssueId));
|
|
1062
1181
|
}
|
|
1063
1182
|
async function escalateStrandedAssignedIssue(input) {
|
|
1183
|
+
if (isStrandedIssueRecoveryIssue(input.issue)) {
|
|
1184
|
+
return escalateStrandedRecoveryIssueInPlace({
|
|
1185
|
+
issue: input.issue,
|
|
1186
|
+
previousStatus: input.previousStatus,
|
|
1187
|
+
latestRun: input.latestRun,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1064
1190
|
const recoveryIssue = await ensureStrandedIssueRecoveryIssue({
|
|
1065
1191
|
issue: input.issue,
|
|
1066
1192
|
previousStatus: input.previousStatus,
|
|
@@ -1118,8 +1244,11 @@ export function recoveryService(db, deps) {
|
|
|
1118
1244
|
.from(issues)
|
|
1119
1245
|
.where(and(isNull(issues.assigneeUserId), inArray(issues.status, ["todo", "in_progress"]), sql `${issues.assigneeAgentId} is not null`));
|
|
1120
1246
|
const result = {
|
|
1247
|
+
assignmentDispatched: 0,
|
|
1121
1248
|
dispatchRequeued: 0,
|
|
1122
1249
|
continuationRequeued: 0,
|
|
1250
|
+
productiveContinuationObserved: 0,
|
|
1251
|
+
successfulContinuationObserved: 0,
|
|
1123
1252
|
orphanBlockersAssigned: 0,
|
|
1124
1253
|
escalated: 0,
|
|
1125
1254
|
skipped: 0,
|
|
@@ -1145,8 +1274,42 @@ export function recoveryService(db, deps) {
|
|
|
1145
1274
|
continue;
|
|
1146
1275
|
}
|
|
1147
1276
|
const latestRun = await getLatestIssueRun(issue.companyId, issue.id);
|
|
1277
|
+
if (isStrandedIssueRecoveryIssue(issue) && isUnsuccessfulTerminalIssueRun(latestRun)) {
|
|
1278
|
+
const updated = await escalateStrandedRecoveryIssueInPlace({
|
|
1279
|
+
issue,
|
|
1280
|
+
previousStatus: issue.status,
|
|
1281
|
+
latestRun,
|
|
1282
|
+
});
|
|
1283
|
+
if (updated) {
|
|
1284
|
+
result.escalated += 1;
|
|
1285
|
+
result.issueIds.push(issue.id);
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
result.skipped += 1;
|
|
1289
|
+
}
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1148
1292
|
if (issue.status === "todo") {
|
|
1149
|
-
if (!latestRun
|
|
1293
|
+
if (!latestRun) {
|
|
1294
|
+
if (await hasQueuedIssueWake(issue.companyId, issue.id)) {
|
|
1295
|
+
result.skipped += 1;
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (await isInvocationBudgetBlocked(issue, agentId)) {
|
|
1299
|
+
result.skipped += 1;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const queued = await enqueueInitialAssignedTodoDispatch(issue, agentId);
|
|
1303
|
+
if (queued) {
|
|
1304
|
+
result.assignmentDispatched += 1;
|
|
1305
|
+
result.issueIds.push(issue.id);
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
result.skipped += 1;
|
|
1309
|
+
}
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (latestRun.status === "succeeded") {
|
|
1150
1313
|
result.skipped += 1;
|
|
1151
1314
|
continue;
|
|
1152
1315
|
}
|
|
@@ -1169,6 +1332,10 @@ export function recoveryService(db, deps) {
|
|
|
1169
1332
|
}
|
|
1170
1333
|
continue;
|
|
1171
1334
|
}
|
|
1335
|
+
if (await isInvocationBudgetBlocked(issue, agentId)) {
|
|
1336
|
+
result.skipped += 1;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1172
1339
|
const queued = await enqueueStrandedIssueRecovery({
|
|
1173
1340
|
issueId: issue.id,
|
|
1174
1341
|
agentId,
|
|
@@ -1190,6 +1357,51 @@ export function recoveryService(db, deps) {
|
|
|
1190
1357
|
result.skipped += 1;
|
|
1191
1358
|
continue;
|
|
1192
1359
|
}
|
|
1360
|
+
if (isSuccessfulInProgressContinuationRun(latestRun)) {
|
|
1361
|
+
const successfulRun = latestRun;
|
|
1362
|
+
if (!isProductiveContinuationRun(successfulRun)) {
|
|
1363
|
+
result.successfulContinuationObserved += 1;
|
|
1364
|
+
result.skipped += 1;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (isRepeatedProductiveContinuationRecovery(successfulRun)) {
|
|
1368
|
+
const updated = await escalateStrandedAssignedIssue({
|
|
1369
|
+
issue,
|
|
1370
|
+
previousStatus: "in_progress",
|
|
1371
|
+
latestRun: successfulRun,
|
|
1372
|
+
comment: "Paperclip automatically retried continuation for this assigned `in_progress` issue and the retry " +
|
|
1373
|
+
"made progress, but it still has no live execution path. Moving it to `blocked` so it is visible for intervention.",
|
|
1374
|
+
});
|
|
1375
|
+
if (updated) {
|
|
1376
|
+
result.escalated += 1;
|
|
1377
|
+
result.issueIds.push(issue.id);
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
result.skipped += 1;
|
|
1381
|
+
}
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (await isInvocationBudgetBlocked(issue, agentId)) {
|
|
1385
|
+
result.skipped += 1;
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
const queued = await enqueueStrandedIssueRecovery({
|
|
1389
|
+
issueId: issue.id,
|
|
1390
|
+
agentId,
|
|
1391
|
+
reason: "issue_continuation_needed",
|
|
1392
|
+
retryReason: "issue_continuation_needed",
|
|
1393
|
+
source: "issue.productive_terminal_continuation_recovery",
|
|
1394
|
+
retryOfRunId: successfulRun.id,
|
|
1395
|
+
});
|
|
1396
|
+
if (queued) {
|
|
1397
|
+
result.continuationRequeued += 1;
|
|
1398
|
+
result.issueIds.push(issue.id);
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
result.skipped += 1;
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1193
1405
|
if (didAutomaticRecoveryFail(latestRun, "issue_continuation_needed")) {
|
|
1194
1406
|
const failureSummary = summarizeRunFailureForIssueComment(latestRun);
|
|
1195
1407
|
const updated = await escalateStrandedAssignedIssue({
|
|
@@ -1209,6 +1421,10 @@ export function recoveryService(db, deps) {
|
|
|
1209
1421
|
}
|
|
1210
1422
|
continue;
|
|
1211
1423
|
}
|
|
1424
|
+
if (await isInvocationBudgetBlocked(issue, agentId)) {
|
|
1425
|
+
result.skipped += 1;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1212
1428
|
const queued = await enqueueStrandedIssueRecovery({
|
|
1213
1429
|
issueId: issue.id,
|
|
1214
1430
|
agentId,
|
|
@@ -1232,7 +1448,7 @@ export function recoveryService(db, deps) {
|
|
|
1232
1448
|
return result;
|
|
1233
1449
|
}
|
|
1234
1450
|
async function collectIssueGraphLivenessFindings() {
|
|
1235
|
-
const [issueRows, relationRows, agentRows, activeRunRows, activeIssueRunRows, wakeRows] = await Promise.all([
|
|
1451
|
+
const [issueRows, relationRows, agentRows, activeRunRows, activeIssueRunRows, wakeRows, interactionRows, approvalRows, recoveryIssueRows,] = await Promise.all([
|
|
1236
1452
|
db
|
|
1237
1453
|
.select({
|
|
1238
1454
|
id: issues.id,
|
|
@@ -1247,7 +1463,10 @@ export function recoveryService(db, deps) {
|
|
|
1247
1463
|
assigneeUserId: issues.assigneeUserId,
|
|
1248
1464
|
createdByAgentId: issues.createdByAgentId,
|
|
1249
1465
|
createdByUserId: issues.createdByUserId,
|
|
1466
|
+
executionPolicy: issues.executionPolicy,
|
|
1250
1467
|
executionState: issues.executionState,
|
|
1468
|
+
monitorNextCheckAt: issues.monitorNextCheckAt,
|
|
1469
|
+
monitorAttemptCount: issues.monitorAttemptCount,
|
|
1251
1470
|
})
|
|
1252
1471
|
.from(issues)
|
|
1253
1472
|
.where(and(isNull(issues.hiddenAt), notInArray(issues.originKind, [RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation]))),
|
|
@@ -1298,7 +1517,43 @@ export function recoveryService(db, deps) {
|
|
|
1298
1517
|
})
|
|
1299
1518
|
.from(agentWakeupRequests)
|
|
1300
1519
|
.where(inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"])),
|
|
1520
|
+
db
|
|
1521
|
+
.select({
|
|
1522
|
+
companyId: issueThreadInteractions.companyId,
|
|
1523
|
+
issueId: issueThreadInteractions.issueId,
|
|
1524
|
+
status: issueThreadInteractions.status,
|
|
1525
|
+
})
|
|
1526
|
+
.from(issueThreadInteractions)
|
|
1527
|
+
.where(eq(issueThreadInteractions.status, "pending")),
|
|
1528
|
+
db
|
|
1529
|
+
.select({
|
|
1530
|
+
companyId: issueApprovals.companyId,
|
|
1531
|
+
issueId: issueApprovals.issueId,
|
|
1532
|
+
status: approvals.status,
|
|
1533
|
+
})
|
|
1534
|
+
.from(issueApprovals)
|
|
1535
|
+
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
|
|
1536
|
+
.where(inArray(approvals.status, ["pending", "revision_requested"])),
|
|
1537
|
+
db
|
|
1538
|
+
.select({
|
|
1539
|
+
companyId: issues.companyId,
|
|
1540
|
+
id: issues.id,
|
|
1541
|
+
status: issues.status,
|
|
1542
|
+
originId: issues.originId,
|
|
1543
|
+
})
|
|
1544
|
+
.from(issues)
|
|
1545
|
+
.where(and(isNull(issues.hiddenAt), eq(issues.originKind, STRANDED_ISSUE_RECOVERY_ORIGIN_KIND), notInArray(issues.status, ["done", "cancelled"]))),
|
|
1301
1546
|
]);
|
|
1547
|
+
const openRecoveryIssues = recoveryIssueRows.flatMap((row) => {
|
|
1548
|
+
const issueId = readNonEmptyString(row.originId);
|
|
1549
|
+
if (!issueId)
|
|
1550
|
+
return [];
|
|
1551
|
+
return [{
|
|
1552
|
+
companyId: row.companyId,
|
|
1553
|
+
issueId,
|
|
1554
|
+
status: row.status,
|
|
1555
|
+
}];
|
|
1556
|
+
});
|
|
1302
1557
|
return classifyIssueGraphLiveness({
|
|
1303
1558
|
issues: issueRows,
|
|
1304
1559
|
relations: relationRows,
|
|
@@ -1320,6 +1575,10 @@ export function recoveryService(db, deps) {
|
|
|
1320
1575
|
status: row.status,
|
|
1321
1576
|
issueId: issueIdFromWakePayload(row.payload),
|
|
1322
1577
|
})),
|
|
1578
|
+
pendingInteractions: interactionRows,
|
|
1579
|
+
pendingApprovals: approvalRows,
|
|
1580
|
+
openRecoveryIssues,
|
|
1581
|
+
now: new Date(),
|
|
1323
1582
|
});
|
|
1324
1583
|
}
|
|
1325
1584
|
async function findOpenLivenessEscalation(companyId, incidentKey) {
|
|
@@ -1422,18 +1681,90 @@ export function recoveryService(db, deps) {
|
|
|
1422
1681
|
}
|
|
1423
1682
|
return result;
|
|
1424
1683
|
}
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1684
|
+
function normalizeIssueGraphLivenessAutoRecoveryLookbackHours(raw) {
|
|
1685
|
+
const numeric = Math.floor(asNumber(raw, DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS));
|
|
1686
|
+
return Math.min(MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, Math.max(MIN_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, numeric));
|
|
1687
|
+
}
|
|
1688
|
+
function livenessDependencyIssueKey(companyId, issueId) {
|
|
1689
|
+
return `${companyId}:${issueId}`;
|
|
1690
|
+
}
|
|
1691
|
+
async function loadLivenessDependencyUpdatedAtByIssue(findings) {
|
|
1692
|
+
const issueIds = [
|
|
1693
|
+
...new Set(findings.flatMap((finding) => finding.dependencyPath.map((entry) => entry.issueId))),
|
|
1694
|
+
];
|
|
1427
1695
|
if (issueIds.length === 0)
|
|
1428
|
-
return
|
|
1696
|
+
return new Map();
|
|
1429
1697
|
const rows = await db
|
|
1430
|
-
.select({ id: issues.id, updatedAt: issues.updatedAt })
|
|
1698
|
+
.select({ id: issues.id, companyId: issues.companyId, updatedAt: issues.updatedAt })
|
|
1431
1699
|
.from(issues)
|
|
1432
|
-
.where(
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1700
|
+
.where(inArray(issues.id, issueIds));
|
|
1701
|
+
return new Map(rows.map((row) => [
|
|
1702
|
+
livenessDependencyIssueKey(row.companyId, row.id),
|
|
1703
|
+
row.updatedAt,
|
|
1704
|
+
]));
|
|
1705
|
+
}
|
|
1706
|
+
function latestDependencyUpdatedAtForLivenessFinding(finding, updatedAtByIssueKey) {
|
|
1707
|
+
const dependencyIssueIds = [...new Set(finding.dependencyPath.map((entry) => entry.issueId))];
|
|
1708
|
+
if (dependencyIssueIds.length === 0)
|
|
1709
|
+
return null;
|
|
1710
|
+
const timestamps = dependencyIssueIds.map((issueId) => updatedAtByIssueKey.get(livenessDependencyIssueKey(finding.companyId, issueId)) ?? null);
|
|
1711
|
+
if (timestamps.some((timestamp) => !timestamp))
|
|
1712
|
+
return null;
|
|
1713
|
+
const [firstTimestamp, ...remainingTimestamps] = timestamps;
|
|
1714
|
+
return remainingTimestamps.reduce((latest, updatedAt) => updatedAt > latest ? updatedAt : latest, firstTimestamp);
|
|
1715
|
+
}
|
|
1716
|
+
function isLivenessFindingInsideAutoRecoveryLookback(finding, cutoff, updatedAtByIssueKey) {
|
|
1717
|
+
const latestUpdatedAt = latestDependencyUpdatedAtForLivenessFinding(finding, updatedAtByIssueKey);
|
|
1718
|
+
return Boolean(latestUpdatedAt && latestUpdatedAt >= cutoff);
|
|
1719
|
+
}
|
|
1720
|
+
async function buildIssueGraphLivenessAutoRecoveryPreview(opts) {
|
|
1721
|
+
const now = opts?.now ?? new Date();
|
|
1722
|
+
const lookbackHours = normalizeIssueGraphLivenessAutoRecoveryLookbackHours(opts?.lookbackHours);
|
|
1723
|
+
const cutoff = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
|
|
1724
|
+
const findings = await collectIssueGraphLivenessFindings();
|
|
1725
|
+
const updatedAtByIssueKey = await loadLivenessDependencyUpdatedAtByIssue(findings);
|
|
1726
|
+
const issueIds = [...new Set(findings.map((finding) => finding.recoveryIssueId))];
|
|
1727
|
+
const recoveryRows = issueIds.length > 0
|
|
1728
|
+
? await db
|
|
1729
|
+
.select({ id: issues.id, identifier: issues.identifier, title: issues.title })
|
|
1730
|
+
.from(issues)
|
|
1731
|
+
.where(inArray(issues.id, issueIds))
|
|
1732
|
+
: [];
|
|
1733
|
+
const recoveryById = new Map(recoveryRows.map((row) => [row.id, row]));
|
|
1734
|
+
const items = [];
|
|
1735
|
+
let skippedOutsideLookback = 0;
|
|
1736
|
+
for (const finding of findings) {
|
|
1737
|
+
const latestDependencyUpdatedAt = latestDependencyUpdatedAtForLivenessFinding(finding, updatedAtByIssueKey);
|
|
1738
|
+
if (!latestDependencyUpdatedAt || latestDependencyUpdatedAt < cutoff) {
|
|
1739
|
+
skippedOutsideLookback += 1;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const recoveryIssue = recoveryById.get(finding.recoveryIssueId);
|
|
1743
|
+
items.push({
|
|
1744
|
+
issueId: finding.issueId,
|
|
1745
|
+
identifier: finding.identifier,
|
|
1746
|
+
title: finding.dependencyPath[0]?.title ?? finding.identifier ?? finding.issueId,
|
|
1747
|
+
state: finding.state,
|
|
1748
|
+
severity: finding.severity,
|
|
1749
|
+
reason: finding.reason,
|
|
1750
|
+
recoveryIssueId: finding.recoveryIssueId,
|
|
1751
|
+
recoveryIdentifier: recoveryIssue?.identifier ?? null,
|
|
1752
|
+
recoveryTitle: recoveryIssue?.title ?? null,
|
|
1753
|
+
recommendedOwnerAgentId: finding.recommendedOwnerAgentId,
|
|
1754
|
+
incidentKey: finding.incidentKey,
|
|
1755
|
+
latestDependencyUpdatedAt: latestDependencyUpdatedAt.toISOString(),
|
|
1756
|
+
dependencyPath: finding.dependencyPath,
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
lookbackHours,
|
|
1761
|
+
cutoff: cutoff.toISOString(),
|
|
1762
|
+
generatedAt: now.toISOString(),
|
|
1763
|
+
findings: findings.length,
|
|
1764
|
+
recoverableFindings: items.length,
|
|
1765
|
+
skippedOutsideLookback,
|
|
1766
|
+
items,
|
|
1767
|
+
};
|
|
1437
1768
|
}
|
|
1438
1769
|
async function resolveEscalationOwnerAgentId(finding, issue) {
|
|
1439
1770
|
const detailedCandidates = finding.recommendedOwnerCandidates.length > 0
|
|
@@ -1482,10 +1813,15 @@ export function recoveryService(db, deps) {
|
|
|
1482
1813
|
async function ensureIssueBlockedByEscalation(input) {
|
|
1483
1814
|
const blockerIds = await existingBlockerIssueIds(input.issue.companyId, input.issue.id);
|
|
1484
1815
|
const nextBlockerIds = [...new Set([...blockerIds, input.escalationIssueId])];
|
|
1816
|
+
const isAlreadyBlockedByEscalation = blockerIds.includes(input.escalationIssueId);
|
|
1817
|
+
const isAlreadyBlocked = input.issue.status === "blocked";
|
|
1818
|
+
if (isAlreadyBlockedByEscalation && isAlreadyBlocked) {
|
|
1819
|
+
return input.issue;
|
|
1820
|
+
}
|
|
1485
1821
|
const update = {
|
|
1486
1822
|
blockedByIssueIds: nextBlockerIds,
|
|
1487
1823
|
};
|
|
1488
|
-
if (
|
|
1824
|
+
if (!isAlreadyBlocked) {
|
|
1489
1825
|
update.status = "blocked";
|
|
1490
1826
|
}
|
|
1491
1827
|
const updated = await issuesSvc.update(input.issue.id, update);
|
|
@@ -1667,16 +2003,22 @@ export function recoveryService(db, deps) {
|
|
|
1667
2003
|
async function reconcileIssueGraphLiveness(opts) {
|
|
1668
2004
|
const findings = await collectIssueGraphLivenessFindings();
|
|
1669
2005
|
const experimentalSettings = await instanceSettings.getExperimental();
|
|
1670
|
-
const autoRecoveryEnabled = asBoolean(experimentalSettings.enableIssueGraphLivenessAutoRecovery,
|
|
2006
|
+
const autoRecoveryEnabled = asBoolean(experimentalSettings.enableIssueGraphLivenessAutoRecovery, true) || opts?.force === true;
|
|
2007
|
+
const lookbackHours = normalizeIssueGraphLivenessAutoRecoveryLookbackHours(opts?.lookbackHours ?? experimentalSettings.issueGraphLivenessAutoRecoveryLookbackHours);
|
|
2008
|
+
const now = new Date();
|
|
2009
|
+
const cutoff = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
|
|
1671
2010
|
const obsoleteRecoveryCleanup = await retireObsoleteLivenessRecoveryIssues(findings);
|
|
2011
|
+
const updatedAtByIssueKey = await loadLivenessDependencyUpdatedAtByIssue(findings);
|
|
1672
2012
|
const result = {
|
|
1673
2013
|
findings: findings.length,
|
|
1674
2014
|
autoRecoveryEnabled,
|
|
2015
|
+
lookbackHours,
|
|
2016
|
+
cutoff: cutoff.toISOString(),
|
|
1675
2017
|
escalationsCreated: 0,
|
|
1676
2018
|
existingEscalations: 0,
|
|
1677
2019
|
skipped: 0,
|
|
1678
2020
|
skippedAutoRecoveryDisabled: 0,
|
|
1679
|
-
|
|
2021
|
+
skippedOutsideLookback: 0,
|
|
1680
2022
|
obsoleteRecoveriesRetired: obsoleteRecoveryCleanup.retired,
|
|
1681
2023
|
obsoleteRecoveriesActiveSkipped: obsoleteRecoveryCleanup.activeSkipped,
|
|
1682
2024
|
obsoleteRecoveryBlockerRelationsRemoved: obsoleteRecoveryCleanup.blockerRelationsRemoved,
|
|
@@ -1688,10 +2030,9 @@ export function recoveryService(db, deps) {
|
|
|
1688
2030
|
result.skippedAutoRecoveryDisabled = findings.length;
|
|
1689
2031
|
return result;
|
|
1690
2032
|
}
|
|
1691
|
-
const now = new Date();
|
|
1692
2033
|
for (const finding of findings) {
|
|
1693
|
-
if (!
|
|
1694
|
-
result.
|
|
2034
|
+
if (!isLivenessFindingInsideAutoRecoveryLookback(finding, cutoff, updatedAtByIssueKey)) {
|
|
2035
|
+
result.skippedOutsideLookback += 1;
|
|
1695
2036
|
result.skipped += 1;
|
|
1696
2037
|
continue;
|
|
1697
2038
|
}
|
|
@@ -1724,6 +2065,7 @@ export function recoveryService(db, deps) {
|
|
|
1724
2065
|
recordWatchdogDecision,
|
|
1725
2066
|
scanSilentActiveRuns,
|
|
1726
2067
|
reconcileStrandedAssignedIssues,
|
|
2068
|
+
buildIssueGraphLivenessAutoRecoveryPreview,
|
|
1727
2069
|
reconcileIssueGraphLiveness,
|
|
1728
2070
|
readRecoveryTimerIntervalMs,
|
|
1729
2071
|
};
|