@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
package/src/server.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { framed } from "./hub-metrics";
|
|
2
|
+
import { setConfigAsync } from "./config-store-write";
|
|
1
3
|
import { drainWriterOnShutdown } from "./db/writer-gateway";
|
|
2
4
|
import { relayHeaderConflict, errMessage, reapEphemeralSelf, startArtifactCheckoutLease, startEphemeralWatchdogFromEnv } from "@humwick/sdk";
|
|
3
5
|
import { authorityDenied } from "./authority-audit";
|
|
@@ -12,7 +14,7 @@ import { beginMainWriteBootPhase, endMainWriteBootPhase } from "./db/main-connec
|
|
|
12
14
|
import { markServerBound, timePostBindPhase } from "./post-bind-startup";
|
|
13
15
|
import type { ServerWebSocket } from "bun";
|
|
14
16
|
import { beginStartupPhase, enableStartupTiming, timeStartupPhase } from "./startup-timing";
|
|
15
|
-
import { initDb, getOrchestrator, startPresenceFlushLoop, primeHostAliasMemoFromDb } from "./db";
|
|
17
|
+
import { initDb, getOrchestrator, startPresenceFlushLoop, primeHostAliasMemoFromDb, onWorkspaceCreate } from "./db";
|
|
16
18
|
import { initGateway, shutdownGatewayReader } from "./db/gateway";
|
|
17
19
|
import { flushRelayEvents } from "./relay-event-drain";
|
|
18
20
|
import { startWalCheckpointer } from "./db/wal-checkpointer";
|
|
@@ -25,7 +27,7 @@ import { emitRelayEvent } from "./events";
|
|
|
25
27
|
import { reconcileAutomationRuns, runDueAutomationsAsync, type AutomationDispatchResult, type AutomationReconcileResult } from "./automations";
|
|
26
28
|
import { getLifecycleManager } from "./lifecycle-manager";
|
|
27
29
|
import { reconcileStewardPolicyModels } from "./steward-policy-model-reconcile";
|
|
28
|
-
import { getConfig
|
|
30
|
+
import { getConfig } from "./config-store";
|
|
29
31
|
export { reconcileProviderModelSeed } from "./provider-model-seed";
|
|
30
32
|
import { ensureEscalationChannelRegistered } from "./connectors";
|
|
31
33
|
import { installNotificationSubscriptionRule } from "./notification-subscription-rules";
|
|
@@ -33,7 +35,6 @@ import { startPlanLiveBindings } from "./services/plan-live-bindings";
|
|
|
33
35
|
import { startEventLoopLagSampler } from "./event-loop-lag";
|
|
34
36
|
import { installLoopBlockRegressionAlarm } from "./loop-block-regression-alarm";
|
|
35
37
|
import { recordWorkspaceIssueActivity } from "./services/spawn-issue-activity";
|
|
36
|
-
import { onWorkspaceCreate } from "./db";
|
|
37
38
|
import { warnIfNoHumanReachableEscalationTarget } from "./workspace-human-escalation";
|
|
38
39
|
import { isTerminalSessionRequestAuthorized } from "./terminal-authz";
|
|
39
40
|
import { resolveBrowserStream } from "./routes/browser";
|
|
@@ -84,16 +85,16 @@ import type { Command } from "./types";
|
|
|
84
85
|
//
|
|
85
86
|
// `relay-version` is still tracked for observability/telemetry, but it no longer
|
|
86
87
|
// triggers restarts.
|
|
87
|
-
export function reconcileRelayVersion(): void {
|
|
88
|
+
export async function reconcileRelayVersion(): Promise<void> {
|
|
88
89
|
const storedVersion = getConfig<string>("system", "relay-version")?.value;
|
|
89
|
-
if (storedVersion !== VERSION)
|
|
90
|
+
if (storedVersion !== VERSION) (await setConfigAsync("system", "relay-version", VERSION, "server-startup"));
|
|
90
91
|
|
|
91
92
|
const storedProtocol = getConfig<string>("system", "runner-protocol-version")?.value;
|
|
92
93
|
const currentProtocol = String(CONTRACT_VERSIONS.runnerProtocol);
|
|
93
94
|
if (storedProtocol === currentProtocol) return;
|
|
94
95
|
// Skip the very first run (no stored protocol yet) — that's initial bootstrap, not an upgrade.
|
|
95
96
|
if (storedProtocol) getLifecycleManager().onRelayUpdated(VERSION);
|
|
96
|
-
|
|
97
|
+
(await setConfigAsync("system", "runner-protocol-version", currentProtocol, "server-startup"));
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
export function runServerStartupRecoveries(): ReturnType<typeof sweepDeferredLineageReportsOnStartup> {
|
|
@@ -116,9 +117,7 @@ export async function startServer(): Promise<void> {
|
|
|
116
117
|
timeStartupPhase("server.database", () => initDb(DB_PATH));
|
|
117
118
|
// #1960 T2 — stand the gateway up EXPLICITLY here rather than leaving it to the lazy self-init
|
|
118
119
|
// (`getGateway()` on first call, gateway.ts), which would race the rest of this boot sequence.
|
|
119
|
-
//
|
|
120
|
-
// ADR 0001 §7 Phase 1); the writer is deliberately left at its in-process default — Phase W (a
|
|
121
|
-
// worker writer surrendering the sole RW connection) remains the #1870 migration.
|
|
120
|
+
// File-backed reads and writes default off-loop; memory fixtures retain local connections (#3339).
|
|
122
121
|
timeStartupPhase("server.gateway", () => initGateway({ readerBackend: gatewayReaderBackendFromEnv() }));
|
|
123
122
|
onShutdown("gateway-reader", shutdownGatewayReader);
|
|
124
123
|
onShutdown("gateway-writer", drainWriterOnShutdown, "drain");
|
|
@@ -137,11 +136,11 @@ export async function startServer(): Promise<void> {
|
|
|
137
136
|
const finishServices = beginStartupPhase("server.services");
|
|
138
137
|
installNotificationSubscriptionRule();
|
|
139
138
|
startPlanLiveBindings();
|
|
140
|
-
reconcileRelayVersion();
|
|
139
|
+
(await reconcileRelayVersion());
|
|
141
140
|
// Register the ntfy escalation channel row from persisted connector config BEFORE the
|
|
142
141
|
// warning check, so a correctly-configured relay doesn't spuriously warn while the async
|
|
143
142
|
// channels-host connect is still in flight (#1510).
|
|
144
|
-
ensureEscalationChannelRegistered();
|
|
143
|
+
(await ensureEscalationChannelRegistered());
|
|
145
144
|
warnIfNoHumanReachableEscalationTarget();
|
|
146
145
|
startEventLoopLagSampler();
|
|
147
146
|
// #1882 (S5, ADR 0001 §5) — per-callsite on-loop-block attribution + the >50ms regression alarm.
|
|
@@ -167,12 +166,12 @@ export async function startServer(): Promise<void> {
|
|
|
167
166
|
},
|
|
168
167
|
});
|
|
169
168
|
} else {
|
|
170
|
-
setInterval(() => {
|
|
169
|
+
setInterval(framed("timer automations", () => {
|
|
171
170
|
void timePostBindPhase("automations.reconcile", reconcileAutomationRuns)
|
|
172
171
|
.then(emitAutomationReconciliations)
|
|
173
172
|
.catch((e) => console.error(`[automations] reconcile failed: ${errMessage(e)}`));
|
|
174
173
|
void timePostBindPhase("automations.dispatch", runDueAutomationsAsync).then(emitAutomationDispatches).catch((e) => console.error(`[automations] dispatch failed: ${errMessage(e)}`));
|
|
175
|
-
}, AUTOMATION_INTERVAL_MS);
|
|
174
|
+
}), AUTOMATION_INTERVAL_MS);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
installTimerIngressSchema();
|
|
@@ -186,7 +185,7 @@ export async function startServer(): Promise<void> {
|
|
|
186
185
|
websocket: createWebSocketHandlers(),
|
|
187
186
|
}));
|
|
188
187
|
markServerBound();
|
|
189
|
-
startServerBackgroundJobs();
|
|
188
|
+
await startServerBackgroundJobs();
|
|
190
189
|
finishStartup();
|
|
191
190
|
console.log(`humwick ${VERSION} running on http://${HOST}:${PORT}`);
|
|
192
191
|
|
|
@@ -280,12 +279,12 @@ export function createFetchHandler(
|
|
|
280
279
|
const terminalStreamMatch = url.pathname.match(/^\/api\/orchestrators\/([^/]+)\/terminal\/([^/]+)\/stream$/);
|
|
281
280
|
if (req.method === "GET" && terminalStreamMatch) {
|
|
282
281
|
if (!server) return new Response("WebSocket upgrade unavailable", { status: 400 });
|
|
283
|
-
if (!isOriginAllowed(req)) return authorityDenied(req, "origin not allowed", 403);
|
|
284
|
-
if (!isAuthorized(req)) return applyCors(req, authorityDenied(req, "unauthorized", 401));
|
|
282
|
+
if (!isOriginAllowed(req)) return (await authorityDenied(req, "origin not allowed", 403));
|
|
283
|
+
if (!isAuthorized(req)) return applyCors(req, (await authorityDenied(req, "unauthorized", 401)));
|
|
285
284
|
const orchestratorId = decodeURIComponent(terminalStreamMatch[1]!);
|
|
286
285
|
const session = decodeURIComponent(terminalStreamMatch[2]!);
|
|
287
286
|
if (!isTerminalSessionRequestAuthorized(req, orchestratorId, session, "read")) {
|
|
288
|
-
return applyCors(req, authorityDenied(req, "forbidden"));
|
|
287
|
+
return applyCors(req, (await authorityDenied(req, "forbidden")));
|
|
289
288
|
}
|
|
290
289
|
const writeAuthorized = isTerminalSessionRequestAuthorized(req, orchestratorId, session, "write");
|
|
291
290
|
const orch = getOrchestrator(orchestratorId);
|
|
@@ -312,7 +311,7 @@ export function createFetchHandler(
|
|
|
312
311
|
}
|
|
313
312
|
const matched = matchRoute(req.method, url.pathname);
|
|
314
313
|
if (!isOriginAllowed(req) && !(req.headers.get("origin") === "null" && matched?.handler === postSecretUpload)) {
|
|
315
|
-
return authorityDenied(req, "origin not allowed", 403);
|
|
314
|
+
return (await authorityDenied(req, "origin not allowed", 403));
|
|
316
315
|
}
|
|
317
316
|
|
|
318
317
|
// KAPI-1 §4.2 — the live `/api/kernel` control mount. Handled as a dedicated subtree (not through
|
|
@@ -322,7 +321,7 @@ export function createFetchHandler(
|
|
|
322
321
|
// default-denies. A request that fails the coarse gate 403s here (matching the resolver's own 403 for
|
|
323
322
|
// a missing/invalid/non-component credential), including `GET /api/kernel/openapi.json`.
|
|
324
323
|
if (isKernelControlPath(url.pathname)) {
|
|
325
|
-
if (!isKernelControlRequestAuthorized(req)) return applyCors(req, authorityDenied(req, "forbidden"));
|
|
324
|
+
if (!isKernelControlRequestAuthorized(req)) return applyCors(req, (await authorityDenied(req, "forbidden")));
|
|
326
325
|
const response = await handleKernelControlRequest(req);
|
|
327
326
|
applyCors(req, response);
|
|
328
327
|
if (logRequests) console.log(`${req.method} ${url.pathname} → ${response.status}`);
|
|
@@ -367,11 +366,11 @@ export function createFetchHandler(
|
|
|
367
366
|
// full server scope), so it stays the authoritative reject for a revoked/
|
|
368
367
|
// invalid session token — it just answers in the caller's protocol.
|
|
369
368
|
if (url.pathname === "/api/mcp") return unauthorizedMcp(req);
|
|
370
|
-
return applyCors(req, authorityDenied(req, "unauthorized", 401));
|
|
369
|
+
return applyCors(req, (await authorityDenied(req, "unauthorized", 401)));
|
|
371
370
|
}
|
|
372
|
-
if (!handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return applyCors(req, authorityDenied(req, "forbidden"));
|
|
371
|
+
if (!handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return applyCors(req, (await authorityDenied(req, "forbidden")));
|
|
373
372
|
}
|
|
374
|
-
if (componentAuth && !handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return applyCors(req, authorityDenied(req, "forbidden"));
|
|
373
|
+
if (componentAuth && !handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return applyCors(req, (await authorityDenied(req, "forbidden")));
|
|
375
374
|
}
|
|
376
375
|
const response = await runMatchedRoute(matched, req, (body) => maybeCompressApiResponse(req, url, body));
|
|
377
376
|
applyCors(req, response);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { getGateway } from "../db/gateway";
|
|
2
|
+
import type { admitAgentActionBody } from "../db/agent-action-write";
|
|
1
3
|
import { isHumanAgentId } from "@humwick/sdk";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { getCommand } from "../commands-db";
|
|
6
|
+
import { getAgentEffortState } from "../db/agent-effort-state";
|
|
5
7
|
import { emitEffortState } from "./agent-effort";
|
|
6
|
-
import { ValidationError, createActivityEventAsync, getAgent
|
|
8
|
+
import { ValidationError, createActivityEventAsync, getAgent } from "../db";
|
|
7
9
|
import { emitCommandEvent } from "../command-events";
|
|
8
10
|
import { emitActivityEvent, emitNewMessage } from "../sse";
|
|
9
11
|
import { effectiveProviderCatalogList } from "../provider-catalog-store";
|
|
@@ -112,15 +114,6 @@ function catalogEffort(agent: AgentCard, effort: string | undefined): { level: s
|
|
|
112
114
|
return { level, efforts: known?.efforts };
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
function sessionMessageForInject(agentId: string, ctx: AuthContext, body: string) {
|
|
116
|
-
const from = ctx.callerAgentId && getAgent(ctx.callerAgentId) ? ctx.callerAgentId : "system";
|
|
117
|
-
return sendMessage({
|
|
118
|
-
from,
|
|
119
|
-
to: agentId,
|
|
120
|
-
kind: "session",
|
|
121
|
-
body,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
117
|
|
|
125
118
|
export function revalidateAgentInstance(snapshot: AgentCard): AgentCard {
|
|
126
119
|
const current = getAgent(snapshot.id);
|
|
@@ -136,7 +129,7 @@ export async function dispatchAgentAction(input: AgentActionInput, ctx: AuthCont
|
|
|
136
129
|
|
|
137
130
|
// #2950 — an agent may change its own effort (only that), gated by its spawn's selfEffort ceiling below.
|
|
138
131
|
const self = input.action === "effort" && Boolean(ctx.callerAgentId) && ctx.callerAgentId === agent.id;
|
|
139
|
-
if (!self) authorizeShutdown(ctx, { agentId: input.agentId, requestedVia: input.requestedVia }, input.action);
|
|
132
|
+
if (!self) (await authorizeShutdown(ctx, { agentId: input.agentId, requestedVia: input.requestedVia }, input.action));
|
|
140
133
|
const requestId = input.action === "effort" ? input.requestId?.trim() || undefined : undefined;
|
|
141
134
|
if (requestId && !REQUEST_ID.test(requestId)) throw new ValidationError("requestId must be a UUID");
|
|
142
135
|
const retried = admittedEffortRequest(agent.id, requestId);
|
|
@@ -210,24 +203,12 @@ export async function dispatchAgentAction(input: AgentActionInput, ctx: AuthCont
|
|
|
210
203
|
// a stale command built from the predecessor's capabilities/session data.
|
|
211
204
|
const event = await createActivityEventAsync(activityInput);
|
|
212
205
|
const current = revalidateAgentInstance(agent);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
// A concurrent retry of the same request committed first.
|
|
220
|
-
if ((raced = admittedEffortRequest(current.id, requestId))) return raced.command;
|
|
221
|
-
// #2966 — a revoke committed during the audit await wins: recheck where the command is inserted.
|
|
222
|
-
if (selfPolicy) assertSelfEffortAllowed(current, requested!.level, requested!.efforts);
|
|
223
|
-
if (effort) {
|
|
224
|
-
// #2984 — the desired version is the change; the command is its audit row, never executed.
|
|
225
|
-
admitted = admitDesiredEffortInternal({ agentId: current.id, effort, role: requestedByRole, commandId, now: Date.now() });
|
|
226
|
-
commandInput.params.effortVersion = admitted.state.desiredVersion;
|
|
227
|
-
}
|
|
228
|
-
return createCommandInternal({ input: commandInput, id: commandId });
|
|
229
|
-
});
|
|
230
|
-
if (raced) return raced;
|
|
206
|
+
const from = ctx.callerAgentId && getAgent(ctx.callerAgentId) ? ctx.callerAgentId : "system";
|
|
207
|
+
const result = await getGateway().write("agents.admitAction", { agent: current, commandId, commandInput, requestId, effort, role: requestedByRole,
|
|
208
|
+
...(selfPolicy ? { selfEffort: requested } : {}), ...(body ? { message: { from, to: current.id, kind: "session", body } } : {}) }, { idempotencyKey: commandId }) as unknown as ReturnType<typeof admitAgentActionBody>;
|
|
209
|
+
const { command } = result;
|
|
210
|
+
if (result.raced) return { action: input.action, command, effortState: result.effortState };
|
|
211
|
+
const { admitted, sessionMessage } = result;
|
|
231
212
|
if (sessionMessage) emitNewMessage(sessionMessage);
|
|
232
213
|
for (const id of admitted?.superseded ?? []) { const canceled = getCommand(id); if (canceled) emitCommandEvent(canceled, "command.canceled"); }
|
|
233
214
|
// No `command.requested` for effort: nothing executes the audit row. `command.pending` only tells the target runner to reconcile.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { getGateway } from "../db/gateway";
|
|
2
|
+
import type { observeEffortBody } from "../db/agent-effort-observe";
|
|
3
|
+
import { sendMessageAsync } from "../db/message-send";
|
|
1
4
|
import type { AgentEffortState } from "@humwick/sdk";
|
|
2
5
|
import { getCommand } from "../commands-db";
|
|
3
6
|
import { emitCommandEvent } from "../command-events";
|
|
4
|
-
import { findMessageByIdempotencyKey,
|
|
7
|
+
import { findMessageByIdempotencyKey, validateAgentSession, writeTransaction } from "../db";
|
|
5
8
|
import { EffortReportRejected, recordEffortObservedInternal, type EffortObservation } from "../db/agent-effort-state";
|
|
6
9
|
import { emitRelayEvent } from "../events";
|
|
7
10
|
import { emitNewMessage } from "../sse";
|
|
@@ -9,47 +12,19 @@ import type { Command, Message } from "../types";
|
|
|
9
12
|
|
|
10
13
|
export { EffortReportRejected };
|
|
11
14
|
|
|
12
|
-
/** #2984 — the prompt that resumes an agent once its own effort change applied. */
|
|
13
|
-
export function selfEffortContinuation(effort: string): string {
|
|
14
|
-
return `Effort is now ${effort}. Continue your work.`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
15
|
/** Dashboard + runner notice: the agent's desired/observed effort changed. */
|
|
18
16
|
export function emitEffortState(state: AgentEffortState): void {
|
|
19
17
|
emitRelayEvent({ type: "agent.effort", source: "server", subject: state.agentId, data: state as unknown as Record<string, unknown> });
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
/** One continuation per owed resume (keyed by the self request's version): a repeated report finds the first. */
|
|
23
|
-
function enqueueSelfContinuation(agentId: string, version: number, effort: string): Message | undefined {
|
|
24
|
-
const idempotencyKey = `effort-continuation:${agentId}:${version}`;
|
|
25
|
-
if (findMessageByIdempotencyKey("system", idempotencyKey)) return undefined;
|
|
26
|
-
return sendMessage({
|
|
27
|
-
from: "system",
|
|
28
|
-
to: agentId,
|
|
29
|
-
kind: "system",
|
|
30
|
-
subject: "Effort changed",
|
|
31
|
-
body: selfEffortContinuation(effort),
|
|
32
|
-
replyExpected: false,
|
|
33
|
-
idempotencyKey,
|
|
34
|
-
payload: { kind: "agent.effort_continuation", agentId, effortVersion: version, effort },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
20
|
export interface EffortReport { instanceId: string; version: number; effort: string; previousEffort: string | null }
|
|
39
21
|
|
|
40
|
-
export
|
|
22
|
+
export { EffortReportFenced, selfEffortContinuation } from "../db/agent-effort-observe";
|
|
41
23
|
|
|
42
24
|
/** A runner applied (or verified) a desired version. Fenced to the agent's current incarnation; the
|
|
43
25
|
* observation and the resume it settles are one transaction. */
|
|
44
|
-
export function reportEffortObserved(agentId: string, report: EffortReport, now = Date.now()): EffortObservation {
|
|
45
|
-
|
|
46
|
-
const observed = writeTransaction(() => {
|
|
47
|
-
const session = validateAgentSession(agentId, { instanceId: report.instanceId });
|
|
48
|
-
if (!session.ok) throw new EffortReportFenced(session.error ?? "stale agent instance");
|
|
49
|
-
const result = recordEffortObservedInternal({ agentId, ...report, now });
|
|
50
|
-
if (result.outcome !== "stale" && result.resumeVersion !== undefined) continuation = enqueueSelfContinuation(agentId, result.resumeVersion, report.effort);
|
|
51
|
-
return result;
|
|
52
|
-
});
|
|
26
|
+
export async function reportEffortObserved(agentId: string, report: EffortReport, now = Date.now()): Promise<EffortObservation> {
|
|
27
|
+
const { observed, continuation } = await getGateway().write("agents.observeEffort", { agentId, ...report, now }) as unknown as ReturnType<typeof observeEffortBody>;
|
|
53
28
|
if (observed.outcome === "applied") {
|
|
54
29
|
const command: Command | null = observed.commandId ? getCommand(observed.commandId) : null;
|
|
55
30
|
if (command) emitCommandEvent(command, "command.succeeded");
|
|
@@ -386,7 +386,7 @@ async function deliverSelfCheck(agent: AgentCard, decision: AgentHangDecision, c
|
|
|
386
386
|
},
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
-
const messages = routeNotification({
|
|
389
|
+
const messages = await routeNotification({
|
|
390
390
|
type: "agent.self_check",
|
|
391
391
|
scope: { agentId: agent.id, parent: agent.spawnedBy, teamId: agent.teamId },
|
|
392
392
|
// Explicit, though the registered `subject-agent` rule resolves the same single recipient: the
|
|
@@ -457,7 +457,7 @@ async function escalate(
|
|
|
457
457
|
|
|
458
458
|
// Reuse the `agent.stuck_busy` notice shape (type, spawn-parent audience, `payload.kind`) so every
|
|
459
459
|
// existing coordinator-side consumer renders it with no new case to handle.
|
|
460
|
-
const messages = routeNotification({
|
|
460
|
+
const messages = await routeNotification({
|
|
461
461
|
type: "agent.stuck_busy",
|
|
462
462
|
scope: { agentId: agent.id, parent: agent.spawnedBy, teamId: agent.teamId },
|
|
463
463
|
message: {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// The agent liveness exchange (#3606/#3608/#3618): one service behind POST /api/agents/:id/heartbeat
|
|
2
|
+
// and the bus `agent.heartbeat` command frame, so both transports apply status, readiness, the
|
|
3
|
+
// unread inbox and the quota piggyback identically. Transports parse and authorize at their edge.
|
|
4
|
+
import { isRecord, type HeartbeatResult, type ProviderQuotaUpdateInput } from "@humwick/sdk";
|
|
5
|
+
import { ValidationError, getAgent, markReadyAsync, recordHeartbeatAsync, setStatusAsync, validateAgentSession } from "../db";
|
|
6
|
+
import { isAgentUnavailable } from "../db/agent-predicates";
|
|
7
|
+
import type { ViewerScope } from "../db/viewer-scope";
|
|
8
|
+
import { emitAgentStatus } from "../agent-status-events";
|
|
9
|
+
import { VALID_AGENT_STATUSES, agentSessionStatus, auditEvent } from "../routes/_shared";
|
|
10
|
+
import { pollAgentMessages } from "../routes/messages";
|
|
11
|
+
import { cleanProviderQuotaUpdate } from "../routes/provider-quotas";
|
|
12
|
+
import { publishHeartbeatQuota } from "../routes/agent-heartbeat-quota";
|
|
13
|
+
import { announceSpawnedAgentReady } from "./register-agent";
|
|
14
|
+
import { flushQueuedDirectMessages } from "./managed-running";
|
|
15
|
+
import { acquireTeamOnReady } from "./team";
|
|
16
|
+
import { isComponentAuthorizedFor } from "../security";
|
|
17
|
+
import type { AgentCard, AgentSessionGuard, ComponentToken } from "../types";
|
|
18
|
+
|
|
19
|
+
type AgentStatusValue = (typeof VALID_AGENT_STATUSES)[number];
|
|
20
|
+
|
|
21
|
+
export interface AgentHeartbeatReport {
|
|
22
|
+
status?: AgentStatusValue;
|
|
23
|
+
ready?: boolean;
|
|
24
|
+
unread?: boolean;
|
|
25
|
+
providerQuota?: ProviderQuotaUpdateInput;
|
|
26
|
+
quotaLeaseToken?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AgentHeartbeatOutcome = { ok: true; result: HeartbeatResult } | { ok: false; error: string; status: number };
|
|
30
|
+
|
|
31
|
+
type AgentActivityState = {
|
|
32
|
+
key: "available" | "busy" | "not-ready" | "offline";
|
|
33
|
+
title: string;
|
|
34
|
+
icon: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function agentActivityState(agent?: Pick<AgentCard, "ready" | "status"> | null): AgentActivityState | null {
|
|
38
|
+
if (!agent) return null;
|
|
39
|
+
if (agent.status === "offline") return { key: "offline", title: "Agent offline", icon: "ti-plug-off" };
|
|
40
|
+
if (!agent.ready) return { key: "not-ready", title: "Agent not ready", icon: "ti-loader" };
|
|
41
|
+
if (agent.status === "busy") return { key: "busy", title: "Agent busy", icon: "ti-activity" };
|
|
42
|
+
return { key: "available", title: "Agent available", icon: "ti-circle-check" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function auditAgentStateTransition(agentId: string, before: AgentCard | null | undefined, after: AgentCard | null | undefined): Promise<void> {
|
|
46
|
+
const previous = agentActivityState(before);
|
|
47
|
+
const next = agentActivityState(after);
|
|
48
|
+
if (!next || previous?.key === next.key) return;
|
|
49
|
+
await auditEvent({
|
|
50
|
+
clientId: "server-agent-" + agentId + "-state-" + next.key + "-" + Date.now(),
|
|
51
|
+
kind: "state",
|
|
52
|
+
title: next.title,
|
|
53
|
+
meta: agentId,
|
|
54
|
+
icon: next.icon,
|
|
55
|
+
view: "agents",
|
|
56
|
+
agentId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cleanAgentStatus(value: unknown): AgentStatusValue {
|
|
61
|
+
if (!VALID_AGENT_STATUSES.includes(value as AgentStatusValue)) throw new ValidationError(`status must be one of: ${VALID_AGENT_STATUSES.join(", ")}`);
|
|
62
|
+
return value as AgentStatusValue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Validates a wire heartbeat body; throws ValidationError. `sourceAgentId` is always the reporting agent. */
|
|
66
|
+
export function cleanHeartbeatReport(agentId: string, body: unknown): AgentHeartbeatReport {
|
|
67
|
+
const raw = isRecord(body) ? body : {};
|
|
68
|
+
if (raw.ready !== undefined && typeof raw.ready !== "boolean") throw new ValidationError("ready must be a boolean");
|
|
69
|
+
if (raw.unread !== undefined && typeof raw.unread !== "boolean") throw new ValidationError("unread must be a boolean");
|
|
70
|
+
const quota = raw.providerQuota === undefined ? undefined : cleanProviderQuotaUpdate(raw.providerQuota);
|
|
71
|
+
if (typeof quota === "string") throw new ValidationError(quota);
|
|
72
|
+
if (quota) quota.sourceAgentId = agentId;
|
|
73
|
+
return {
|
|
74
|
+
...(raw.status === undefined ? {} : { status: cleanAgentStatus(raw.status) }),
|
|
75
|
+
...(raw.ready === undefined ? {} : { ready: raw.ready }),
|
|
76
|
+
...(raw.unread ? { unread: true } : {}),
|
|
77
|
+
...(quota ? { providerQuota: quota } : {}),
|
|
78
|
+
...(typeof raw.quotaLeaseToken === "string" ? { quotaLeaseToken: raw.quotaLeaseToken } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// #3606 — a reported status/readiness equal to the live card is a liveness tick, not a transition: no
|
|
83
|
+
// row write, no agent.status event. Presence still advances through the coalescing heartbeat buffer,
|
|
84
|
+
// except for a terminal row, which a beat would resurrect.
|
|
85
|
+
export async function noteUnchangedLifecycle(agentId: string, current: AgentCard, guard: AgentSessionGuard | undefined): Promise<void> {
|
|
86
|
+
if (current.status !== "offline" && current.status !== "stale") await recordHeartbeatAsync(agentId, { guard });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function applyAgentStatus(agentId: string, status: AgentStatusValue, guard: AgentSessionGuard | undefined, before: AgentCard | null): Promise<boolean> {
|
|
90
|
+
// #1895 (S3.2 L10, review finding 3) — routed through the async gateway facade.
|
|
91
|
+
if (!(await setStatusAsync(agentId, status, guard))) return false;
|
|
92
|
+
await auditAgentStateTransition(agentId, before, getAgent(agentId));
|
|
93
|
+
await emitAgentStatus(agentId);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function applyAgentReady(agentId: string, ready: boolean, guard: AgentSessionGuard | undefined, before: AgentCard | null): Promise<boolean> {
|
|
98
|
+
// #1895 (S3.2 L10, review finding 3) — routed through the async gateway facade.
|
|
99
|
+
if (!(await markReadyAsync(agentId, ready, guard))) return false;
|
|
100
|
+
await auditAgentStateTransition(agentId, before, getAgent(agentId));
|
|
101
|
+
// #308/#2432 — a spawned child's false→true ready wakes its parent (announceSpawnedAgentReady dedups).
|
|
102
|
+
// #1927 — the awaited audit lets a same-instance PATCH flip ready back first, so re-read the live
|
|
103
|
+
// card and session guard before firing, never the pre-audit snapshot.
|
|
104
|
+
if (ready && !before?.ready) {
|
|
105
|
+
const live = getAgent(agentId);
|
|
106
|
+
const stillCurrentInstance = validateAgentSession(agentId, guard).ok;
|
|
107
|
+
if (live?.ready === true && stillCurrentInstance) {
|
|
108
|
+
await announceSpawnedAgentReady(agentId, "agent-ready");
|
|
109
|
+
(await acquireTeamOnReady(agentId)); // #3107 — before the flush, so seat mail reaches a new holder
|
|
110
|
+
// #416 — also flush queued direct messages: a not-ready agent that becomes ready
|
|
111
|
+
// may have queued messages waiting (e.g. starting up, clearing context).
|
|
112
|
+
(await flushQueuedDirectMessages(agentId));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await emitAgentStatus(agentId);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One liveness tick. Only a value that differs from the live card takes the transition path; the rest is a coalesced beat. */
|
|
120
|
+
export async function applyAgentHeartbeat(agentId: string, report: AgentHeartbeatReport, guard: AgentSessionGuard | undefined, viewer: ViewerScope | null): Promise<AgentHeartbeatOutcome> {
|
|
121
|
+
const session = validateAgentSession(agentId, guard);
|
|
122
|
+
if (!session.ok) return { ok: false, error: session.error!, status: agentSessionStatus(session.error) };
|
|
123
|
+
// #416 — capture unavailability before heartbeat to detect the transition.
|
|
124
|
+
const beforeHeartbeat = getAgent(agentId);
|
|
125
|
+
if (beforeHeartbeat?.kind === "channel" && !beforeHeartbeat.ready) return { ok: false, error: "channel agent not ready; re-register required", status: 409 };
|
|
126
|
+
const notFound = { ok: false, error: "agent not found", status: 404 } as const;
|
|
127
|
+
const wasUnavailable = beforeHeartbeat ? isAgentUnavailable(beforeHeartbeat) : false;
|
|
128
|
+
let current = beforeHeartbeat;
|
|
129
|
+
const { status, ready } = report;
|
|
130
|
+
if (status !== undefined && status !== current?.status) {
|
|
131
|
+
if (!(await applyAgentStatus(agentId, status, guard, current))) return notFound;
|
|
132
|
+
current = getAgent(agentId);
|
|
133
|
+
}
|
|
134
|
+
if (ready !== undefined && ready !== current?.ready) {
|
|
135
|
+
if (!(await applyAgentReady(agentId, ready, guard, current))) return notFound;
|
|
136
|
+
}
|
|
137
|
+
// A reported terminal status must not be undone by the beat's offline/stale resurrection.
|
|
138
|
+
if (status !== "offline" && status !== "stale") {
|
|
139
|
+
// #1468 — route the API heartbeat through the same coalescing buffer as the bus path.
|
|
140
|
+
if (!(await recordHeartbeatAsync(agentId, { guard }))) return notFound;
|
|
141
|
+
}
|
|
142
|
+
if (wasUnavailable) {
|
|
143
|
+
const after = getAgent(agentId);
|
|
144
|
+
if (after && !isAgentUnavailable(after)) (await flushQueuedDirectMessages(agentId));
|
|
145
|
+
}
|
|
146
|
+
const result: HeartbeatResult = { ok: true };
|
|
147
|
+
if (report.unread) result.messages = await pollAgentMessages(viewer, { for: agentId, unread: true, limit: 100, turnDelivery: true });
|
|
148
|
+
if (report.providerQuota) {
|
|
149
|
+
try { result.providerQuota = await publishHeartbeatQuota(report.providerQuota, report.quotaLeaseToken); }
|
|
150
|
+
catch (error) { console.warn(`heartbeat quota publish failed for ${agentId}:`, error); }
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, result };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** #3618 — the bus `agent.heartbeat` frame. A socket reports only for the agent it registered as,
|
|
156
|
+
* under that registration's session guard and the scopes its upgrade credential carries. */
|
|
157
|
+
export async function heartbeatFromBus(conn: { agentId?: string; instanceId: string; epoch: number; componentAuth?: ComponentToken }, target: string, params: Record<string, unknown>): Promise<{ status: "succeeded" | "failed" | "rejected"; result?: Record<string, unknown>; error?: string }> {
|
|
158
|
+
if (!conn.agentId || target !== conn.agentId) return { status: "rejected", error: "agent.heartbeat reports only for the registered agent" };
|
|
159
|
+
let report: AgentHeartbeatReport;
|
|
160
|
+
try { report = cleanHeartbeatReport(conn.agentId, params); }
|
|
161
|
+
catch (error) { if (error instanceof ValidationError) return { status: "failed", error: error.message }; throw error; }
|
|
162
|
+
const denied = (scope: string) => conn.componentAuth && !isComponentAuthorizedFor(conn.componentAuth, { scope, resource: { agentId: conn.agentId, target: conn.agentId } });
|
|
163
|
+
if ((report.unread && denied("message:read")) || (report.providerQuota && denied("quota:write"))) return { status: "rejected", error: "component token lacks heartbeat scope" };
|
|
164
|
+
const outcome = await applyAgentHeartbeat(conn.agentId, report, { instanceId: conn.instanceId, epoch: conn.epoch }, null);
|
|
165
|
+
return outcome.ok ? { status: "succeeded", result: outcome.result as unknown as Record<string, unknown> } : { status: "failed", error: outcome.error };
|
|
166
|
+
}
|
|
@@ -10,7 +10,7 @@ export async function setAgentReportVerbosity(input: {
|
|
|
10
10
|
requestedVia: "http" | "mcp";
|
|
11
11
|
reason?: string;
|
|
12
12
|
}, ctx: AuthContext): Promise<{ action: "reportVerbosity"; reportVerbosity: ReportVerbosity }> {
|
|
13
|
-
authorizeShutdown(ctx, { agentId: input.agentId, requestedVia: input.requestedVia });
|
|
13
|
+
(await authorizeShutdown(ctx, { agentId: input.agentId, requestedVia: input.requestedVia }));
|
|
14
14
|
const agent = getAgent(input.agentId);
|
|
15
15
|
if (!agent) throw new ShutdownTargetError("agent not found");
|
|
16
16
|
if (!isReportVerbosity(input.reportVerbosity)) throw new ValidationError("reportVerbosity must be off, quiet, normal or verbose");
|
|
@@ -34,7 +34,7 @@ export async function reconcileBrowserInventory(orchestratorId: string, inventor
|
|
|
34
34
|
const live = new Set(inventory.liveSessionIds);
|
|
35
35
|
const closed = new Set(inventory.closedSessionIds);
|
|
36
36
|
for (const row of listBrowserSessionRows({ orchestratorId, activeOnly: true, limit: 1000 })) {
|
|
37
|
-
if (getAgent(row.agentId)?.status === "offline") reapAgentBrowserSessions(row.agentId);
|
|
37
|
+
if (getAgent(row.agentId)?.status === "offline") (await reapAgentBrowserSessions(row.agentId));
|
|
38
38
|
const idleMs = inventory.idleMs?.[row.id];
|
|
39
39
|
if (idleMs !== undefined) noteBrowserActivity(row, idleMs, Date.now());
|
|
40
40
|
if (live.has(row.id) || (!registration && !closed.has(row.id))) continue;
|
|
@@ -44,9 +44,9 @@ export async function reconcileBrowserInventory(orchestratorId: string, inventor
|
|
|
44
44
|
if (command?.status === "pending") continue;
|
|
45
45
|
}
|
|
46
46
|
const now = Date.now();
|
|
47
|
-
transitionAndEmit(row.id, [row.status], {
|
|
47
|
+
(await transitionAndEmit(row.id, [row.status], {
|
|
48
48
|
status: row.status === "stopping" || closed.has(row.id) ? "stopped" : "failed", stoppedAt: now,
|
|
49
49
|
error: registration ? "browser absent from orchestrator registration" : "browser process ended on host",
|
|
50
|
-
}, now);
|
|
50
|
+
}, now));
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getGateway } from "../db/gateway";
|
|
1
2
|
// #3489 H4 — the one write path for browser session rows after insert: every change is pushed as
|
|
2
3
|
// `browser.session.<status>`, so the dashboard follows starts, stops, idle expiry and crashes without polling.
|
|
3
4
|
import type { BrowserSession, BrowserSessionStatus } from "@humwick/sdk";
|
|
@@ -26,8 +27,8 @@ export function emitBrowserSession(row: BrowserSessionRow): void {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/** Applies a guarded transition and announces it; undefined when the row already left `from`. */
|
|
29
|
-
export function transitionAndEmit(id: string, from: readonly BrowserSessionStatus[], patch: BrowserSessionPatch, now: number): BrowserSessionRow | undefined {
|
|
30
|
-
const row =
|
|
30
|
+
export async function transitionAndEmit(id: string, from: readonly BrowserSessionStatus[], patch: BrowserSessionPatch, now: number): Promise<BrowserSessionRow | undefined> {
|
|
31
|
+
const row = await getGateway().write("browser.transition", [id, from, patch, now]) as unknown as BrowserSessionRow | undefined;
|
|
31
32
|
if (!row) return undefined;
|
|
32
33
|
if (row.status === "running") lastActivity.set(row.id, lastActivity.get(row.id) ?? now);
|
|
33
34
|
else if (row.status === "stopped" || row.status === "failed") lastActivity.delete(row.id);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getGateway } from "../db/gateway";
|
|
2
|
+
import type { startBrowserBody, stopBrowserBody } from "../db/browser-session-write";
|
|
1
3
|
// #3489 — Relay-managed browser sessions. The server owns identity, limits and lifecycle state; the agent's own
|
|
2
4
|
// orchestrator owns the Chromium process and executes `browser.start` / `browser.stop` from the command bus.
|
|
3
5
|
// Command results are folded into the row as they arrive (and again on read, which also settles a missed event).
|
|
@@ -6,14 +8,14 @@ import { isRecord, stringValue, type BrowserLimits, type BrowserSession } from "
|
|
|
6
8
|
import { getBrowserLimits } from "../browser-config";
|
|
7
9
|
import { canAccessBrowserSession, type BrowserCaller } from "../browser-authz";
|
|
8
10
|
import { emitCommandEvent } from "../command-events";
|
|
9
|
-
|
|
11
|
+
|
|
10
12
|
import { subscribeRelayEvents } from "../events";
|
|
11
13
|
import type { Command } from "../types";
|
|
12
14
|
import { getCommandAsync } from "../commands-write";
|
|
13
15
|
import { getAgentProfile } from "../config-store";
|
|
14
16
|
import { getAgent, getOrchestrator } from "../db";
|
|
15
17
|
import {
|
|
16
|
-
|
|
18
|
+
getBrowserSessionRow, listBrowserSessionRows, type BrowserSessionRow,
|
|
17
19
|
} from "../db/browser-sessions";
|
|
18
20
|
import { getLaunchFact } from "../db/launch-facts";
|
|
19
21
|
import { generationLaunchParams } from "../db/generation-launch";
|
|
@@ -67,17 +69,13 @@ export async function startBrowserSession(caller: BrowserCaller, input: { agentI
|
|
|
67
69
|
const limits = getBrowserLimits();
|
|
68
70
|
// Settle finished stops first, so a reaped session stops counting against the limits.
|
|
69
71
|
for (const row of [...listBrowserSessionRows({ agentId, activeOnly: true, limit: 100 }), ...listBrowserSessionRows({ orchestratorId: host.id, activeOnly: true, limit: 100 })]) await reconcile(row);
|
|
70
|
-
// Count and insert run in one synchronous stretch, so two concurrent starts cannot both pass the cap.
|
|
71
|
-
if (countActiveBrowserSessions({ agentId }) >= limits.perAgent) {
|
|
72
|
-
throw new BrowserSessionError(`agent ${agentId} already has ${limits.perAgent} browser session(s) (limit from settings browser/default)`, 409);
|
|
73
|
-
}
|
|
74
|
-
if (countActiveBrowserSessions({ orchestratorId: host.id }) >= limits.perHost) {
|
|
75
|
-
throw new BrowserSessionError(`host ${host.id} already runs ${limits.perHost} browser session(s) (limit from settings browser/default)`, 409);
|
|
76
|
-
}
|
|
77
72
|
const id = randomUUID();
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
const result = await getGateway().write("browser.start", { id, commandId: randomUUID(), agentId, instanceId: agent.instanceId, epoch: agent.epoch, orchestratorId: host.id, limits }, { idempotencyKey: id }) as unknown as ReturnType<typeof startBrowserBody>;
|
|
74
|
+
if (result.conflict === "agentLimit") throw new BrowserSessionError(`agent ${agentId} already has ${limits.perAgent} browser session(s) (limit from settings browser/default)`, 409);
|
|
75
|
+
if (result.conflict === "hostLimit") throw new BrowserSessionError(`host ${host.id} already runs ${limits.perHost} browser session(s) (limit from settings browser/default)`, 409);
|
|
76
|
+
if (result.conflict) throw new BrowserSessionError("browser session target changed during admission", 409);
|
|
77
|
+
const { command, row } = result;
|
|
78
|
+
emitBrowserSession(row);
|
|
81
79
|
emitCommandEvent(command, "command.requested");
|
|
82
80
|
return { session: publicView(await settle(id, signal)), limits };
|
|
83
81
|
}
|
|
@@ -92,16 +90,16 @@ async function reconcile(row: BrowserSessionRow): Promise<BrowserSessionRow> {
|
|
|
92
90
|
const patch = command.status === "succeeded"
|
|
93
91
|
? { status: "running" as const, cdpUrl: stringValue(result.cdpUrl), pid: typeof result.pid === "number" ? result.pid : undefined, profileDir: stringValue(result.profileDir) }
|
|
94
92
|
: { status: "failed" as const, error: command.error ?? `browser.start ${command.status}`, stoppedAt: now };
|
|
95
|
-
return transitionAndEmit(row.id, ["starting"], patch, now) ?? getBrowserSessionRow(row.id)!;
|
|
93
|
+
return (await transitionAndEmit(row.id, ["starting"], patch, now)) ?? getBrowserSessionRow(row.id)!;
|
|
96
94
|
}
|
|
97
95
|
if (row.status === "stopping" && row.stopCommandId) {
|
|
98
96
|
const command = await getCommandAsync(row.stopCommandId);
|
|
99
97
|
if (!command || !isTerminalCommandStatus(command.status)) {
|
|
100
98
|
if (now - row.updatedAt < SETTLE_TIMEOUT_MS) return row;
|
|
101
|
-
return transitionAndEmit(row.id, ["stopping"], { status: "failed", stoppedAt: now, error: "browser stop did not settle" }, now) ?? row;
|
|
99
|
+
return (await transitionAndEmit(row.id, ["stopping"], { status: "failed", stoppedAt: now, error: "browser stop did not settle" }, now)) ?? row;
|
|
102
100
|
}
|
|
103
101
|
const error = command.status === "succeeded" ? undefined : command.error ?? `browser.stop ${command.status}`;
|
|
104
|
-
return transitionAndEmit(row.id, ["stopping"], { status: "stopped", stoppedAt: now, ...(error ? { error } : {}) }, now) ?? getBrowserSessionRow(row.id)!;
|
|
102
|
+
return (await transitionAndEmit(row.id, ["stopping"], { status: "stopped", stoppedAt: now, ...(error ? { error } : {}) }, now)) ?? getBrowserSessionRow(row.id)!;
|
|
105
103
|
}
|
|
106
104
|
return row;
|
|
107
105
|
}
|
|
@@ -133,27 +131,23 @@ export async function listBrowserSessions(caller: BrowserCaller, filter: { agent
|
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
/** Issue `browser.stop`; idempotent for a session that is already stopping or over. */
|
|
136
|
-
function requestStop(row: BrowserSessionRow, reason: string): BrowserSessionRow {
|
|
134
|
+
async function requestStop(row: BrowserSessionRow, reason: string): Promise<BrowserSessionRow> {
|
|
137
135
|
if (row.status !== "starting" && row.status !== "running") return row;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
params: { sessionId: row.id, agentId: row.agentId, orchestratorId: host.id, reason } } });
|
|
143
|
-
const stopping = transitionAndEmit(row.id, ["starting", "running"], { status: "stopping", stopCommandId: command.id }, now);
|
|
144
|
-
emitCommandEvent(command, "command.requested");
|
|
145
|
-
return stopping ?? getBrowserSessionRow(row.id)!;
|
|
136
|
+
const result = await getGateway().write("browser.stop", { id: row.id, reason, commandId: randomUUID() }, { idempotencyKey: row.id }) as unknown as ReturnType<typeof stopBrowserBody>;
|
|
137
|
+
if (result.row) emitBrowserSession(result.row);
|
|
138
|
+
if (result.command) emitCommandEvent(result.command, "command.requested");
|
|
139
|
+
return result.row ?? row;
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
export async function stopBrowserSession(caller: BrowserCaller, id: string, signal?: AbortSignal): Promise<BrowserSession> {
|
|
149
|
-
const row = requestStop(await reconcile(accessible(caller, id)), "requested");
|
|
143
|
+
const row = await requestStop(await reconcile(accessible(caller, id)), "requested");
|
|
150
144
|
return publicView(row.status === "stopping" ? await settle(id, signal) : row);
|
|
151
145
|
}
|
|
152
146
|
|
|
153
147
|
/** Agent exit: every live session of the agent is stopped with it. Synchronous; the commands settle later. */
|
|
154
|
-
export function reapAgentBrowserSessions(agentId: string): number {
|
|
148
|
+
export async function reapAgentBrowserSessions(agentId: string): Promise<number> {
|
|
155
149
|
const live = listBrowserSessionRows({ agentId, activeOnly: true, limit: 100 }).filter((row) => row.status !== "stopping");
|
|
156
|
-
for (const row of live) requestStop(row, "agent-exit");
|
|
150
|
+
for (const row of live) await requestStop(row, "agent-exit");
|
|
157
151
|
return live.length;
|
|
158
152
|
}
|
|
159
153
|
|