@humwick/server 0.130.2 → 0.130.3
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/bundled/prompts/manifest.json +1 -0
- package/bundled/prompts/task.hydration.md +1 -1
- package/bundled/roles/builder.md +1 -1
- package/docs/guides/sub-coordinators.md +12 -5
- package/docs/guides/tools-coordination.md +1 -0
- package/docs/guides/workflows.md +16 -0
- package/docs/openapi.json +73 -4
- package/package.json +8 -7
- package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-Bq9kr21N.js} +2 -2
- package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-Dqiroy06.js} +2 -2
- package/public/assets/{TraceView-Wimm0Czv.js → TraceView-DYU962-H.js} +2 -2
- package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DhPfzioE.js} +2 -2
- package/public/assets/{activity-CrSurn-r.js → activity-DzCcQTee.js} +2 -2
- package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-SgDt9i3T.js} +2 -2
- package/public/assets/agent-liveness-Bq-s73jW.js +2 -0
- package/public/assets/{agent-name-DKbiIhBo.js → agent-name-CcY3Aijy.js} +2 -2
- package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-cxtRBn6O.js} +2 -2
- package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-DGYbIhaI.js} +2 -2
- package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-DHbftV0N.js} +2 -2
- package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CY113sp-.js} +2 -2
- package/public/assets/{agents-B-v66C-4.js → agents-BPzY7IlQ.js} +2 -2
- package/public/assets/{analytics-BqCb-LKi.js → analytics-DENFPv_a.js} +2 -2
- package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-KRvSc7sl.js} +2 -2
- package/public/assets/{artifacts-DJevXcvg.js → artifacts-C9gSmL3s.js} +2 -2
- package/public/assets/{artifacts-D32zyw4Y.js → artifacts-DT-HkI5O.js} +2 -2
- package/public/assets/{automation-BpRajx_5.js → automation-BsmjQhKD.js} +2 -2
- package/public/assets/{backlog-D_UjTdzX.js → backlog-8NYH_62t.js} +2 -2
- package/public/assets/{busy-health-N3VEh4ao.js → busy-health-D3hwvYb4.js} +2 -2
- package/public/assets/{channels-9jopungq.js → channels-DCJ5WL7O.js} +2 -2
- package/public/assets/{chat-CSXrTB3C.js → chat-DAIl357U.js} +3 -3
- package/public/assets/{code-preview-p1NnmqK0.js → code-preview-CzrZfr9U.js} +2 -2
- package/public/assets/{connectors-CZdqlDNL.js → connectors-xTxtlyou.js} +2 -2
- package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BpebFLK2.js} +2 -2
- package/public/assets/{crate-agent-30aXTKva.js → crate-agent-BHvI5wXC.js} +2 -2
- package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-DGT0o3wJ.js} +2 -2
- package/public/assets/{deploys-DoN6bAb5.js → deploys-hqWOHSHE.js} +2 -2
- package/public/assets/{display-BrKWFGJz.js → display-CDeH4I8T.js} +2 -2
- package/public/assets/{files-D6XEu8Ng.js → files-BTeGVmTg.js} +2 -2
- package/public/assets/{fleet-C7jQWbw2.js → fleet-DgXtNgCa.js} +2 -2
- package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BHEt5yc_.js} +2 -2
- package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-MLtodyPp.js} +3 -3
- package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-CAe664Z8.js} +2 -2
- package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-mapF4Fj9.js} +2 -2
- package/public/assets/{index-CRHep3M9.js → index-CqqgOI5R.js} +5 -5
- package/public/assets/{insights-CzrtkpOE.js → insights-DNyf7IzF.js} +2 -2
- package/public/assets/{integrations-aVsUmigt.js → integrations-DlYOHq7T.js} +2 -2
- package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-CeWBNWn7.js} +2 -2
- package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-Cib8nqdE.js} +2 -2
- package/public/assets/{maintenance-NfjqFZAk.js → maintenance-DSn1cPm7.js} +2 -2
- package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-C3CkM81m.js} +2 -2
- package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-W4VQQPAv.js} +3 -3
- package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CMsTdhjq.js} +2 -2
- package/public/assets/{memory-DNsCl6UX.js → memory-BV17qtO4.js} +2 -2
- package/public/assets/{messages-DopU1L1X.js → messages-Bm_1AqSA.js} +2 -2
- package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-Dg-vfP09.js} +2 -2
- package/public/assets/{overview-C1DCTzzV.js → overview-DcYCrvlr.js} +2 -2
- package/public/assets/{pairs-DE_m6rrE.js → pairs-8o9nuG3V.js} +2 -2
- package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-BsRyBMea.js} +2 -2
- package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-DETKFn_E.js} +2 -2
- package/public/assets/{projects-Br2Bd2h1.js → projects-M_m6AHws.js} +2 -2
- package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-C_mNCgeL.js} +2 -2
- package/public/assets/provider-display-BEeLXC_D.js +2 -0
- package/public/assets/{registry-Cw38_6gW.js → registry-DDHHHE5N.js} +2 -2
- package/public/assets/run-card-Dt4I4jaP.js +2 -0
- package/public/assets/{secrets-DxI-vzSq.js → secrets-BkVwxI5O.js} +2 -2
- package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-Crq-6a-B.js} +2 -2
- package/public/assets/{settings-CKZTVGIx.js → settings-CYcavMFu.js} +2 -2
- package/public/assets/shipping-H6gzEeZB.js +2 -0
- package/public/assets/{stats-DyUBKyWI.js → stats-BJVeWkpV.js} +2 -2
- package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-BcHyw1Vw.js} +2 -2
- package/public/assets/store-DKPDIZBe.js +9 -0
- package/public/assets/{tasks-Bh7Chr4-.js → tasks-qJjlQPpx.js} +2 -2
- package/public/assets/{teams-bowI7qVY.js → teams-Cd5Sk1uX.js} +2 -2
- package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-CpY5_m7W.js} +3 -3
- package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-DI_2llFR.js} +2 -2
- package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-0yJA_buM.js} +2 -2
- package/public/assets/{use-decisions-samMQhQf.js → use-decisions-uJXHf34W.js} +2 -2
- package/public/assets/{use-project-list-59LheVP7.js → use-project-list-CpWl6eHQ.js} +2 -2
- package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-BdxBW8mL.js} +2 -2
- package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-3IndfpOc.js} +2 -2
- package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-CIVg7XB1.js} +2 -2
- package/public/assets/{work-queue-CSvpuNLz.js → work-queue-B9rnLwHZ.js} +2 -2
- package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-X8wxyhzC.js} +2 -2
- package/public/assets/{workflows-DdmxRSyg.js → workflows-o5YyMFW7.js} +2 -2
- package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CzEGoAng.js} +2 -2
- package/public/cosmos.html +4 -4
- package/public/index.html +28 -28
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-headless.ts +1 -1
- package/runner/src/adapters/claude-launch-exit.ts +26 -0
- package/runner/src/adapters/claude-session-probe.ts +1 -1
- package/runner/src/adapters/claude-tmux.ts +38 -10
- package/runner/src/adapters/claude.ts +28 -75
- package/runner/src/adapters/provider-quota-harvest.ts +20 -71
- package/runner/src/agent-runner/lifecycle-milestones.ts +2 -2
- package/runner/src/agent-runner/liveness-exchange.ts +104 -0
- package/runner/src/claude-system-prompt.ts +49 -0
- package/runner/src/launch-assembly.ts +14 -10
- package/runner/src/profile-home.ts +1 -1
- package/runner/src/project-instructions.ts +2 -2
- package/runner/src/provider-command.ts +18 -0
- package/runner/src/provider-setup.ts +2 -1
- package/runner/src/runner-core.ts +14 -15
- package/runner/src/runner-helpers.ts +0 -8
- package/runner/src/session-scratch.ts +6 -2
- package/runner/test-support/provisioning-context.ts +4 -2
- package/runner/test-support/runner-harness.ts +2 -1
- package/scripts/steward-policy-cleanup.ts +2 -2
- package/src/agent-authz.ts +2 -2
- package/src/agent-drive-arbiter.ts +3 -8
- package/src/agent-exit-cleanup-async.ts +2 -2
- package/src/agent-exit-cleanup.ts +23 -19
- package/src/agent-lifecycle-events.ts +112 -85
- package/src/agent-ref.ts +3 -2
- package/src/agent-status-events.ts +43 -14
- package/src/auth/password.ts +4 -3
- package/src/auth/session-refresh.ts +1 -1
- package/src/auth/session.test-fixture.ts +2 -2
- package/src/auth/session.ts +7 -7
- package/src/automations/crud.ts +8 -81
- package/src/automations/dispatch.ts +33 -58
- package/src/automations/index.ts +1 -1
- package/src/automations/persistence.ts +65 -0
- package/src/automations/precheck.ts +18 -16
- package/src/automations/read-async.ts +11 -0
- package/src/automations/reconcile.ts +30 -39
- package/src/automations/storage.ts +33 -10
- package/src/automations/write-async.ts +33 -0
- package/src/branch-land-notice.ts +3 -3
- package/src/branch-landed.ts +25 -23
- package/src/bus.ts +21 -19
- package/src/cd-deploy-notify.ts +2 -2
- package/src/ci/notifications.ts +4 -4
- package/src/ci/root-missing.ts +3 -3
- package/src/ci/transition-notify.ts +5 -5
- package/src/claude-app/oauth-http.ts +3 -3
- package/src/cli/workflow.ts +12 -3
- package/src/commands-db.ts +4 -4
- package/src/commands-write.ts +8 -0
- package/src/compaction-watch.ts +10 -8
- package/src/config-store-write.ts +2 -2
- package/src/config.ts +4 -11
- package/src/connectors.ts +10 -8
- package/src/context-advisory.ts +22 -21
- package/src/db/agent-action-write.ts +36 -0
- package/src/db/agent-control-async.ts +9 -0
- package/src/db/agent-control-write.ts +49 -0
- package/src/db/agent-effort-observe.ts +22 -0
- package/src/db/agent-meta.ts +14 -3
- package/src/db/agents-async.ts +8 -5
- package/src/db/agents.ts +15 -12
- package/src/db/auth-identities.ts +2 -2
- package/src/db/automation-read-ops.ts +11 -0
- package/src/db/automation-write-ops.ts +46 -0
- package/src/db/blackboard.ts +2 -2
- package/src/db/browser-session-write.ts +32 -0
- package/src/db/bus-outbox-write-ops.ts +3 -1
- package/src/db/checkout-refresh-schema.ts +47 -0
- package/src/db/checkout-refresh.ts +5 -5
- package/src/db/command-agent-profile-backfill.ts +39 -0
- package/src/db/command-agent-profiles.ts +5 -1
- package/src/db/command-retention.ts +126 -0
- package/src/db/commands-planner-stats.test-fixture.ts +8 -0
- package/src/db/commands-read-ops.ts +16 -0
- package/src/db/connection.ts +1 -182
- package/src/db/context-carry-write.ts +74 -0
- package/src/db/continuations.ts +4 -4
- package/src/db/db-error-registry.ts +12 -0
- package/src/db/gateway.ts +26 -133
- package/src/db/history-retention-ops.ts +13 -0
- package/src/db/inbox-async.ts +2 -2
- package/src/db/inbox-read-ops.ts +1 -1
- package/src/db/inbox.ts +9 -3
- package/src/db/issue-associations.ts +2 -2
- package/src/db/issue-refs-async.ts +4 -1
- package/src/db/issue-refs-read-ops.ts +6 -0
- package/src/db/issue-refs.ts +8 -8
- package/src/db/kernel-event-log-schema.ts +2 -0
- package/src/db/land-boundaries.ts +51 -5
- package/src/db/land-gate-attestations.ts +2 -2
- package/src/db/landing-receipts.ts +2 -2
- package/src/db/lifecycle-context-write.ts +5 -0
- package/src/db/lifecycle-tail-async.ts +76 -0
- package/src/db/lifecycle-tail-write-ops.ts +62 -0
- package/src/db/lifecycle-write-window.ts +2 -1
- package/src/db/lineage-pair-dedupe-sql.ts +3 -3
- package/src/db/main-connection-mode.ts +11 -0
- package/src/db/main-connection-writes.ts +20 -2
- package/src/db/maintenance-job-runs-async.ts +2 -2
- package/src/db/maintenance-job-write-ops.ts +1 -1
- package/src/db/maintenance-pauses.ts +2 -2
- package/src/db/message-send.ts +4 -0
- package/src/db/messages.ts +31 -25
- package/src/db/migrations.ts +18 -1
- package/src/db/notification-write-ops.ts +19 -0
- package/src/db/operator-answer-write.ts +19 -0
- package/src/db/orchestrators.ts +10 -4
- package/src/db/pairs-async.ts +22 -1
- package/src/db/pairs-write-ops.ts +8 -3
- package/src/db/pairs.ts +41 -48
- package/src/db/plan-dispatch-schema.ts +5 -1
- package/src/db/plan-ready-notices.ts +43 -0
- package/src/db/plan-tools-include-migration.ts +36 -0
- package/src/db/plans-write-ops.ts +2 -0
- package/src/db/presence-buffer.ts +5 -0
- package/src/db/presence-loop.ts +18 -23
- package/src/db/presence-write.ts +0 -6
- package/src/db/project-entries-async.ts +14 -0
- package/src/db/project-entries-read-ops.ts +5 -1
- package/src/db/project-entries-write-ops.ts +8 -0
- package/src/db/project-entries.ts +15 -12
- package/src/db/project-journal.ts +2 -2
- package/src/db/project-trust-approvals.ts +2 -2
- package/src/db/prompt-templates.ts +4 -4
- package/src/db/read-op-marshaling-fixtures.ts +18 -0
- package/src/db/read-ops.ts +4 -0
- package/src/db/retention.ts +13 -2
- package/src/db/review-publish-write.ts +8 -0
- package/src/db/runtime-admin-async.ts +19 -0
- package/src/db/runtime-admin-write-ops.ts +36 -0
- package/src/db/runtime-tail-async.ts +288 -0
- package/src/db/runtime-tail-write-ops.ts +97 -0
- package/src/db/schema.ts +4 -1
- package/src/db/spawn-budget.ts +5 -0
- package/src/db/spawn-delegation.ts +1 -11
- package/src/db/sql-audit-indexes.ts +3 -0
- package/src/db/stats-async.ts +5 -3
- package/src/db/task-entity.ts +5 -4
- package/src/db/team-claim-policy.ts +60 -0
- package/src/db/teams-async.ts +4 -0
- package/src/db/teams-write-ops.ts +4 -0
- package/src/db/terminal-events.ts +15 -0
- package/src/db/timer-cutover-bodies.ts +292 -0
- package/src/db/timer-cutover-core.ts +37 -278
- package/src/db/timer-cutover-state.ts +40 -0
- package/src/db/timer-cutover-write-ops.ts +1 -40
- package/src/db/token-db-write-ops.ts +2 -0
- package/src/db/token-standing-async.ts +12 -0
- package/src/db/token-standing.ts +6 -9
- package/src/db/token-usage-retention.ts +33 -0
- package/src/db/token-usage-write-ops.ts +1 -1
- package/src/db/token-usage.ts +4 -1
- package/src/db/user-invites-async.ts +13 -0
- package/src/db/user-invites.ts +3 -3
- package/src/db/users.ts +14 -6
- package/src/db/workspaces.ts +8 -5
- package/src/db/write-ops.ts +31 -12
- package/src/db/writer-fault-seam.ts +2 -9
- package/src/db/writer-fault-test-ops.ts +18 -0
- package/src/db/writer-gateway.ts +42 -46
- package/src/dev.ts +8 -3
- package/src/event-loop-lag.ts +4 -2
- package/src/event-loop-yield.ts +2 -2
- package/src/events.ts +10 -8
- package/src/host-provider-store.ts +5 -4
- package/src/hub-metrics-report.ts +5 -1
- package/src/hub-metrics.ts +38 -18
- package/src/injection-sweep-pacing.ts +6 -8
- package/src/land-gate-notifications.ts +11 -10
- package/src/land-gate-queue-progress.ts +3 -3
- package/src/land-settle-dispatch.ts +52 -92
- package/src/lifecycle-context-pressure.ts +11 -10
- package/src/lifecycle-manager.ts +104 -98
- package/src/maintenance/breaker.ts +18 -16
- package/src/maintenance/checkout-refresh.ts +18 -1
- package/src/maintenance/critical-job-watchdog.ts +3 -3
- package/src/maintenance/events.ts +3 -3
- package/src/maintenance/history-retention-job.ts +30 -0
- package/src/maintenance/jobs.ts +29 -67
- package/src/maintenance/long-running-work.ts +3 -1
- package/src/maintenance/offline-agent-prune-job.ts +28 -0
- package/src/maintenance/orphaned-sessions.ts +5 -4
- package/src/maintenance/pauses.ts +4 -10
- package/src/maintenance/plan-ready-notice-job.ts +71 -0
- package/src/maintenance/scheduler.ts +18 -13
- package/src/maintenance/stalled-delivery-pager.ts +4 -4
- package/src/maintenance/teams.ts +19 -30
- package/src/maintenance/workspaces.ts +17 -27
- package/src/managed-policy-escalation.ts +7 -7
- package/src/managed-policy.ts +3 -3
- package/src/managed-provider-gate.ts +5 -4
- package/src/managed-spawn-reservation.ts +5 -4
- package/src/mcp/index.ts +8 -8
- package/src/mcp/output-descriptions.ts +1 -1
- package/src/mcp/presentation.ts +2 -1
- package/src/mcp/runtime-output.ts +6 -1
- package/src/mcp/self-care-visibility.ts +2 -2
- package/src/mcp/spawn-targets-matrix.ts +10 -1
- package/src/mcp/tool-groups.ts +3 -2
- package/src/mcp/tool-roles.ts +81 -0
- package/src/mcp/tools-workspace.ts +1 -1
- package/src/mcp-blackboard-tools.ts +6 -5
- package/src/mcp-decision-tools.ts +7 -7
- package/src/mcp-egress-tools.ts +10 -9
- package/src/mcp-issue-activity-tools.ts +2 -2
- package/src/mcp-issue-association-tools.ts +4 -4
- package/src/mcp-issue-ref-tools.ts +3 -3
- package/src/mcp-merge-tools.ts +5 -5
- package/src/mcp-notification-subscription-tools.ts +14 -15
- package/src/mcp-output-levels.test-fixture.ts +1 -1
- package/src/mcp-profile-trust-tools.ts +1 -1
- package/src/mcp-project-journal-tools.ts +4 -12
- package/src/mcp-project-tools.ts +17 -18
- package/src/mcp-routing-policy-tools.ts +3 -2
- package/src/mcp-team-tools.ts +6 -6
- package/src/mcp-workflow-tools.ts +5 -3
- package/src/memory-area-injection.ts +13 -12
- package/src/memory-boundary-capture.ts +6 -6
- package/src/memory-injection.ts +45 -43
- package/src/merge-pause-maintenance.ts +5 -4
- package/src/notification-router.ts +44 -17
- package/src/notify.ts +5 -5
- package/src/observed-reset-refresh.ts +4 -4
- package/src/operator-alarm.ts +2 -5
- package/src/project-ingest.ts +6 -6
- package/src/project-instructions.ts +8 -1
- package/src/project-trust.ts +12 -11
- package/src/prompt-resolver.ts +3 -3
- package/src/provider-quota-async.ts +5 -7
- package/src/provider-timeline-event.ts +4 -7
- package/src/quota-band-delivery.ts +2 -2
- package/src/recipe-runner.ts +4 -3
- package/src/retention-config-store.ts +4 -3
- package/src/reviewer-pipeline.ts +11 -10
- package/src/routes/_route.ts +4 -4
- package/src/routes/_shared.ts +13 -12
- package/src/routes/agent-effort.ts +5 -5
- package/src/routes/agent-heartbeat-quota.ts +23 -0
- package/src/routes/agent-keep.ts +1 -1
- package/src/routes/agent-reporting.ts +1 -1
- package/src/routes/agent-self-effort.ts +5 -5
- package/src/routes/agent-sessions.ts +24 -24
- package/src/routes/agents-spawn.ts +1 -1
- package/src/routes/agents.ts +17 -119
- package/src/routes/auth.ts +14 -15
- package/src/routes/authority-audit.ts +2 -2
- package/src/routes/automations.ts +6 -5
- package/src/routes/channel-binding-delete.ts +1 -1
- package/src/routes/channel-output.ts +3 -3
- package/src/routes/claude-api-budget.ts +5 -5
- package/src/routes/commands-listing.ts +1 -1
- package/src/routes/commands.ts +29 -27
- package/src/routes/context-resets.ts +2 -2
- package/src/routes/coordinator-teams.ts +1 -1
- package/src/routes/decisions.ts +2 -2
- package/src/routes/host-secret-files.ts +1 -1
- package/src/routes/hosts.ts +11 -11
- package/src/routes/inbox.ts +3 -2
- package/src/routes/index.ts +2 -1
- package/src/routes/integrations.ts +9 -7
- package/src/routes/issue-activity.ts +2 -2
- package/src/routes/issue-associations.ts +4 -4
- package/src/routes/issue-refs.ts +3 -3
- package/src/routes/kernel-timers-operator.ts +1 -1
- package/src/routes/memory.ts +1 -1
- package/src/routes/merges.ts +11 -11
- package/src/routes/messages.ts +23 -17
- package/src/routes/orchestrator-bootstrap.ts +1 -1
- package/src/routes/orchestrator-host-registration.ts +2 -0
- package/src/routes/orchestrator-host-utilization.ts +2 -2
- package/src/routes/orchestrator-proxy.ts +2 -2
- package/src/routes/orchestrator.ts +27 -18
- package/src/routes/pairs.ts +8 -8
- package/src/routes/project-profile-instructions.ts +1 -1
- package/src/routes/project-trust.ts +1 -1
- package/src/routes/projects.ts +16 -16
- package/src/routes/prompt-templates.ts +12 -12
- package/src/routes/provider-quotas.ts +10 -10
- package/src/routes/run-command.ts +1 -1
- package/src/routes/secrets-backup.ts +2 -2
- package/src/routes/secrets-import.ts +1 -1
- package/src/routes/secrets-vault.ts +7 -7
- package/src/routes/settings.ts +1 -1
- package/src/routes/shipping.ts +9 -1
- package/src/routes/spawn-policy.ts +8 -7
- package/src/routes/sse.ts +2 -2
- package/src/routes/team-templates.ts +7 -7
- package/src/routes/teams.ts +10 -10
- package/src/routes/tokens.ts +18 -17
- package/src/routes/users.ts +24 -24
- package/src/routes/viewer-read.ts +3 -3
- package/src/routes/web-tool-calls.ts +4 -4
- package/src/routes/workflows.ts +2 -2
- package/src/routes/workspaces.ts +17 -16
- package/src/routing-policy-push.ts +6 -6
- package/src/runtime-grant.ts +17 -11
- package/src/runtime-tokens.ts +20 -28
- package/src/secrets-broker/config.ts +1 -1
- package/src/secrets-broker/service.ts +2 -2
- package/src/security.ts +3 -1
- package/src/self-resume-carry.ts +21 -110
- package/src/self-resume-supersede.ts +4 -3
- package/src/self-resume.ts +54 -66
- package/src/server-background.ts +1 -1
- package/src/server.ts +21 -22
- package/src/services/agent-action.ts +12 -31
- package/src/services/agent-effort.ts +7 -32
- package/src/services/agent-hang-watchdog-job.ts +2 -2
- package/src/services/agent-heartbeat.ts +166 -0
- package/src/services/agent-reporting.ts +1 -1
- package/src/services/browser-inventory.ts +3 -3
- package/src/services/browser-session-state.ts +3 -2
- package/src/services/browser-session.ts +21 -27
- package/src/services/bus-command-update.ts +9 -9
- package/src/services/checkout-refresh-state.ts +1 -0
- package/src/services/checkout-refresh.test-fixture.ts +3 -2
- package/src/services/command-host.ts +7 -2
- package/src/services/coordinator-digest.ts +3 -2
- package/src/services/dispatch-command.ts +12 -13
- package/src/services/effective-launch-driver.ts +14 -9
- package/src/services/host-changed-paths.test-fixture.ts +5 -6
- package/src/services/host-project-readiness.ts +2 -2
- package/src/services/host-providers.ts +1 -1
- package/src/services/host-utilization.ts +9 -9
- package/src/services/idle-agent-digest.ts +17 -12
- package/src/services/idle-epics.ts +4 -4
- package/src/services/interrupt-agent.ts +12 -9
- package/src/services/issue-activity.ts +3 -3
- package/src/services/issue-associations.ts +15 -18
- package/src/services/issue-lifecycle-comments.ts +3 -2
- package/src/services/issue-lifecycle.ts +14 -13
- package/src/services/issue-refs.ts +4 -5
- package/src/services/kernel-timers-cutover-chaos-worker.ts +1 -1
- package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
- package/src/services/kernel-timers-ingress.ts +3 -3
- package/src/services/kernel-timers-message-authority-chaos-worker.ts +3 -10
- package/src/services/kernel-timers-operator-chaos-worker.ts +3 -2
- package/src/services/kernel-timers-outbox.ts +1 -1
- package/src/services/kernel-timers-stage10-operator.ts +1 -1
- package/src/services/kernel-timers.ts +4 -3
- package/src/services/land-gate-placement.ts +11 -2
- package/src/services/land-issue-relations.ts +5 -4
- package/src/services/land-restart-retry.ts +7 -6
- package/src/services/landed-ancestry.ts +6 -5
- package/src/services/legacy-landing-host.ts +4 -1
- package/src/services/legacy-landing-recovery.ts +8 -6
- package/src/services/managed-running.ts +17 -17
- package/src/services/memory-areas.ts +3 -3
- package/src/services/merge-pause.ts +9 -15
- package/src/services/operator-decisions.ts +37 -41
- package/src/services/orchestrator-registration-recovery.ts +6 -2
- package/src/services/orchestrator-upgrade.ts +14 -12
- package/src/services/permission-decision.ts +4 -3
- package/src/services/plan-ci-notice.ts +1 -1
- package/src/services/plan-conformance.ts +4 -4
- package/src/services/plan-dispatch-policy.ts +10 -3
- package/src/services/plan-dispatch.ts +35 -14
- package/src/services/plan-graph.ts +39 -261
- package/src/services/plan-import.ts +1 -1
- package/src/services/plan-live-bindings.ts +8 -37
- package/src/services/plan-task-create.ts +1 -1
- package/src/services/plan-terminal-refs.ts +2 -12
- package/src/services/plan-wake-digest.ts +8 -3
- package/src/services/profile-trust.ts +2 -2
- package/src/services/project.ts +11 -10
- package/src/services/prompt-templates.ts +6 -5
- package/src/services/provider-hold-nudge.ts +9 -9
- package/src/services/provider-hold-record.ts +13 -12
- package/src/services/provider-state-audit.ts +19 -18
- package/src/services/provisioning-harness/driver.ts +1 -1
- package/src/services/provisioning-harness/fixture-matrix.ts +3 -3
- package/src/services/provisioning-harness/lifecycle.ts +24 -22
- package/src/services/provisioning-harness/prepare.ts +16 -17
- package/src/services/rate-limit-resume.ts +10 -9
- package/src/services/red-main-auto-revert.ts +7 -5
- package/src/services/red-main-controller-deps.ts +20 -3
- package/src/services/red-main-incident.ts +3 -1
- package/src/services/red-main-quarantine.ts +2 -3
- package/src/services/register-agent.ts +22 -22
- package/src/services/restart-project-role.ts +1 -1
- package/src/services/restart-spawn.ts +17 -16
- package/src/services/review-publish.ts +7 -24
- package/src/services/review-ref-reconcile.ts +29 -38
- package/src/services/runtime-dependency-alert.ts +27 -0
- package/src/services/scheduler-command.ts +11 -9
- package/src/services/scheduler.ts +5 -5
- package/src/services/self-effort-grant.ts +5 -25
- package/src/services/send-message.ts +16 -33
- package/src/services/shutdown-agent.ts +17 -14
- package/src/services/spawn-agent.ts +12 -41
- package/src/services/spawn-chain-inspector.ts +1 -1
- package/src/services/spawn-cwd-placement.ts +21 -5
- package/src/services/spawn-issue-activity.ts +8 -6
- package/src/services/spawn-launch.ts +17 -1
- package/src/services/spawn-loss-escalation.ts +1 -1
- package/src/services/spawn-registration-watchdog.ts +2 -2
- package/src/services/spawn-repo-probe.ts +7 -5
- package/src/services/spawn-tree-cleanup.ts +7 -8
- package/src/services/steward-reaper.ts +4 -4
- package/src/services/task-context-hydration.ts +25 -1
- package/src/services/task-delivery.ts +20 -34
- package/src/services/task-entity.ts +40 -203
- package/src/services/task-issue-link.ts +13 -20
- package/src/services/task-issue-weave.ts +12 -11
- package/src/services/task-lifecycle.ts +27 -31
- package/src/services/task-repo-cwd.ts +37 -0
- package/src/services/task-review-hold.ts +4 -17
- package/src/services/task-semantic-events.ts +13 -11
- package/src/services/task-state-reconcile.ts +3 -4
- package/src/services/task-validation.ts +2 -2
- package/src/services/team-templates.ts +5 -5
- package/src/services/team.ts +31 -98
- package/src/services/terminal-provider-exit.ts +6 -5
- package/src/services/timer-message-fire-chaos-worker.ts +7 -3
- package/src/services/timer-message-fire-outbox.ts +1 -1
- package/src/services/transient-agent-reaper.ts +22 -19
- package/src/services/turn-reconcile-audit.ts +9 -9
- package/src/services/user-onboarding.ts +8 -6
- package/src/services/user-profile.ts +6 -9
- package/src/services/work-finality.ts +16 -4
- package/src/services/workflow-auto-start.ts +2 -1
- package/src/services/workflow-caller.ts +61 -4
- package/src/services/workflow-child-proof-worker.ts +1 -1
- package/src/services/workflow-control-proof-worker.ts +1 -1
- package/src/services/workflow-coordinator.ts +8 -2
- package/src/services/workflow-decisions.ts +5 -5
- package/src/services/workflow-deferred-cleanup.ts +1 -1
- package/src/services/workflow-effect-driver.ts +42 -19
- package/src/services/workflow-effects.ts +41 -27
- package/src/services/workflow-pins.ts +7 -4
- package/src/services/workflow-reconcile.ts +1 -0
- package/src/services/workflow-resume-pins.ts +3 -3
- package/src/services/workflow-routing-defaults.ts +18 -0
- package/src/services/workflow-run-view.ts +4 -3
- package/src/services/workflow-run.test-fixture.ts +1 -1
- package/src/services/workflow-runs.ts +34 -22
- package/src/services/workflow-supervisor-state.ts +21 -1
- package/src/services/workflow-supervisor.ts +4 -3
- package/src/services/workspace-recovery.ts +4 -3
- package/src/spawn-admission.ts +10 -2
- package/src/spawn-placement.ts +27 -9
- package/src/spawn-targets.ts +8 -8
- package/src/sse-agent-status.ts +15 -6
- package/src/sse.ts +49 -24
- package/src/startup-context-injection.ts +61 -57
- package/src/steward-handoff.ts +4 -3
- package/src/steward-policy-cleanup.ts +9 -8
- package/src/steward.ts +2 -2
- package/src/task-dispatch-live.ts +5 -2
- package/src/task-dispatcher.ts +2 -2
- package/src/token-db.ts +2 -2
- package/src/token-usage-async.ts +2 -4
- package/src/user-awaiting-reply.ts +5 -5
- package/src/workspace-actions.ts +24 -29
- package/src/workspace-auto-merge.ts +14 -12
- package/src/workspace-changed-paths-client.ts +16 -5
- package/src/workspace-cleanup-stale.ts +30 -7
- package/src/workspace-command-settle.ts +24 -16
- package/src/workspace-config.ts +1 -1
- package/src/workspace-conflict-resolver.ts +9 -7
- package/src/workspace-dead-owner-recovery.ts +5 -4
- package/src/workspace-escalation.ts +12 -11
- package/src/workspace-human-escalation.ts +3 -3
- package/src/workspace-land-gate-watchdog.ts +3 -3
- package/src/workspace-land-refusal.ts +20 -0
- package/src/workspace-land-stopped.ts +1 -0
- package/src/workspace-merge.ts +10 -26
- package/src/workspace-nothing-to-land-notice.ts +2 -2
- package/src/workspace-orphans.ts +18 -37
- package/src/workspace-pr-completion.ts +10 -8
- package/src/workspace-rekey.ts +3 -3
- package/src/workspace-steward-failure.ts +2 -2
- package/src/workspace-steward-wake.ts +2 -2
- package/public/assets/agent-liveness-CaNwK0Zf.js +0 -2
- package/public/assets/provider-display-DsuWMvVL.js +0 -2
- package/public/assets/run-card-CpKqRbhg.js +0 -2
- package/public/assets/shipping-B9YtnwtG.js +0 -2
- package/public/assets/store-DcClXLH0.js +0 -9
- package/runner/src/agent-runner/http-liveness.ts +0 -85
- package/src/db/on-loop-write-transaction.ts +0 -34
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { stringValue } from "@humwick/sdk";
|
|
2
2
|
import { childWorkflowsEnabled } from "../config";
|
|
3
3
|
import { getAgentProfile } from "../config-store";
|
|
4
|
-
import { getAgent, ValidationError } from "../db";
|
|
5
|
-
import
|
|
4
|
+
import { activeTeamCoordinatorId, getAgent, ValidationError } from "../db";
|
|
5
|
+
import { taskOwnerRoleTeamId } from "../db/task-owner";
|
|
6
|
+
import type { AgentCard, TaskDetail } from "../types";
|
|
6
7
|
import { ServiceAuthError } from "./errors";
|
|
7
8
|
import type { AuthContext } from "./auth-context";
|
|
8
9
|
import { canSpawn } from "./spawn-agent";
|
|
@@ -15,14 +16,70 @@ export function assertWorkflowCallerLive(agentId: string): void {
|
|
|
15
16
|
if (workflowCallerGone(agentId)) throw new ValidationError("WORKFLOW_CALLER_GONE: caller is canceled or reaped");
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
type WorkflowTaskSubject = Pick<TaskDetail, "createdBy" | "ownerAgentId" | "teamId" | "owner">;
|
|
20
|
+
|
|
21
|
+
/** An agent seated as its team's coordinator: it owns work and runs, it never builds a run's Task. */
|
|
22
|
+
export function isCoordinatorSeat(agent: Pick<AgentCard, "id" | "teamId"> | null | undefined): boolean {
|
|
23
|
+
return Boolean(agent?.teamId && activeTeamCoordinatorId(agent.teamId) === agent.id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A spawned coordinator seat: a delegated sub-coordinator, never a worker it runs. */
|
|
27
|
+
function delegatedSeat(agentId: string | null | undefined): AgentCard | null {
|
|
28
|
+
const agent = agentId ? getAgent(agentId) : null;
|
|
29
|
+
return agent?.spawnedBy && isCoordinatorSeat(agent) ? agent : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Who may own a Task's runs (#3769): its assigned agent, its team's seat, its owner role's seat, its creator. */
|
|
33
|
+
function taskOwnerCandidates(task: WorkflowTaskSubject): Array<string | null | undefined> {
|
|
34
|
+
return [task.ownerAgentId, activeTeamCoordinatorId(task.teamId), activeTeamCoordinatorId(taskOwnerRoleTeamId(task.owner)), task.createdBy];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** #3769 — the delegated sub-coordinator that owns a Task: the first candidate holding its private team's seat. */
|
|
38
|
+
export function delegatedTaskOwner(task: WorkflowTaskSubject): AgentCard | null {
|
|
39
|
+
for (const id of taskOwnerCandidates(task)) {
|
|
40
|
+
const seat = delegatedSeat(id);
|
|
41
|
+
if (seat) return seat;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** #3818 — a Task its parent coordinator created (or holds the owner seat of) is the sub-coordinator's queue, no hand-over needed. */
|
|
47
|
+
function parentQueuedTask(seat: AgentCard, task: WorkflowTaskSubject): boolean {
|
|
48
|
+
const parent = seat.spawnedBy;
|
|
49
|
+
return Boolean(parent && (task.createdBy === parent || activeTeamCoordinatorId(taskOwnerRoleTeamId(task.owner)) === parent));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** THE workflow-control predicate (start, read, resume, pause, cancel, budget): the Task's coordinators and admins, and
|
|
53
|
+
* a delegated sub-coordinator that created it, is assigned it, holds its team's seat or was spawned by its creator.
|
|
54
|
+
* A worker is never authorized. */
|
|
55
|
+
export function canControlWorkflowTask(ctx: AuthContext, task: WorkflowTaskSubject): boolean {
|
|
56
|
+
if (canControlTaskReviewHold(ctx, task)) return true;
|
|
57
|
+
const caller = ctx.callerAgentId;
|
|
58
|
+
const seat = delegatedSeat(caller);
|
|
59
|
+
return Boolean(seat && (taskOwnerCandidates(task).includes(seat.id) || parentQueuedTask(seat, task)));
|
|
60
|
+
}
|
|
61
|
+
|
|
18
62
|
/** Workflow authority delegates execution to Relay, never the caller's direct-spawn capability. */
|
|
19
63
|
export function admitWorkflowCaller(ctx: AuthContext, task: TaskDetail, workflow?: { name: string; source: string }) {
|
|
20
64
|
const agent = ctx.callerAgentId ? getAgent(ctx.callerAgentId) : null;
|
|
21
65
|
if (!agent || (!agent.spawnedBy && canSpawn(ctx))) return undefined;
|
|
22
66
|
if (!childWorkflowsEnabled()) throw new ServiceAuthError("WORKFLOW_DISABLED: child workflows are disabled");
|
|
23
67
|
assertWorkflowCallerLive(agent.id);
|
|
24
|
-
if (task.ownerAgentId !== agent.id && task.createdBy !== agent.id && !
|
|
25
|
-
|
|
68
|
+
if (task.ownerAgentId !== agent.id && task.createdBy !== agent.id && !canControlWorkflowTask(ctx, task)) throw new ServiceAuthError("WORKFLOW_TASK_FORBIDDEN: caller must own or create the Task, or be a sub-coordinator of the coordinator that created it (hand one over with relay_task_assign_agent)");
|
|
69
|
+
return workflowCallerPolicy(agent, ctx.constraints?.profile, workflow);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** #3769 — a run a Task coordinator starts on a Task a live delegated sub-coordinator owns is that owner's run:
|
|
73
|
+
* its spawn lineage, its quota and its notices, exactly as if the owner had started it. */
|
|
74
|
+
export function admitWorkflowTaskOwner(task: TaskDetail, workflow?: { name: string; source: string }) {
|
|
75
|
+
const owner = delegatedTaskOwner(task);
|
|
76
|
+
if (!owner || workflowCallerGone(owner.id)) return undefined;
|
|
77
|
+
if (!childWorkflowsEnabled()) throw new ServiceAuthError(`WORKFLOW_DISABLED: T${task.id} belongs to sub-coordinator ${owner.id}, and child workflows are disabled`);
|
|
78
|
+
return workflowCallerPolicy(owner, undefined, workflow);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function workflowCallerPolicy(agent: AgentCard, profileName: string | undefined, workflow?: { name: string; source: string }) {
|
|
82
|
+
const profile = getAgentProfile(profileName ?? stringValue(agent.meta?.profile) ?? "worker")?.value;
|
|
26
83
|
const policy = profile?.workflows;
|
|
27
84
|
const maxConcurrentRuns = policy?.maxConcurrentRuns ?? 1;
|
|
28
85
|
const maxLiveAgents = policy?.maxLiveAgents ?? 2;
|
|
@@ -43,7 +43,7 @@ if (phase === "crash") {
|
|
|
43
43
|
setWorkflowSupervisorTestOptions({ fault(label) {
|
|
44
44
|
if (label === "after-child-complete") process.kill(process.pid, "SIGKILL");
|
|
45
45
|
} });
|
|
46
|
-
announceBranchLanded({ workspace: { id: WF.workspace, repoRoot: repo.cwd, branch: WF.branch, baseRef: "origin/main", ownerAgentId: WF.builder, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [task.id] });
|
|
46
|
+
await announceBranchLanded({ workspace: { id: WF.workspace, repoRoot: repo.cwd, branch: WF.branch, baseRef: "origin/main", ownerAgentId: WF.builder, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [task.id] });
|
|
47
47
|
await workflowTaskDelivered();
|
|
48
48
|
await settleChildProofStops();
|
|
49
49
|
throw new Error("restart seam was not reached");
|
|
@@ -76,7 +76,7 @@ async function build(node: Node) {
|
|
|
76
76
|
git(node.cwd!, "push", "-q", "origin", "HEAD:main");
|
|
77
77
|
node.sha = sha; save();
|
|
78
78
|
upsertWorkspace({ ...getWorkspace(node.workspace!)!, landedAt: Date.now(), landedSha: sha });
|
|
79
|
-
announceBranchLanded({ workspace: { id: node.workspace!, repoRoot: node.cwd!, branch: node.branch!, baseRef: "main", ownerAgentId: node.agent!, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [node.taskId] });
|
|
79
|
+
await announceBranchLanded({ workspace: { id: node.workspace!, repoRoot: node.cwd!, branch: node.branch!, baseRef: "main", ownerAgentId: node.agent!, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [node.taskId] });
|
|
80
80
|
await workflowTaskDelivered(); await settleChildProofStops(); await settleLandLifecycleDrives();
|
|
81
81
|
meta.events.push({ kind: "land", id: node.id }); save();
|
|
82
82
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
|
|
2
2
|
import { getTaskDetailAsync } from "../db";
|
|
3
3
|
import { taskOwnerTarget } from "../task-owner";
|
|
4
|
+
import { delegatedTaskOwner, workflowCallerGone } from "./workflow-caller";
|
|
4
5
|
|
|
5
|
-
/** Notices and decisions share the starter, then
|
|
6
|
+
/** Notices and decisions share the starter, then the Task's live owning sub-coordinator (#3769), then its durable
|
|
7
|
+
* owner, then the run coordinator: the root hears of a sub-coordinator's Task only once that owner is gone. */
|
|
6
8
|
export async function workflowCoordinatorTarget(run: WorkflowRunRecord): Promise<string> {
|
|
7
|
-
|
|
9
|
+
if (run.callerAgentId) return run.callerAgentId;
|
|
10
|
+
const task = await getTaskDetailAsync(run.taskId);
|
|
11
|
+
const owner = task ? delegatedTaskOwner(task) : null;
|
|
12
|
+
if (owner && !workflowCallerGone(owner.id)) return owner.id;
|
|
13
|
+
return taskOwnerTarget(task) ?? run.coordinator;
|
|
8
14
|
}
|
|
@@ -42,7 +42,7 @@ export async function performWorkflowDecision(run: WorkflowRunRecord, effect: Wo
|
|
|
42
42
|
workflow: { runId: run.id, step: run.step, effectId: effect.id, waitKey: effect.waitKey!, ...(multi ? { items } : {}), ...(step.outputSchema ? { outputSchema: step.outputSchema } : {}) },
|
|
43
43
|
};
|
|
44
44
|
if (spec.answeredBy === "coordinator") {
|
|
45
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
45
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
46
46
|
subject: `Workflow ${run.id}: ${request.title}`,
|
|
47
47
|
body: [request.title, request.context, `Choices (JSON data): ${JSON.stringify(items)}`,
|
|
48
48
|
`Answer with relay_workflow_resume({runId: "${run.id}", decision: {action: "answer", waitKey: "${effect.waitKey}", answers: {${multi ? 'approved: ["item-id"]' : 'choice: "option-id"'}}}, note: "reason"}).`,
|
|
@@ -63,16 +63,16 @@ export function workflowDecisionSignal(effectId: string): Record<string, unknown
|
|
|
63
63
|
return { outcome: "complete", decisionId: decision.id, outputs: workflowDecisionAnswer(decision, decision.answers!, decision.note) };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function performWithdrawWorkflowDecision(effect: WorkflowEffect): Record<string, unknown
|
|
67
|
-
withdrawWorkflowDecision(String(effect.payload.effectId));
|
|
66
|
+
export async function performWithdrawWorkflowDecision(effect: WorkflowEffect): Promise<Record<string, unknown>> {
|
|
67
|
+
(await withdrawWorkflowDecision(String(effect.payload.effectId)));
|
|
68
68
|
return { withdrawn: true };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async function fileOperatorDecision(run: WorkflowRunRecord, request: WorkflowDecisionRequest, note?: string): Promise<Record<string, unknown>> {
|
|
72
72
|
const task = await getTaskDetailAsync(run.taskId);
|
|
73
|
-
const decision = fileWorkflowDecision({ actor: { id: run.coordinator, kind: "agent" }, callerAgentId: run.coordinator, scopes: ["*", "command:spawn"] }, {
|
|
73
|
+
const decision = (await fileWorkflowDecision({ actor: { id: run.coordinator, kind: "agent" }, callerAgentId: run.coordinator, scopes: ["*", "command:spawn"] }, {
|
|
74
74
|
...request,
|
|
75
|
-
}, request.workflow, task?.projectId, note);
|
|
75
|
+
}, request.workflow, task?.projectId, note));
|
|
76
76
|
return { decisionId: decision.id };
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -31,7 +31,7 @@ async function resumeHosts(sup: Supervisor, orchestratorId?: string): Promise<vo
|
|
|
31
31
|
|
|
32
32
|
export async function notifyDeferredWorkflowCleanup(run: WorkflowRunRecord, effect: WorkflowEffect): Promise<void> {
|
|
33
33
|
const proof = run.landed?.sha ? await workflowLandVerification(run, run.landed.sha) : undefined;
|
|
34
|
-
notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
34
|
+
await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
35
35
|
subject: `Workflow ${run.id}: cleanup deferred`,
|
|
36
36
|
body: `Workflow ${run.id}: ${proof?.verified ? `landed as ${run.landed?.sha}. verified: true.` : proof?.line ?? "No land receipt."} Shutdowns on offline hosts are recorded durably and will run when those hosts re-register. The run can finish with its CI result.`,
|
|
37
37
|
payload: { ...(proof ? { verified: proof.verified } : {}), kind: "workflow.cleanup-deferred", runId: run.id, effectId: effect.id, deferred: effect.result?.deferred },
|
|
@@ -19,12 +19,14 @@ import { ServiceAuthError } from "./errors";
|
|
|
19
19
|
|
|
20
20
|
import { performWorkflowEffect } from "./workflow-effects";
|
|
21
21
|
import { workflowCleanupDeferred } from "./workflow-deferred-cleanup";
|
|
22
|
+
import { PinnedHostCapacityError } from "../spawn-admission";
|
|
22
23
|
|
|
23
|
-
import { invoke, live, runAgents, noteAgent, testOptions, type Supervisor } from "./workflow-supervisor-state";
|
|
24
|
+
import { effectDue, invoke, retryResult, live, runAgents, noteAgent, announceChange, supervisorNow, testOptions, type Supervisor } from "./workflow-supervisor-state";
|
|
24
25
|
type Json = Record<string, unknown>;
|
|
25
26
|
const EFFECT_BATCH = 50;
|
|
26
27
|
const MAX_EFFECT_ATTEMPTS = 3;
|
|
27
28
|
const REAP_RETRY_MS = 60 * 60_000;
|
|
29
|
+
const SLOT_WAIT_MS = 30 * 60_000;
|
|
28
30
|
function isPermanentEffectError(error: unknown): boolean {
|
|
29
31
|
return error instanceof WorkflowValueError || error instanceof ValidationError || error instanceof McpNotFoundError || error instanceof McpAuthError
|
|
30
32
|
|| error instanceof ServiceAuthError || error instanceof ReviewHoldConflictError;
|
|
@@ -39,21 +41,31 @@ async function settleEffect(sup: Supervisor, run: WorkflowRunRecord, effect: Wor
|
|
|
39
41
|
run = { ...run, snapshot: root.snapshot };
|
|
40
42
|
}
|
|
41
43
|
try {
|
|
44
|
+
if (effect.kind === "dispatch" && !run.cancel && run.status === "running" && run.awaiting?.slot && run.awaiting.key === effect.waitKey && supervisorNow() >= run.awaiting.slot.deadline) {
|
|
45
|
+
throw new ValidationError(`Timed out waiting for a slot on ${run.awaiting.slot.host} after 30 minutes`);
|
|
46
|
+
}
|
|
42
47
|
if (effect.kind === "dispatch" && run.rootRunId && !run.cancel) await invoke(sup, "reserveAgent", { runId: run.id, effectId: effect.id });
|
|
43
48
|
if (effect.kind === "script" && !(await invoke<{ reserved: boolean }>(sup, "reserveScript", { runId: run.id, effectId: effect.id })).reserved) return false;
|
|
44
49
|
const performed = effect.kind === "cancel-children" ? await cancelChildren(sup, run, effect) : effect.kind === "child" ? await createChild(sup, run, effect) : await performWorkflowEffect(run, effect, effect.kind === "reap" || effect.kind === "dispatch" ? await runAgents(sup, run.id) : [], (op, input) => invoke(sup, op, input));
|
|
45
50
|
if (!performed && effect.kind !== "script") return false;
|
|
46
51
|
result = performed ?? { pending: true };
|
|
47
52
|
} catch (error) {
|
|
53
|
+
if (effect.kind === "dispatch" && error instanceof PinnedHostCapacityError) {
|
|
54
|
+
if (!run.awaiting?.slot) {
|
|
55
|
+
await invoke(sup, "waitForSlot", { id: effect.id, host: error.orchestratorId, now: supervisorNow(), timeoutMs: SLOT_WAIT_MS });
|
|
56
|
+
announceChange();
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
48
60
|
const message = errMessage(error);
|
|
49
61
|
if (workflowOwnsAgentCleanup(run) && ["cancel-children", "reap"].includes(effect.kind)) {
|
|
50
62
|
if (run.status !== "done") await invoke(sup, "cleanup", { runId: run.id, complete: false, error: message });
|
|
51
|
-
await invoke(sup, "settleEffect", { id: effect.id, status: "pending", error: message });
|
|
63
|
+
await invoke(sup, "settleEffect", { id: effect.id, status: "pending", error: message, result: retryResult(effect.result, effect.result ?? {}, supervisorNow()) });
|
|
52
64
|
return false;
|
|
53
65
|
}
|
|
54
66
|
const failed = isPermanentEffectError(error) || effect.attempts + 1 >= MAX_EFFECT_ATTEMPTS;
|
|
55
67
|
console.warn(`workflow effect ${effect.id} ${failed ? "failed" : "will retry"}: ${message}`);
|
|
56
|
-
await invoke(sup, "settleEffect", { id: effect.id, status: failed ? "failed" : "pending", error: message });
|
|
68
|
+
await invoke(sup, "settleEffect", { id: effect.id, status: failed ? "failed" : "pending", error: message, now: supervisorNow() });
|
|
57
69
|
if (failed && effect.kind === "dispatch") await releaseUnusedDispatch(sup, effect);
|
|
58
70
|
if (failed && effect.kind === "script") await invoke(sup, "releaseReservation", { effectId: effect.id });
|
|
59
71
|
const refusal = failed && effect.kind === "dispatch" ? sup.refusable.get(run.id) : undefined;
|
|
@@ -81,14 +93,15 @@ async function settleEffect(sup: Supervisor, run: WorkflowRunRecord, effect: Wor
|
|
|
81
93
|
// The reap becomes a dormant deferred-cleanup record until its host returns (#3658).
|
|
82
94
|
const deferred = Array.isArray(result.deferred) && result.deferred.length > 0;
|
|
83
95
|
if (deferred || Array.isArray(result.retry) && result.retry.length && (workflowOwnsAgentCleanup(run) || Date.now() - effect.createdAt < REAP_RETRY_MS)) {
|
|
84
|
-
const
|
|
96
|
+
const status = deferred && !(Array.isArray(result.retry) && result.retry.length) ? "deferred" : "pending";
|
|
97
|
+
const pending = await invoke<WorkflowEffect>(sup, "settleEffect", { id: effect.id, status, result: status === "pending" ? retryResult(effect.result, result, supervisorNow()) : result, error: JSON.stringify(result.retry ?? result.deferred).slice(0, 2000) });
|
|
85
98
|
if (!workflowCleanupDeferred(pending) && workflowOwnsAgentCleanup(run) && run.status !== "done") await invoke(sup, "cleanup", { runId: run.id, complete: false, error: JSON.stringify(result.retry).slice(0, 2000) });
|
|
86
99
|
return workflowCleanupDeferred(pending);
|
|
87
100
|
}
|
|
88
101
|
if (effect.kind === "reap" && run.status === "cancelled" && !run.rootRunId) await invoke(sup, "cleanup", { runId: run.id, complete: true });
|
|
89
102
|
if (effect.kind === "dispatch" && !result.spawnRequestId) await releaseUnusedDispatch(sup, effect);
|
|
90
103
|
if (effect.kind === "script" && result.pending !== true) await invoke(sup, "releaseReservation", { effectId: effect.id });
|
|
91
|
-
await invoke(sup, "settleEffect", { id: effect.id, status: "done", result });
|
|
104
|
+
await invoke(sup, "settleEffect", { id: effect.id, status: "done", result, now: supervisorNow() });
|
|
92
105
|
return true;
|
|
93
106
|
}
|
|
94
107
|
|
|
@@ -145,23 +158,33 @@ async function createChild(sup: Supervisor, run: WorkflowRunRecord, effect: Work
|
|
|
145
158
|
return { childRunId: child.id };
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
/** Perform up to a batch of due pending effects. A backing-off effect is skipped before any read and does not
|
|
162
|
+
* count toward the batch, so effects that never converge cannot starve newer ones (#3827). */
|
|
148
163
|
export async function drainEffects(sup: Supervisor): Promise<number> {
|
|
149
|
-
const
|
|
164
|
+
const now = supervisorNow();
|
|
150
165
|
let settled = 0;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
let attempted = 0;
|
|
167
|
+
let cursor: string | undefined;
|
|
168
|
+
do {
|
|
169
|
+
const page = await invoke<{ items: WorkflowEffect[]; cursor?: string }>(sup, "pendingEffects", { limit: EFFECT_BATCH, ...(cursor ? { cursor } : {}) });
|
|
170
|
+
for (const effect of page.items) {
|
|
171
|
+
if (live() !== sup) return settled;
|
|
172
|
+
if (!effectDue(effect, now)) continue;
|
|
173
|
+
if (attempted++ === EFFECT_BATCH) return settled;
|
|
174
|
+
try {
|
|
175
|
+
if (effect.kind === "cancel-agent-tree") {
|
|
176
|
+
if (await performSpawnTreeCleanup(sup, effect)) settled++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const run = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.runId });
|
|
180
|
+
// #3757 — a paused run spawns, messages, arms and reaps nothing until its unpause; only its pause notices go out.
|
|
181
|
+
if (run.pause && effect.kind !== "pause-notice") continue;
|
|
182
|
+
if (await settleEffect(sup, run, effect)) settled += 1;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.warn(`workflow effect ${effect.id} could not be settled: ${errMessage(error)}`);
|
|
157
185
|
}
|
|
158
|
-
const run = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.runId });
|
|
159
|
-
// #3757 — a paused run spawns, messages, arms and reaps nothing until its unpause; only its pause notices go out.
|
|
160
|
-
if (run.pause && effect.kind !== "pause-notice") continue;
|
|
161
|
-
if (await settleEffect(sup, run, effect)) settled += 1;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.warn(`workflow effect ${effect.id} could not be settled: ${errMessage(error)}`);
|
|
164
186
|
}
|
|
165
|
-
|
|
187
|
+
cursor = page.cursor;
|
|
188
|
+
} while (cursor);
|
|
166
189
|
return settled;
|
|
167
190
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { planCiNotice } from "./plan-ci-notice";
|
|
2
2
|
import { performWorkflowDecision, performWithdrawWorkflowDecision } from "./workflow-decisions";
|
|
3
|
-
import { getCommandAsync } from "../commands-write";
|
|
4
3
|
// #3045 — the src half of a workflow run: the kernel writes effect intents and waits; this module
|
|
5
4
|
// performs each effect through the existing services and reads the durable facts that end a wait.
|
|
6
5
|
// Every effect is keyed by its intent id, so a re-drive after a crash repeats nothing.
|
|
@@ -37,12 +36,15 @@ import { isStewardHandbackPendingError } from "../db/withdraw-land-error";
|
|
|
37
36
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, UnlandedWorkRefusal } from "./shutdown-agent";
|
|
38
37
|
import { OfflineControlHostError, resolveControlOrchestrator } from "./agent-control-host";
|
|
39
38
|
import { workflowCoordinatorTarget } from "./workflow-coordinator";
|
|
39
|
+
import { supervisorNow } from "./workflow-supervisor-state";
|
|
40
|
+
import { isTerminalCommandStatus } from "../db/command-terminal-status";
|
|
41
|
+
import { isCoordinatorSeat } from "./workflow-caller";
|
|
40
42
|
import { deliveredWorkspace, ensureWorkflowLanding, landedSignal, WORKFLOW_CTX } from "./workflow-land";
|
|
41
43
|
import type { AuthContext } from "./auth-context";
|
|
42
44
|
import type { Command, TaskDetail } from "../types";
|
|
43
45
|
|
|
44
46
|
type Json = Record<string, unknown>;
|
|
45
|
-
type WorkflowOp = (op: string, input: Json) => Promise<unknown>;
|
|
47
|
+
export type WorkflowOp = (op: string, input: Json) => Promise<unknown>;
|
|
46
48
|
|
|
47
49
|
function short(sha: string | undefined): string {
|
|
48
50
|
return sha ? sha.slice(0, 12) : "an unknown head";
|
|
@@ -169,16 +171,21 @@ async function createStepTask(run: WorkflowRunRecord, effect: WorkflowEffect): P
|
|
|
169
171
|
return task.id;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
/** The builder of record if it still runs: the agent a loop's findings go to (warm, never respawned).
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
/** The builder of record if it still runs: the agent a loop's findings go to (warm, never respawned). It is the run's
|
|
175
|
+
* newest registered agent on the input Task, else the Task's assigned agent unless that is a coordinator seat (#3769):
|
|
176
|
+
* a sub-coordinator assigned the Task owns its runs and never builds them. */
|
|
177
|
+
export async function liveBuilder(run: Pick<WorkflowRunRecord, "id" | "taskId">, call: WorkflowOp): Promise<{ id: string | undefined; live: boolean }> {
|
|
178
|
+
const assigned = (await requireTask(run.taskId)).ownerAgentId ?? undefined;
|
|
179
|
+
const { items } = await call("runAgents", { runId: run.id }) as { items: WorkflowAgent[] };
|
|
180
|
+
const id = items.filter((entry) => entry.taskId === run.taskId && entry.agentId).at(-1)?.agentId
|
|
181
|
+
?? (assigned && !isCoordinatorSeat(getAgent(assigned)) ? assigned : undefined);
|
|
175
182
|
const agent = id ? getAgent(id) : null;
|
|
176
183
|
return { id, live: Boolean(agent && agent.status !== "offline") };
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
async function warmBuilder(
|
|
180
|
-
const builder = await liveBuilder(
|
|
181
|
-
if (!builder.live) throw new ValidationError(`the builder of T${taskId} (${builder.id ?? "none bound"}) is gone, so the findings cannot reach it`);
|
|
186
|
+
async function warmBuilder(run: WorkflowRunRecord, call: WorkflowOp): Promise<string> {
|
|
187
|
+
const builder = await liveBuilder(run, call);
|
|
188
|
+
if (!builder.live) throw new ValidationError(`the builder of T${run.taskId} (${builder.id ?? "none bound"}) is gone, so the findings cannot reach it`);
|
|
182
189
|
return builder.id!;
|
|
183
190
|
}
|
|
184
191
|
|
|
@@ -252,14 +259,19 @@ function reapTargets(run: WorkflowRunRecord, effect: WorkflowEffect, agents: rea
|
|
|
252
259
|
&& (agentIds ? agentIds.has(agent.id) : workflowReapIncludesAgent(run, effect.payload, agent)));
|
|
253
260
|
}
|
|
254
261
|
|
|
262
|
+
/** A stop the host found no target for is retried this rarely: a missing target does not reappear each pass. */
|
|
263
|
+
const TARGET_NOT_FOUND_RETRY_MS = 15 * 60_000;
|
|
264
|
+
|
|
255
265
|
/** The run's latest stop command for an agent. A failed or timed-out one is terminal and never runs again,
|
|
256
|
-
* so the next attempt gets its own id; a pump therefore retries the stop instead of rereading the failure.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
* so the next attempt gets its own id; a pump therefore retries the stop instead of rereading the failure.
|
|
267
|
+
* One read of the newest attempt, whatever the history (#3827). */
|
|
268
|
+
export async function currentStop(baseId: string, now = supervisorNow()): Promise<{ id: string; prior?: Command }> {
|
|
269
|
+
const latest = await getGateway().read("commands.latestAttempt", { baseId });
|
|
270
|
+
if (!latest) return { id: baseId };
|
|
271
|
+
const { attempt, command } = latest;
|
|
272
|
+
const retry = (command.status === "failed" || command.status === "timed_out")
|
|
273
|
+
&& (command.result?.targetNotFound !== true || now - command.updatedAt >= TARGET_NOT_FOUND_RETRY_MS);
|
|
274
|
+
return retry ? { id: `${baseId}:${attempt + 1}` } : { id: command.id, prior: command };
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
/** Shut each target's live generation down through the shutdown service. The unlanded-work guard's
|
|
@@ -269,6 +281,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
|
|
|
269
281
|
const kept: Array<{ spawnRequestId: string; reason: string }> = [];
|
|
270
282
|
const retry: Array<{ spawnRequestId: string; reason: string }> = [];
|
|
271
283
|
const deferred: Array<{ spawnRequestId: string; orchestratorId: string }> = [];
|
|
284
|
+
let inFlight = false;
|
|
272
285
|
for (const agent of targets) {
|
|
273
286
|
const current = agent.agentId ? getAgent(agent.agentId) : null;
|
|
274
287
|
const agentId = current?.meta?.spawnRequestId === agent.spawnRequestId ? agent.agentId : undefined;
|
|
@@ -289,6 +302,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
|
|
|
289
302
|
if (stopId && command.type === "agent.spawn" && command.status === "canceled") command = (await shutdownAgent(stop, WORKFLOW_CTX)).command;
|
|
290
303
|
if (stopId && (command.status !== "succeeded" || command.result?.processTreeStopped !== true)) {
|
|
291
304
|
retry.push({ spawnRequestId: agent.spawnRequestId, reason: `shutdown ${command.id}: ${command.status}` });
|
|
305
|
+
if (!isTerminalCommandStatus(command.status)) inFlight = true;
|
|
292
306
|
continue;
|
|
293
307
|
}
|
|
294
308
|
reaped.push({ spawnRequestId: agent.spawnRequestId, ...(agent.agentId ? { agentId: agent.agentId } : {}) });
|
|
@@ -299,7 +313,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
|
|
|
299
313
|
else retry.push(entry);
|
|
300
314
|
}
|
|
301
315
|
}
|
|
302
|
-
return { reaped, ...(kept.length ? { kept } : {}), ...(retry.length ? { retry } : {}), ...(deferred.length ? { deferred } : {}) };
|
|
316
|
+
return { reaped, ...(kept.length ? { kept } : {}), ...(retry.length ? { retry } : {}), ...(deferred.length ? { deferred } : {}), ...(inFlight ? { inFlight } : {}) };
|
|
303
317
|
}
|
|
304
318
|
|
|
305
319
|
/** What a timed-out wait was waiting for, in words. */
|
|
@@ -360,7 +374,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
360
374
|
if (run.cancel && ["dispatch", "findings", "clear-hold", "hold", "close"].includes(effect.kind)) return { stale: true };
|
|
361
375
|
switch (effect.kind) {
|
|
362
376
|
case "decision": return performWorkflowDecision(run, effect, call);
|
|
363
|
-
case "withdraw-decision": return performWithdrawWorkflowDecision(effect);
|
|
377
|
+
case "withdraw-decision": return (await performWithdrawWorkflowDecision(effect));
|
|
364
378
|
case "cancel-children": throw new ValidationError("tree cancellation is owned by the supervisor");
|
|
365
379
|
case "child": throw new ValidationError("child effects are owned by the workflow supervisor");
|
|
366
380
|
case "close": return closeWorkflowTask(run, effect);
|
|
@@ -368,7 +382,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
368
382
|
case "started": {
|
|
369
383
|
const quietFor = await planQuietingRunNotice(run.taskId, "started");
|
|
370
384
|
if (quietFor) return { quiet: "plan-dispatched", planId: quietFor };
|
|
371
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
385
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
372
386
|
subject: `${runLabel(run)} started`,
|
|
373
387
|
body: `${runLabel(run)} started at step ${run.definition.start}. You hear back only on a spawn failure, an escalation or the result; relay_workflow_get shows the run and its agents.`,
|
|
374
388
|
idempotencyKey: key,
|
|
@@ -379,7 +393,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
379
393
|
}
|
|
380
394
|
case "landed": return { superseded: "the CI verdict owns the coordinator notice" };
|
|
381
395
|
case "pause-notice": {
|
|
382
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), { ...workflowPauseNotice(runLabel(run), run, effect), idempotencyKey: key, replyExpected: false });
|
|
396
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), { ...workflowPauseNotice(runLabel(run), run, effect), idempotencyKey: key, replyExpected: false });
|
|
383
397
|
return { messageId: message.id };
|
|
384
398
|
}
|
|
385
399
|
case "reap":
|
|
@@ -395,9 +409,9 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
395
409
|
} else if (typeof effect.payload.taskId === "number") await holdTask(run, "routes its review");
|
|
396
410
|
if (typeof effect.payload.taskId === "number") {
|
|
397
411
|
const taskId = effect.payload.taskId;
|
|
398
|
-
const builder = resume ? await liveBuilder(taskId) : undefined;
|
|
412
|
+
const builder = resume ? await liveBuilder({ id: run.id, taskId }, call) : undefined;
|
|
399
413
|
if (builder?.live) {
|
|
400
|
-
const message = notifySystemMessage(builder.id!, {
|
|
414
|
+
const message = await notifySystemMessage(builder.id!, {
|
|
401
415
|
subject: `${runLabel(run)} resumed at ${String(effect.payload.step)}`,
|
|
402
416
|
body: brief!,
|
|
403
417
|
idempotencyKey: key,
|
|
@@ -413,9 +427,9 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
413
427
|
return dispatchRole(run, effect, agents, await createStepTask(run, effect), role, brief, call);
|
|
414
428
|
}
|
|
415
429
|
case "findings": {
|
|
416
|
-
const builder = await warmBuilder(run
|
|
430
|
+
const builder = await warmBuilder(run, call);
|
|
417
431
|
const head = str(effect.payload.head);
|
|
418
|
-
const message = notifySystemMessage(builder, {
|
|
432
|
+
const message = await notifySystemMessage(builder, {
|
|
419
433
|
subject: `${runLabel(run)}: ${String(effect.payload.step)} returned NO-GO on ${short(head)}`,
|
|
420
434
|
body: [
|
|
421
435
|
`${String(effect.payload.step)} (T${String(effect.payload.taskId)}) returned NO-GO on head ${head ?? "(unknown)"}. Round ${String(effect.payload.round)} starts now.`,
|
|
@@ -438,7 +452,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
438
452
|
const head = str(effect.payload.head);
|
|
439
453
|
const ensured = await ensureWorkflowLanding(run, effect);
|
|
440
454
|
if (ensured.outcome === "REFUSED" && ensured.held === true) {
|
|
441
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
455
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
442
456
|
subject: `${runLabel(run)}: GO, awaiting your review hold release`,
|
|
443
457
|
body: `${runLabel(run)}: review returned GO on ${head ?? "an unknown head"}. Your review hold stays set; nothing is submitted for landing. After your review, clear it with relay_task_set_review_hold (taskId: ${run.taskId}, held: false, expectedHeadSha: ${head ?? "the held head"}). The run waits for the land.\n${workflowNotice(run, [])}`,
|
|
444
458
|
idempotencyKey: key,
|
|
@@ -457,7 +471,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
457
471
|
if (effect.payload.reason === "ci-red" || effect.payload.reason === "ci-late") {
|
|
458
472
|
const sha = str(effect.payload.landedSha) ?? run.landed?.sha ?? "";
|
|
459
473
|
const proof = await workflowLandVerification(run, sha);
|
|
460
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
474
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
461
475
|
subject: `${runLabel(run)} ${proof.verified ? "landed" : "land verification FAILED"}, CI on main ${effect.payload.reason === "ci-red" ? "red" : "late"}`,
|
|
462
476
|
body: workflowNotice(run, [proof.verified ? `T${run.taskId} landed as ${sha}. ${proof.line}` : proof.line, ...(effect.payload.reason === "ci-red" ? ciRedLead(runLabel(run), effect.payload) : [ciResultLine(effect.payload.ci as WorkflowCiVerdict)!])]),
|
|
463
477
|
idempotencyKey: `workflow:landed:${run.id}:${sha}:${effect.waitKey ?? effect.id}`,
|
|
@@ -471,7 +485,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
471
485
|
const lead = effect.payload.reason === "run-overdue" ? await workflowOverdueLead(run, effect.payload) : escalationLead(run, effect.payload);
|
|
472
486
|
if ((effect.payload.reason === "step-timeout" || effect.payload.reason === "run-overdue")
|
|
473
487
|
&& isRecord(effect.payload.waitingFor) && effect.payload.waitingFor.kind === "landed") lead.push(await workflowLandNotice(run));
|
|
474
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
488
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
475
489
|
subject: `${runLabel(run)} ${run.status === "failed" ? "failed" : "escalated"}`,
|
|
476
490
|
body: workflowNotice(run, [...(run.status === "failed" ? [`Script failed: ${String(effect.payload.reason)}`] : lead), `Outcome: ${effect.payload.alert ? "still waiting" : run.status === "failed" ? "failed" : "escalated"}. Landed: ${run.landed?.sha ?? "none"}. Reason: ${String(effect.payload.reason)}.`, ciResultLine(isRecord(effect.payload.ci) ? effect.payload.ci as WorkflowCiVerdict : undefined) ?? "CI on main: pending or not configured."]),
|
|
477
491
|
idempotencyKey: key,
|
|
@@ -507,7 +521,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
|
|
|
507
521
|
const nothingToLand = effect.payload.nothingToLand === true || child?.output?.nothingToLand === true;
|
|
508
522
|
const rounds = typeof effect.payload.rounds === "number" ? effect.payload.rounds : child?.output?.rounds ?? child?.round;
|
|
509
523
|
const outcome = !sha ? "finished its script steps" : !proof?.verified ? `has an unverified land receipt for ${sha}` : nothingToLand ? `had nothing to land: its reviewed head is already on main as ${sha}` : `landed as ${sha}`;
|
|
510
|
-
const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
524
|
+
const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
|
|
511
525
|
subject: `${runLabel(run)} ${proof?.verified === false ? "land verification FAILED" : "done"}${nothingToLand ? ", nothing to land" : ""}${ci?.status === "fail" ? ", CI on main red" : ""}`,
|
|
512
526
|
body: workflowNotice(run, [`${runLabel(run)} is done: T${run.taskId} ${outcome}${rounds === undefined ? "" : ` after ${rounds} round(s)`}.${proof ? ` ${proof.line}` : ""}`, ciLine ?? (child || !run.rootRunId ? "CI on main: not configured." : "CI on main: unknown.")], children.items, !!children.cursor),
|
|
513
527
|
idempotencyKey: key,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #3121 — per-step pins on a workflow run: the wire parser every surface shares, and the start check that
|
|
2
2
|
// resolves each step's launch (pin, role file and dispatch policy) through the spawn's own seam and refuses a
|
|
3
3
|
// launch that cannot run before anything spawns. Precedence (call pin > YAML step) is the kernel's workflowStepPin.
|
|
4
|
-
import { cleanString, isRecord, isSpawnProvider, SPAWN_PROVIDERS, stringValue, type SpawnProvider, type TaskDispatch } from "@humwick/sdk";
|
|
4
|
+
import { cleanString, isRecord, isSpawnProvider, SPAWN_PROVIDERS, stringValue, type SpawnProvider, type TaskDispatch, type WorkflowRunPins } from "@humwick/sdk";
|
|
5
5
|
import { findProviderCatalogModel, type ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
|
|
6
6
|
import {
|
|
7
7
|
stampWorkflowPins, WORKFLOW_PIN_FIELDS, WorkflowDefinitionError, workflowStepPin,
|
|
@@ -15,11 +15,12 @@ import { sameHostIdentity } from "../utils";
|
|
|
15
15
|
import { roleSpawnableProviders } from "./spawn-project-role";
|
|
16
16
|
import { resolveSpawnLaunch, roleLaunchDefaults } from "./spawn-launch";
|
|
17
17
|
import { ServiceAuthError } from "./errors";
|
|
18
|
+
import { PinnedHostCapacityError } from "../spawn-admission";
|
|
18
19
|
import type { ProjectRole } from "../project-roles";
|
|
19
20
|
import type { AgentCard } from "../types";
|
|
20
21
|
import type { AuthContext } from "./auth-context";
|
|
21
22
|
|
|
22
|
-
export type WorkflowPins =
|
|
23
|
+
export type WorkflowPins = WorkflowRunPins;
|
|
23
24
|
|
|
24
25
|
export const PIN_SOURCE_LABEL = { start: "start pin", resume: "resume pin", team: "team default", global: "global default", workflow: "workflow file" } as const;
|
|
25
26
|
|
|
@@ -118,12 +119,14 @@ export async function assertStepLaunches(check: StepLaunchCheck, ctx: AuthContex
|
|
|
118
119
|
const from = declared.crossProvider;
|
|
119
120
|
const avoid = from && !pin?.provider ? check.avoided?.(from) ?? launched.get(from) : undefined;
|
|
120
121
|
const defaults = roleLaunchDefaults(role);
|
|
121
|
-
const
|
|
122
|
+
const input = {
|
|
122
123
|
profile: defaults.profile, spawnCap: { callerAgentId: ctx.callerAgentId },
|
|
123
124
|
provider: (pin?.provider as SpawnProvider | undefined) ?? defaults.provider, dispatchPin: { model: pin?.model, effort: pin?.effort },
|
|
124
125
|
dispatchShape: stepTaskDispatch(check.inputTask, declared) ?? defaults.dispatchShape, orchestratorId: pin?.orchestratorId, ...(avoid ? { avoidProviders: [avoid] } : {}), projectRole: role,
|
|
125
126
|
pinSources: pin?.from,
|
|
126
|
-
}
|
|
127
|
+
};
|
|
128
|
+
const launch = await resolveSpawnLaunch(input).catch((error: Error) => {
|
|
129
|
+
if (error instanceof PinnedHostCapacityError) return resolveSpawnLaunch({ ...input, spawnCap: { ...input.spawnCap, attach: true } });
|
|
127
130
|
throw new ValidationError(`${where}: ${error.message}`);
|
|
128
131
|
});
|
|
129
132
|
const runnable = roleSpawnableProviders(pin?.orchestratorId, role, launch.catalog);
|
|
@@ -48,6 +48,7 @@ async function alertStalled(sup: Supervisor, run: WorkflowRunRecord, wait: Await
|
|
|
48
48
|
|
|
49
49
|
/** A wait past its deadline alerts the coordinator once and keeps waiting, like a stopped land (#3045 H1). */
|
|
50
50
|
async function alertTimeout(sup: Supervisor, run: WorkflowRunRecord, wait: Awaiting, taskId: number | undefined): Promise<boolean> {
|
|
51
|
+
if (wait.slot) return false;
|
|
51
52
|
const timeoutMs = wait.deadline !== undefined ? wait.deadline - wait.since : waitTimeoutMs(run.definition, run.step);
|
|
52
53
|
const now = workflowRunClock(run, supervisorNow());
|
|
53
54
|
if (wait.timedOutAt !== undefined || now < wait.since + timeoutMs) return false;
|
|
@@ -4,16 +4,16 @@ import type { WorkflowAgent, WorkflowRunRecord } from "@humwick/kernel/workflow.
|
|
|
4
4
|
import { getAgent, getTaskDetailAsync, ValidationError } from "../db";
|
|
5
5
|
import type { ProjectRole } from "../project-roles";
|
|
6
6
|
import { resolveSpawnProjectRole } from "./spawn-project-role";
|
|
7
|
-
import { liveBuilder, stepAgentProvider } from "./workflow-effects";
|
|
7
|
+
import { liveBuilder, stepAgentProvider, type WorkflowOp } from "./workflow-effects";
|
|
8
8
|
import { assertStepLaunches, unmetPinFields } from "./workflow-pins";
|
|
9
9
|
import type { AuthContext } from "./auth-context";
|
|
10
10
|
|
|
11
11
|
/** A resume's new pins: a live warm builder must already meet its step's pin, since the run hands it findings
|
|
12
12
|
* instead of spawning and replacing it would take its workspace; and each named step's launch must resolve and run. */
|
|
13
|
-
export async function assertResumePins(run: WorkflowRunRecord, pins: Record<string, WorkflowCallPin>, task: NonNullable<Awaited<ReturnType<typeof getTaskDetailAsync>>>, ctx: AuthContext, agents: readonly WorkflowAgent[]): Promise<void> {
|
|
13
|
+
export async function assertResumePins(run: WorkflowRunRecord, pins: Record<string, WorkflowCallPin>, task: NonNullable<Awaited<ReturnType<typeof getTaskDetailAsync>>>, ctx: AuthContext, agents: readonly WorkflowAgent[], call: WorkflowOp): Promise<void> {
|
|
14
14
|
const merged = { ...run.pins, ...pins };
|
|
15
15
|
const steps = Object.keys(run.definition.steps).filter((step) => pins[step]);
|
|
16
|
-
const builder = await liveBuilder(run
|
|
16
|
+
const builder = await liveBuilder(run, call);
|
|
17
17
|
const agent = builder.live ? getAgent(builder.id!) : null;
|
|
18
18
|
for (const step of steps.filter((name) => run.definition.steps[name]!.task === "input")) {
|
|
19
19
|
const pin = workflowStepPin(run.definition, merged, step, run.routingDefaults);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StepRoutingDefaults, TaskDetail } from "@humwick/sdk";
|
|
2
|
+
import type { WorkflowRoutingDefaults } from "@humwick/kernel/workflow-definition";
|
|
3
|
+
import { getAgent, getTeam } from "../db";
|
|
4
|
+
import { taskOwnerRoleTeamId } from "../db/task-owner";
|
|
5
|
+
|
|
6
|
+
/** #3829 — defaults follow the Task's durable owner, then its nearest configured ancestor. */
|
|
7
|
+
export function resolveWorkflowRoutingDefaults(defaults: StepRoutingDefaults | undefined, task: Pick<TaskDetail, "owner" | "teamId" | "createdBy">, callerAgentId?: string): WorkflowRoutingDefaults | undefined {
|
|
8
|
+
if (!defaults) return undefined;
|
|
9
|
+
const coordinatorId = callerAgentId ?? task.createdBy;
|
|
10
|
+
const teamId = taskOwnerRoleTeamId(task.owner) ?? task.teamId ?? (coordinatorId ? getAgent(coordinatorId)?.teamId : undefined);
|
|
11
|
+
const visited = new Set<string>();
|
|
12
|
+
for (let current = teamId; current && !visited.has(current); current = getTeam(current)?.parentTeamId) {
|
|
13
|
+
visited.add(current);
|
|
14
|
+
const team = defaults.teams?.[current];
|
|
15
|
+
if (team) return { global: defaults.global, team, teamId: current };
|
|
16
|
+
}
|
|
17
|
+
return { global: defaults.global, ...(teamId ? { teamId } : {}) };
|
|
18
|
+
}
|
|
@@ -6,9 +6,9 @@ import { workflowText } from "./workflow-notice";
|
|
|
6
6
|
|
|
7
7
|
type Awaiting = NonNullable<WorkflowRunRecord["awaiting"]>;
|
|
8
8
|
|
|
9
|
-
function awaitingView({ kind, taskId, sha, deadline, timedOutAt }: Awaiting): NonNullable<WorkflowRunView["awaiting"]> {
|
|
9
|
+
function awaitingView({ kind, taskId, sha, deadline, timedOutAt, slot }: Awaiting): NonNullable<WorkflowRunView["awaiting"]> {
|
|
10
10
|
return {
|
|
11
|
-
kind, ...(taskId !== undefined ? { taskId } : {}), ...(sha ? { sha } : {}), ...(deadline !== undefined ? { deadline } : {}), ...(timedOutAt !== undefined ? { timedOutAt } : {}),
|
|
11
|
+
kind, ...(taskId !== undefined ? { taskId } : {}), ...(sha ? { sha } : {}), ...(deadline !== undefined ? { deadline } : {}), ...(timedOutAt !== undefined ? { timedOutAt } : {}), ...(slot ? { slot } : {}),
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ export function runView(run: WorkflowRunRecord): WorkflowRunView {
|
|
|
23
23
|
/** The declared steps in order; `land` follows the last. */
|
|
24
24
|
steps: Object.entries(run.definition.steps).map(([name, step]) => {
|
|
25
25
|
const pin = workflowStepPin(run.definition, run.pins, name, run.routingDefaults);
|
|
26
|
-
return { name, role: step.role, ...(step.gate ? { gate: step.gate } : {}), ...(step.crossProvider ? { crossProvider: step.crossProvider } : {}), ...(pin ? { pin } : {}) };
|
|
26
|
+
return { name, role: step.role, started: run.entered.includes(name), ...(step.gate ? { gate: step.gate } : {}), ...(step.crossProvider ? { crossProvider: step.crossProvider } : {}), ...(pin ? { pin } : {}) };
|
|
27
27
|
}),
|
|
28
28
|
...(run.definition.ciMain ? { ciMain: { ...(run.definition.ciMain.workflow ? { workflow: run.definition.ciMain.workflow } : {}) } } : {}),
|
|
29
29
|
ref: run.ref,
|
|
@@ -43,6 +43,7 @@ export function runView(run: WorkflowRunRecord): WorkflowRunView {
|
|
|
43
43
|
...(run.pause ? { paused: run.pause } : {}),
|
|
44
44
|
...(run.reason ? { reason: run.reason } : {}),
|
|
45
45
|
...(run.resumes?.length ? { resumes: run.resumes } : {}),
|
|
46
|
+
...(run.pinChanges?.length ? { pinChanges: run.pinChanges } : {}),
|
|
46
47
|
...(run.output ? { output: run.output } : {}),
|
|
47
48
|
createdAt: run.createdAt,
|
|
48
49
|
updatedAt: run.updatedAt,
|
|
@@ -90,7 +90,7 @@ export function installWorkflowHost(options: { confirmStops?: boolean } = {}): (
|
|
|
90
90
|
export async function initWorkflowDb(): Promise<void> {
|
|
91
91
|
await stopWorkflowServingForTests();
|
|
92
92
|
await settleLandLifecycleDrives();
|
|
93
|
-
flushTaskStatusPulses();
|
|
93
|
+
await flushTaskStatusPulses();
|
|
94
94
|
initDb(":memory:");
|
|
95
95
|
}
|
|
96
96
|
|