secondshift 0.1.0b12__py3-none-any.whl
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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Open the issues a finished run's agent proposed.
|
|
2
|
+
|
|
3
|
+
This reads only what the run stored, so a retry needs no agent or sandbox.
|
|
4
|
+
The token is limited to this repository and to issues. It never reaches the
|
|
5
|
+
sandbox or the model.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from secondshift.connections.github import shows_attribution
|
|
11
|
+
from secondshift.connections.models import Connection
|
|
12
|
+
from secondshift.github import credentials
|
|
13
|
+
from secondshift.github.errors import GitHubError
|
|
14
|
+
from secondshift.github.issues import open_issue, open_issue_titled
|
|
15
|
+
from secondshift.repositories.github_access import github_access
|
|
16
|
+
from secondshift.repositories.models import Repository
|
|
17
|
+
from secondshift.runs.dao import (
|
|
18
|
+
create_delivery,
|
|
19
|
+
finish_delivery,
|
|
20
|
+
get_run,
|
|
21
|
+
has_proposed_issues,
|
|
22
|
+
opened_issues,
|
|
23
|
+
run_delivery,
|
|
24
|
+
unopened_issues,
|
|
25
|
+
)
|
|
26
|
+
from secondshift.runs.execution.issues import issue_labels, issue_limit
|
|
27
|
+
from secondshift.runs.models import Run, RunDelivery, RunIssue
|
|
28
|
+
from secondshift.runs.reporting.collection import plural
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
APP_WRITE_HINT = (
|
|
33
|
+
"GitHub refused to open issues. Give the GitHub App Issues write "
|
|
34
|
+
"permission, then retry."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def record_pending_issues(run: Run) -> None:
|
|
39
|
+
"""Shown as pending from the start, and kept if opening fails."""
|
|
40
|
+
if issue_limit(run.plan):
|
|
41
|
+
create_delivery(
|
|
42
|
+
run, RunDelivery.Kind.ISSUES, f"Issues in {run.repository.full_name}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def issues_delivery(run: Run) -> RunDelivery | None:
|
|
47
|
+
return run_delivery(run, RunDelivery.Kind.ISSUES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def skip_reason(run: Run, repository: Repository) -> str:
|
|
51
|
+
"""Why this run opens no issues, or an empty string if it should."""
|
|
52
|
+
if repository.source_type != Repository.SourceType.GITHUB:
|
|
53
|
+
return "Local repositories cannot receive issues."
|
|
54
|
+
if not repository.allow_issues:
|
|
55
|
+
return "An owner has not allowed issues for this repository."
|
|
56
|
+
if run.status != Run.Status.SUCCEEDED:
|
|
57
|
+
return "The run did not succeed."
|
|
58
|
+
if not has_proposed_issues(run):
|
|
59
|
+
return "The agent proposed no issues."
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def issue_body(run: Run, issue: RunIssue) -> str:
|
|
64
|
+
"""The agent's text, and a footer when the connection shows attribution."""
|
|
65
|
+
if not shows_attribution(run.repository.github_connection):
|
|
66
|
+
return issue.body
|
|
67
|
+
footer = f"Opened by SecondShift from run #{run.pk} at {run.commit_sha[:12]}."
|
|
68
|
+
return f"{issue.body}\n\n---\n\n{footer}" if issue.body else footer
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def open_run_issues(run_id: int) -> None:
|
|
72
|
+
run = get_run(
|
|
73
|
+
run_id,
|
|
74
|
+
"job__repository__github_connection",
|
|
75
|
+
"task__repository__github_connection",
|
|
76
|
+
)
|
|
77
|
+
delivery = issues_delivery(run)
|
|
78
|
+
if delivery is None or delivery.status == RunDelivery.Status.DELIVERED:
|
|
79
|
+
return
|
|
80
|
+
repository = run.repository
|
|
81
|
+
if reason := skip_reason(run, repository):
|
|
82
|
+
finish_delivery(delivery, RunDelivery.Status.SKIPPED, reason)
|
|
83
|
+
return
|
|
84
|
+
delivery.attempts += 1
|
|
85
|
+
try:
|
|
86
|
+
access = github_access(repository, credentials.ISSUES_WRITE)
|
|
87
|
+
except GitHubError as error:
|
|
88
|
+
with_token = (
|
|
89
|
+
repository.github_connection is not None
|
|
90
|
+
and repository.github_connection.auth_strategy
|
|
91
|
+
== Connection.AuthStrategy.API_KEY
|
|
92
|
+
)
|
|
93
|
+
# Token errors already say what to fix, such as an expired token.
|
|
94
|
+
finish_delivery(
|
|
95
|
+
delivery,
|
|
96
|
+
RunDelivery.Status.FAILED,
|
|
97
|
+
str(error) if with_token else APP_WRITE_HINT,
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
# Issues opened by an earlier attempt keep their number.
|
|
102
|
+
for issue in unopened_issues(run):
|
|
103
|
+
found = open_issue_titled(access.token, access.full_name, issue.title)
|
|
104
|
+
if found is None:
|
|
105
|
+
found = open_issue(
|
|
106
|
+
access.token,
|
|
107
|
+
access.full_name,
|
|
108
|
+
issue.title,
|
|
109
|
+
issue_body(run, issue),
|
|
110
|
+
issue_labels(run.plan),
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
issue.already_open = True
|
|
114
|
+
issue.number, issue.url = found.number, found.url
|
|
115
|
+
issue.save(update_fields=["number", "url", "already_open"])
|
|
116
|
+
except GitHubError as error:
|
|
117
|
+
# These messages never contain the token.
|
|
118
|
+
finish_delivery(
|
|
119
|
+
delivery, RunDelivery.Status.FAILED, f"{done(run)}{error}".strip()
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
except Exception:
|
|
123
|
+
logger.exception("opening issues for run %s failed", run_id)
|
|
124
|
+
finish_delivery(
|
|
125
|
+
delivery,
|
|
126
|
+
RunDelivery.Status.FAILED,
|
|
127
|
+
f"{done(run)}Opening issues failed unexpectedly.".strip(),
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
finish_delivery(delivery, RunDelivery.Status.DELIVERED, done(run).strip())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def done(run: Run) -> str:
|
|
134
|
+
"""What has happened so far, as a sentence or two followed by a space."""
|
|
135
|
+
issues = opened_issues(run)
|
|
136
|
+
opened = sum(1 for issue in issues if not issue.already_open)
|
|
137
|
+
already = len(issues) - opened
|
|
138
|
+
parts = []
|
|
139
|
+
if opened:
|
|
140
|
+
parts.append(f"Opened {plural(opened, 'issue')}.")
|
|
141
|
+
if already:
|
|
142
|
+
verb = "was" if already == 1 else "were"
|
|
143
|
+
parts.append(f"{plural(already, 'issue')} {verb} already open.")
|
|
144
|
+
return " ".join(parts) + " " if parts else ""
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import djclick as click
|
|
2
|
+
|
|
3
|
+
from secondshift.runs.retention import prune_runs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.option(
|
|
8
|
+
"--days", type=int, default=None, help="Override SECONDSHIFT_RUN_RETENTION_DAYS."
|
|
9
|
+
)
|
|
10
|
+
def command(days: int | None) -> None:
|
|
11
|
+
"""Delete finished runs past retention, with their output and DBOS records."""
|
|
12
|
+
click.echo(f"Pruned {prune_runs(days)} run(s)")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import FrameType
|
|
6
|
+
|
|
7
|
+
import djclick as click
|
|
8
|
+
from dbos import DBOS, DBOSClient
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
|
|
11
|
+
from secondshift.connections.agents import retry_credential_cleanups
|
|
12
|
+
from secondshift.jobs.autosync import server_syncs, sync_moved_repositories
|
|
13
|
+
from secondshift.mcp_server.audit import prune_tool_calls
|
|
14
|
+
from secondshift.repositories.github_check import check_due_sources
|
|
15
|
+
from secondshift.repositories.sources import prune_source_cache
|
|
16
|
+
from secondshift.runs.orchestration.dbos_runtime import (
|
|
17
|
+
launch_dbos_worker,
|
|
18
|
+
new_dbos_client,
|
|
19
|
+
)
|
|
20
|
+
from secondshift.runs.orchestration.dispatch import dispatch_waiting_tasks
|
|
21
|
+
from secondshift.runs.orchestration.periodic import PeriodicTask
|
|
22
|
+
from secondshift.runs.orchestration.reconciliation import reconcile_terminal_workflows
|
|
23
|
+
from secondshift.runs.orchestration.schedules import synchronize_schedules
|
|
24
|
+
from secondshift.runs.retention import prune_runs
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
PRUNE_INTERVAL_SECONDS = 24 * 60 * 60
|
|
29
|
+
# How often to look for GitHub connections due for their daily check.
|
|
30
|
+
SOURCE_CHECK_POLL_SECONDS = 60 * 60
|
|
31
|
+
# How often to ask the runner again to remove logins it kept.
|
|
32
|
+
CREDENTIAL_CLEANUP_SECONDS = 60
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def remove_cached_logins() -> None:
|
|
36
|
+
if removed := retry_credential_cleanups():
|
|
37
|
+
logger.info("removed %s cached agent login(s) from the runner", removed)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def worker_tasks(client: DBOSClient) -> list[PeriodicTask]:
|
|
41
|
+
"""What the worker repeats, in the order it runs them on each pass."""
|
|
42
|
+
|
|
43
|
+
def reconcile() -> None:
|
|
44
|
+
if count := reconcile_terminal_workflows(client):
|
|
45
|
+
logger.warning("reconciled %s run(s) from terminal DBOS workflows", count)
|
|
46
|
+
|
|
47
|
+
def schedules() -> None:
|
|
48
|
+
logger.debug("synchronized %s DBOS schedule(s)", synchronize_schedules(client))
|
|
49
|
+
|
|
50
|
+
def prune() -> None:
|
|
51
|
+
logger.info("pruned %s run(s) past retention", prune_runs(client=client))
|
|
52
|
+
logger.info("pruned %s MCP tool call(s) past retention", prune_tool_calls())
|
|
53
|
+
cache = prune_source_cache()
|
|
54
|
+
logger.info(
|
|
55
|
+
"pruned %s snapshot(s) and %s mirror(s) from the source cache, %s bytes",
|
|
56
|
+
cache.snapshots,
|
|
57
|
+
cache.mirrors,
|
|
58
|
+
cache.bytes,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def repositories() -> None:
|
|
62
|
+
if synced := sync_moved_repositories():
|
|
63
|
+
logger.info("synced %s repository(s) with new commits", synced)
|
|
64
|
+
|
|
65
|
+
def sources() -> None:
|
|
66
|
+
if checked := check_due_sources():
|
|
67
|
+
logger.info("checked %s GitHub connection(s)", checked)
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
PeriodicTask("reconcile runs", every=0, run=reconcile),
|
|
71
|
+
PeriodicTask("dispatch waiting tasks", every=0, run=dispatch_waiting_tasks),
|
|
72
|
+
PeriodicTask("sync schedules", every=0, run=schedules),
|
|
73
|
+
PeriodicTask("prune runs", every=PRUNE_INTERVAL_SECONDS, run=prune),
|
|
74
|
+
PeriodicTask(
|
|
75
|
+
"sync repositories",
|
|
76
|
+
every=settings.REPOSITORY_SYNC_INTERVAL_SECONDS,
|
|
77
|
+
run=repositories,
|
|
78
|
+
enabled=server_syncs,
|
|
79
|
+
),
|
|
80
|
+
PeriodicTask(
|
|
81
|
+
"check GitHub connections",
|
|
82
|
+
every=SOURCE_CHECK_POLL_SECONDS,
|
|
83
|
+
run=sources,
|
|
84
|
+
),
|
|
85
|
+
PeriodicTask(
|
|
86
|
+
"remove cached agent logins",
|
|
87
|
+
every=CREDENTIAL_CLEANUP_SECONDS,
|
|
88
|
+
run=remove_cached_logins,
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@click.command()
|
|
94
|
+
@click.option("--sync-interval", type=int, default=15, show_default=True)
|
|
95
|
+
@click.option("--ready-file", type=click.Path(path_type=Path), hidden=True)
|
|
96
|
+
def command(sync_interval: int, ready_file: Path | None) -> None:
|
|
97
|
+
"""Run DBOS workers and reconcile database-backed schedules."""
|
|
98
|
+
stopped = threading.Event()
|
|
99
|
+
|
|
100
|
+
def stop(signum: int, frame: FrameType | None) -> None:
|
|
101
|
+
stopped.set()
|
|
102
|
+
|
|
103
|
+
previous = signal.signal(signal.SIGTERM, stop)
|
|
104
|
+
client = None
|
|
105
|
+
try:
|
|
106
|
+
launch_dbos_worker()
|
|
107
|
+
click.echo("DBOS worker started")
|
|
108
|
+
client = new_dbos_client()
|
|
109
|
+
tasks = worker_tasks(client)
|
|
110
|
+
if ready_file is not None:
|
|
111
|
+
ready_file.touch(mode=0o600)
|
|
112
|
+
while not stopped.is_set():
|
|
113
|
+
for task in tasks:
|
|
114
|
+
if stopped.is_set():
|
|
115
|
+
break
|
|
116
|
+
task.run_if_due()
|
|
117
|
+
if stopped.wait(sync_interval):
|
|
118
|
+
break
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
click.echo("Stopping DBOS worker")
|
|
121
|
+
finally:
|
|
122
|
+
try:
|
|
123
|
+
if client is not None:
|
|
124
|
+
client.destroy()
|
|
125
|
+
finally:
|
|
126
|
+
try:
|
|
127
|
+
DBOS.destroy(workflow_completion_timeout_sec=10)
|
|
128
|
+
finally:
|
|
129
|
+
signal.signal(signal.SIGTERM, previous)
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Versioned data operations used by generated runs migrations.
|
|
2
|
+
|
|
3
|
+
Keep these functions independent of current models and job configuration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from django.apps.registry import Apps
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_constraints_now_v1(schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
14
|
+
"""Run the foreign key checks that row changes left for commit.
|
|
15
|
+
|
|
16
|
+
A squashed migration runs these steps in one transaction. PostgreSQL
|
|
17
|
+
refuses to alter a table while those checks are still pending.
|
|
18
|
+
"""
|
|
19
|
+
if schema_editor.connection.vendor == "postgresql":
|
|
20
|
+
schema_editor.execute("SET CONSTRAINTS ALL IMMEDIATE")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def backfill_tasks_v1(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
24
|
+
"""Preserve the assignment actually recorded on each historical run."""
|
|
25
|
+
run_model = apps.get_model("runs", "Run")
|
|
26
|
+
task_model = apps.get_model("runs", "Task")
|
|
27
|
+
alias = schema_editor.connection.alias
|
|
28
|
+
while runs := list(
|
|
29
|
+
run_model.objects.using(alias).filter(task__isnull=True).order_by("pk")[:500]
|
|
30
|
+
):
|
|
31
|
+
tasks = task_model.objects.using(alias).bulk_create(
|
|
32
|
+
[
|
|
33
|
+
task_model(
|
|
34
|
+
job_id=run.job_id,
|
|
35
|
+
origin=run.started_by,
|
|
36
|
+
commit_sha=run.commit_sha,
|
|
37
|
+
input_snapshot=run.input_snapshot,
|
|
38
|
+
scheduled_for=run.scheduled_for,
|
|
39
|
+
created_at=run.created_at,
|
|
40
|
+
)
|
|
41
|
+
for run in runs
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
for run, task in zip(runs, tasks, strict=True):
|
|
45
|
+
run.task_id = task.pk
|
|
46
|
+
run_model.objects.using(alias).bulk_update(runs, ["task"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def backfill_agent_connections_v1(
|
|
50
|
+
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Record the agent login each ready plan chose, so its cap counts it."""
|
|
53
|
+
run_model = apps.get_model("runs", "Run")
|
|
54
|
+
login_model = apps.get_model("connections", "AgentConnection")
|
|
55
|
+
alias = schema_editor.connection.alias
|
|
56
|
+
logins = set(login_model.objects.using(alias).values_list("pk", flat=True))
|
|
57
|
+
runs = run_model.objects.using(alias).filter(
|
|
58
|
+
execution_plan__ready=True,
|
|
59
|
+
execution_plan__execution__connection_id__isnull=False,
|
|
60
|
+
)
|
|
61
|
+
changed = []
|
|
62
|
+
for run in runs.iterator():
|
|
63
|
+
login = run.execution_plan["execution"]["connection_id"]
|
|
64
|
+
if login in logins:
|
|
65
|
+
run.agent_connection_id = login
|
|
66
|
+
changed.append(run)
|
|
67
|
+
run_model.objects.using(alias).bulk_update(changed, ["agent_connection"], 500)
|
|
68
|
+
check_constraints_now_v1(schema_editor)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
PLAN_LIMIT_SETTINGS_V1 = {
|
|
72
|
+
"max_turns": "AGENT_MAX_TURNS",
|
|
73
|
+
"max_tool_calls": "AGENT_MAX_TOOL_CALLS",
|
|
74
|
+
"max_tool_result_chars": "AGENT_MAX_TOOL_RESULT_CHARS",
|
|
75
|
+
"token_budget": "MODEL_RUN_TOKEN_BUDGET",
|
|
76
|
+
"timeout_seconds": "RUN_TIMEOUT_SECONDS",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def picked_v1(values: Any, defaults: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
"""The known keys of a stored block, with defaults for missing ones."""
|
|
82
|
+
values = values if isinstance(values, dict) else {}
|
|
83
|
+
return {key: values.get(key, default) for key, default in defaults.items()}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalized_plan_v1(
|
|
87
|
+
plan: dict[str, Any], fallback: dict[str, Any]
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
"""One stored plan in the shape every reader now expects.
|
|
90
|
+
|
|
91
|
+
Plans from older releases lack blocks added later. A run created without
|
|
92
|
+
a plan keeps it as not ready, with its job and commit from the run.
|
|
93
|
+
"""
|
|
94
|
+
agent = plan.get("agent") or {}
|
|
95
|
+
limits = agent.get("limits") or {}
|
|
96
|
+
model = plan.get("model")
|
|
97
|
+
return {
|
|
98
|
+
"execution": picked_v1(
|
|
99
|
+
plan.get("execution"),
|
|
100
|
+
{
|
|
101
|
+
"backend": "pydantic",
|
|
102
|
+
"label": "",
|
|
103
|
+
"ready": True,
|
|
104
|
+
"problem": "",
|
|
105
|
+
"connection_id": None,
|
|
106
|
+
"credential_key": None,
|
|
107
|
+
"owner_id": None,
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
"ready": bool(plan.get("ready", False)),
|
|
111
|
+
"agent": {
|
|
112
|
+
"limits": {
|
|
113
|
+
key: int(limits.get(key, getattr(settings, name)))
|
|
114
|
+
for key, name in PLAN_LIMIT_SETTINGS_V1.items()
|
|
115
|
+
},
|
|
116
|
+
"tools": list(agent.get("tools") or []),
|
|
117
|
+
"subagents": int(agent.get("subagents") or 0),
|
|
118
|
+
},
|
|
119
|
+
"job": picked_v1(
|
|
120
|
+
plan.get("job") or fallback["job"],
|
|
121
|
+
{"key": "", "skill_path": "", "config_hash": ""},
|
|
122
|
+
),
|
|
123
|
+
"repository": picked_v1(
|
|
124
|
+
plan.get("repository") or fallback["repository"],
|
|
125
|
+
{"full_name": "", "config_sha": ""},
|
|
126
|
+
),
|
|
127
|
+
"runner": picked_v1(
|
|
128
|
+
plan.get("runner"),
|
|
129
|
+
{
|
|
130
|
+
"image": settings.RUNNER_IMAGE,
|
|
131
|
+
"network_policy": "none",
|
|
132
|
+
"collect_changes": False,
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
"pull_request": (
|
|
136
|
+
picked_v1(plan["pull_request"], {"base": "", "run_ci": False})
|
|
137
|
+
if plan.get("pull_request")
|
|
138
|
+
else None
|
|
139
|
+
),
|
|
140
|
+
"issues": (
|
|
141
|
+
picked_v1(plan["issues"], {"max": 0, "labels": []})
|
|
142
|
+
if plan.get("issues")
|
|
143
|
+
else None
|
|
144
|
+
),
|
|
145
|
+
"model": (
|
|
146
|
+
picked_v1(
|
|
147
|
+
model,
|
|
148
|
+
{
|
|
149
|
+
"connection_id": 0,
|
|
150
|
+
"connection_name": "",
|
|
151
|
+
"provider": "",
|
|
152
|
+
"family": "",
|
|
153
|
+
"default_model": "",
|
|
154
|
+
"allowed_models": [],
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
if isinstance(model, dict)
|
|
158
|
+
else None
|
|
159
|
+
),
|
|
160
|
+
"capabilities": [
|
|
161
|
+
picked_v1(
|
|
162
|
+
capability,
|
|
163
|
+
{
|
|
164
|
+
"binding": "",
|
|
165
|
+
"kind": "",
|
|
166
|
+
"required": True,
|
|
167
|
+
"requested_tools": [],
|
|
168
|
+
"connection_id": None,
|
|
169
|
+
"connection_name": None,
|
|
170
|
+
"effective_tools": [],
|
|
171
|
+
"authorized": False,
|
|
172
|
+
"reasons": [],
|
|
173
|
+
"missing": None,
|
|
174
|
+
"external": None,
|
|
175
|
+
"tool_rules": {},
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
for capability in plan.get("capabilities") or []
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def normalize_execution_plans_v1(
|
|
184
|
+
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Rewrite every stored plan in the current shape."""
|
|
187
|
+
run_model = apps.get_model("runs", "Run")
|
|
188
|
+
alias = schema_editor.connection.alias
|
|
189
|
+
runs = run_model.objects.using(alias).select_related(
|
|
190
|
+
"job__repository", "task__repository"
|
|
191
|
+
)
|
|
192
|
+
changed = []
|
|
193
|
+
for run in runs.iterator(chunk_size=500):
|
|
194
|
+
job = run.job
|
|
195
|
+
repository = job.repository if job else run.task.repository
|
|
196
|
+
fallback = {
|
|
197
|
+
"job": {
|
|
198
|
+
"key": job.key if job else "",
|
|
199
|
+
"skill_path": job.skill_path if job else "",
|
|
200
|
+
"config_hash": job.config_hash if job else "",
|
|
201
|
+
},
|
|
202
|
+
"repository": {
|
|
203
|
+
"full_name": repository.full_name if repository else "",
|
|
204
|
+
"config_sha": run.commit_sha,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
normalized = normalized_plan_v1(run.execution_plan or {}, fallback)
|
|
208
|
+
if normalized != run.execution_plan:
|
|
209
|
+
run.execution_plan = normalized
|
|
210
|
+
changed.append(run)
|
|
211
|
+
run_model.objects.using(alias).bulk_update(changed, ["execution_plan"], 500)
|
|
212
|
+
check_constraints_now_v1(schema_editor)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def record_run_starts_v1(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
216
|
+
"""Keep what started each run on the run, before tasks drop their origin.
|
|
217
|
+
|
|
218
|
+
MCP starts were recorded as manual runs with an MCP task. Runs from
|
|
219
|
+
before starts were recorded count as scheduled.
|
|
220
|
+
"""
|
|
221
|
+
run_model = apps.get_model("runs", "Run")
|
|
222
|
+
alias = schema_editor.connection.alias
|
|
223
|
+
runs = run_model.objects.using(alias)
|
|
224
|
+
runs.filter(task__origin="mcp").update(started_by="mcp")
|
|
225
|
+
runs.filter(started_by="").update(started_by="schedule")
|
|
226
|
+
check_constraints_now_v1(schema_editor)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def assign_task_repositories_v1(
|
|
230
|
+
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Give every run a task, and every task its repository and fingerprint."""
|
|
233
|
+
run_model = apps.get_model("runs", "Run")
|
|
234
|
+
task_model = apps.get_model("runs", "Task")
|
|
235
|
+
alias = schema_editor.connection.alias
|
|
236
|
+
for run in (
|
|
237
|
+
run_model.objects.using(alias)
|
|
238
|
+
.filter(task__isnull=True)
|
|
239
|
+
.select_related("job")
|
|
240
|
+
.iterator()
|
|
241
|
+
):
|
|
242
|
+
run.task = task_model.objects.using(alias).create(
|
|
243
|
+
job_id=run.job_id,
|
|
244
|
+
repository_id=run.job.repository_id,
|
|
245
|
+
commit_sha=run.commit_sha,
|
|
246
|
+
scheduled_for=run.scheduled_for,
|
|
247
|
+
created_at=run.created_at,
|
|
248
|
+
)
|
|
249
|
+
run.save(update_fields=["task"])
|
|
250
|
+
tasks = task_model.objects.using(alias).select_related("job")
|
|
251
|
+
changed = []
|
|
252
|
+
for task in tasks.iterator():
|
|
253
|
+
if task.repository_id is None:
|
|
254
|
+
task.repository_id = task.job.repository_id
|
|
255
|
+
task.fingerprint = str(task.input_snapshot.get("submission_fingerprint", ""))
|
|
256
|
+
changed.append(task)
|
|
257
|
+
task_model.objects.using(alias).bulk_update(
|
|
258
|
+
changed, ["repository", "fingerprint"], 500
|
|
259
|
+
)
|
|
260
|
+
check_constraints_now_v1(schema_editor)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# The summaries v0.1.0b10 wrote, by the failure each one names.
|
|
264
|
+
FAILURE_HEADLINES_V1 = {
|
|
265
|
+
"Timed out": "timeout",
|
|
266
|
+
"Budget used up": "budget",
|
|
267
|
+
"The run could not start": "starting",
|
|
268
|
+
"Couldn't check out the repository": "checkout",
|
|
269
|
+
"Couldn't start the sandbox": "sandbox",
|
|
270
|
+
"The skill run failed": "skill",
|
|
271
|
+
"The run could not be recorded": "finishing",
|
|
272
|
+
}
|
|
273
|
+
STAGES_V1 = {"starting", "checkout", "sandbox", "skill", "finishing"}
|
|
274
|
+
ENQUEUE_FAILED_V1 = "The durable workflow could not be enqueued."
|
|
275
|
+
MODEL_PROVIDER_PREFIX_V1 = "The model provider answered "
|
|
276
|
+
TOOL_RULE_PREFIX_V1 = "Blocked by a tool restriction: "
|
|
277
|
+
WORKFLOW_PREFIX_V1 = "The durable workflow failed unexpectedly ("
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def failure_from_summary_v1(summary: str) -> tuple[str, str]:
|
|
281
|
+
"""The failure kind and reason an older failed run's summary names."""
|
|
282
|
+
if summary == ENQUEUE_FAILED_V1:
|
|
283
|
+
return "not_queued", summary
|
|
284
|
+
text = " ".join(summary.split())
|
|
285
|
+
if text.startswith(WORKFLOW_PREFIX_V1) and text.endswith(")."):
|
|
286
|
+
return "other", f"Unexpected {text[len(WORKFLOW_PREFIX_V1) : -2]}."
|
|
287
|
+
for headline, kind in FAILURE_HEADLINES_V1.items():
|
|
288
|
+
if not text.startswith(f"{headline}: "):
|
|
289
|
+
continue
|
|
290
|
+
detail = text[len(headline) + 2 :]
|
|
291
|
+
if kind in STAGES_V1 and detail.startswith(MODEL_PROVIDER_PREFIX_V1):
|
|
292
|
+
reason = detail.removeprefix(MODEL_PROVIDER_PREFIX_V1)
|
|
293
|
+
return "model_provider", reason.replace(": ", " · ", 1)
|
|
294
|
+
if kind in STAGES_V1 and detail.startswith(TOOL_RULE_PREFIX_V1):
|
|
295
|
+
return "tool_rule", detail.removeprefix(TOOL_RULE_PREFIX_V1)
|
|
296
|
+
return kind, detail
|
|
297
|
+
return "other", summary
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def record_run_failures_v1(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
301
|
+
"""Record why each earlier failed run failed, from its summary."""
|
|
302
|
+
run_model = apps.get_model("runs", "Run")
|
|
303
|
+
runs = run_model.objects.using(schema_editor.connection.alias).filter(
|
|
304
|
+
status="failed", failure=""
|
|
305
|
+
)
|
|
306
|
+
changed = []
|
|
307
|
+
for run in runs.only("pk", "summary").iterator():
|
|
308
|
+
run.failure, run.failure_reason = failure_from_summary_v1(run.summary)
|
|
309
|
+
changed.append(run)
|
|
310
|
+
run_model.objects.using(schema_editor.connection.alias).bulk_update(
|
|
311
|
+
changed, ["failure", "failure_reason"], 500
|
|
312
|
+
)
|
|
313
|
+
check_constraints_now_v1(schema_editor)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Generated by Django 6.0.9 on 2026-10-26 14:22
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import uuid
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('jobs', '0001_initial'),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='Run',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('status', models.CharField(choices=[('queued', 'Queued'), ('running', 'Running'), ('succeeded', 'Succeeded'), ('failed', 'Failed'), ('canceled', 'Canceled')], default='queued', max_length=16)),
|
|
22
|
+
('commit_sha', models.CharField(max_length=64)),
|
|
23
|
+
('scheduled_for', models.DateTimeField()),
|
|
24
|
+
('started_at', models.DateTimeField(blank=True, null=True)),
|
|
25
|
+
('finished_at', models.DateTimeField(blank=True, null=True)),
|
|
26
|
+
('sandbox_runtime', models.CharField(blank=True, max_length=32)),
|
|
27
|
+
('exit_code', models.IntegerField(blank=True, null=True)),
|
|
28
|
+
('summary', models.TextField(blank=True)),
|
|
29
|
+
('input_snapshot', models.JSONField(blank=True, default=dict)),
|
|
30
|
+
('execution_plan', models.JSONField(blank=True, default=dict)),
|
|
31
|
+
('output_excerpt', models.TextField(blank=True)),
|
|
32
|
+
('input_path', models.CharField(blank=True, max_length=500)),
|
|
33
|
+
('input_size_bytes', models.BigIntegerField(blank=True, null=True)),
|
|
34
|
+
('output_path', models.CharField(blank=True, max_length=500)),
|
|
35
|
+
('output_size_bytes', models.BigIntegerField(blank=True, null=True)),
|
|
36
|
+
('output_sha256', models.CharField(blank=True, max_length=64)),
|
|
37
|
+
('output_truncated', models.BooleanField(default=False)),
|
|
38
|
+
('dbos_workflow_id', models.CharField(blank=True, db_index=True, max_length=200)),
|
|
39
|
+
('runner_session_key', models.UUIDField(default=uuid.uuid4, editable=False)),
|
|
40
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
41
|
+
('job', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='runs', to='jobs.job')),
|
|
42
|
+
],
|
|
43
|
+
options={
|
|
44
|
+
'ordering': ['-created_at'],
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
migrations.CreateModel(
|
|
48
|
+
name='RunEvent',
|
|
49
|
+
fields=[
|
|
50
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
51
|
+
('level', models.CharField(default='info', max_length=16)),
|
|
52
|
+
('message', models.TextField()),
|
|
53
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
|
54
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
55
|
+
('run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='runs.run')),
|
|
56
|
+
],
|
|
57
|
+
options={
|
|
58
|
+
'ordering': ['created_at'],
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
migrations.AddIndex(
|
|
62
|
+
model_name='run',
|
|
63
|
+
index=models.Index(fields=['status', 'created_at'], name='runs_status_created_at_idx'),
|
|
64
|
+
),
|
|
65
|
+
migrations.AddConstraint(
|
|
66
|
+
model_name='run',
|
|
67
|
+
constraint=models.UniqueConstraint(fields=('job', 'scheduled_for'), name='unique_job_scheduled_run'),
|
|
68
|
+
),
|
|
69
|
+
]
|