@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
|
@@ -18,7 +18,7 @@ import { launchWorkflowRun, prepareWorkflowRun } from "./workflow-runs";
|
|
|
18
18
|
import { ensureSupervisor, invoke, type Supervisor } from "./workflow-supervisor";
|
|
19
19
|
import { stampPins } from "./workflow-pins";
|
|
20
20
|
import { emitPlan } from "./plan-graph";
|
|
21
|
-
import { dependencyStage, memberReady, occupiedSlots, planMembers, prerequisiteState, slotFromRun, taskSucceeded, unsupportedPlanShape } from "./plan-dispatch-policy";
|
|
21
|
+
import { dependencyStage, memberReady, occupiedSlots, planMembers, planNodeSettled, prerequisiteState, slotFromRun, taskSucceeded, unsupportedPlanShape } from "./plan-dispatch-policy";
|
|
22
22
|
import type { AuthContext } from "./auth-context";
|
|
23
23
|
import { planSlotOccupies, type Plan, type PlanDispatch, type PlanDispatchSlot, type TaskDetail } from "../types";
|
|
24
24
|
|
|
@@ -44,9 +44,13 @@ export async function startPlanDispatch(input: { planId?: unknown; expectedVersi
|
|
|
44
44
|
if (plan.archived) throw new ValidationError(`plan ${plan.id} is archived`);
|
|
45
45
|
const problems = unsupportedPlanShape(plan);
|
|
46
46
|
if (problems.length) throw new ValidationError(problems.join("; "));
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
47
|
+
const nodes = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
48
|
+
const allMembers = planMembers(plan);
|
|
49
|
+
const allTasks = await Promise.all(allMembers.map((member) => getTaskDetailAsync(member.taskId)));
|
|
50
|
+
const unfinished = allMembers.flatMap((member, index) => planNodeSettled(nodes.get(member.nodeId)!, allTasks[index]!) ? [] : [{ member, task: allTasks[index]! }]);
|
|
51
|
+
const members = unfinished.map(({ member }) => member);
|
|
52
|
+
const tasks = unfinished.map(({ task }) => task);
|
|
53
|
+
const memberIds = new Set(plan.nodes.map((node) => Number(node.refs?.taskId)));
|
|
50
54
|
for (const member of tasks) {
|
|
51
55
|
for (const id of member?.dependsOn ?? []) {
|
|
52
56
|
if (memberIds.has(id)) continue;
|
|
@@ -55,7 +59,7 @@ export async function startPlanDispatch(input: { planId?: unknown; expectedVersi
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
const repos = new Set(tasks.map((task) => task?.repo).filter(Boolean));
|
|
58
|
-
if (repos.size !== 1) throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: slice 1a dispatches one repo; the plan's Tasks name ${repos.size ? [...repos].join(", ") : "no repo"}`);
|
|
62
|
+
if (tasks.length && repos.size !== 1) throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: slice 1a dispatches one repo; the plan's Tasks name ${repos.size ? [...repos].join(", ") : "no repo"}`);
|
|
59
63
|
let cwd: string | undefined;
|
|
60
64
|
for (const [index, task] of tasks.entries()) {
|
|
61
65
|
if (!task || task.status === "canceled") throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: T${members[index]!.taskId} is missing or canceled`);
|
|
@@ -115,6 +119,8 @@ function ensureListening(): void {
|
|
|
115
119
|
if (event.type === "task.stage" || event.type === "task.status" || event.type === "task.updated") {
|
|
116
120
|
const plans = watched.get(Number(event.data.id ?? event.subject));
|
|
117
121
|
if (plans?.size) void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile(plans));
|
|
122
|
+
} else if (event.type === "plan.changed" && event.subject && dispatching.has(event.subject)) {
|
|
123
|
+
void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile([event.subject!]));
|
|
118
124
|
} else if (dispatching.size && (event.type === "workflow.changed" || event.type === "branch.landed" || event.type === "ci.status")) {
|
|
119
125
|
void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile());
|
|
120
126
|
}
|
|
@@ -184,9 +190,22 @@ async function reconcilePlan(planId: string): Promise<void> {
|
|
|
184
190
|
changed ||= next !== slot;
|
|
185
191
|
slots.set(slot.taskId, next);
|
|
186
192
|
}
|
|
193
|
+
const nodes = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
194
|
+
const nodesForTask = new Map(plan.nodes.filter((node) => node.refs?.taskId).map((node) => [Number(node.refs!.taskId), node]));
|
|
195
|
+
const prerequisites = new Map<string, string[]>();
|
|
196
|
+
for (const edge of plan.edges) {
|
|
197
|
+
if (!prerequisites.has(edge.to)) prerequisites.set(edge.to, []);
|
|
198
|
+
prerequisites.get(edge.to)!.push(edge.from);
|
|
199
|
+
}
|
|
200
|
+
const settledNode = async (nodeId: string, requiredStage?: Parameters<typeof taskSucceeded>[2]) => {
|
|
201
|
+
const node = nodes.get(nodeId)!;
|
|
202
|
+
const id = Number(node.refs?.taskId);
|
|
203
|
+
return planNodeSettled(node, Number.isSafeInteger(id) && id > 0 ? await task(id) : null, slots.get(id), requiredStage);
|
|
204
|
+
};
|
|
187
205
|
if (dispatch.state === "active") {
|
|
188
206
|
for (const { nodeId, taskId } of members) {
|
|
189
207
|
const slot = slots.get(taskId);
|
|
208
|
+
if (!slot && await settledNode(nodeId)) continue;
|
|
190
209
|
if (slot && (slot.status !== "launch-failed" && slot.status !== "failed" && slot.status !== "cancelled")) {
|
|
191
210
|
const dispatched = planSlotOccupies(slot.status) ? await task(taskId) : null;
|
|
192
211
|
if (dispatched) await escalateRegressions(plan, nodeId, dispatched, memberTaskIds, task);
|
|
@@ -207,14 +226,16 @@ async function reconcilePlan(planId: string): Promise<void> {
|
|
|
207
226
|
for (const id of member.dependsOn) {
|
|
208
227
|
const prerequisite = await task(id);
|
|
209
228
|
const required = dependencyStage(member, id);
|
|
210
|
-
|
|
229
|
+
const node = nodesForTask.get(id);
|
|
230
|
+
if (node ? await settledNode(node.id, required) : taskSucceeded(prerequisite, slots.get(id), required)) succeeded.add(id);
|
|
211
231
|
else {
|
|
212
232
|
const state = prerequisiteState(prerequisite, required);
|
|
213
233
|
if (state.state === "unreachable") {
|
|
214
|
-
notifyPlan(plan, `T${taskId} prerequisite unreachable`, `Plan ${plan.id}, node ${nodeId} (T${taskId}) waits on T${id}, which is ${state.reason}. Replace the edge with relay_task_remove_dependency then relay_task_add_dependency, cancel the node, or wait for T${id} to be reopened. The dependent keeps waiting.`, `plan:${plan.id}:prereq:T${id}:${state.reason}`, { kind: "plan.prerequisite-unreachable", planId, nodeId, taskId, prerequisiteTaskId: id, reason: state.reason, nextActions: ["replace-edge", "cancel-node", "wait-for-reopen"] });
|
|
234
|
+
await notifyPlan(plan, `T${taskId} prerequisite unreachable`, `Plan ${plan.id}, node ${nodeId} (T${taskId}) waits on T${id}, which is ${state.reason}. Replace the edge with relay_task_remove_dependency then relay_task_add_dependency, cancel the node, or wait for T${id} to be reopened. The dependent keeps waiting.`, `plan:${plan.id}:prereq:T${id}:${state.reason}`, { kind: "plan.prerequisite-unreachable", planId, nodeId, taskId, prerequisiteTaskId: id, reason: state.reason, nextActions: ["replace-edge", "cancel-node", "wait-for-reopen"] });
|
|
215
235
|
}
|
|
216
236
|
}
|
|
217
237
|
}
|
|
238
|
+
if (!(await Promise.all((prerequisites.get(nodeId) ?? []).map((id) => settledNode(id)))).every(Boolean)) continue;
|
|
218
239
|
if (!memberReady(member, undefined, (id) => succeeded.has(id))) continue;
|
|
219
240
|
if (occupiedSlots(slots.values()) >= dispatch.concurrency) continue;
|
|
220
241
|
const claimed = await write<{ slot?: PlanDispatchSlot; reserved: boolean }>("planDispatch.slot.reserve", { planId, taskId, nodeId });
|
|
@@ -223,7 +244,7 @@ async function reconcilePlan(planId: string): Promise<void> {
|
|
|
223
244
|
if (fault === "after-reserve") { halted = true; throw new Error("plan dispatch test fault: the process stopped after reserving a slot"); }
|
|
224
245
|
slots.set(taskId, await launch(plan, claimed.slot));
|
|
225
246
|
}
|
|
226
|
-
const settled = await Promise.all(
|
|
247
|
+
const settled = await Promise.all(plan.nodes.map((node) => settledNode(node.id)));
|
|
227
248
|
if (settled.every(Boolean)) {
|
|
228
249
|
await openAudit(plan, dispatch, members.map(({ taskId }) => tasks.get(taskId)).find(Boolean) ?? null);
|
|
229
250
|
changed = true;
|
|
@@ -245,7 +266,7 @@ async function escalateRegressions(plan: Plan, nodeId: string, member: TaskDetai
|
|
|
245
266
|
const state = prerequisiteState(prerequisite, required);
|
|
246
267
|
if (state.state === "satisfied") continue;
|
|
247
268
|
const now = prerequisite ? `status ${prerequisite.status}, stage ${prerequisite.deliverableStage ?? "none"}${state.state === "unreachable" ? `, ${state.reason}` : ""}` : "missing";
|
|
248
|
-
notifyPlan(plan, `T${member.id} prerequisite regressed`, `Plan ${plan.id}, node ${nodeId} (T${member.id}) was dispatched on T${id} reaching ${required}, which no longer holds (${now}). The run continues; check whether its work is still valid, or cancel the node.`, `plan:${plan.id}:prereq:T${id}:regressed`, { kind: "plan.prerequisite-unreachable", planId: plan.id, nodeId, taskId: member.id, prerequisiteTaskId: id, reason: "regressed", requiredStage: required, nextActions: ["check-run", "cancel-node", "wait-for-recovery"] });
|
|
269
|
+
await notifyPlan(plan, `T${member.id} prerequisite regressed`, `Plan ${plan.id}, node ${nodeId} (T${member.id}) was dispatched on T${id} reaching ${required}, which no longer holds (${now}). The run continues; check whether its work is still valid, or cancel the node.`, `plan:${plan.id}:prereq:T${id}:regressed`, { kind: "plan.prerequisite-unreachable", planId: plan.id, nodeId, taskId: member.id, prerequisiteTaskId: id, reason: "regressed", requiredStage: required, nextActions: ["check-run", "cancel-node", "wait-for-recovery"] });
|
|
249
270
|
}
|
|
250
271
|
}
|
|
251
272
|
|
|
@@ -279,7 +300,7 @@ async function launch(plan: Plan, slot: PlanDispatchSlot): Promise<PlanDispatchS
|
|
|
279
300
|
const started = await launchWorkflowRun(prepared, { ...(inputs ? { inputs } : {}), ...(pins ? { pins } : {}), requestKey: `plan:${plan.id}:T${task.id}:a${slot.attempt}` }, AUTOPILOT_CTX);
|
|
280
301
|
return (await write<PlanDispatchSlot | undefined>("planDispatch.slot.update", { taskId: slot.taskId, attempt: slot.attempt, runId: String(started.runId), status: String(started.status) })) ?? slot;
|
|
281
302
|
} catch (error) {
|
|
282
|
-
notifyPlan(plan, `T${slot.taskId} could not start`, `Plan ${plan.id} could not start T${slot.taskId}'s workflow: ${errMessage(error)}. Its slot is free and its dependents wait. Fix the cause, then start its workflow by hand (relay_workflow_start); the plan adopts that run.`, `plan:${plan.id}:launch-failed:T${slot.taskId}:a${slot.attempt}`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: slot.taskId });
|
|
303
|
+
await notifyPlan(plan, `T${slot.taskId} could not start`, `Plan ${plan.id} could not start T${slot.taskId}'s workflow: ${errMessage(error)}. Its slot is free and its dependents wait. Fix the cause, then start its workflow by hand (relay_workflow_start); the plan adopts that run.`, `plan:${plan.id}:launch-failed:T${slot.taskId}:a${slot.attempt}`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: slot.taskId });
|
|
283
304
|
return (await write<PlanDispatchSlot | undefined>("planDispatch.slot.update", { taskId: slot.taskId, attempt: slot.attempt, status: "launch-failed", error: errMessage(error).slice(0, 2000) })) ?? slot;
|
|
284
305
|
}
|
|
285
306
|
}
|
|
@@ -310,7 +331,7 @@ async function driveAudit(sup: Supervisor, plan: Plan): Promise<boolean> {
|
|
|
310
331
|
const started = await launchWorkflowRun(prepared, { requestKey: `plan:${plan.id}:audit` }, AUTOPILOT_CTX);
|
|
311
332
|
await write("planDispatch.audit.update", { planId: plan.id, runId: String(started.runId), status: String(started.status) });
|
|
312
333
|
} catch (error) {
|
|
313
|
-
notifyPlan(plan, "audit could not start", `Plan ${plan.id}: every node landed, but its conformance audit (T${audit.taskId}) could not start: ${errMessage(error)}. Start ${PLAN_CONFORMANCE_WORKFLOW} on T${audit.taskId} by hand, or audit and record relay_plan_conformance yourself.`, `plan:${plan.id}:audit-failed`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: audit.taskId });
|
|
334
|
+
await notifyPlan(plan, "audit could not start", `Plan ${plan.id}: every node landed, but its conformance audit (T${audit.taskId}) could not start: ${errMessage(error)}. Start ${PLAN_CONFORMANCE_WORKFLOW} on T${audit.taskId} by hand, or audit and record relay_plan_conformance yourself.`, `plan:${plan.id}:audit-failed`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: audit.taskId });
|
|
314
335
|
await write("planDispatch.audit.update", { planId: plan.id, status: "launch-failed" });
|
|
315
336
|
}
|
|
316
337
|
return true;
|
|
@@ -324,14 +345,14 @@ async function driveAudit(sup: Supervisor, plan: Plan): Promise<boolean> {
|
|
|
324
345
|
dispatching.delete(plan.id);
|
|
325
346
|
watchPlan(plan.id, new Set());
|
|
326
347
|
const slots = plan.dispatch!.slots.map((slot) => `${slot.nodeId} T${slot.taskId} ${slot.runId ?? "no run"} ${slot.landedSha?.slice(0, 12) ?? ""}`.trim());
|
|
327
|
-
notifyPlan(plan, "done", [`Plan ${plan.id}${plan.title ? ` "${plan.title}"` : ""} is done: conformance passed on ${audit.mainSha} at version ${plan.version}.`, ...slots, `Audit T${audit.taskId} run ${audit.runId}.`].join("\n"), `plan:${plan.id}:result:v${plan.version}`, { kind: "plan.result", planId: plan.id, planVersion: plan.version, mainSha: audit.mainSha });
|
|
348
|
+
await notifyPlan(plan, "done", [`Plan ${plan.id}${plan.title ? ` "${plan.title}"` : ""} is done: conformance passed on ${audit.mainSha} at version ${plan.version}.`, ...slots, `Audit T${audit.taskId} run ${audit.runId}.`].join("\n"), `plan:${plan.id}:result:v${plan.version}`, { kind: "plan.result", planId: plan.id, planVersion: plan.version, mainSha: audit.mainSha });
|
|
328
349
|
}
|
|
329
350
|
return true;
|
|
330
351
|
}
|
|
331
352
|
|
|
332
|
-
function notifyPlan(plan: Plan, subject: string, body: string, idempotencyKey: string, payload: Json): void {
|
|
353
|
+
async function notifyPlan(plan: Plan, subject: string, body: string, idempotencyKey: string, payload: Json): Promise<void> {
|
|
333
354
|
const to = planNoticeTarget(plan);
|
|
334
|
-
if (to) notifySystemMessage(to, { subject: `Plan ${plan.title ?? plan.id}: ${subject}`, body, idempotencyKey, replyExpected: false, payload });
|
|
355
|
+
if (to) await notifySystemMessage(to, { subject: `Plan ${plan.title ?? plan.id}: ${subject}`, body, idempotencyKey, replyExpected: false, payload });
|
|
335
356
|
}
|
|
336
357
|
|
|
337
358
|
// --- serving ---------------------------------------------------------------------------------------
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { resolveIssueRefFromStringAsync } from "../db/issue-refs-async";
|
|
2
|
+
import { removeAssociationAsync, associateIssueAsync } from "../db/runtime-tail-async";
|
|
3
|
+
import { deletePlanAsync, createPlanRowAsync } from "../db/plans-async";
|
|
1
4
|
import { hasScope, isRecord } from "@humwick/sdk";
|
|
2
|
-
import {
|
|
5
|
+
import { sendMessageAsync, addTaskDependency, getAgent, getEntry, getPlan, getPlanAsync, getPlanByTeam, getPlanByThread, getTaskDetail, listPlansWithBoundOwner, planThreadExists, removeTaskDependency, replacePlanNodeScoped, sendMessageWithResultAsync, setPlanArchivedAsync, ValidationError } from "../db";
|
|
3
6
|
// The shared #2520 patch-time source derivation lives beside the writer op body it mirrors;
|
|
4
7
|
// plan-graph op bodies are imported directly (the barrel only re-exports plan-graph-async).
|
|
5
8
|
import { applyPatchStatusSourceDerivation, bindPlanNodeBody } from "../db/plan-graph";
|
|
6
9
|
import { writeTransaction } from "../db/connection";
|
|
7
|
-
import {
|
|
8
|
-
|
|
10
|
+
import { planNodeIssueEntityId } from "../db/issue-associations";
|
|
11
|
+
|
|
9
12
|
import { emitRelayEvent } from "../events";
|
|
10
13
|
import { cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
11
14
|
import { resolveActorIssueRepo } from "../agent-issue-repo";
|
|
@@ -84,67 +87,6 @@ function cleanOwnerRef(value: unknown): PlanOwnerRef | undefined {
|
|
|
84
87
|
return Object.keys(out).length ? out : undefined;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
// #220: a bare `#NNN` issueId ref needs a default repo to resolve against — issue-refs.ts
|
|
88
|
-
// throws without one, contrary to its own error text advertising the bare form as accepted.
|
|
89
|
-
// Resolve it the same way #533 resolves chat `#NNN` linkify: the CALLING agent's own project
|
|
90
|
-
// (cwd → project → issueRepo), falling back to the single global env default. This scopes the
|
|
91
|
-
// default to the actor's actual repo rather than guessing from teamId — agents have no
|
|
92
|
-
// team→project FK; cwd is the only durable link (see agent-issue-repo.ts).
|
|
93
|
-
// Shared with the Task issue-link path (#2214) via resolveActorIssueRepo.
|
|
94
|
-
function cleanRefs(value: unknown, actor: string): PlanNode["refs"] | undefined {
|
|
95
|
-
if (value === undefined || value === null) return undefined;
|
|
96
|
-
const ref = cleanRecordRef(value, "refs");
|
|
97
|
-
if (!ref) return undefined;
|
|
98
|
-
const out: PlanNode["refs"] = {};
|
|
99
|
-
const issueId = cleanString(ref.issueId, "refs.issueId", { max: 1000 });
|
|
100
|
-
if (issueId) {
|
|
101
|
-
out.issueId = resolveIssueRefFromString({ ref: issueId, defaultRepo: resolveActorIssueRepo(actor) }).id;
|
|
102
|
-
}
|
|
103
|
-
// #836: refs.taskId is the live binding seam to a durable Task. Validate it resolves to a
|
|
104
|
-
// deliverable Task; store the canonical numeric id as a string. Once bound, the node's
|
|
105
|
-
// label/scopeBlurb/status PROJECT from the Task (see projectPlanOverTasks) — no double-keeping.
|
|
106
|
-
const taskIdRaw = ref.taskId;
|
|
107
|
-
const taskIdStr = typeof taskIdRaw === "number" ? String(taskIdRaw) : cleanString(taskIdRaw, "refs.taskId", { max: 240 });
|
|
108
|
-
if (taskIdStr) {
|
|
109
|
-
const taskId = parseTaskId(taskIdStr);
|
|
110
|
-
if (taskId === undefined) throw new ValidationError("refs.taskId must be a positive task id");
|
|
111
|
-
if (!getTaskDetail(taskId)) throw new ValidationError(`refs.taskId ${taskId} is not a deliverable task`);
|
|
112
|
-
out.taskId = String(taskId);
|
|
113
|
-
}
|
|
114
|
-
// agentId/landingRef remain populated by live binding in later slices (#838); reject here.
|
|
115
|
-
const unsupported = ["agentId", "landingRef"].filter((key) => cleanString(ref[key], `refs.${key}`, { max: 240 }));
|
|
116
|
-
if (unsupported.length > 0) throw new ValidationError("agentId/landingRef refs are populated by live binding in a later slice");
|
|
117
|
-
return Object.keys(out).length ? out : undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
121
|
-
if (!node.refs?.issueId) return;
|
|
122
|
-
associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function removePlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
126
|
-
if (!node.refs?.issueId) return;
|
|
127
|
-
removeAssociation(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function syncChangedPlanNodeIssueAssociation(planId: string, before: PlanNode | undefined, after: PlanNode): void {
|
|
131
|
-
if (before?.refs?.issueId && before.refs.issueId !== after.refs?.issueId) {
|
|
132
|
-
removeAssociation(before.refs.issueId, "plan-node", planNodeIssueEntityId(planId, before.id));
|
|
133
|
-
}
|
|
134
|
-
if (after.refs?.issueId) syncPlanNodeIssueAssociation(planId, after);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function cleanBlackboardRefs(value: unknown, planTeamId?: string): string[] | undefined {
|
|
138
|
-
const refs = cleanStringArray(value, "node.blackboardRefs", { itemMax: 120, maxItems: 100 });
|
|
139
|
-
if (!refs?.length) return refs;
|
|
140
|
-
if (!planTeamId) throw new ValidationError("plan has no team; cannot reference blackboard entries");
|
|
141
|
-
for (const ref of refs) {
|
|
142
|
-
const entry = getEntry(ref);
|
|
143
|
-
if (!entry || entry.teamId !== planTeamId) throw new ValidationError(`blackboard entry not found for plan team: ${ref}`);
|
|
144
|
-
}
|
|
145
|
-
return refs;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
90
|
function cleanStatusSource(value: unknown): PlanNode["statusSource"] {
|
|
149
91
|
const statusSource = optionalEnum(value, "statusSource", ["manual", "bound"] as const, "manual");
|
|
150
92
|
return statusSource;
|
|
@@ -181,27 +123,6 @@ function applyBoundStatus(node: PlanNode): PlanNode {
|
|
|
181
123
|
return { ...node, status: boundStatusForAgent(getAgent(agentId)) ?? "pending" };
|
|
182
124
|
}
|
|
183
125
|
|
|
184
|
-
function cleanNode(input: PlanNodeInput, actor: string, planTeamId?: string): PlanNode {
|
|
185
|
-
if (!isRecord(input)) throw new ValidationError("node must be an object");
|
|
186
|
-
const scopeBlurb = cleanString(input.scopeBlurb, "node.scopeBlurb", { max: 2000 });
|
|
187
|
-
const ownerRef = cleanOwnerRef(input.ownerRef);
|
|
188
|
-
const lane = cleanString(input.lane, "node.lane", { max: 120 });
|
|
189
|
-
const refs = cleanRefs(input.refs, actor);
|
|
190
|
-
const blackboardRefs = cleanBlackboardRefs(input.blackboardRefs, planTeamId);
|
|
191
|
-
const statusSource = deriveStatusSource(input.statusSource, input.status, Boolean(refs?.taskId || refs?.issueId));
|
|
192
|
-
return applyBoundStatus({
|
|
193
|
-
id: cleanString(input.id, "node.id", { required: true, max: 120 })!,
|
|
194
|
-
label: cleanString(input.label, "node.label", { required: true, max: PLAN_NODE_LABEL_MAX })!,
|
|
195
|
-
...(scopeBlurb ? { scopeBlurb } : {}),
|
|
196
|
-
...(ownerRef ? { ownerRef } : {}),
|
|
197
|
-
...(lane ? { lane } : {}),
|
|
198
|
-
status: optionalEnum(input.status, "node.status", PLAN_STATUSES, "pending") as PlanStatus,
|
|
199
|
-
statusSource,
|
|
200
|
-
...(refs ? { refs } : {}),
|
|
201
|
-
...(blackboardRefs ? { blackboardRefs } : {}),
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
126
|
// The aggregate writer owns persisted validation (issue/task/blackboard existence and bound
|
|
206
127
|
// presence). This companion only validates shape, producing clone-safe command data.
|
|
207
128
|
function cleanNodeForAggregate(input: PlanNodeInput): PlanNode {
|
|
@@ -247,14 +168,6 @@ function cleanPatchForAggregate(patch: PlanNodePatch): Partial<PlanNode> {
|
|
|
247
168
|
return out;
|
|
248
169
|
}
|
|
249
170
|
|
|
250
|
-
function cleanNodes(value: unknown, actor: string, planTeamId?: string): PlanNode[] {
|
|
251
|
-
if (value === undefined || value === null) return [];
|
|
252
|
-
if (!Array.isArray(value)) throw new ValidationError("nodes must be an array");
|
|
253
|
-
const nodes = value.map((item) => cleanNode(item as PlanNodeInput, actor, planTeamId));
|
|
254
|
-
assertUnique(nodes.map((node) => node.id), "node id");
|
|
255
|
-
return nodes;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
171
|
function cleanEdge(input: unknown): PlanEdge {
|
|
259
172
|
if (!isRecord(input)) throw new ValidationError("edge must be an object");
|
|
260
173
|
return {
|
|
@@ -340,19 +253,19 @@ export function emitPlan(type: "plan.created" | "plan.changed" | "plan.deleted",
|
|
|
340
253
|
|
|
341
254
|
// #1035: a plan anchors to a messages.thread_id, but a fresh coordinator has no thread yet —
|
|
342
255
|
// forcing it to send a throwaway message just to mint one. When no threadId is supplied, mint the
|
|
343
|
-
// thread server-side by self-threading a lightweight system message (the same
|
|
256
|
+
// thread server-side by self-threading a lightweight system message (the same sendMessageAsync path the
|
|
344
257
|
// caller would have used). Sent AS the actor when that agent is registered, else the reserved
|
|
345
258
|
// `system` sink, so the mint always passes the sender-registration gate.
|
|
346
|
-
function mintPlanThread(ctx: AuthContext, channel: string | undefined): number {
|
|
259
|
+
async function mintPlanThread(ctx: AuthContext, channel: string | undefined): Promise<number> {
|
|
347
260
|
ensureCanMintThread(ctx);
|
|
348
261
|
const actor = actorId(ctx);
|
|
349
262
|
const sender = getAgent(actor) ? actor : "system";
|
|
350
|
-
const root =
|
|
263
|
+
const root = (await sendMessageAsync({
|
|
351
264
|
from: sender,
|
|
352
265
|
to: "system",
|
|
353
266
|
body: "Plan thread root",
|
|
354
267
|
...(channel ? { channel } : {}),
|
|
355
|
-
});
|
|
268
|
+
}));
|
|
356
269
|
return root.threadId ?? root.id;
|
|
357
270
|
}
|
|
358
271
|
|
|
@@ -364,7 +277,7 @@ async function mintPlanThreadAsync(ctx: AuthContext, channel: string | undefined
|
|
|
364
277
|
return root.threadId ?? root.id;
|
|
365
278
|
}
|
|
366
279
|
|
|
367
|
-
// #1043: the mint inserts a real message row through the db `
|
|
280
|
+
// #1043: the mint inserts a real message row through the db `sendMessageAsync` path, which normally
|
|
368
281
|
// lives behind the `message:send` scope. `plans:write` alone must not be a side-door around it, so
|
|
369
282
|
// gate the mint on `message:send`. Internal `system` principals (empty-scope trusted callers) and
|
|
370
283
|
// full-reach `*` tokens pass through untouched — `hasScope` short-circuits `*`.
|
|
@@ -375,78 +288,16 @@ function ensureCanMintThread(ctx: AuthContext): void {
|
|
|
375
288
|
}
|
|
376
289
|
}
|
|
377
290
|
|
|
378
|
-
export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
|
|
379
|
-
|
|
380
|
-
const actor = actorId(ctx);
|
|
381
|
-
const explicitTeamId = cleanString(input.teamId, "teamId", { max: 120 });
|
|
382
|
-
const teamId = explicitTeamId ?? (input.teamId === undefined || input.teamId === null ? getAgent(actor)?.teamId : undefined);
|
|
383
|
-
const nodes = cleanNodes(input.nodes, actor, teamId);
|
|
384
|
-
const edges = cleanEdges(input.edges);
|
|
385
|
-
validateEdges(nodes, edges);
|
|
386
|
-
const title = cleanString(input.title, "title", { max: 240 });
|
|
387
|
-
const objective = cleanString(input.objective, "objective", { max: 4000 });
|
|
388
|
-
const acceptance = cleanPlanAcceptance(input.acceptance);
|
|
389
|
-
// Resolve the thread only AFTER the payload validates (#1043): auto-minting first left an orphan
|
|
390
|
-
// 'Plan thread root' row behind on every rejected create.
|
|
391
|
-
const threadId = input.threadId === undefined || input.threadId === null
|
|
392
|
-
? mintPlanThread(ctx, channel)
|
|
393
|
-
: cleanThreadId(input.threadId);
|
|
394
|
-
if (!planThreadExists(threadId)) throw new ValidationError(`thread ${threadId} not found`);
|
|
395
|
-
const plan = createPlanRow({
|
|
396
|
-
threadId,
|
|
397
|
-
teamId,
|
|
398
|
-
channel,
|
|
399
|
-
title,
|
|
400
|
-
objective,
|
|
401
|
-
nodes,
|
|
402
|
-
edges,
|
|
403
|
-
acceptance,
|
|
404
|
-
createdBy: actor,
|
|
405
|
-
});
|
|
406
|
-
for (const node of plan.nodes) syncPlanNodeIssueAssociation(plan.id, node);
|
|
407
|
-
emitPlan("plan.created", plan, actor);
|
|
408
|
-
return plan;
|
|
291
|
+
export async function createPlan(input: PlanCreateInput, ctx: AuthContext): Promise<Plan> {
|
|
292
|
+
return createPlanAsync(input, ctx);
|
|
409
293
|
}
|
|
410
294
|
|
|
411
|
-
export function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthContext): Plan {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (plan.nodes.some((item) => item.id === node.id)) throw new ValidationError(`node ${node.id} already exists`);
|
|
418
|
-
return { ...plan, nodes: [...plan.nodes, node] };
|
|
419
|
-
}, actor);
|
|
420
|
-
syncPlanNodeIssueAssociation(updated.id, node); emitPlan("plan.changed", updated, actor);
|
|
421
|
-
return updated;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePatch, ctx: AuthContext): Plan {
|
|
425
|
-
const actor = actorId(ctx);
|
|
426
|
-
// S6 #2079: re-apply the patch onto the latest node map (by nodeId); a distinct-node edit survives.
|
|
427
|
-
let existing!: PlanNode; let next!: PlanNode;
|
|
428
|
-
const updated = replacePlanNodeScoped(planId, (plan) => {
|
|
429
|
-
const current = plan.nodes.find((node) => node.id === nodeId);
|
|
430
|
-
if (!current) throw new ValidationError(`node ${nodeId} not found`); existing = current;
|
|
431
|
-
const statusSource = patch.statusSource !== undefined ? cleanStatusSource(patch.statusSource) : current.statusSource;
|
|
432
|
-
const base: PlanNode = {
|
|
433
|
-
...current,
|
|
434
|
-
...(patch.label !== undefined ? { label: cleanString(patch.label, "label", { required: true, max: PLAN_NODE_LABEL_MAX })! } : {}),
|
|
435
|
-
...(patch.scopeBlurb !== undefined ? optionalProp("scopeBlurb", cleanString(patch.scopeBlurb, "scopeBlurb", { max: 2000 })) : {}),
|
|
436
|
-
...(patch.ownerRef !== undefined ? optionalProp("ownerRef", cleanOwnerRef(patch.ownerRef)) : {}),
|
|
437
|
-
...(patch.lane !== undefined ? optionalProp("lane", cleanString(patch.lane, "lane", { max: 120 })) : {}),
|
|
438
|
-
...(patch.status !== undefined ? { status: optionalEnum(patch.status, "status", PLAN_STATUSES) as PlanStatus } : {}),
|
|
439
|
-
...(patch.refs !== undefined ? optionalProp("refs", cleanRefs(patch.refs, actor)) : {}),
|
|
440
|
-
...(patch.blackboardRefs !== undefined ? optionalProp("blackboardRefs", cleanBlackboardRefs(patch.blackboardRefs, plan.teamId)) : {}),
|
|
441
|
-
statusSource,
|
|
442
|
-
};
|
|
443
|
-
// #2520 RESOLUTION — the ONE shared patch-time source derivation (lives beside the writer op
|
|
444
|
-
// body it mirrors; see applyPatchStatusSourceDerivation in db/plan-graph.ts).
|
|
445
|
-
next = applyBoundStatus(applyPatchStatusSourceDerivation(patch, current, base));
|
|
446
|
-
return { ...plan, nodes: plan.nodes.map((node) => node.id === nodeId ? next : node) };
|
|
447
|
-
}, actor);
|
|
448
|
-
syncChangedPlanNodeIssueAssociation(updated.id, existing, next); emitPlan("plan.changed", updated, actor);
|
|
449
|
-
return updated;
|
|
295
|
+
export async function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthContext): Promise<Plan> {
|
|
296
|
+
return addPlanNodeAsync(planId, nodeInput, ctx);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePatch, ctx: AuthContext): Promise<Plan> {
|
|
300
|
+
return updatePlanNodeAsync(planId, nodeId, patch, ctx);
|
|
450
301
|
}
|
|
451
302
|
|
|
452
303
|
function optionalProp<K extends keyof PlanNode>(key: K, value: PlanNode[K] | undefined): Partial<PlanNode> {
|
|
@@ -470,72 +321,20 @@ function assertEdgeKeepsAcyclic(plan: Plan, edge: PlanEdge): void {
|
|
|
470
321
|
validateEdges(projected.nodes, dedupeEdges([...projected.edges, edge]));
|
|
471
322
|
}
|
|
472
323
|
|
|
473
|
-
export function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
|
|
474
|
-
|
|
475
|
-
const edge = cleanEdge(edgeInput);
|
|
476
|
-
assertEdgeKeepsAcyclic(plan, edge);
|
|
477
|
-
// #836: when BOTH endpoints are Task-bound, the canonical edge store is task_dependencies,
|
|
478
|
-
// not the plan JSON. Plan edge {from,to} means `to` depends on `from`. The combined-graph
|
|
479
|
-
// pre-check above already rejected cross-store cycles; the edge surfaces back via projection.
|
|
480
|
-
const fromTask = boundTaskId(plan, edge.from);
|
|
481
|
-
const toTask = boundTaskId(plan, edge.to);
|
|
482
|
-
if (fromTask !== undefined && toTask !== undefined) {
|
|
483
|
-
addTaskDependency(toTask, fromTask, actorId(ctx));
|
|
484
|
-
const projected = projectPlanOverTasks(existingPlan(planId));
|
|
485
|
-
emitPlan("plan.changed", projected, actorId(ctx));
|
|
486
|
-
return projected;
|
|
487
|
-
}
|
|
488
|
-
// S6 #2079: fence behind the version-CAS; re-append + re-check acyclicity on the fresh snapshot.
|
|
489
|
-
const updated = replacePlanNodeScoped(planId, (fresh) => {
|
|
490
|
-
assertEdgeKeepsAcyclic(fresh, edge);
|
|
491
|
-
return { ...fresh, edges: dedupeEdges([...fresh.edges, edge]) };
|
|
492
|
-
}, actorId(ctx));
|
|
493
|
-
emitPlan("plan.changed", updated, actorId(ctx)); return updated;
|
|
324
|
+
export async function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Promise<Plan> {
|
|
325
|
+
return addPlanEdgeAsync(planId, edgeInput, ctx);
|
|
494
326
|
}
|
|
495
327
|
|
|
496
|
-
export function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Plan {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
edges: plan.edges.filter((edge) => edge.from !== nodeId && edge.to !== nodeId),
|
|
507
|
-
};
|
|
508
|
-
}, actor);
|
|
509
|
-
removePlanNodeIssueAssociation(updated.id, removed); emitPlan("plan.changed", updated, actor);
|
|
510
|
-
return updated;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
|
|
514
|
-
const plan = existingPlan(planId);
|
|
515
|
-
const edge = cleanEdge(edgeInput);
|
|
516
|
-
// A bound-pair edge can live in BOTH task_dependencies and the plan JSON, and projection unions
|
|
517
|
-
// them: clear both stores or the edge reappears (#2877).
|
|
518
|
-
const fromTask = boundTaskId(plan, edge.from);
|
|
519
|
-
const toTask = boundTaskId(plan, edge.to);
|
|
520
|
-
const depRemoved = fromTask !== undefined && toTask !== undefined && removeTaskDependency(toTask, fromTask, actorId(ctx));
|
|
521
|
-
// S6 #2079: fence behind the version-CAS; re-resolve the edge (and its not-found check) on fresh.
|
|
522
|
-
const updated = replacePlanNodeScoped(planId, (fresh) => {
|
|
523
|
-
const edges = fresh.edges.filter((item) => item.from !== edge.from || item.to !== edge.to);
|
|
524
|
-
if (!depRemoved && edges.length === fresh.edges.length) throw new ValidationError(`edge ${edge.from}->${edge.to} not found`);
|
|
525
|
-
return { ...fresh, edges };
|
|
526
|
-
}, actorId(ctx));
|
|
527
|
-
const projected = projectPlanOverTasks(updated);
|
|
528
|
-
emitPlan("plan.changed", projected, actorId(ctx));
|
|
529
|
-
return projected;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export function deletePlanGraph(planId: string, ctx: AuthContext): { id: string; deleted: true } {
|
|
533
|
-
const plan = existingPlan(planId);
|
|
534
|
-
const actor = actorId(ctx);
|
|
535
|
-
deletePlan(plan.id, actor);
|
|
536
|
-
for (const node of plan.nodes) removePlanNodeIssueAssociation(plan.id, node);
|
|
537
|
-
emitPlan("plan.deleted", plan, actor);
|
|
538
|
-
return { id: plan.id, deleted: true };
|
|
328
|
+
export async function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Promise<Plan> {
|
|
329
|
+
return (await removePlanNodeAsync(planId, nodeId, ctx)).plan;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Promise<Plan> {
|
|
333
|
+
return removePlanEdgeAsync(planId, edgeInput, ctx);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function deletePlanGraph(planId: string, ctx: AuthContext): Promise<{ id: string; deleted: true }> {
|
|
337
|
+
return deletePlanGraphAsync(planId, ctx);
|
|
539
338
|
}
|
|
540
339
|
|
|
541
340
|
export async function archivePlanGraphAsync(planId: string, archived: boolean, ctx: AuthContext): Promise<Plan> {
|
|
@@ -547,29 +346,12 @@ export async function archivePlanGraphAsync(planId: string, archived: boolean, c
|
|
|
547
346
|
|
|
548
347
|
// In-process (legacy/unit-test) sibling of the gateway-backed applyBoundOwnersAsync path (production
|
|
549
348
|
// never takes this branch). S6 #2079: fence each write behind the version-CAS; re-derive on fresh.
|
|
550
|
-
export function updateBoundPlanNodesForAgent(agentId: string, status: PlanStatus, actor = "server"): Plan[] {
|
|
551
|
-
|
|
552
|
-
node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status !== "landed" && node.status !== status;
|
|
553
|
-
const updated: Plan[] = [];
|
|
554
|
-
for (const plan of listPlansWithBoundOwner(agentId)) {
|
|
555
|
-
// Skip-no-op on the initial snapshot: don't even fence a write when there's nothing to flip.
|
|
556
|
-
if (!plan.nodes.some(boundFlip)) continue;
|
|
557
|
-
// Preserve that contract THROUGH a CAS-conflict retry: if a re-read snapshot no longer needs a
|
|
558
|
-
// flip (a concurrent writer already applied it), return null so no version bump / event fires.
|
|
559
|
-
let wrote = false;
|
|
560
|
-
const next = replacePlanNodeScoped(plan.id, (fresh) => {
|
|
561
|
-
wrote = fresh.nodes.some(boundFlip);
|
|
562
|
-
return wrote ? { ...fresh, nodes: fresh.nodes.map((node) => (boundFlip(node) ? { ...node, status } : node)) } : null;
|
|
563
|
-
}, actor);
|
|
564
|
-
if (!wrote) continue; // the retry resolved to a no-op — nothing was written, so don't emit/collect
|
|
565
|
-
emitPlan("plan.changed", next, actor); updated.push(next);
|
|
566
|
-
}
|
|
567
|
-
return updated;
|
|
349
|
+
export async function updateBoundPlanNodesForAgent(agentId: string, status: PlanStatus, actor = "server"): Promise<Plan[]> {
|
|
350
|
+
return updateBoundPlanNodesForAgentAsync(agentId, status, actor);
|
|
568
351
|
}
|
|
569
352
|
|
|
570
|
-
export function updateBoundPlanNodesFromAgent(agentId: string, actor = "server"): Plan[] {
|
|
571
|
-
|
|
572
|
-
return status ? updateBoundPlanNodesForAgent(agentId, status, actor) : [];
|
|
353
|
+
export async function updateBoundPlanNodesFromAgent(agentId: string, actor = "server"): Promise<Plan[]> {
|
|
354
|
+
return updateBoundPlanNodesFromAgentAsync(agentId, boundStatusForAgent(getAgent(agentId)), actor);
|
|
573
355
|
}
|
|
574
356
|
|
|
575
357
|
/** Gateway-only live-binding path. The read precheck avoids submitting a no-op writer job. */
|
|
@@ -581,7 +363,7 @@ export async function updateBoundPlanNodesForAgentAsync(agentId: string, status:
|
|
|
581
363
|
}
|
|
582
364
|
|
|
583
365
|
export async function updateBoundPlanNodesFromAgentAsync(agentId: string, status: PlanStatus | undefined, actor = "server"): Promise<Plan[]> {
|
|
584
|
-
return status ? updateBoundPlanNodesForAgentAsync(agentId, status, actor) : [];
|
|
366
|
+
return status ? (await updateBoundPlanNodesForAgentAsync(agentId, status, actor)) : [];
|
|
585
367
|
}
|
|
586
368
|
|
|
587
369
|
// #836: `view: "tasks"` returns the pure task-backed DAG synthesized from a team's deliverable
|
|
@@ -623,12 +405,8 @@ export interface BindPlanNodeInput {
|
|
|
623
405
|
taskId?: number;
|
|
624
406
|
}
|
|
625
407
|
|
|
626
|
-
export function bindPlanNodeToTask(planId: string, nodeId: string, input: BindPlanNodeInput, ctx: AuthContext): { plan: Plan; taskId: number } {
|
|
627
|
-
|
|
628
|
-
const result = writeTransaction(() => bindPlanNodeBody({ planId, nodeId, taskId: input.taskId, actor }, Date.now()));
|
|
629
|
-
emitPlan("plan.changed", result.plan, actor);
|
|
630
|
-
emitPlanTaskEvents(result.taskEvents);
|
|
631
|
-
return { plan: result.plan, taskId: result.taskId! };
|
|
408
|
+
export async function bindPlanNodeToTask(planId: string, nodeId: string, input: BindPlanNodeInput, ctx: AuthContext): Promise<{ plan: Plan; taskId: number }> {
|
|
409
|
+
return bindPlanNodeToTaskAsync(planId, nodeId, input, ctx);
|
|
632
410
|
}
|
|
633
411
|
|
|
634
412
|
// HTTP and MCP can await. Keep the synchronous entry points above for the event reducers and
|
|
@@ -698,6 +476,6 @@ export async function bindPlanNodeToTaskAsync(planId: string, nodeId: string, in
|
|
|
698
476
|
// The aggregate ack is after COMMIT. Emit every consumer-facing task intent only here,
|
|
699
477
|
// never from the worker, so failed aggregate writes are observably silent.
|
|
700
478
|
emitPlan("plan.changed", result.plan, actor);
|
|
701
|
-
emitPlanTaskEvents(result.taskEvents);
|
|
479
|
+
await emitPlanTaskEvents(result.taskEvents);
|
|
702
480
|
return { plan: result.plan, taskId: result.taskId! };
|
|
703
481
|
}
|
|
@@ -60,7 +60,7 @@ export async function importPlan(input: Record<string, unknown>, ctx: AuthContex
|
|
|
60
60
|
const { dryRun, ...args } = preparePlanImport(input, ctx);
|
|
61
61
|
if (dryRun) return await getGateway().read("planGraph.import.preview", args) as unknown as PlanImportResult;
|
|
62
62
|
const result = await getGateway().write("planGraph.import", args) as unknown as PlanImportWriteResult;
|
|
63
|
-
emitPlanTaskEvents(result.taskEvents);
|
|
63
|
+
await emitPlanTaskEvents(result.taskEvents);
|
|
64
64
|
if (result.plan) emitPlan(args.expectedVersion === 0 ? "plan.created" : "plan.changed", result.plan, args.actor);
|
|
65
65
|
return result.receipt;
|
|
66
66
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { updateBoundPlanNodesFromAgentAsync, updateBoundPlanNodesForAgentAsync } from "./plan-graph";
|
|
2
|
+
import { getTeamAsync } from "../db";
|
|
3
|
+
import { trackRelayEvent } from "../relay-event-drain";
|
|
3
4
|
import { subscribeRelayEvents, type RelayEvent } from "../events";
|
|
4
5
|
import { routeNotification } from "../notification-router";
|
|
5
|
-
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
let unsubscribe: (() => void) | undefined;
|
|
8
9
|
|
|
@@ -48,7 +49,7 @@ async function notifyBoundExit(event: RelayEvent, updatedPlans: Awaited<ReturnTy
|
|
|
48
49
|
const target = team?.coordinatorId ?? `team:${plan.teamId}`;
|
|
49
50
|
const blockedNodes = plan.nodes.filter((node) => node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status === "blocked");
|
|
50
51
|
if (blockedNodes.length === 0) continue;
|
|
51
|
-
routeNotification({
|
|
52
|
+
await routeNotification({
|
|
52
53
|
type: "plan.bound_agent_exited",
|
|
53
54
|
scope: { agentId, planId: plan.id, teamId: plan.teamId },
|
|
54
55
|
recipients: [target],
|
|
@@ -92,39 +93,9 @@ async function handlePlanBindingEventAsync(event: RelayEvent): Promise<void> {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
function handlePlanBindingEvent(event: RelayEvent): void {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (getGateway().stats.backend === "in-process") {
|
|
99
|
-
if (event.type === "agent.status") {
|
|
100
|
-
const agentId = agentIdFromEvent(event);
|
|
101
|
-
if (agentId) updateBoundPlanNodesFromAgent(agentId, event.source || "server");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (event.type === "branch.landed") {
|
|
105
|
-
const agentId = stringField(event.data.author);
|
|
106
|
-
if (agentId) updateBoundPlanNodesForAgent(agentId, "landed", event.source || "server");
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (event.type === "agent.exited") {
|
|
110
|
-
const agentId = agentIdFromEvent(event);
|
|
111
|
-
if (!agentId || isGracefulExitReason(stringField(event.data.reason))) return;
|
|
112
|
-
const updated = updateBoundPlanNodesForAgent(agentId, "blocked", event.source || "server");
|
|
113
|
-
const reason = stringField(event.data.reason);
|
|
114
|
-
for (const plan of updated) {
|
|
115
|
-
if (!plan.teamId) continue;
|
|
116
|
-
const team = getTeam(plan.teamId);
|
|
117
|
-
const nodes = plan.nodes.filter((node) => node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status === "blocked");
|
|
118
|
-
if (nodes.length) routeNotification({ type: "plan.bound_agent_exited", scope: { agentId, planId: plan.id, teamId: plan.teamId }, recipients: [team?.coordinatorId ?? `team:${plan.teamId}`], message: { subject: "Bound plan node blocked", body: `Agent \`${agentId}\` exited${reason ? ` (${reason})` : ""}; blocked plan node(s) ${nodes.map((node) => `\`${node.id}\``).join(", ")} in \`${plan.id}\`.`, payload: { kind: "plan.bound_agent_exited", planId: plan.id, agentId, nodeIds: nodes.map((node) => node.id), reason }, replyExpected: false } });
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// Event listeners are synchronous. Serialize the asynchronous aggregate submissions so an
|
|
124
|
-
// agent's rapid status sequence reaches the FIFO writer in event order.
|
|
125
|
-
pending = pending.then(() => handlePlanBindingEventAsync(event)).catch((error) => {
|
|
126
|
-
console.error("plan live binding update failed", error);
|
|
127
|
-
});
|
|
96
|
+
const run = pending.then(() => handlePlanBindingEventAsync(event));
|
|
97
|
+
pending = run.catch(() => {});
|
|
98
|
+
trackRelayEvent(run, "plan.live-binding");
|
|
128
99
|
}
|
|
129
100
|
|
|
130
101
|
export function startPlanLiveBindings(): void {
|
|
@@ -72,7 +72,7 @@ export async function createPlanTaskAsync(input: CreateTaskEntityInput, extras:
|
|
|
72
72
|
};
|
|
73
73
|
// The caller's key makes a retry return the first result instead of a second Task or node.
|
|
74
74
|
const result = await getGateway().write("planGraph.task.create", args, { idempotencyKey: options.idempotencyKey ?? randomUUID() }) as unknown as CreatePlanTaskResult;
|
|
75
|
-
emitPlanTaskEvents(result.taskEvents);
|
|
75
|
+
await emitPlanTaskEvents(result.taskEvents);
|
|
76
76
|
if (result.plan) emitPlan("plan.changed", result.plan, createdBy);
|
|
77
77
|
// A replay returns the first write's result; the receipt reads the Task as it is now.
|
|
78
78
|
const task = await getTaskDetailAsync(result.task.id) ?? result.task;
|