@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.
- package/dist/adapters/builtin-adapter-types.d.ts.map +1 -1
- package/dist/adapters/builtin-adapter-types.js +1 -0
- package/dist/adapters/builtin-adapter-types.js.map +1 -1
- package/dist/adapters/plugin-loader.d.ts +1 -0
- package/dist/adapters/plugin-loader.d.ts.map +1 -1
- package/dist/adapters/plugin-loader.js +12 -1
- package/dist/adapters/plugin-loader.js.map +1 -1
- package/dist/adapters/plugin-loader.test.d.ts +2 -0
- package/dist/adapters/plugin-loader.test.d.ts.map +1 -0
- package/dist/adapters/plugin-loader.test.js +37 -0
- package/dist/adapters/plugin-loader.test.js.map +1 -0
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +68 -5
- package/dist/adapters/registry.js.map +1 -1
- package/dist/adapters/registry.test.d.ts +2 -0
- package/dist/adapters/registry.test.d.ts.map +1 -0
- package/dist/adapters/registry.test.js +35 -0
- package/dist/adapters/registry.test.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +5 -7
- package/dist/app.js.map +1 -1
- package/dist/auth/better-auth.d.ts +10 -0
- package/dist/auth/better-auth.d.ts.map +1 -1
- package/dist/auth/better-auth.js +47 -0
- package/dist/auth/better-auth.js.map +1 -1
- package/dist/auth/workspace-login-handoff-exchange.d.ts +75 -0
- package/dist/auth/workspace-login-handoff-exchange.d.ts.map +1 -0
- package/dist/auth/workspace-login-handoff-exchange.js +73 -0
- package/dist/auth/workspace-login-handoff-exchange.js.map +1 -0
- package/dist/auth/workspace-login-handoff-plugin.d.ts +40 -0
- package/dist/auth/workspace-login-handoff-plugin.d.ts.map +1 -0
- package/dist/auth/workspace-login-handoff-plugin.js +140 -0
- package/dist/auth/workspace-login-handoff-plugin.js.map +1 -0
- package/dist/auth/workspace-login-handoff.d.ts +189 -0
- package/dist/auth/workspace-login-handoff.d.ts.map +1 -0
- package/dist/auth/workspace-login-handoff.js +319 -0
- package/dist/auth/workspace-login-handoff.js.map +1 -0
- package/dist/build-info.json +3 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/dev-runner-worktree.d.ts.map +1 -1
- package/dist/dev-runner-worktree.js +5 -0
- package/dist/dev-runner-worktree.js.map +1 -1
- package/dist/duplex-aggregate-ceiling-env.d.ts +21 -0
- package/dist/duplex-aggregate-ceiling-env.d.ts.map +1 -0
- package/dist/duplex-aggregate-ceiling-env.js +23 -0
- package/dist/duplex-aggregate-ceiling-env.js.map +1 -0
- package/dist/embedded-postgres-supervisor.d.ts +27 -0
- package/dist/embedded-postgres-supervisor.d.ts.map +1 -0
- package/dist/embedded-postgres-supervisor.js +72 -0
- package/dist/embedded-postgres-supervisor.js.map +1 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -7
- package/dist/index.js.map +1 -1
- package/dist/instrumentation.d.ts +33 -0
- package/dist/instrumentation.d.ts.map +1 -1
- package/dist/instrumentation.js +68 -1
- package/dist/instrumentation.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +35 -10
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +8 -1
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/logger.js +5 -2
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/redact-sensitive.d.ts.map +1 -1
- package/dist/middleware/redact-sensitive.js +4 -0
- package/dist/middleware/redact-sensitive.js.map +1 -1
- package/dist/middleware/validate.d.ts +3 -1
- package/dist/middleware/validate.d.ts.map +1 -1
- package/dist/middleware/validate.js +37 -0
- package/dist/middleware/validate.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +20 -0
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/activity.js +4 -4
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/adapter-login-route-spine.d.ts +81 -0
- package/dist/routes/adapter-login-route-spine.d.ts.map +1 -0
- package/dist/routes/adapter-login-route-spine.js +40 -0
- package/dist/routes/adapter-login-route-spine.js.map +1 -0
- package/dist/routes/adapters.d.ts +33 -0
- package/dist/routes/adapters.d.ts.map +1 -1
- package/dist/routes/adapters.js +35 -1
- package/dist/routes/adapters.js.map +1 -1
- package/dist/routes/adapters.test.d.ts +2 -0
- package/dist/routes/adapters.test.d.ts.map +1 -0
- package/dist/routes/adapters.test.js +58 -0
- package/dist/routes/adapters.test.js.map +1 -0
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +281 -91
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/cases.d.ts +11 -22
- package/dist/routes/cases.d.ts.map +1 -1
- package/dist/routes/cases.js +13 -13
- package/dist/routes/companies.js +3 -3
- package/dist/routes/decision-training.js +6 -6
- package/dist/routes/decisions.js +4 -4
- package/dist/routes/decisions.js.map +1 -1
- package/dist/routes/environments.d.ts.map +1 -1
- package/dist/routes/environments.js +79 -6
- package/dist/routes/environments.js.map +1 -1
- package/dist/routes/execution-workspaces.d.ts.map +1 -1
- package/dist/routes/execution-workspaces.js +384 -16
- package/dist/routes/execution-workspaces.js.map +1 -1
- package/dist/routes/file-resources.js +2 -2
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +58 -4
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/instance-settings.d.ts.map +1 -1
- package/dist/routes/instance-settings.js +40 -0
- package/dist/routes/instance-settings.js.map +1 -1
- package/dist/routes/issues.d.ts +10 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +495 -65
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/openapi.d.ts.map +1 -1
- package/dist/routes/openapi.js +241 -122
- package/dist/routes/openapi.js.map +1 -1
- package/dist/routes/pipelines.js +19 -19
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +22 -0
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +2 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/routines.js +2 -2
- package/dist/routes/routines.js.map +1 -1
- package/dist/routes/secrets.d.ts +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +39 -50
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/setup-token-route.test.js +50 -3
- package/dist/routes/setup-token-route.test.js.map +1 -1
- package/dist/services/adapter-login-lease.d.ts +50 -0
- package/dist/services/adapter-login-lease.d.ts.map +1 -0
- package/dist/services/adapter-login-lease.js +26 -0
- package/dist/services/adapter-login-lease.js.map +1 -0
- package/dist/services/adapter-login-lease.test.d.ts +2 -0
- package/dist/services/adapter-login-lease.test.d.ts.map +1 -0
- package/dist/services/adapter-login-lease.test.js +68 -0
- package/dist/services/adapter-login-lease.test.js.map +1 -0
- package/dist/services/agent-action-audit.js +1 -1
- package/dist/services/agent-secret-bindings.d.ts +15 -0
- package/dist/services/agent-secret-bindings.d.ts.map +1 -1
- package/dist/services/agent-secret-bindings.js +2 -2
- package/dist/services/agent-secret-bindings.js.map +1 -1
- package/dist/services/agents.d.ts +13 -13
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +46 -5
- package/dist/services/agents.js.map +1 -1
- package/dist/services/artifact-review-documents.d.ts +34 -0
- package/dist/services/artifact-review-documents.d.ts.map +1 -0
- package/dist/services/artifact-review-documents.js +196 -0
- package/dist/services/artifact-review-documents.js.map +1 -0
- package/dist/services/assets.d.ts +1 -1
- package/dist/services/codex-device-login-credential-read.d.ts +58 -0
- package/dist/services/codex-device-login-credential-read.d.ts.map +1 -0
- package/dist/services/codex-device-login-credential-read.js +118 -0
- package/dist/services/codex-device-login-credential-read.js.map +1 -0
- package/dist/services/codex-device-login-reaper.d.ts.map +1 -1
- package/dist/services/codex-device-login-reaper.js +8 -5
- package/dist/services/codex-device-login-reaper.js.map +1 -1
- package/dist/services/codex-device-login-service.d.ts +127 -36
- package/dist/services/codex-device-login-service.d.ts.map +1 -1
- package/dist/services/codex-device-login-service.js +204 -84
- package/dist/services/codex-device-login-service.js.map +1 -1
- package/dist/services/company-portability.d.ts +3 -1
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +76 -20
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/company-skill-policy.d.ts +1 -1
- package/dist/services/costs.d.ts +1 -1
- package/dist/services/decisions.d.ts +6 -6
- package/dist/services/duplex-observability-recorder.d.ts +62 -0
- package/dist/services/duplex-observability-recorder.d.ts.map +1 -0
- package/dist/services/duplex-observability-recorder.js +54 -0
- package/dist/services/duplex-observability-recorder.js.map +1 -0
- package/dist/services/environment-config.js +5 -5
- package/dist/services/environment-config.js.map +1 -1
- package/dist/services/environment-custom-image-runtime.d.ts +116 -0
- package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
- package/dist/services/environment-custom-image-runtime.js +255 -1
- package/dist/services/environment-custom-image-runtime.js.map +1 -1
- package/dist/services/environment-custom-images.d.ts +39 -0
- package/dist/services/environment-custom-images.d.ts.map +1 -1
- package/dist/services/environment-custom-images.js +219 -17
- package/dist/services/environment-custom-images.js.map +1 -1
- package/dist/services/environment-driver-traits.d.ts +109 -0
- package/dist/services/environment-driver-traits.d.ts.map +1 -0
- package/dist/services/environment-driver-traits.js +102 -0
- package/dist/services/environment-driver-traits.js.map +1 -0
- package/dist/services/environment-execution-target.d.ts +4 -0
- package/dist/services/environment-execution-target.d.ts.map +1 -1
- package/dist/services/environment-execution-target.js +63 -7
- package/dist/services/environment-execution-target.js.map +1 -1
- package/dist/services/environment-run-orchestrator.d.ts +27 -1
- package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
- package/dist/services/environment-run-orchestrator.js +24 -4
- package/dist/services/environment-run-orchestrator.js.map +1 -1
- package/dist/services/environment-runtime.d.ts +118 -20
- package/dist/services/environment-runtime.d.ts.map +1 -1
- package/dist/services/environment-runtime.js +436 -91
- package/dist/services/environment-runtime.js.map +1 -1
- package/dist/services/environments.d.ts.map +1 -1
- package/dist/services/environments.js +20 -2
- package/dist/services/environments.js.map +1 -1
- package/dist/services/execution-workspace-branch-ownership.d.ts +4 -0
- package/dist/services/execution-workspace-branch-ownership.d.ts.map +1 -0
- package/dist/services/execution-workspace-branch-ownership.js +9 -0
- package/dist/services/execution-workspace-branch-ownership.js.map +1 -0
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +3 -0
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts +2 -0
- package/dist/services/execution-workspaces.d.ts.map +1 -1
- package/dist/services/execution-workspaces.js +90 -2
- package/dist/services/execution-workspaces.js.map +1 -1
- package/dist/services/external-objects.d.ts +19 -19
- package/dist/services/finance.d.ts +1 -1
- package/dist/services/goals.d.ts +2 -2
- package/dist/services/heartbeat-policy.d.ts +3 -0
- package/dist/services/heartbeat-policy.d.ts.map +1 -0
- package/dist/services/heartbeat-policy.js +10 -0
- package/dist/services/heartbeat-policy.js.map +1 -0
- package/dist/services/heartbeat-run-runtime-status.test.js +1 -1
- package/dist/services/heartbeat-run-runtime-status.test.js.map +1 -1
- package/dist/services/heartbeat.d.ts +23 -1
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +192 -11
- 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 +2 -0
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-dependency-wakeups.d.ts +57 -9
- package/dist/services/issue-dependency-wakeups.d.ts.map +1 -1
- package/dist/services/issue-dependency-wakeups.js +146 -14
- package/dist/services/issue-dependency-wakeups.js.map +1 -1
- package/dist/services/issue-dependency-wakeups.test.d.ts +2 -0
- package/dist/services/issue-dependency-wakeups.test.d.ts.map +1 -0
- package/dist/services/issue-dependency-wakeups.test.js +183 -0
- package/dist/services/issue-dependency-wakeups.test.js.map +1 -0
- package/dist/services/issue-recovery-actions.d.ts +6 -1
- package/dist/services/issue-recovery-actions.d.ts.map +1 -1
- package/dist/services/issue-recovery-actions.js +123 -44
- package/dist/services/issue-recovery-actions.js.map +1 -1
- package/dist/services/issue-thread-interactions.d.ts +8 -0
- package/dist/services/issue-thread-interactions.d.ts.map +1 -1
- package/dist/services/issue-thread-interactions.js +220 -10
- package/dist/services/issue-thread-interactions.js.map +1 -1
- package/dist/services/issues.d.ts +8 -7
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +84 -12
- package/dist/services/issues.js.map +1 -1
- package/dist/services/local-service-supervisor.d.ts +61 -1
- package/dist/services/local-service-supervisor.d.ts.map +1 -1
- package/dist/services/local-service-supervisor.js +221 -25
- package/dist/services/local-service-supervisor.js.map +1 -1
- package/dist/services/login-command.d.ts +46 -0
- package/dist/services/login-command.d.ts.map +1 -0
- package/dist/services/login-command.js +87 -0
- package/dist/services/login-command.js.map +1 -0
- package/dist/services/login-command.test.d.ts +2 -0
- package/dist/services/login-command.test.d.ts.map +1 -0
- package/dist/services/login-command.test.js +71 -0
- package/dist/services/login-command.test.js.map +1 -0
- package/dist/services/managed-workspace-identity.d.ts +140 -0
- package/dist/services/managed-workspace-identity.d.ts.map +1 -0
- package/dist/services/managed-workspace-identity.js +277 -0
- package/dist/services/managed-workspace-identity.js.map +1 -0
- package/dist/services/plugin-environment-driver.d.ts +15 -1
- package/dist/services/plugin-environment-driver.d.ts.map +1 -1
- package/dist/services/plugin-environment-driver.js +23 -1
- package/dist/services/plugin-environment-driver.js.map +1 -1
- package/dist/services/plugin-host-services.d.ts +3 -2
- package/dist/services/plugin-host-services.d.ts.map +1 -1
- package/dist/services/plugin-host-services.js +11 -2
- package/dist/services/plugin-host-services.js.map +1 -1
- package/dist/services/plugin-loader.d.ts.map +1 -1
- package/dist/services/plugin-loader.js +39 -8
- package/dist/services/plugin-loader.js.map +1 -1
- package/dist/services/plugin-managed-agents.d.ts.map +1 -1
- package/dist/services/plugin-managed-agents.js +50 -5
- package/dist/services/plugin-managed-agents.js.map +1 -1
- package/dist/services/plugin-manifest-validator.d.ts.map +1 -1
- package/dist/services/plugin-manifest-validator.js +5 -2
- package/dist/services/plugin-manifest-validator.js.map +1 -1
- package/dist/services/plugin-registry.d.ts +5 -5
- package/dist/services/plugin-worker-manager.d.ts +158 -11
- package/dist/services/plugin-worker-manager.d.ts.map +1 -1
- package/dist/services/plugin-worker-manager.js +1139 -79
- package/dist/services/plugin-worker-manager.js.map +1 -1
- package/dist/services/projects.d.ts +1 -1
- package/dist/services/recovery/disposition-repair.d.ts +24 -0
- package/dist/services/recovery/disposition-repair.d.ts.map +1 -0
- package/dist/services/recovery/disposition-repair.js +125 -0
- package/dist/services/recovery/disposition-repair.js.map +1 -0
- package/dist/services/recovery/service.d.ts +6 -6
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +856 -384
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/recovery/stranded-notice.js +4 -4
- package/dist/services/recovery/stranded-notice.js.map +1 -1
- package/dist/services/recovery/stranded-notice.test.js +1 -1
- package/dist/services/recovery/stranded-notice.test.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.d.ts +1 -1
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
- package/dist/services/recovery/successful-run-handoff.js +6 -4
- package/dist/services/recovery/successful-run-handoff.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.test.js +1 -1
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
- package/dist/services/recovery-observability.d.ts +1 -1
- package/dist/services/recovery-observability.d.ts.map +1 -1
- package/dist/services/recovery-observability.js +4 -1
- package/dist/services/recovery-observability.js.map +1 -1
- package/dist/services/routines.d.ts.map +1 -1
- package/dist/services/routines.js +86 -1
- package/dist/services/routines.js.map +1 -1
- package/dist/services/runtime-exposure/loopback-listener.js +1 -1
- package/dist/services/runtime-exposure/loopback-listener.js.map +1 -1
- package/dist/services/runtime-exposure/loopback-listener.test.js +1 -1
- package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -1
- package/dist/services/scripts/codex-auth-read.cjs +93 -0
- package/dist/services/secret-proposal-authorization.d.ts +15 -0
- package/dist/services/secret-proposal-authorization.d.ts.map +1 -0
- package/dist/services/secret-proposal-authorization.js +29 -0
- package/dist/services/secret-proposal-authorization.js.map +1 -0
- package/dist/services/secret-proposal-notifications.d.ts +18 -0
- package/dist/services/secret-proposal-notifications.d.ts.map +1 -0
- package/dist/services/secret-proposal-notifications.js +37 -0
- package/dist/services/secret-proposal-notifications.js.map +1 -0
- package/dist/services/secret-proposals.d.ts +20 -10
- package/dist/services/secret-proposals.d.ts.map +1 -1
- package/dist/services/secret-proposals.js +205 -14
- package/dist/services/secret-proposals.js.map +1 -1
- package/dist/services/secrets.d.ts +3 -3
- package/dist/services/settings-visibility.d.ts +17 -0
- package/dist/services/settings-visibility.d.ts.map +1 -0
- package/dist/services/settings-visibility.js +29 -0
- package/dist/services/settings-visibility.js.map +1 -0
- package/dist/services/setup-token-reaper.d.ts +36 -0
- package/dist/services/setup-token-reaper.d.ts.map +1 -0
- package/dist/services/setup-token-reaper.js +38 -0
- package/dist/services/setup-token-reaper.js.map +1 -0
- package/dist/services/setup-token-reaper.test.d.ts +2 -0
- package/dist/services/setup-token-reaper.test.d.ts.map +1 -0
- package/dist/services/setup-token-reaper.test.js +123 -0
- package/dist/services/setup-token-reaper.test.js.map +1 -0
- package/dist/services/setup-token-session.d.ts +57 -45
- package/dist/services/setup-token-session.d.ts.map +1 -1
- package/dist/services/setup-token-session.js +118 -110
- package/dist/services/setup-token-session.js.map +1 -1
- package/dist/services/setup-token-session.test.js +88 -47
- package/dist/services/setup-token-session.test.js.map +1 -1
- package/dist/services/setup-token-transport-binding.d.ts +20 -12
- package/dist/services/setup-token-transport-binding.d.ts.map +1 -1
- package/dist/services/setup-token-transport-binding.js +59 -32
- package/dist/services/setup-token-transport-binding.js.map +1 -1
- package/dist/services/setup-token-transport-binding.test.js +50 -17
- package/dist/services/setup-token-transport-binding.test.js.map +1 -1
- package/dist/services/smoke-lab.d.ts +1 -1
- package/dist/services/stalled-review-decisions.d.ts +1 -1
- package/dist/services/status-cards.d.ts +168 -168
- package/dist/services/tool-access-policy.d.ts +2 -2
- package/dist/services/tool-gateway.d.ts +1 -1
- package/dist/services/work-products.d.ts +19 -0
- package/dist/services/work-products.d.ts.map +1 -1
- package/dist/services/work-products.js +50 -3
- package/dist/services/work-products.js.map +1 -1
- package/dist/services/workspace-git-operation-scheduler.test.js +10 -3
- package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -1
- package/dist/services/workspace-instance-cleanup.d.ts +9 -0
- package/dist/services/workspace-instance-cleanup.d.ts.map +1 -1
- package/dist/services/workspace-instance-cleanup.js +29 -3
- package/dist/services/workspace-instance-cleanup.js.map +1 -1
- package/dist/services/workspace-login-handoff-issuer.d.ts +95 -0
- package/dist/services/workspace-login-handoff-issuer.d.ts.map +1 -0
- package/dist/services/workspace-login-handoff-issuer.js +147 -0
- package/dist/services/workspace-login-handoff-issuer.js.map +1 -0
- package/dist/services/workspace-operations.d.ts +4 -1
- package/dist/services/workspace-operations.d.ts.map +1 -1
- package/dist/services/workspace-operations.js +29 -15
- package/dist/services/workspace-operations.js.map +1 -1
- package/dist/services/workspace-readiness.d.ts +56 -0
- package/dist/services/workspace-readiness.d.ts.map +1 -0
- package/dist/services/workspace-readiness.js +250 -0
- package/dist/services/workspace-readiness.js.map +1 -0
- package/dist/services/workspace-realization.d.ts.map +1 -1
- package/dist/services/workspace-realization.js +5 -33
- package/dist/services/workspace-realization.js.map +1 -1
- package/dist/services/workspace-runtime-exposure.test.js +321 -12
- package/dist/services/workspace-runtime-exposure.test.js.map +1 -1
- package/dist/services/workspace-runtime-leases.d.ts.map +1 -1
- package/dist/services/workspace-runtime-leases.js +1 -1
- package/dist/services/workspace-runtime-leases.js.map +1 -1
- package/dist/services/workspace-runtime-read-model.d.ts +8 -8
- package/dist/services/workspace-runtime-read-model.test.js +20 -0
- package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
- package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
- package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
- package/dist/services/workspace-runtime.d.ts +77 -4
- package/dist/services/workspace-runtime.d.ts.map +1 -1
- package/dist/services/workspace-runtime.js +1020 -126
- package/dist/services/workspace-runtime.js.map +1 -1
- package/dist/worktree-config.d.ts.map +1 -1
- package/dist/worktree-config.js +3 -76
- package/dist/worktree-config.js.map +1 -1
- package/dist/worktree-seed-manifest.d.ts +8 -0
- package/dist/worktree-seed-manifest.d.ts.map +1 -0
- package/dist/worktree-seed-manifest.js +47 -0
- package/dist/worktree-seed-manifest.js.map +1 -0
- package/package.json +34 -30
- package/skills/paperclip/SKILL.md +2 -0
- package/skills/paperclip/references/api-reference.md +44 -2
- package/ui-dist/assets/CompanyExport-DnEsO-St.js +4 -0
- package/ui-dist/assets/{abnfDiagram-N423BO3Z-BvgoV-u5.js → abnfDiagram-N423BO3Z-2ff8TsQ0.js} +1 -1
- package/ui-dist/assets/{arc-BInnUBUU.js → arc-B2AXsH34.js} +1 -1
- package/ui-dist/assets/architectureDiagram-T3A2C74G-BDxtKMxk.js +36 -0
- package/ui-dist/assets/{blockDiagram-VBNYF7ZC-D9kzAkLN.js → blockDiagram-VBNYF7ZC-BB8T5Mj5.js} +1 -1
- package/ui-dist/assets/{c4Diagram-5PPSVZJV-BwzsgV_0.js → c4Diagram-5PPSVZJV-lMo8qk-x.js} +1 -1
- package/ui-dist/assets/channel-DCTVZpyq.js +1 -0
- package/ui-dist/assets/{chunk-2GRJ4B5K-D2EQ9vrl.js → chunk-2GRJ4B5K-CsS7EB3G.js} +1 -1
- package/ui-dist/assets/{chunk-2Q5K7J3B-DOA_8fSd.js → chunk-2Q5K7J3B-DFyQPjoR.js} +1 -1
- package/ui-dist/assets/{chunk-5RXB4S5H-CT5ukRiT.js → chunk-5RXB4S5H-No2SHOQR.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-D2gOlkyB.js → chunk-5VM5RSS4-DdxQO7V9.js} +1 -1
- package/ui-dist/assets/{chunk-6Q2QTUOP-DUb6aG8c.js → chunk-6Q2QTUOP-gbHjh-aK.js} +1 -1
- package/ui-dist/assets/{chunk-GF5L2VYU-BPabG3Cx.js → chunk-GF5L2VYU-h-BBl6NY.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-scNfhlZz.js → chunk-JWPE2WC7-Q3pJdTz5.js} +1 -1
- package/ui-dist/assets/{chunk-KBJHAD2P-BcRAzRpq.js → chunk-KBJHAD2P-Ba8LGLSx.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-BiZ0-EGh.js → chunk-RYQCIY6F-9VcvykBo.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-lJULxFIE.js → chunk-XXDRQBXY-NVbjDH4W.js} +1 -1
- package/ui-dist/assets/classDiagram-JCYQIIEL-CzJ88u6h.js +1 -0
- package/ui-dist/assets/classDiagram-v2-OCEON4UE-CzJ88u6h.js +1 -0
- package/ui-dist/assets/cose-bilkent-JH36ORCC-Cdz4wtUh.js +1 -0
- package/ui-dist/assets/{cynefin-VYW2F7L2-B34jJY1x.js → cynefin-VYW2F7L2-MpzJkW5s.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-MW4NZA55-DQ8QK8JK.js → cynefinDiagram-MW4NZA55-_Nx8av7i.js} +1 -1
- package/ui-dist/assets/{dagre-VZM6K2ZE-B7lSKgxH.js → dagre-VZM6K2ZE-DcZTQ4Oh.js} +1 -1
- package/ui-dist/assets/{diagram-7IWD3JNH-B3hLAFGE.js → diagram-7IWD3JNH-UyLi2mee.js} +1 -1
- package/ui-dist/assets/{diagram-B4RE2ZJO-CH9Ro9e0.js → diagram-B4RE2ZJO-B6p0Ivqg.js} +1 -1
- package/ui-dist/assets/{diagram-LBJQPF4R-DdPVMii7.js → diagram-LBJQPF4R-C8u6pa97.js} +1 -1
- package/ui-dist/assets/{diagram-Q27KOJAE-f5VXswjc.js → diagram-Q27KOJAE-DbwyagW0.js} +1 -1
- package/ui-dist/assets/{diagram-UB23O5K3-DGqxixsR.js → diagram-UB23O5K3-Cn5BMEH7.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-BXEA7PRR-C-1klg_w.js → ebnfDiagram-BXEA7PRR-vE0IZma9.js} +1 -1
- package/ui-dist/assets/{erDiagram-JOGREHBK-Dt4qK4gv.js → erDiagram-JOGREHBK-3OZVIlVG.js} +1 -1
- package/ui-dist/assets/{flowDiagram-UKHOOZJN-9ZlrTuET.js → flowDiagram-UKHOOZJN-C94FiPyr.js} +1 -1
- package/ui-dist/assets/ganttDiagram-PKOTCBZU-DMVaPmF-.js +292 -0
- package/ui-dist/assets/{gitGraphDiagram-DS77QQ5N-HH2fSgad.js → gitGraphDiagram-DS77QQ5N-yeFtJ689.js} +1 -1
- package/ui-dist/assets/{index-Cm6iEYPt.js → index-B4ZF3vY8.js} +288 -224
- package/ui-dist/assets/{index-CHXG4GPL.js → index-Bi_vqkcQ.js} +1 -1
- package/ui-dist/assets/{index-Bc9g6q63.js → index-BjDGQg9-.js} +1 -1
- package/ui-dist/assets/{index-FHzrJ7iQ.js → index-BnpyUMTb.js} +1 -1
- package/ui-dist/assets/{index-C1mpWCcn.js → index-BoqQTVMA.js} +1 -1
- package/ui-dist/assets/index-Bq-Mr-4x.js +1 -0
- package/ui-dist/assets/{index-DceuCrr8.js → index-Brb2U7N4.js} +1 -1
- package/ui-dist/assets/{index-DQIrrn2L.js → index-BsBybVwH.js} +1 -1
- package/ui-dist/assets/{index-CQhaM6qd.js → index-C2iv4Wgu.js} +1 -1
- package/ui-dist/assets/{index-DV2FOgyD.js → index-CKxIu7uO.js} +1 -1
- package/ui-dist/assets/{index-DU4U1fuv.js → index-Cb4VGIwp.js} +1 -1
- package/ui-dist/assets/{index-B61ORkIb.js → index-ChpszwcO.js} +1 -1
- package/ui-dist/assets/{index-DlVBBF8o.js → index-CoQFdT8N.js} +1 -1
- package/ui-dist/assets/index-D6P8_f0W.css +1 -0
- package/ui-dist/assets/index-DKdmghUG.js +1 -0
- package/ui-dist/assets/{index-CVTL3aAL.js → index-DaBci7Bx.js} +1 -1
- package/ui-dist/assets/{index-diFs0CNC.js → index-DjYaNiXZ.js} +1 -1
- package/ui-dist/assets/{index-Bf7tyGxE.js → index-DxTBJl4h.js} +1 -1
- package/ui-dist/assets/{index-DsdVneqe.js → index-Dz3E8nn0.js} +1 -1
- package/ui-dist/assets/{index-DvFUXI-Q.js → index-J7dz_hap.js} +1 -1
- package/ui-dist/assets/{index-CLu3Tfpv.js → index-JMp2Gpnz.js} +1 -1
- package/ui-dist/assets/{index-CcRTJ93J.js → index-Kp6wavQt.js} +1 -1
- package/ui-dist/assets/{index-BvFx-4KW.js → index-LBhYZ0L4.js} +1 -1
- package/ui-dist/assets/{index-BbAhzCq8.js → index-c3C0P6l8.js} +4 -4
- package/ui-dist/assets/{index-BY5ITKmG.js → index-pBEPheTW.js} +1 -1
- package/ui-dist/assets/{infoDiagram-6WML65LV-2jai2fw-.js → infoDiagram-6WML65LV-BiL7jKLA.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-WSZJBQD7-CUmGhmEb.js → ishikawaDiagram-WSZJBQD7-DYcQy41D.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-NVQOT4AX-CGQuPDO1.js → journeyDiagram-NVQOT4AX-Bxva6JgA.js} +1 -1
- package/ui-dist/assets/{kanban-definition-27J2QSJJ-zgEPcbHl.js → kanban-definition-27J2QSJJ-B2HpN34Q.js} +1 -1
- package/ui-dist/assets/{linear-BVf5VtLS.js → linear-Dq8qSESw.js} +1 -1
- package/ui-dist/assets/{mermaid.core-D2TFkGfI.js → mermaid.core-Ikay8Cpl.js} +35 -35
- package/ui-dist/assets/{mindmap-definition-FAOFIHXS-xoAQ0Lb_.js → mindmap-definition-FAOFIHXS-C-SIdYSY.js} +1 -1
- package/ui-dist/assets/{pegDiagram-VL7TDLO6-CBR1PWLr.js → pegDiagram-VL7TDLO6-C7rIJ_13.js} +1 -1
- package/ui-dist/assets/{pieDiagram-7S7Q4E2Y-BMPnzK_e.js → pieDiagram-7S7Q4E2Y-3gx0a0ve.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-CIZ2JOQS-CPtVUPkR.js → quadrantDiagram-CIZ2JOQS-BiwM9QGv.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-AXF67PYL-DC0lsZV1.js → railroadDiagram-AXF67PYL-rFS8XcFA.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-LRYGKXZP-BF6k05SB.js → requirementDiagram-LRYGKXZP-CEAUDUTQ.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-W5VNT64P-CRN0ibiP.js → sankeyDiagram-W5VNT64P-B7anpjlM.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-SI44F4Z6-CQtTiPWm.js → sequenceDiagram-SI44F4Z6-CJEdJvm_.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-BdjuGZd1.js → sizeCapture-X5ZJPWSS-CU1Vm6c1.js} +1 -1
- package/ui-dist/assets/{stateDiagram-OKZ733FA-B99onxyh.js → stateDiagram-OKZ733FA-wxVV9WIi.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-BIFLqlqR.js +1 -0
- package/ui-dist/assets/{swimlanes-SLNWSIFB-BIByPbrL.js → swimlanes-SLNWSIFB-Cd-C1Z_k.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-CoBROkkd.js +8 -0
- package/ui-dist/assets/{timeline-definition-Z64GVDOM-I1NgOcn9.js → timeline-definition-Z64GVDOM-SWngk4aR.js} +1 -1
- package/ui-dist/assets/{vennDiagram-T6HMQDX7-D6t2Njcl.js → vennDiagram-T6HMQDX7-DFdPUr36.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-T6FBY63Y-BiYKm9gh.js → wardleyDiagram-T6FBY63Y-C6IlrZ7t.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-ELKLHX3M-BhwXKWIa.js → xychartDiagram-ELKLHX3M-BNm6hHGl.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +0 -36
- package/ui-dist/assets/channel-qoSYbCn0.js +0 -1
- package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +0 -1
- package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +0 -1
- package/ui-dist/assets/cose-bilkent-JH36ORCC-Do8pe8JB.js +0 -1
- package/ui-dist/assets/ganttDiagram-PKOTCBZU-CE3lyvFq.js +0 -292
- package/ui-dist/assets/index-BHiUiToL.css +0 -1
- package/ui-dist/assets/index-hkanj4t6.js +0 -1
- package/ui-dist/assets/index-yRJnh4vx.js +0 -1
- package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +0 -1
- 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,
|
|
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)) &&
|
|
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
|
-
|
|
1893
|
-
const
|
|
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
|
-
|
|
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 === "
|
|
2142
|
-
? "
|
|
2143
|
-
: cause === "
|
|
2144
|
-
? "
|
|
2145
|
-
: "
|
|
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 =
|
|
1974
|
+
const routing = resolveStrandedRecoveryRouting({
|
|
2193
1975
|
issue: input.issue,
|
|
2194
1976
|
latestRun: input.latestRun,
|
|
2195
|
-
recoveryCause,
|
|
2196
|
-
preferredOwnerAgentId: input.recoveryOwnerAgentId,
|
|
2197
1977
|
});
|
|
2198
|
-
const
|
|
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:
|
|
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
|
-
? "
|
|
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
|
-
? "
|
|
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
|
-
? "
|
|
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
|
-
? "
|
|
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
|
-
? "
|
|
2238
|
-
: "
|
|
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
|
-
? "
|
|
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
|
-
? "
|
|
2243
|
-
: "
|
|
2244
|
-
wakePolicy:
|
|
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
|
-
:
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
|
2495
|
-
|
|
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
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
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
|
-
|
|
4640
|
+
blockerIssueIds: readiness.blockerIssueIds,
|
|
4641
|
+
blockedTransitionAt: candidate.blockedTransitionAt,
|
|
4172
4642
|
});
|
|
4173
|
-
const existingWake = await
|
|
4643
|
+
const existingWake = await findExistingIssueBlockersResolvedWakeForReadyState(db, {
|
|
4174
4644
|
companyId,
|
|
4175
|
-
|
|
4645
|
+
dependentIssueId: candidate.id,
|
|
4646
|
+
blockerIssueIds: readiness.blockerIssueIds,
|
|
4647
|
+
blockedTransitionAt: candidate.blockedTransitionAt,
|
|
4176
4648
|
});
|
|
4177
4649
|
if (existingWake) {
|
|
4178
4650
|
result.existingWakeSkipped += 1;
|