@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
|
@@ -4,6 +4,7 @@ import { spawnTreeContains } from "../db/spawn-tree";
|
|
|
4
4
|
import { cleanSpawnBudget } from "../spawn-budget";
|
|
5
5
|
import { readWorkflowRunBudgetAsync, reconcileRunBudgetAsync } from "../token-usage-async";
|
|
6
6
|
import { getRoutingPolicyEnvelope } from "../routing-policy-service";
|
|
7
|
+
import { resolveWorkflowRoutingDefaults } from "./workflow-routing-defaults";
|
|
7
8
|
|
|
8
9
|
// #3045 — the workflow supervisor: mounts the kernel `workflow` module, starts runs, and pumps them.
|
|
9
10
|
// A pass recovers crashed drivers, signals each waiting run from durable src facts (level-triggered,
|
|
@@ -14,10 +15,10 @@ import { errMessage, fireAndForget, stringValue, type WorkflowRunAgent, type Wor
|
|
|
14
15
|
import { validateWorkflowValue } from "@humwick/kernel/workflow-values";
|
|
15
16
|
import { OpError } from "@humwick/kernel";
|
|
16
17
|
|
|
17
|
-
import { admitWorkflowCaller, assertWorkflowCallerLive } from "./workflow-caller";
|
|
18
|
+
import { admitWorkflowCaller, admitWorkflowTaskOwner, assertWorkflowCallerLive, canControlWorkflowTask } from "./workflow-caller";
|
|
18
19
|
import { taskVerificationCheck } from "../task-verification";
|
|
19
20
|
|
|
20
|
-
import { workflow, workflowEffectId, type WorkflowEffect, type WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
|
|
21
|
+
import { assertWorkflowFuturePins, workflow, workflowEffectId, type WorkflowEffect, type WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
|
|
21
22
|
import { getAgent, getTaskDetailAsync, ValidationError } from "../db";
|
|
22
23
|
import type { WorkflowRunCursor, WorkflowRunTaskRow } from "../db/workflow-task-read-ops";
|
|
23
24
|
import { getGateway } from "../db/gateway";
|
|
@@ -31,7 +32,6 @@ import { McpNotFoundError } from "../mcp-errors";
|
|
|
31
32
|
import { ServiceAuthError } from "./errors";
|
|
32
33
|
import { canSpawn, spawnLineageAgentId, taskLinkedRepoCwd } from "./spawn-agent";
|
|
33
34
|
import { resolveSpawnProjectWorkflow } from "./spawn-project-role";
|
|
34
|
-
import { canControlTaskReviewHold } from "./task-review-hold";
|
|
35
35
|
import { updateTaskEntityAsync } from "./task-entity";
|
|
36
36
|
|
|
37
37
|
import { releaseRunHold } from "./workflow-effects";
|
|
@@ -77,19 +77,19 @@ export async function prepareWorkflowRun(input: { workflow: string; taskId: numb
|
|
|
77
77
|
if (!task || task.kind !== "deliverable") throw new ValidationError("task not found");
|
|
78
78
|
if (task.status === "canceled") throw new ValidationError(`task ${task.id} is canceled; a workflow runs on a Task that is still wanted`);
|
|
79
79
|
const caller = admitWorkflowCaller(ctx, task);
|
|
80
|
-
if (!caller && !
|
|
80
|
+
if (!caller && !canControlWorkflowTask(ctx, task)) {
|
|
81
81
|
throw new ServiceAuthError(`starting a workflow on task ${task.id} is coordinator-only: the run sets the review hold and clears it on GO — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
82
82
|
}
|
|
83
83
|
if (!caller && !canSpawn(ctx)) throw new ServiceAuthError("starting a workflow dispatches its roles, so it requires the command:spawn scope");
|
|
84
84
|
const lineageId = spawnLineageAgentId(ctx, task);
|
|
85
85
|
const lineage = lineageId ? getAgent(lineageId) : null;
|
|
86
|
-
const cwd = input.cwd ?? await taskLinkedRepoCwd(task.id, stringValue(lineage?.meta?.cwd));
|
|
86
|
+
const cwd = input.cwd ?? await taskLinkedRepoCwd(task.id, stringValue(lineage?.meta?.cwd), lineage ?? undefined);
|
|
87
87
|
const read = await resolveSpawnProjectWorkflow({ workflow: input.workflow, cwd, orchestratorId: input.orchestratorId, preferHost: lineage?.machine, allowedOrchestratorIds: ctx.constraints?.orchestrators });
|
|
88
88
|
return { task, cwd, read };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** relay_workflow_start: read the landed workflow and its roles, check every step's pin, start one run on the Task. */
|
|
92
|
-
export async function startWorkflowRun(input: { workflow: string; taskId: number; pins?: WorkflowPins; inputs?: Json; requestKey?: string; budget?: unknown }, ctx: AuthContext): Promise<Json> {
|
|
92
|
+
export async function startWorkflowRun(input: { workflow: string; taskId: number; cwd?: string; pins?: WorkflowPins; inputs?: Json; requestKey?: string; budget?: unknown }, ctx: AuthContext): Promise<Json> {
|
|
93
93
|
return launchWorkflowRun(await prepareWorkflowRun(input, ctx), input, ctx);
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -99,18 +99,17 @@ export async function launchWorkflowRun(prepared: PreparedWorkflowRun, input: {
|
|
|
99
99
|
const { task, cwd, read } = prepared;
|
|
100
100
|
const budget = cleanSpawnBudget(input.budget);
|
|
101
101
|
const { definition } = read.workflow;
|
|
102
|
+
// #3769 — a Task a delegated sub-coordinator owns runs as its run, whoever starts it: the step spawns' lineage and the run's origin agree.
|
|
103
|
+
const caller = admitWorkflowCaller(ctx, task, read.workflow) ?? admitWorkflowTaskOwner(task, read.workflow);
|
|
102
104
|
await getGateway().read("spawnTree.validateTarget", { type: "agent.spawn", source: ctx.actor.id, target: read.orchestratorId,
|
|
103
|
-
params: { spawnedBy: ctx.callerAgentId, taskId: task.id, cwd, orchestratorId: read.orchestratorId, profile: "worker", workflowRunId: "preflight" } });
|
|
104
|
-
const caller = admitWorkflowCaller(ctx, task, read.workflow);
|
|
105
|
+
params: { spawnedBy: caller?.callerAgentId ?? ctx.callerAgentId, taskId: task.id, cwd, orchestratorId: read.orchestratorId, profile: "worker", workflowRunId: "preflight" } });
|
|
105
106
|
const capabilityError = workflowCapabilityError(definition);
|
|
106
107
|
if (capabilityError) throw new ValidationError(capabilityError);
|
|
107
108
|
validateWorkflowValue(definition.inputs, input.inputs ?? {}, "INPUT_INVALID");
|
|
108
109
|
if (Object.values(definition.steps).some(step => step.verify) && !taskVerificationCheck(task.doneWhen)) throw new ValidationError(`T${task.id} needs a nonempty verify: section in doneWhen`);
|
|
109
110
|
const pins = input.pins ? stampPins(definition, input.pins, "start") : undefined;
|
|
110
111
|
const defaults = (await getRoutingPolicyEnvelope()).policy.stepDefaults;
|
|
111
|
-
const
|
|
112
|
-
const teamId = task.teamId ?? (coordinatorId ? getAgent(coordinatorId)?.teamId : undefined);
|
|
113
|
-
const routingDefaults = defaults ? { global: defaults.global, ...(teamId ? { team: defaults.teams?.[teamId], teamId } : {}) } : undefined;
|
|
112
|
+
const routingDefaults = resolveWorkflowRoutingDefaults(defaults, task, ctx.callerAgentId);
|
|
114
113
|
await assertStepLaunches({ definition, pins, routingDefaults, steps: Object.keys(definition.steps), roles: read.workflow.roles, inputTask: task }, ctx);
|
|
115
114
|
for (const [name, snapshot] of Object.entries(read.workflow.closure ?? {})) {
|
|
116
115
|
if (name === read.workflow.name) continue;
|
|
@@ -147,7 +146,7 @@ export async function launchWorkflowRun(prepared: PreparedWorkflowRun, input: {
|
|
|
147
146
|
});
|
|
148
147
|
if (!created && run.workflow !== read.workflow.name) throw new ValidationError(`task ${task.id} already runs workflow "${run.workflow}" (${run.id}); a Task has one open run`);
|
|
149
148
|
if (!created && pins && !sameCallPins(run.pins, pins)) {
|
|
150
|
-
throw new ValidationError(`task ${task.id} already runs ${run.id} with other pins;
|
|
149
|
+
throw new ValidationError(`task ${task.id} already runs ${run.id} with other pins; use relay_workflow_resume with pins to change not-yet-started steps`);
|
|
151
150
|
}
|
|
152
151
|
if (!created && budget && budget.tokenCap !== run.tokenBudget?.tokenCap) {
|
|
153
152
|
throw new ValidationError(`task ${task.id} already runs ${run.id} with another budget; a run keeps its original token cap`);
|
|
@@ -181,9 +180,9 @@ async function settleRefusable(sup: Supervisor, run: WorkflowRunRecord): Promise
|
|
|
181
180
|
throw error;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
/** A run is visible to its coordinator, the input Task's coordinators and admins. */
|
|
185
|
-
export function canViewRun(ctx: AuthContext, coordinator: string, task: Parameters<typeof
|
|
186
|
-
return ctx.callerAgentId === coordinator || Boolean(ctx.callerAgentId && spawnTreeContains(ctx.callerAgentId, coordinator)) || Boolean(task &&
|
|
183
|
+
/** A run is visible to its coordinator, the input Task's coordinators, its owning sub-coordinator and admins. */
|
|
184
|
+
export function canViewRun(ctx: AuthContext, coordinator: string, task: Parameters<typeof canControlWorkflowTask>[1] | null): boolean {
|
|
185
|
+
return ctx.callerAgentId === coordinator || Boolean(ctx.callerAgentId && spawnTreeContains(ctx.callerAgentId, coordinator)) || Boolean(task && canControlWorkflowTask(ctx, task));
|
|
187
186
|
}
|
|
188
187
|
|
|
189
188
|
/** #3352 — the run's cap and observed spend, as admission counts them. */
|
|
@@ -191,7 +190,7 @@ export async function readWorkflowRunBudget(runId: string, ctx: AuthContext): Pr
|
|
|
191
190
|
const sup = await ensureSupervisor();
|
|
192
191
|
const run = await readRun(sup, runId);
|
|
193
192
|
if (!canViewRun(ctx, run.coordinator, await getTaskDetailAsync(run.taskId))) {
|
|
194
|
-
throw new ServiceAuthError(`workflow run ${runId} is visible to its coordinator, the Task's coordinators and admins — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
193
|
+
throw new ServiceAuthError(`workflow run ${runId} is visible to its coordinator, the Task's coordinators, its owning sub-coordinator and admins — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
195
194
|
}
|
|
196
195
|
if (!run.tokenBudget) throw new McpNotFoundError(`workflow run ${runId} has no token budget`);
|
|
197
196
|
return await readWorkflowRunBudgetAsync(run.id, run.tokenBudget.tokenCap) as unknown as Json;
|
|
@@ -218,7 +217,7 @@ async function readRun(sup: Supervisor, runId: string): Promise<WorkflowRunRecor
|
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
/** relay_workflow_resume: answer the run's escalation by re-entering it at its escalated step or a named one (#3045 H2),
|
|
221
|
-
* replacing the pins of each step `pins` names (#3121). */
|
|
220
|
+
* replacing the pins of each step `pins` names (#3121); pins alone on a running or paused run update future steps (#3806). */
|
|
222
221
|
export async function resumeWorkflowRun(input: { runId: string; activationId?: string; step?: string; note?: string; pins?: WorkflowPins; decision?: WorkflowDecisionResponse }, ctx: AuthContext): Promise<Json> {
|
|
223
222
|
const sup = await ensureSupervisor();
|
|
224
223
|
const run = await readRun(sup, input.runId);
|
|
@@ -232,14 +231,27 @@ export async function resumeWorkflowRun(input: { runId: string; activationId?: s
|
|
|
232
231
|
return { ...runView(await readRun(sup, run.id)), resumed: input.decision.action === "answer" };
|
|
233
232
|
}
|
|
234
233
|
const task = await getTaskDetailAsync(run.taskId);
|
|
235
|
-
if (!task || !(ctx.callerAgentId === run.callerAgentId ||
|
|
234
|
+
if (!task || !(ctx.callerAgentId === run.callerAgentId || canControlWorkflowTask(ctx, task))) {
|
|
236
235
|
throw new ServiceAuthError(`resuming workflow run ${run.id} is coordinator-only: the run sets the review hold and clears it on GO — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
237
236
|
}
|
|
238
237
|
if (run.callerAgentId && ctx.actor.kind !== "system") assertWorkflowCallerLive(run.callerAgentId);
|
|
239
238
|
if (!run.callerAgentId && !canSpawn(ctx)) throw new ServiceAuthError("resuming a workflow can dispatch its roles, so it requires the command:spawn scope");
|
|
240
239
|
if (task.status === "canceled") throw new ValidationError(`T${task.id} is canceled, so its run ${run.id} cannot resume`);
|
|
241
240
|
const pins = input.pins ? stampPins(run.definition, input.pins, "resume") : undefined;
|
|
242
|
-
if (pins
|
|
241
|
+
if (pins && !input.step && !input.activationId && run.status === "running") {
|
|
242
|
+
await serialized(sup, async () => {
|
|
243
|
+
const latest = await readRun(sup, run.id);
|
|
244
|
+
assertWorkflowFuturePins(latest, pins);
|
|
245
|
+
await assertResumePins(latest, pins, task, ctx, await runAgents(sup, run.id), (op, args) => invoke(sup, op, args));
|
|
246
|
+
await invoke(sup, "amendPins", { runId: run.id, pins, by: ctx.callerAgentId ?? ctx.actor.id, note: input.note });
|
|
247
|
+
}).catch((error: unknown) => {
|
|
248
|
+
if (error instanceof OpError && (error.code === "conflict" || error.code === "invalid_input")) throw new ValidationError(error.message);
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
251
|
+
await requestPump(sup);
|
|
252
|
+
return { ...runView(await readRun(sup, run.id)), resumed: false, pinsUpdated: true };
|
|
253
|
+
}
|
|
254
|
+
if (pins) await assertResumePins(run, pins, task, ctx, await runAgents(sup, run.id), (op, args) => invoke(sup, op, args));
|
|
243
255
|
// Between passes, so no pass is performing the end-of-run reap the resume voids. A fact that already ends the
|
|
244
256
|
// current wait (a delivery, a land) is signaled first and wins: the resume then refuses.
|
|
245
257
|
const resume = serialized(sup, async () => {
|
|
@@ -277,12 +289,12 @@ export async function cancelWorkflowRun(input: { runId: string; reason?: string
|
|
|
277
289
|
return { ...runView(await readRun(sup, run.id)), cancelled };
|
|
278
290
|
}
|
|
279
291
|
|
|
280
|
-
/** The run a coordinator-only control acts on: its caller, the input Task's coordinators and admins may. */
|
|
292
|
+
/** The run a coordinator-only control acts on: its caller, the input Task's coordinators, its owning sub-coordinator and admins may. */
|
|
281
293
|
async function controlledRun(runId: string, ctx: AuthContext, verb: string): Promise<{ sup: Supervisor; run: WorkflowRunRecord; by: string }> {
|
|
282
294
|
const sup = await ensureSupervisor();
|
|
283
295
|
const run = await readRun(sup, runId);
|
|
284
296
|
const task = await getTaskDetailAsync(run.taskId);
|
|
285
|
-
if (!task || !(ctx.callerAgentId === run.callerAgentId ||
|
|
297
|
+
if (!task || !(ctx.callerAgentId === run.callerAgentId || canControlWorkflowTask(ctx, task))) {
|
|
286
298
|
throw new ServiceAuthError(`${verb} workflow run ${run.id} is coordinator-only — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
287
299
|
}
|
|
288
300
|
return { sup, run, by: ctx.callerAgentId ?? ctx.actor.id };
|
|
@@ -310,7 +322,7 @@ export async function getWorkflowRun(runId: string, ctx: AuthContext, options: W
|
|
|
310
322
|
await sup.notes;
|
|
311
323
|
const run = await readRun(sup, runId);
|
|
312
324
|
if (!canViewRun(ctx, run.coordinator, await getTaskDetailAsync(run.taskId))) {
|
|
313
|
-
throw new ServiceAuthError(`workflow run ${runId} is visible to its coordinator, the Task's coordinators and admins — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
325
|
+
throw new ServiceAuthError(`workflow run ${runId} is visible to its coordinator, the Task's coordinators, its owning sub-coordinator and admins — ${COORDINATOR_AUTHORITY_HINT}`);
|
|
314
326
|
}
|
|
315
327
|
const scripts = options.verbose && Object.values(run.definition.steps).some(step => step.script)
|
|
316
328
|
? await invoke<{ items: Array<WorkflowScriptRun & { id: string; runId: string }>; cursor?: string }>(sup, "scriptAttempts", { runId, ...(options.scriptCursor ? { cursor: options.scriptCursor } : {}) }) : undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { errMessage } from "@humwick/sdk";
|
|
2
2
|
import type { ModuleHost, ModuleHostOptions } from "@humwick/kernel";
|
|
3
3
|
import type { WorkflowRuntime } from "@humwick/kernel/workflow-runtime";
|
|
4
|
-
import { workflow, type WorkflowAgent } from "@humwick/kernel/workflow.pilot";
|
|
4
|
+
import { workflow, type WorkflowAgent, type WorkflowEffect } from "@humwick/kernel/workflow.pilot";
|
|
5
5
|
import { getDbGeneration } from "../db";
|
|
6
6
|
import { emitRelayEvent } from "../events";
|
|
7
7
|
|
|
@@ -50,6 +50,26 @@ export function supervisorNow(): number {
|
|
|
50
50
|
return (testOptions.now ?? Date.now)();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const QUICK_RETRIES = 3;
|
|
54
|
+
const RETRY_BASE_MS = 1000;
|
|
55
|
+
const RETRY_MAX_MS = 60_000;
|
|
56
|
+
|
|
57
|
+
/** #3827 — a retried effect's result with its next attempt. A stop still in flight (`inFlight`) is re-read next
|
|
58
|
+
* pass; consecutive terminal misses (a failed stop, a gone target, a stop without proof) get a few quick retries,
|
|
59
|
+
* then exponential backoff to a minute, so an effect that never converges costs one attempt a minute, not one a pass. */
|
|
60
|
+
export function retryResult(previous: Json | undefined, result: Json, now: number): Json {
|
|
61
|
+
if (result.inFlight === true) return result;
|
|
62
|
+
const failures = (typeof previous?.failures === "number" ? previous.failures : 0) + 1;
|
|
63
|
+
const delay = failures <= QUICK_RETRIES ? 0 : Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** Math.min(failures - QUICK_RETRIES - 1, 6));
|
|
64
|
+
return { ...result, failures, nextAttemptAt: now + delay };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether a pending effect's backoff has run out. */
|
|
68
|
+
export function effectDue(effect: WorkflowEffect, now: number): boolean {
|
|
69
|
+
const at = effect.result?.nextAttemptAt;
|
|
70
|
+
return typeof at !== "number" || at <= now;
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
/** The supervisor bound to the current database handle (a test that swaps the handle rebuilds it). */
|
|
54
74
|
export function live(): Supervisor | undefined {
|
|
55
75
|
return current && current.generation === getDbGeneration() ? current : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// #3045 — drives durable workflow transitions, reconciliation and external effects.
|
|
2
|
+
import { framed } from "../hub-metrics";
|
|
2
3
|
import { errMessage, fireAndForget, isRecord, stringValue } from "@humwick/sdk";
|
|
3
4
|
import { createModuleHost, type ModuleHost } from "@humwick/kernel";
|
|
4
5
|
import { workflow } from "@humwick/kernel/workflow.pilot";
|
|
@@ -57,10 +58,10 @@ function startSupervisor(): Supervisor {
|
|
|
57
58
|
}),
|
|
58
59
|
landed: new Map(), tail: predecessor, queued: undefined, notes: predecessor, refusable: new Map(),
|
|
59
60
|
};
|
|
60
|
-
sup.ticker = setInterval(() => {
|
|
61
|
-
if (live() === sup)
|
|
61
|
+
sup.ticker = setInterval(framed("timer workflow-supervisor", () => {
|
|
62
|
+
if (live() === sup) return fireAndForget("[workflow] supervisor pump", () => requestPump(sup));
|
|
62
63
|
else if (sup.ticker) clearInterval(sup.ticker);
|
|
63
|
-
}, workflowScriptsEnabled() ? 1000 : SCHEDULER_TICK_MS);
|
|
64
|
+
}), workflowScriptsEnabled() ? 1000 : SCHEDULER_TICK_MS);
|
|
64
65
|
sup.ticker.unref?.();
|
|
65
66
|
setSupervisor(sup);
|
|
66
67
|
return sup;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { insertRecoveredWorkspaceAsync } from "../db/runtime-admin-async";
|
|
1
2
|
// #1502 — server-authoritative self-heal of a dropped/never-written isolated-workspace registration.
|
|
2
3
|
//
|
|
3
4
|
// A live worker holds its own workspace descriptor in AGENT_RELAY_WORKSPACE_JSON, but the workspace
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
// 404), never a best-effort guess, because the output is a landing authorization.
|
|
19
20
|
|
|
20
21
|
import { resolve } from "node:path";
|
|
21
|
-
import { getDb, getOrchestrator,
|
|
22
|
+
import { getDb, getOrchestrator, workspaceLandMetadataFrom, type RecoveredWorkspaceInput } from "../db";
|
|
22
23
|
import { parseJson, isPathWithinBase } from "../utils";
|
|
23
24
|
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
24
25
|
import { hasScope } from "@humwick/sdk/types";
|
|
@@ -69,7 +70,7 @@ function resolveOwningOrchestrator(ctx: AuthContext, params: Record<string, unkn
|
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
export function recoverIsolatedWorkspace(ctx: AuthContext, hint: WorkspaceRecoveryHint | undefined, pathId: string | undefined): WorkspaceRecovery {
|
|
73
|
+
export async function recoverIsolatedWorkspace(ctx: AuthContext, hint: WorkspaceRecoveryHint | undefined, pathId: string | undefined): Promise<WorkspaceRecovery> {
|
|
73
74
|
const caller = ctx.callerAgentId;
|
|
74
75
|
if (!caller) return REFUSE;
|
|
75
76
|
// Minting a land/merge target is a privileged write — require command:write, not baseline identity
|
|
@@ -163,6 +164,6 @@ export function recoverIsolatedWorkspace(ctx: AuthContext, hint: WorkspaceRecove
|
|
|
163
164
|
ownerAgentId: caller,
|
|
164
165
|
landMetadata: workspaceLandMetadataFrom(params),
|
|
165
166
|
};
|
|
166
|
-
const result =
|
|
167
|
+
const result = (await insertRecoveredWorkspaceAsync(input));
|
|
167
168
|
return { ok: true, workspace: result.workspace, created: result.created };
|
|
168
169
|
}
|
package/src/spawn-admission.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Orchestrator, SpawnProvider } from "./types";
|
|
2
|
+
import { ValidationError } from "@humwick/sdk";
|
|
3
|
+
|
|
4
|
+
export class PinnedHostCapacityError extends ValidationError {
|
|
5
|
+
constructor(message: string, readonly orchestratorId: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
export interface AdmissionReason {
|
|
4
11
|
check: "host" | "provider" | "auth" | "capacity";
|
|
5
12
|
code: string;
|
|
6
13
|
message: string;
|
|
7
14
|
}
|
|
8
|
-
export interface Admission { eligible: boolean; refusals: AdmissionReason[]; uncertainties: AdmissionReason[] }
|
|
15
|
+
export interface Admission { eligible: boolean; refusals: AdmissionReason[]; uncertainties: AdmissionReason[]; agentCapOnly?: boolean }
|
|
9
16
|
|
|
10
17
|
/** Interpret all probe legs independently: a failure on one never hides another leg's timeout. */
|
|
11
18
|
export function providerProbeAdmission(item: Orchestrator, provider: SpawnProvider, pinned = false): Admission {
|
|
@@ -40,7 +47,8 @@ export function providerProbeAdmission(item: Orchestrator, provider: SpawnProvid
|
|
|
40
47
|
|
|
41
48
|
export function assessSpawnAdmission(item: Orchestrator, provider: SpawnProvider, checks: AdmissionReason[] = [], pinned = false): Admission {
|
|
42
49
|
const probe = providerProbeAdmission(item, provider, pinned);
|
|
43
|
-
return { eligible: probe.eligible && checks.length === 0, refusals: [...checks, ...probe.refusals], uncertainties: probe.uncertainties
|
|
50
|
+
return { eligible: probe.eligible && checks.length === 0, refusals: [...checks, ...probe.refusals], uncertainties: probe.uncertainties,
|
|
51
|
+
agentCapOnly: probe.eligible && checks.length > 0 && checks.every(reason => reason.code === "agent_cap") };
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export function admissionMessages(admission: Admission): string {
|
package/src/spawn-placement.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { HOST_BAND_CONFIG } from "./config";
|
|
|
13
13
|
import { hostProviderRefusal, liveHostDisabledProviders } from "./host-provider-store";
|
|
14
14
|
|
|
15
15
|
import { hostSupportsModel } from "./spawn-models";
|
|
16
|
-
import { admissionMessages, assessSpawnAdmission, type AdmissionReason, type Admission } from "./spawn-admission";
|
|
16
|
+
import { admissionMessages, assessSpawnAdmission, PinnedHostCapacityError, type AdmissionReason, type Admission } from "./spawn-admission";
|
|
17
17
|
|
|
18
18
|
const MATCHED_HOST_IDENTITY: HostIdentity = Object.freeze({ matched: true });
|
|
19
19
|
|
|
@@ -180,10 +180,14 @@ export function selectSpawnPlacement(
|
|
|
180
180
|
const orchestrator = getOrchestrator(orchestratorId);
|
|
181
181
|
if (!orchestrator) throw new McpNotFoundError(`orchestrator ${orchestratorId} not found`);
|
|
182
182
|
const view = orchestrator.hostId ? byId.get(orchestrator.hostId) : undefined;
|
|
183
|
-
const admission = hostSpawnAdmission(orchestrator, view, identities, provider, model, effort, liveHostDisabledProviders(), true, spawnHostCapAdmission(capOpts)(view),
|
|
183
|
+
const admission = hostSpawnAdmission(orchestrator, view, identities, provider, model, effort, liveHostDisabledProviders(), true, spawnHostCapAdmission(capOpts)(view), [
|
|
184
|
+
...repoPlatformAdmission(view, placementPlatforms), ...workspaceHostAdmission(orchestrator, view, workspaceMode, canLand, capOpts, true),
|
|
185
|
+
]);
|
|
184
186
|
const detail = admissionMessages(admission);
|
|
185
187
|
if (!admission.eligible) {
|
|
186
|
-
|
|
188
|
+
const message = spawnPlacementRefusal(listOrchestrators(), views, provider, model, effort, orchestratorId, capOpts, admission);
|
|
189
|
+
if (admission.agentCapOnly) throw new PinnedHostCapacityError(message, orchestratorId);
|
|
190
|
+
throw new ValidationError(message);
|
|
187
191
|
}
|
|
188
192
|
return { orchestrator, reason: `Explicit host pin: ${orchestrator.hostname} (${orchestrator.id})${detail ? `; ${detail}` : ""}.` };
|
|
189
193
|
}
|
|
@@ -241,7 +245,25 @@ function headroomSummary(view: HostView | undefined): string {
|
|
|
241
245
|
return `(${round(cores)} free cores, ${round(freeThreadsOf(view)!)} free threads, band ${view.band})`;
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
export interface SpawnCapOptions {
|
|
248
|
+
export interface SpawnCapOptions {
|
|
249
|
+
callerAgentId?: string;
|
|
250
|
+
attach?: boolean;
|
|
251
|
+
buildWork?: boolean;
|
|
252
|
+
workspaceDepsDisabledHosts?: readonly string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Pins and landBlocked cannot turn isolated dependency/build work into a light hub probe (#3805). */
|
|
256
|
+
function workspaceHostAdmission(item: Orchestrator, view: HostView | undefined, mode: WorkspaceMode | undefined, canLand: boolean, opts: SpawnCapOptions, pinned = false): AdmissionReason[] {
|
|
257
|
+
if (mode !== "isolated") return [];
|
|
258
|
+
const hubWork = (opts.buildWork ?? canLand) || (!opts.attach && !opts.workspaceDepsDisabledHosts?.includes(item.id));
|
|
259
|
+
if (hubWork && view?.policy.noLandingWorkspaces !== false && isRelayHost(item, view)) {
|
|
260
|
+
return [{ check: "host", code: "hub_workspace", message: `no builders on the hub: Relay server host ${item.hostname} (${item.id}) refuses isolated build or dependency-install workspaces, including pinned spawns and workflow steps; choose a non-hub host or set Fleet > Edit host > Isolated workspaces to Include this host (noLandingWorkspaces=false)` }];
|
|
261
|
+
}
|
|
262
|
+
if (pinned || !canLand) return [];
|
|
263
|
+
const landing = view?.policy.noLandingWorkspaces === true ? "host excludes landing workspaces"
|
|
264
|
+
: view?.policy.noLandingWorkspaces !== false && isRelayHost(item, view) ? "Relay server host excluded by default" : undefined;
|
|
265
|
+
return landing ? [{ check: "host", code: "landing", message: landing }] : [];
|
|
266
|
+
}
|
|
245
267
|
|
|
246
268
|
// Repository tools must run on the host even when the agent cannot land or pins a host (#3563).
|
|
247
269
|
function repoPlatformAdmission(view: HostView | undefined, platforms?: readonly string[] | null): AdmissionReason[] {
|
|
@@ -282,11 +304,7 @@ export function spawnPlacementCandidates(
|
|
|
282
304
|
const extra = repoPlatformAdmission(view, placementPlatforms);
|
|
283
305
|
const capacity = capBlock(view);
|
|
284
306
|
if (capacity) extra.push({ check: "capacity", code: "agent_cap", message: capacity });
|
|
285
|
-
|
|
286
|
-
const landing = view?.policy.noLandingWorkspaces === true ? "host excludes landing workspaces"
|
|
287
|
-
: view?.policy.noLandingWorkspaces !== false && isRelayHost(item, view) ? "Relay server host excluded by default" : undefined;
|
|
288
|
-
if (landing) extra.push({ check: "host", code: "landing", message: landing });
|
|
289
|
-
}
|
|
307
|
+
extra.push(...workspaceHostAdmission(item, view, workspaceMode, canLand, capOpts));
|
|
290
308
|
const admission = hostSpawnAdmission(item, view, identities, provider, model, effort, hostDisabled, false, undefined, extra);
|
|
291
309
|
if (!admission.eligible) exclusions.push(`${item.hostname}: ${admissionMessages(admission)}`);
|
|
292
310
|
else if (admission.uncertainties.length) warnings.push(`${item.hostname}: ${admissionMessages(admission)}`);
|
package/src/spawn-targets.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiBudgetStatus } from "./db/claude-api-budget";
|
|
2
2
|
import { apiBudgetStatusAsync } from "./claude-api-budget-async";
|
|
3
|
-
import { isOpenAiApiModel, type ApiBudgetStatus } from "@humwick/sdk";
|
|
3
|
+
import { API_BUDGET_IDS, isOpenAiApiModel, type ApiBudgetStatus } from "@humwick/sdk";
|
|
4
4
|
import { collapseProviderQuotaRows } from "@humwick/sdk/provider-quota-order";
|
|
5
5
|
import { resolveQuotaPacing, subscriptionExpiryForWindow, subscriptionExpiryPreference, type ResolvedQuotaPacing } from "./quota-pacing";
|
|
6
6
|
import { countLiveSpawnedAgents, countLiveSpawnedAgentsAsync, getAgent, getAgentAsync, listOrchestrators, listOrchestratorsAsync, listProviderQuotas, providerQuotaRoutingSnapshotAsync } from "./db";
|
|
@@ -53,7 +53,7 @@ export function buildSpawnTargets(input: { callerId?: string; quota: number }):
|
|
|
53
53
|
const me = input.callerId ? getAgent(input.callerId) : undefined;
|
|
54
54
|
return buildSpawnTargetsFrom(input, {
|
|
55
55
|
me,
|
|
56
|
-
|
|
56
|
+
apiBudgets: API_BUDGET_IDS.map((id) => apiBudgetStatus(now, id)),
|
|
57
57
|
orchestrators: listOrchestrators(),
|
|
58
58
|
hostViews: listHostViews(now),
|
|
59
59
|
liveChildren: input.callerId ? countLiveSpawnedAgents(input.callerId) : 0,
|
|
@@ -67,17 +67,17 @@ export function buildSpawnTargets(input: { callerId?: string; quota: number }):
|
|
|
67
67
|
/** Worker-safe discovery path: every durable input is read through the reader gateway. */
|
|
68
68
|
export async function buildSpawnTargetsAsync(input: { callerId?: string; quota: number }): Promise<Record<string, unknown>> {
|
|
69
69
|
const now = Date.now();
|
|
70
|
-
const [me, orchestrators, hostViews, liveChildren, snapshot,
|
|
70
|
+
const [me, orchestrators, hostViews, liveChildren, snapshot, apiBudgets] = await Promise.all([
|
|
71
71
|
input.callerId ? getAgentAsync(input.callerId) : Promise.resolve(null),
|
|
72
72
|
listOrchestratorsAsync(),
|
|
73
73
|
listHostViewsAsync(),
|
|
74
74
|
input.callerId ? countLiveSpawnedAgentsAsync(input.callerId) : Promise.resolve(0),
|
|
75
75
|
providerQuotaRoutingSnapshotAsync(now),
|
|
76
|
-
apiBudgetStatusAsync(
|
|
76
|
+
Promise.all(API_BUDGET_IDS.map((id) => apiBudgetStatusAsync(id))),
|
|
77
77
|
]);
|
|
78
78
|
return buildSpawnTargetsFrom(input, {
|
|
79
79
|
me: me ?? undefined,
|
|
80
|
-
|
|
80
|
+
apiBudgets,
|
|
81
81
|
orchestrators,
|
|
82
82
|
hostViews,
|
|
83
83
|
liveChildren,
|
|
@@ -90,7 +90,7 @@ export async function buildSpawnTargetsAsync(input: { callerId?: string; quota:
|
|
|
90
90
|
|
|
91
91
|
function buildSpawnTargetsFrom(input: { callerId?: string; quota: number }, data: {
|
|
92
92
|
me?: ReturnType<typeof getAgent>;
|
|
93
|
-
|
|
93
|
+
apiBudgets: ApiBudgetStatus[];
|
|
94
94
|
orchestrators: Orchestrator[];
|
|
95
95
|
hostViews: readonly HostView[];
|
|
96
96
|
liveChildren: number;
|
|
@@ -130,10 +130,10 @@ function buildSpawnTargetsFrom(input: { callerId?: string; quota: number }, data
|
|
|
130
130
|
...(o.utilization && Number.isFinite(o.utilization.agentSlotsUsedThisDaemon) && Number.isFinite(o.utilization.agentSlotsMaxThisDaemon) && o.utilization.agentSlotsMaxThisDaemon > 0
|
|
131
131
|
? { spawn_slots: { used: o.utilization.agentSlotsUsedThisDaemon, max: o.utilization.agentSlotsMaxThisDaemon } }
|
|
132
132
|
: {}),
|
|
133
|
-
providers: providerMatrixFor(o, data.quotaAdvisories, data.catalog, data.profiles, hostDisabled, data.
|
|
133
|
+
providers: providerMatrixFor(o, data.quotaAdvisories, data.catalog, data.profiles, hostDisabled, data.apiBudgets.find((budget) => budget.budgetId === "openai-api")),
|
|
134
134
|
};
|
|
135
135
|
}),
|
|
136
|
-
apiBudgets:
|
|
136
|
+
apiBudgets: data.apiBudgets.map((budget) => ({ ...budget, source: "relay-accounting" })),
|
|
137
137
|
...operatorDisabledProviders(data.catalog),
|
|
138
138
|
profiles: spawnProfilesSummary(data.profiles),
|
|
139
139
|
// #3327 — preferences steer automatic spawns inside the dispatcher; never copy them into explicit pins.
|
package/src/sse-agent-status.ts
CHANGED
|
@@ -2,15 +2,23 @@ import { isRecord } from "@humwick/sdk";
|
|
|
2
2
|
|
|
3
3
|
const FULL_CARD_KEYS = ["name", "kind", "label", "tags", "systemTags", "instanceId", "epoch", "capabilities", "providerCapabilities"];
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export interface AgentStatusChange {
|
|
6
|
+
previous?: number;
|
|
7
|
+
seq: number;
|
|
8
|
+
full: Record<string, unknown>;
|
|
9
|
+
delta: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Build once in source order; consumers need only the last sequence they received. */
|
|
6
13
|
export class AgentStatusDeltas {
|
|
7
|
-
private readonly agents = new Map<string, Map<string, string | undefined
|
|
14
|
+
private readonly agents = new Map<string, { seq: number; fields: Map<string, string | undefined> }>();
|
|
8
15
|
|
|
9
16
|
remove(id: string): void { this.agents.delete(id); }
|
|
10
17
|
|
|
11
|
-
project(data: Record<string, unknown
|
|
18
|
+
project(data: Record<string, unknown>, seq: number): AgentStatusChange {
|
|
12
19
|
const id = data.id as string;
|
|
13
|
-
const
|
|
20
|
+
const baseline = this.agents.get(id);
|
|
21
|
+
const previous = baseline?.fields;
|
|
14
22
|
const fields = new Map<string, string | undefined>();
|
|
15
23
|
const delta: Record<string, unknown> = { id, status: data.status, statusSeq: data.statusSeq, epoch: data.epoch };
|
|
16
24
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -29,7 +37,8 @@ export class AgentStatusDeltas {
|
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
if (data.status === "offline") this.remove(id);
|
|
32
|
-
else this.agents.set(id, fields);
|
|
33
|
-
return
|
|
40
|
+
else this.agents.set(id, { seq, fields });
|
|
41
|
+
return { previous: baseline?.seq, seq, full: data,
|
|
42
|
+
delta: !previous || FULL_CARD_KEYS.some((key) => previous.get(key) !== fields.get(key)) ? data : delta };
|
|
34
43
|
}
|
|
35
44
|
}
|
package/src/sse.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { sendMessageWithResultAsync } from "./db/message-send";
|
|
1
2
|
import { eventVisibleTo } from "./sse-visibility";
|
|
2
3
|
import { taskVisibleTo, memoizeViewerScope } from "./db/viewer-scope";
|
|
3
4
|
import { agentPrincipalSink } from "./db/agent-ownership";
|
|
4
|
-
import { getAgent, getMessage, getOperatorDecision, getOrchestrator, matchingDeliveryAgents, onWorkspaceChange,
|
|
5
|
+
import { getAgent, getMessage, getOperatorDecision, getOrchestrator, matchingDeliveryAgents, onWorkspaceChange, setChannelSubscriberProbe } from "./db";
|
|
5
6
|
import { hasReplayGap, type BusEvent } from "./bus-outbox";
|
|
6
7
|
import { replayEventsSliceAsync } from "./bus-outbox-async";
|
|
7
8
|
import { getFannedSeq } from "./sse-outbox-cursor";
|
|
@@ -14,10 +15,11 @@ import type { ActivityEvent, AgentCard, Message, RelayInjectionCategory, RelayNo
|
|
|
14
15
|
import { isHumanAgentId } from "@humwick/sdk";
|
|
15
16
|
import { messageVisibleTo, visibleMessages, type MessageViewer } from "./message-visibility";
|
|
16
17
|
import { emitNotificationCreated, notificationForMessage, notificationForDelivery } from "./dashboard-notifications";
|
|
17
|
-
import { AgentStatusDeltas } from "./sse-agent-status";
|
|
18
|
+
import { AgentStatusDeltas, type AgentStatusChange } from "./sse-agent-status";
|
|
19
|
+
import { getDbGeneration } from "./db/connection";
|
|
18
20
|
import { AgentStatusSubscribers, wantsAgentStatus } from "./transport-subscriptions";
|
|
19
21
|
import { withLoopFrameCapture } from "./loop-block-attribution";
|
|
20
|
-
import { recordEmitBytes } from "./hub-metrics";
|
|
22
|
+
import { framed, recordEmitBytes } from "./hub-metrics";
|
|
21
23
|
|
|
22
24
|
interface Connection {
|
|
23
25
|
id: string;
|
|
@@ -27,7 +29,8 @@ interface Connection {
|
|
|
27
29
|
queuedBytes: number;
|
|
28
30
|
/** Bytes ever enqueued on the controller; the replay pump measures its byte budget against it. */
|
|
29
31
|
writtenBytes: number;
|
|
30
|
-
statusDeltas?:
|
|
32
|
+
statusDeltas?: Map<string, number>;
|
|
33
|
+
replayDeltas?: AgentStatusDeltas;
|
|
31
34
|
// Pending backpressure re-check. Set when a stream crosses the byte floor; cleared
|
|
32
35
|
// once it drains or is closed after making no progress during the grace interval.
|
|
33
36
|
drainCheck?: Timer;
|
|
@@ -46,6 +49,8 @@ interface Connection {
|
|
|
46
49
|
const connections = new Map<string, Connection>();
|
|
47
50
|
const statusSubscribers = new AgentStatusSubscribers<Connection>("sse");
|
|
48
51
|
const encoder = new TextEncoder();
|
|
52
|
+
let sourceDeltas = new AgentStatusDeltas();
|
|
53
|
+
let statusGeneration = getDbGeneration();
|
|
49
54
|
|
|
50
55
|
const SSE_KEEPALIVE_MS = 15_000;
|
|
51
56
|
const SSE_RETRY_MS = 5_000;
|
|
@@ -65,7 +70,7 @@ subscribeRelayEvents((event) => {
|
|
|
65
70
|
fanoutBytes = 0;
|
|
66
71
|
withLoopFrameCapture(FANOUT_FRAME, () => fanout(event));
|
|
67
72
|
recordEmitBytes(event.type, fanoutBytes);
|
|
68
|
-
});
|
|
73
|
+
}, "sse.fanout");
|
|
69
74
|
|
|
70
75
|
function fanoutAgent(agentId: string, cache: Map<string, AgentCard | null>): AgentCard | null {
|
|
71
76
|
if (!cache.has(agentId)) cache.set(agentId, getAgent(agentId));
|
|
@@ -90,14 +95,14 @@ export function createSSEStream(agentId: string | null, lastEventId = 0, statusD
|
|
|
90
95
|
{
|
|
91
96
|
start(controller) {
|
|
92
97
|
connId = crypto.randomUUID();
|
|
93
|
-
const keepalive = setInterval(() => {
|
|
98
|
+
const keepalive = setInterval(framed("timer sse-keepalive", () => {
|
|
94
99
|
const conn = connections.get(connId);
|
|
95
100
|
if (!conn) return;
|
|
96
101
|
sendEncoded(conn, encoder.encode(": keepalive\n\n"));
|
|
97
|
-
}, SSE_KEEPALIVE_MS);
|
|
102
|
+
}), SSE_KEEPALIVE_MS);
|
|
98
103
|
|
|
99
104
|
connections.set(connId, { id: connId, agentId, controller, keepalive, queuedBytes: 0, writtenBytes: 0,
|
|
100
|
-
statusDeltas: statusDeltas ? new
|
|
105
|
+
statusDeltas: statusDeltas ? new Map() : undefined, viewer });
|
|
101
106
|
statusSubscribers.add(connections.get(connId)!);
|
|
102
107
|
|
|
103
108
|
const replay = replayPlan(lastEventId, agentId === null);
|
|
@@ -216,18 +221,34 @@ function sendDirect(conn: Connection, event: string, data: unknown, id?: number)
|
|
|
216
221
|
writeFrame(conn, sseFrame(event, data, id));
|
|
217
222
|
}
|
|
218
223
|
|
|
219
|
-
// #
|
|
220
|
-
// through it in the order the client receives it, so the per-connection delta baseline matches the client.
|
|
224
|
+
// #3608 — share live deltas and encoded frames; select full/delta in client order even during replay.
|
|
221
225
|
function tracksStatus(conn: Connection, event: RelayEvent): boolean {
|
|
222
226
|
return conn.statusDeltas !== undefined && (event.type === "agent.status" || event.type === "agent.removed");
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
// An ephemeral frame carries no `id:` line: it must not move the client's Last-Event-ID resume cursor.
|
|
230
|
+
function frameId(event: RelayEvent): number | undefined {
|
|
231
|
+
return event.ephemeral ? undefined : event.seq;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Ephemeral events all carry seq 0, so they take a unique negative baseline token: two live-only
|
|
235
|
+
// frames must never look like the same client-visible state.
|
|
236
|
+
let ephemeralStatusToken = 0;
|
|
237
|
+
|
|
225
238
|
// Reads `conn.statusDeltas` when called, not when queued: a replay.gap swaps in a fresh baseline.
|
|
226
|
-
function
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
deltas.
|
|
230
|
-
|
|
239
|
+
function prepareStatusFrame(event: RelayEvent, deltas: AgentStatusDeltas, full = sseFrame(event.type, event.data, frameId(event))) {
|
|
240
|
+
const id = event.data.id as string;
|
|
241
|
+
const token = event.ephemeral ? --ephemeralStatusToken : event.seq;
|
|
242
|
+
const change: AgentStatusChange | undefined = event.type === "agent.status" ? deltas.project(event.data, token) : undefined;
|
|
243
|
+
if (!change) deltas.remove(id);
|
|
244
|
+
let delta: Uint8Array | undefined;
|
|
245
|
+
return (conn: Connection): Uint8Array => {
|
|
246
|
+
const previous = conn.statusDeltas!.get(id);
|
|
247
|
+
if (event.data.status === "offline" || !change) conn.statusDeltas!.delete(id);
|
|
248
|
+
else conn.statusDeltas!.set(id, token);
|
|
249
|
+
return change && previous !== undefined && previous === change.previous
|
|
250
|
+
? (delta ??= change.delta === change.full ? full : sseFrame(event.type, change.delta, frameId(event))) : full;
|
|
251
|
+
};
|
|
231
252
|
}
|
|
232
253
|
|
|
233
254
|
function flushPendingLive(conn: Connection): void {
|
|
@@ -282,14 +303,17 @@ function fanout(event: RelayEvent): void {
|
|
|
282
303
|
sendTaskChanged(event.data as unknown as Task, event.type, event.seq);
|
|
283
304
|
return;
|
|
284
305
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
306
|
+
const chunk = sseFrame(event.type, event.data, frameId(event));
|
|
307
|
+
if (statusGeneration !== getDbGeneration()) {
|
|
308
|
+
sourceDeltas = new AgentStatusDeltas();
|
|
309
|
+
statusGeneration = getDbGeneration();
|
|
310
|
+
}
|
|
311
|
+
const status = event.type === "agent.status" || event.type === "agent.removed" ? prepareStatusFrame(event, sourceDeltas, chunk) : undefined;
|
|
288
312
|
const recipients = event.type === "agent.status" ? statusSubscribers.forEvent(event.subject, event.data.id) : connections.values();
|
|
289
313
|
const visible = memoizeViewerScope((viewer) => eventVisibleTo(viewer, event));
|
|
290
314
|
for (const conn of recipients) {
|
|
291
315
|
if (!visible(conn.viewer)) continue;
|
|
292
|
-
sendEncoded(conn, chunk,
|
|
316
|
+
sendEncoded(conn, chunk, conn.statusDeltas && status ? () => status(conn) : undefined);
|
|
293
317
|
}
|
|
294
318
|
}
|
|
295
319
|
|
|
@@ -394,9 +418,9 @@ interface RelayInjectionTimelineInput {
|
|
|
394
418
|
idempotencyKey?: string;
|
|
395
419
|
}
|
|
396
420
|
|
|
397
|
-
export function emitRelayInjectionEvent(input: RelayInjectionTimelineInput): Message {
|
|
421
|
+
export async function emitRelayInjectionEvent(input: RelayInjectionTimelineInput): Promise<Message> {
|
|
398
422
|
const timestamp = input.timestamp ?? Date.now();
|
|
399
|
-
const result =
|
|
423
|
+
const result = (await sendMessageWithResultAsync({
|
|
400
424
|
from: input.source ?? input.agentId,
|
|
401
425
|
to: input.source ? input.agentId : agentPrincipalSink(input.agentId),
|
|
402
426
|
kind: "session",
|
|
@@ -416,7 +440,7 @@ export function emitRelayInjectionEvent(input: RelayInjectionTimelineInput): Mes
|
|
|
416
440
|
},
|
|
417
441
|
},
|
|
418
442
|
},
|
|
419
|
-
});
|
|
443
|
+
}));
|
|
420
444
|
if (result.created || result.updated) emitNewMessage(result.message);
|
|
421
445
|
return result.message;
|
|
422
446
|
}
|
|
@@ -561,7 +585,7 @@ async function pumpReplay(connId: string, plan: ReplayPlan): Promise<void> {
|
|
|
561
585
|
// The client's REST reload covers everything up to the ceiling; the buffered live tail follows.
|
|
562
586
|
// That reload brings full cards, so restart the delta baseline as a fresh connection would.
|
|
563
587
|
sendDirect(conn, "replay.gap", { since: plan.since }, ceiling);
|
|
564
|
-
|
|
588
|
+
conn.statusDeltas?.clear();
|
|
565
589
|
break;
|
|
566
590
|
}
|
|
567
591
|
if (!slice.truncated) break;
|
|
@@ -570,6 +594,7 @@ async function pumpReplay(connId: string, plan: ReplayPlan): Promise<void> {
|
|
|
570
594
|
drained = true;
|
|
571
595
|
} finally {
|
|
572
596
|
conn.replaying = false;
|
|
597
|
+
conn.replayDeltas = undefined;
|
|
573
598
|
// Only flush into a still-open controller — a cancelled connection was already removed from
|
|
574
599
|
// `connections` (and stopped receiving anything into `pendingLive` the moment it was removed,
|
|
575
600
|
// since fanout only iterates live map entries), so there is nothing left to deliver.
|
|
@@ -615,7 +640,7 @@ function sendReplayEvent(conn: Connection, event: RelayEvent, agentCache: Map<st
|
|
|
615
640
|
return;
|
|
616
641
|
}
|
|
617
642
|
if (tracksStatus(conn, event)) {
|
|
618
|
-
writeFrame(conn,
|
|
643
|
+
writeFrame(conn, prepareStatusFrame(event, conn.replayDeltas ??= new AgentStatusDeltas())(conn));
|
|
619
644
|
return;
|
|
620
645
|
}
|
|
621
646
|
sendDirect(conn, event.type, event.data, event.seq);
|