@humwick/server 0.130.2 → 0.130.4
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 +175 -4
- package/package.json +8 -7
- package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-BOkmEvgZ.js} +2 -2
- package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-BkaDJp0r.js} +2 -2
- package/public/assets/{TraceView-Wimm0Czv.js → TraceView-B99Uu2yT.js} +2 -2
- package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DUIfTTFF.js} +2 -2
- package/public/assets/{activity-CrSurn-r.js → activity-ZIX0N1El.js} +2 -2
- package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-kq9TPYFw.js} +2 -2
- package/public/assets/agent-liveness-DSQY1Aa_.js +2 -0
- package/public/assets/{agent-name-DKbiIhBo.js → agent-name-C_7F4DFR.js} +2 -2
- package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-BNOcrGcY.js} +2 -2
- package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-B6yAh70a.js} +2 -2
- package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-CcrhmOzF.js} +2 -2
- package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CXSLrKiq.js} +2 -2
- package/public/assets/{agents-B-v66C-4.js → agents-DIZG97_X.js} +2 -2
- package/public/assets/{analytics-BqCb-LKi.js → analytics-rasmoPjR.js} +2 -2
- package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-tBpTrEPb.js} +2 -2
- package/public/assets/{artifacts-DJevXcvg.js → artifacts-BNkrynP1.js} +2 -2
- package/public/assets/{artifacts-D32zyw4Y.js → artifacts-Bjq-ZyBA.js} +2 -2
- package/public/assets/{automation-BpRajx_5.js → automation-Cx422L3L.js} +2 -2
- package/public/assets/{backlog-D_UjTdzX.js → backlog-RuFQ8PUu.js} +2 -2
- package/public/assets/{busy-health-N3VEh4ao.js → busy-health-BvNQ9g5x.js} +2 -2
- package/public/assets/{channels-9jopungq.js → channels-DNbKAS0I.js} +2 -2
- package/public/assets/{chat-CSXrTB3C.js → chat-Bp3Fx73h.js} +3 -3
- package/public/assets/{code-preview-p1NnmqK0.js → code-preview-KIRZA6cz.js} +2 -2
- package/public/assets/{connectors-CZdqlDNL.js → connectors-k7vO4vvc.js} +2 -2
- package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BuVoq1aw.js} +2 -2
- package/public/assets/{crate-agent-30aXTKva.js → crate-agent-irqlt4NH.js} +2 -2
- package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-D_rurNBz.js} +2 -2
- package/public/assets/{deploys-DoN6bAb5.js → deploys-yzZqIpxE.js} +2 -2
- package/public/assets/{display-BrKWFGJz.js → display-BemJG1f_.js} +2 -2
- package/public/assets/{files-D6XEu8Ng.js → files-DXWcrxOz.js} +2 -2
- package/public/assets/{fleet-C7jQWbw2.js → fleet-TEuOCZXE.js} +2 -2
- package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BGoDAFI-.js} +2 -2
- package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-BB9eq9nN.js} +2 -2
- package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-qy2nTD_1.js} +3 -3
- package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-BGPHxuwT.js} +2 -2
- package/public/assets/{index-CRHep3M9.js → index-BC5XZc0z.js} +5 -5
- package/public/assets/{index-MYAIwYTX.css → index-D8Rp9pz2.css} +1 -1
- package/public/assets/{insights-CzrtkpOE.js → insights-C-KQG0h-.js} +2 -2
- package/public/assets/{integrations-aVsUmigt.js → integrations-ClPRPyKU.js} +2 -2
- package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-DbH8J9Ak.js} +2 -2
- package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-PH4zVn24.js} +2 -2
- package/public/assets/{maintenance-NfjqFZAk.js → maintenance-5lV43uTL.js} +2 -2
- package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-BdgU35aX.js} +2 -2
- package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-DNT4OUwQ.js} +3 -3
- package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CnCcPy0A.js} +2 -2
- package/public/assets/{memory-DNsCl6UX.js → memory--GgWH1gH.js} +2 -2
- package/public/assets/{messages-DopU1L1X.js → messages-CzFl_97O.js} +2 -2
- package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-CVvJVkzv.js} +2 -2
- package/public/assets/overview-DsrZvE86.js +2 -0
- package/public/assets/{pairs-DE_m6rrE.js → pairs-D2d0d2py.js} +2 -2
- package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-_SMuSbYF.js} +2 -2
- package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-Apn15y43.js} +2 -2
- package/public/assets/{projects-Br2Bd2h1.js → projects-D3B6kto5.js} +2 -2
- package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-DnPB6Ez6.js} +2 -2
- package/public/assets/provider-display-bWwiuedV.js +2 -0
- package/public/assets/{registry-Cw38_6gW.js → registry-D790cIhN.js} +2 -2
- package/public/assets/run-card-BCro-Odj.js +2 -0
- package/public/assets/{secrets-DxI-vzSq.js → secrets-CVXvfvbU.js} +2 -2
- package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-D3Xxqb9w.js} +2 -2
- package/public/assets/{settings-CKZTVGIx.js → settings-CoUC7Jci.js} +2 -2
- package/public/assets/shipping-D_Lp9n_J.js +2 -0
- package/public/assets/{stats-DyUBKyWI.js → stats-CXiNjB9S.js} +2 -2
- package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-C703YIy1.js} +2 -2
- package/public/assets/store-BgDALGIX.js +9 -0
- package/public/assets/{tasks-Bh7Chr4-.js → tasks-DO0WpJCx.js} +2 -2
- package/public/assets/{teams-bowI7qVY.js → teams-FGkdi_tL.js} +2 -2
- package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-Dh8dSC7e.js} +3 -3
- package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-B5Zriip3.js} +2 -2
- package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-CRUVDvLC.js} +2 -2
- package/public/assets/{use-decisions-samMQhQf.js → use-decisions-CoaqRh4l.js} +2 -2
- package/public/assets/{use-project-list-59LheVP7.js → use-project-list-X-BgT53X.js} +2 -2
- package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-0X_UZkk-.js} +2 -2
- package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-CkLhVBGu.js} +2 -2
- package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-BkTi7eim.js} +2 -2
- package/public/assets/{work-queue-CSvpuNLz.js → work-queue-DDOLEyKo.js} +2 -2
- package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-CzOsB8BM.js} +2 -2
- package/public/assets/{workflows-DdmxRSyg.js → workflows-DJilvmRM.js} +2 -2
- package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CHcfbLR3.js} +2 -2
- package/public/cosmos.html +4 -4
- package/public/index.html +29 -29
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-api.ts +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 +3 -2
- 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/login-link.ts +35 -0
- 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/index.ts +7 -1
- package/src/cli/init.ts +53 -0
- package/src/cli/orchestrator.ts +1 -7
- package/src/cli/setup.ts +6 -4
- 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 +3 -12
- 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/workflow-execution.ts +3 -3
- 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 +12 -16
- 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/init.ts +299 -0
- package/src/injection-sweep-pacing.ts +6 -8
- package/src/invite-link.ts +16 -9
- 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 +44 -17
- 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-providers.ts +18 -0
- 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 +7 -3
- 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 +3 -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 +4 -5
- package/src/routes/secrets-import.ts +3 -4
- package/src/routes/secrets-vault.ts +12 -14
- 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-install.ts +30 -0
- 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 +25 -26
- 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/auth-context.ts +6 -0
- 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-api-key.ts +73 -0
- 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/setup.ts +47 -8
- 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/utils.ts +7 -0
- 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/overview-C1DCTzzV.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/db/schema.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { COMMAND_RETENTION_SCHEMA } from "./command-retention";
|
|
2
|
+
import { TOKEN_USAGE_RETENTION_SCHEMA } from "./token-usage-retention";
|
|
1
3
|
import { DURABLE_PAYLOAD_SCHEMA } from "./durable-payloads"; import { PLAN_IMPORT_SCHEMA_SQL } from "./plan-import-schema";
|
|
2
4
|
import { PROJECT_ROOTS_SCHEMA } from "./project-root-schema";
|
|
3
5
|
import { seedBuiltInTokenProfiles } from "../token-db";
|
|
@@ -964,7 +966,6 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
964
966
|
shutdown_requested_at INTEGER
|
|
965
967
|
);
|
|
966
968
|
CREATE INDEX IF NOT EXISTS idx_automation_runs_automation ON automation_runs(automation_id, created_at);
|
|
967
|
-
CREATE INDEX IF NOT EXISTS idx_automation_runs_status ON automation_runs(status);
|
|
968
969
|
CREATE INDEX IF NOT EXISTS idx_automation_runs_task ON automation_runs(task_id);
|
|
969
970
|
|
|
970
971
|
CREATE TABLE IF NOT EXISTS task_events (
|
|
@@ -1264,6 +1265,8 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
1264
1265
|
${PLAN_IMPORT_SCHEMA_SQL};
|
|
1265
1266
|
${IDLE_DIGEST_SCHEMA_SQL};
|
|
1266
1267
|
${TASK_TOKEN_TOTALS_SCHEMA};
|
|
1268
|
+
${TOKEN_USAGE_RETENTION_SCHEMA};
|
|
1269
|
+
${COMMAND_RETENTION_SCHEMA};
|
|
1267
1270
|
${LONG_RUNNING_WORK_SCHEMA};
|
|
1268
1271
|
${SHIPPING_STATS_SCHEMA};
|
|
1269
1272
|
${OPERATOR_ACTIONS_SCHEMA};
|
package/src/db/spawn-budget.ts
CHANGED
|
@@ -45,6 +45,11 @@ function observedTokens(agentIds: string[]): ObservedSubtreeSpend {
|
|
|
45
45
|
COALESCE(SUM(usage_status IS NOT NULL), 0) AS unknownReports
|
|
46
46
|
FROM session_token_usage WHERE agent_id IN (SELECT value FROM json_each(?))`)
|
|
47
47
|
.get(ids) as { tokens: number; unknownReports: number };
|
|
48
|
+
const retired = getDb().query(`SELECT COALESCE(SUM(tokens), 0) AS tokens,
|
|
49
|
+
COALESCE(SUM(unknown_reports), 0) AS unknownReports FROM retired_token_usage_totals
|
|
50
|
+
WHERE agent_id IN (SELECT value FROM json_each(?))`).get(ids) as { tokens: number; unknownReports: number };
|
|
51
|
+
row.tokens += retired.tokens;
|
|
52
|
+
row.unknownReports += retired.unknownReports;
|
|
48
53
|
const live = getDb().query(`SELECT COALESCE(SUM(status = 'busy'), 0) AS busyAgents,
|
|
49
54
|
COALESCE(SUM(CASE WHEN json_valid(meta) THEN MAX(0, COALESCE(json_extract(meta, '$.unreportedTokenUsage'), 0)) ELSE 0 END), 0) AS unreportedReports
|
|
50
55
|
FROM agents WHERE id IN (SELECT value FROM json_each(?))`)
|
|
@@ -5,12 +5,11 @@ import { getPersistedAgentRuntimeCard as getAgent } from "./agent-card-read";
|
|
|
5
5
|
import { getDb, ValidationError } from "./connection";
|
|
6
6
|
import { runnerSpawnGrantForAgent } from "../runtime-grant";
|
|
7
7
|
import { optionalNonNegativeInt } from "../validation";
|
|
8
|
-
import { sameHostIdentity } from "../utils";
|
|
9
|
-
import { getOrchestrator } from "./orchestrators";
|
|
10
8
|
import { stringValue } from "@humwick/sdk";
|
|
11
9
|
import type { CreateCommandInput } from "../types";
|
|
12
10
|
import { getLaunchFact } from "./launch-facts";
|
|
13
11
|
import { spawnProcessGoneSql } from "./spawn-process-evidence";
|
|
12
|
+
import { WORKFLOW_EXECUTIONS_SQL } from "./workflow-execution";
|
|
14
13
|
|
|
15
14
|
/** Read-only preflight runs before project or Task creation; the writer repeats it. */
|
|
16
15
|
export function assertDelegatedSpawnTarget(input: CreateCommandInput) {
|
|
@@ -37,15 +36,7 @@ export function assertDelegatedSpawnTarget(input: CreateCommandInput) {
|
|
|
37
36
|
return !agent || agent.status === "offline" || agent.meta?.lifecycleAction === "shutting-down";
|
|
38
37
|
})) throw new ValidationError("SPAWN_SUBTREE_STOPPING: caller or ancestor has stopped");
|
|
39
38
|
const projectId = delegatedProjectTarget(input, state);
|
|
40
|
-
|
|
41
|
-
const targetHost = stringValue(input.params?.orchestratorId);
|
|
42
|
-
if (parentHost && targetHost) {
|
|
43
|
-
// #3650: machine is the raw os.hostname() ("WBOX"); orchestrator identity is configured ("wbox"/"wbox-1").
|
|
44
|
-
const target = getOrchestrator(targetHost);
|
|
45
|
-
const sameHost = sameHostIdentity(parentHost, targetHost)
|
|
46
|
-
|| (target && [target.id, target.hostname, target.agentId].some(identity => sameHostIdentity(identity, parentHost)));
|
|
47
|
-
if (!sameHost) throw new ValidationError("SPAWN_PROJECT_MISMATCH: delegated launches stay on their parent's host");
|
|
48
|
-
}
|
|
39
|
+
// #3769 — placement owns the host; delegatedProjectTarget checks its registered project checkout.
|
|
49
40
|
if (state.depth === 1 && profile === "sub-coordinator") throw new ValidationError("SPAWN_DEPTH_EXCEEDED: sub-coordinators cannot delegate another coordinator");
|
|
50
41
|
const workflow = Boolean(input.params?.workflowRunId);
|
|
51
42
|
const grant = runnerSpawnGrantForAgent(parentId);
|
|
@@ -63,7 +54,7 @@ export function assertDelegatedSpawnAdmission(input: CreateCommandInput, now: nu
|
|
|
63
54
|
if (input.type !== "agent.spawn") return;
|
|
64
55
|
const workflowRunId = stringValue(input.params?.workflowRunId);
|
|
65
56
|
const run = workflowRunId && getDb().query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'kernel_entity__workflow__run'").get()
|
|
66
|
-
? getDb().query(
|
|
57
|
+
? getDb().query(`SELECT callerAgentId, coordinator FROM ${WORKFLOW_EXECUTIONS_SQL} WHERE scope = 'local' AND id = ?`)
|
|
67
58
|
.get(workflowRunId) as { callerAgentId: string | null; coordinator: string } | null : null;
|
|
68
59
|
const origin = run?.callerAgentId ?? run?.coordinator;
|
|
69
60
|
if (origin && participatesInSpawnDelegation(origin) && input.params?.spawnedBy !== origin) {
|
|
@@ -44,6 +44,9 @@ export const SQL_AUDIT_INDEXES = [
|
|
|
44
44
|
`CREATE INDEX IF NOT EXISTS idx_task_events_land_workspace ON task_events(json_extract(metadata, '$.workspaceId'))
|
|
45
45
|
WHERE type = 'deliverable' AND json_extract(metadata, '$.land.requested') = 1`,
|
|
46
46
|
LAND_WITHDRAWN_INDEX_SQL,
|
|
47
|
+
// #3779 spawnRequestId -> Task resolution (every register/land/deliver auth check): a full task_events scan blocked the loop.
|
|
48
|
+
`CREATE INDEX IF NOT EXISTS idx_task_events_spawn_request ON task_events(json_extract(metadata, '$.spawnRequestId'))
|
|
49
|
+
WHERE type = 'agent.spawn_requested'`,
|
|
47
50
|
`CREATE INDEX IF NOT EXISTS idx_task_events_land_bound_workspace ON task_events(${LAND_WORKSPACE_EXPRESSION})
|
|
48
51
|
WHERE type = 'deliverable' AND json_extract(metadata, '$.land.requested') = 1`,
|
|
49
52
|
`CREATE INDEX IF NOT EXISTS idx_task_events_deliverable_workspace ON task_events(${LAND_WORKSPACE_EXPRESSION}, task_id)
|
package/src/db/stats-async.ts
CHANGED
|
@@ -117,7 +117,9 @@ export function buildDbGatewayCheck(stats: DbGatewayStats): HealthCheck {
|
|
|
117
117
|
const detail = [
|
|
118
118
|
`writer=${stats.writerBackend}`,
|
|
119
119
|
`reader=${stats.readerBackend}`,
|
|
120
|
-
|
|
120
|
+
// #3798 — the op split counts executed writes; the registered op definitions are named as such.
|
|
121
|
+
`onLoopOps=${stats.writer.onLoopOps}; writerThreadOps=${stats.writer.writerThreadOps}`,
|
|
122
|
+
`opDefinitions onLoop=${specs.length - offLoop} offLoop=${offLoop}`,
|
|
121
123
|
`mainConnectionWrites1h=${JSON.stringify(writes)}`,
|
|
122
124
|
...(wedged ? [`writer wedged: op ${wedged.op} running ${Math.round(wedged.runningMs / 1000)}s`] : []),
|
|
123
125
|
...(shedCount > 0 ? [`${shedCount} reader shed(s)`] : []),
|
|
@@ -128,8 +130,8 @@ export function buildDbGatewayCheck(stats: DbGatewayStats): HealthCheck {
|
|
|
128
130
|
deployRole: "load",
|
|
129
131
|
detail,
|
|
130
132
|
metrics: {
|
|
131
|
-
onLoopOps:
|
|
132
|
-
writerThreadOps:
|
|
133
|
+
onLoopOps: stats.writer.onLoopOps,
|
|
134
|
+
writerThreadOps: stats.writer.writerThreadOps,
|
|
133
135
|
mainConnectionWrites1h: Object.values(writes).reduce((sum, count) => sum + count, 0),
|
|
134
136
|
mainConnectionBootWrites: mainConnectionBootWrites(),
|
|
135
137
|
writerQueued: stats.writer.queued,
|
package/src/db/task-entity.ts
CHANGED
|
@@ -1085,14 +1085,15 @@ export function deliverableTaskIdsByOwnerAgent(agentId: string): number[] {
|
|
|
1085
1085
|
// event (recorded BEFORE the child registers) carries the spawnRequestId. The landing worker's
|
|
1086
1086
|
// `meta.spawnRequestId` resolves back through it to the Task. Returns every matching task id so a
|
|
1087
1087
|
// reused spawnRequestId can't silently drop a link.
|
|
1088
|
+
export const TASK_IDS_BY_SPAWN_REQUEST_SQL = `SELECT DISTINCT te.task_id AS id FROM task_events te INDEXED BY idx_task_events_spawn_request
|
|
1089
|
+
JOIN tasks t ON t.id = te.task_id AND t.kind = '${DELIVERABLE_TASK_KIND}'
|
|
1090
|
+
WHERE te.type = 'agent.spawn_requested' AND json_extract(te.metadata, '$.spawnRequestId') = ?
|
|
1091
|
+
ORDER BY te.task_id ASC`;
|
|
1088
1092
|
export function deliverableTaskIdsBySpawnRequestId(spawnRequestId: string): number[] {
|
|
1089
1093
|
const cleaned = spawnRequestId?.trim();
|
|
1090
1094
|
if (!cleaned) return [];
|
|
1091
1095
|
return (getDb()
|
|
1092
|
-
.query(
|
|
1093
|
-
JOIN tasks t ON t.id = te.task_id AND t.kind = '${DELIVERABLE_TASK_KIND}'
|
|
1094
|
-
WHERE te.type = 'agent.spawn_requested' AND json_extract(te.metadata, '$.spawnRequestId') = ?
|
|
1095
|
-
ORDER BY te.task_id ASC`)
|
|
1096
|
+
.query(TASK_IDS_BY_SPAWN_REQUEST_SQL)
|
|
1096
1097
|
.all(cleaned) as Array<{ id: number }>)
|
|
1097
1098
|
.map((r) => r.id);
|
|
1098
1099
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { STALE_TTL_MS } from "../config";
|
|
2
|
+
import { getAgent } from "./agents";
|
|
3
|
+
import { countLiveTeamMembers, getTeam, claimTeamWithAdoption, isClaimableTeam } from "./teams";
|
|
4
|
+
import type { Team } from "../types";
|
|
5
|
+
export class TeamAuthError extends Error {}
|
|
6
|
+
export class TeamNotFoundError extends Error {}
|
|
7
|
+
function lineageRootForAgent(agentId: string): string | undefined {
|
|
8
|
+
let current = getAgent(agentId);
|
|
9
|
+
const seen = new Set<string>();
|
|
10
|
+
while (current) {
|
|
11
|
+
if (seen.has(current.id)) return undefined;
|
|
12
|
+
seen.add(current.id);
|
|
13
|
+
if (!current.spawnedBy) return current.id;
|
|
14
|
+
current = getAgent(current.spawnedBy);
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function coordinatorIsLive(team: Team): boolean {
|
|
20
|
+
if (!team.coordinatorId) return false;
|
|
21
|
+
const coordinator = getAgent(team.coordinatorId);
|
|
22
|
+
if (!coordinator || coordinator.status === "offline") return false;
|
|
23
|
+
if (coordinator.teamId === team.id) return countLiveTeamMembers(team.id, Date.now() - STALE_TTL_MS) > 0;
|
|
24
|
+
return coordinator.lastSeen > Date.now() - STALE_TTL_MS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** How a claim was authorized — carried into the audit trail so an orphan takeover is
|
|
28
|
+
* distinguishable from a routine same-lineage reclaim. */
|
|
29
|
+
export type TeamClaimMode = "admin" | "same-lineage" | "orphan-takeover";
|
|
30
|
+
|
|
31
|
+
// #1454 — live owners require same-lineage authority; dead owners permit orphan takeover.
|
|
32
|
+
|
|
33
|
+
function assertCanClaimTeam(admin: boolean, team: Team, callerAgentId: string): TeamClaimMode {
|
|
34
|
+
const caller = getAgent(callerAgentId);
|
|
35
|
+
const callerTeam = caller?.teamId ? getTeam(caller.teamId) : null;
|
|
36
|
+
if (!admin && callerTeam?.parentTeamId && team.id !== callerTeam.id) {
|
|
37
|
+
throw new TeamAuthError("a sub-coordinator may claim only its own private team");
|
|
38
|
+
}
|
|
39
|
+
const callerRoot = lineageRootForAgent(callerAgentId);
|
|
40
|
+
const teamRoot = team.rootCoordinatorId ?? team.coordinatorId;
|
|
41
|
+
// Same-lineage reclaim is the routine fast path (a replacement spawned under the original
|
|
42
|
+
// founder) and stays allowed regardless of owner liveness.
|
|
43
|
+
if (callerRoot && teamRoot && callerRoot === teamRoot) return "same-lineage";
|
|
44
|
+
if (admin) return "admin";
|
|
45
|
+
// #1454 orphan takeover: a replacement coordinator of a DIFFERENT lineage may adopt the team only
|
|
46
|
+
// when its owning coordinator is not live (offline, or stale past the heartbeat TTL). This is the
|
|
47
|
+
// sole security boundary for the cross-lineage path — a healthy owner is never displaced.
|
|
48
|
+
if (!coordinatorIsLive(team)) return "orphan-takeover";
|
|
49
|
+
throw new TeamAuthError(`caller ${callerAgentId} cannot claim team ${team.id}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export function claimAuthorizedTeam(teamId: string, agentId: string, admin: boolean) {
|
|
54
|
+
if (!isClaimableTeam(getTeam(teamId))) throw new TeamNotFoundError(`active team ${teamId} not found`);
|
|
55
|
+
let captured: { mode: TeamClaimMode; priorCoordinatorId: string | null } | undefined;
|
|
56
|
+
const result = claimTeamWithAdoption(teamId, agentId, team => {
|
|
57
|
+
captured = { mode: assertCanClaimTeam(admin, team, agentId), priorCoordinatorId: team.coordinatorId ?? null };
|
|
58
|
+
});
|
|
59
|
+
return { ...result, ...captured! };
|
|
60
|
+
}
|
package/src/db/teams-async.ts
CHANGED
|
@@ -56,3 +56,7 @@ export function setTeamBriefAsync(input: Parameters<typeof setTeamBrief>[0]): Pr
|
|
|
56
56
|
export function dissolveTeamAsync(teamId: string, input?: Parameters<typeof dissolveTeam>[1], requestId: string = randomUUID()): Promise<ReturnType<typeof dissolveTeam>> {
|
|
57
57
|
return getGateway().write("teams.dissolve", { teamId, input }, { idempotencyKey: requestId }) as unknown as Promise<ReturnType<typeof dissolveTeam>>;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
export function ensureTeamAsync(agentId: string, opts: { projectId?: string } = {}): Promise<NonNullable<ReturnType<typeof getTeam>>> {
|
|
61
|
+
return write("teams.ensure", { agentId, id: `team_${randomUUID()}`, ...opts });
|
|
62
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { claimAuthorizedTeam } from "./team-claim-policy";
|
|
2
|
+
import { ensureTeamInternal } from "./teams";
|
|
1
3
|
// Team writer operations. Guards that were formerly adjacent sync statements now execute inside
|
|
2
4
|
// the writer transaction; async callers submit a main-thread generated id and never rely on worker
|
|
3
5
|
// process-local state.
|
|
@@ -10,6 +12,8 @@ import {
|
|
|
10
12
|
import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
|
|
11
13
|
|
|
12
14
|
export const TEAMS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
|
|
15
|
+
"teams.ensure": { offLoop: true, idempotent: true, fn: (a: { agentId: string; id: string; projectId?: string }, ctx) => ensureTeamInternal(a.agentId, a.id, ctx.now, false, a.projectId ?? null) },
|
|
16
|
+
"teams.claimAuthorized": { offLoop: true, idempotent: true, fn: (a: { teamId: string; agentId: string; admin: boolean }) => claimAuthorizedTeam(a.teamId, a.agentId, a.admin) },
|
|
13
17
|
"teams.zeroMembers.set": { offLoop: true, idempotent: true, fn: (a: { teamId: string; ts: number }) => setTeamZeroMembersSince(a.teamId, a.ts) },
|
|
14
18
|
"teams.zeroMembers.clear": { offLoop: true, idempotent: true, fn: (a: { teamId: string }, ctx) => clearTeamZeroMembersSince(a.teamId, ctx.now) },
|
|
15
19
|
"teams.brief.set": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof setTeamBrief>[0], ctx) => setTeamBrief(a, ctx.now) },
|
|
@@ -111,3 +111,18 @@ export function clearAgentNotice(child: Pick<AgentCard, "id" | "epoch" | "meta">
|
|
|
111
111
|
getDb().query("DELETE FROM agent_exit_notices WHERE agent_id = ? AND epoch = ? AND spawn_request_id = ? AND kind = ?")
|
|
112
112
|
.run(child.id, child.epoch, stringValue(child.meta?.spawnRequestId) ?? "", kind);
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
export interface AgentNoticeSlot {
|
|
116
|
+
child: Pick<AgentCard, "id" | "epoch" | "meta">;
|
|
117
|
+
kind: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Called inside notifications.agentNotice, so failure rolls back the slot and all sends. */
|
|
121
|
+
export function insertAgentNoticeSlot({ child, kind }: AgentNoticeSlot, now: number): boolean {
|
|
122
|
+
if (kind === "agent.ready") {
|
|
123
|
+
getDb().query("DELETE FROM agent_exit_notices WHERE agent_id = ? AND kind = 'agent.ready' AND NOT (epoch = ? AND spawn_request_id = ?)")
|
|
124
|
+
.run(child.id, child.epoch, stringValue(child.meta?.spawnRequestId) ?? "");
|
|
125
|
+
}
|
|
126
|
+
return getDb().query("INSERT OR IGNORE INTO agent_exit_notices (agent_id, epoch, spawn_request_id, kind, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
127
|
+
.run(child.id, child.epoch, stringValue(child.meta?.spawnRequestId) ?? "", kind, now).changes > 0;
|
|
128
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { getDb, currentWriteTransactionContext } from "./connection";
|
|
2
|
+
import { clearExpiredLegacyMessageClaimsInTx, timerMessageDeadlineSnapshot } from "./timer-message-cutover";
|
|
3
|
+
import { normalizePhase, readCutover, appendJournal } from "./timer-cutover-state";
|
|
4
|
+
import type { KernelTimersDesiredMode, TimerMessageBackend, TimerMessageCutoverRow } from "./timer-message-flip";
|
|
5
|
+
import type { TimerMessageCutoverPhase, TimerMessageCutoverJournalEntry, TimerMessageDeadlineGuard } from "./timer-cutover-core";
|
|
6
|
+
import type { WriterOpSpec } from "./write-ops";
|
|
7
|
+
|
|
8
|
+
export interface CutoverWriterState {
|
|
9
|
+
ownerId: string; epoch?: number; leaseMs: number; quiesceTimeoutMs: number; worstCaseMs: number;
|
|
10
|
+
authoritativeActivation: boolean; operationLeaseFence?: { requestId: string; ownerId: string };
|
|
11
|
+
}
|
|
12
|
+
export interface CutoverStepArgs {
|
|
13
|
+
abort: { now: number; reason: string };
|
|
14
|
+
activate: { now: number; epoch: number };
|
|
15
|
+
completeLegacy: { now: number; epoch: number };
|
|
16
|
+
completeTransfer: { now: number; epoch: number; mode: KernelTimersDesiredMode; expectedBackend: TimerMessageBackend };
|
|
17
|
+
reverse: { now: number; epoch: number; targetMode: KernelTimersDesiredMode; sourceObservedMode: KernelTimersDesiredMode };
|
|
18
|
+
clearClaims: { now: number };
|
|
19
|
+
acquire: { now: number; create: boolean };
|
|
20
|
+
renew: { now: number; epoch: number };
|
|
21
|
+
closeGate: { epoch: number; now: number };
|
|
22
|
+
transition: { epoch: number; phase: TimerMessageCutoverPhase; reason: string; now: number };
|
|
23
|
+
finishAbort: { epoch: number; reason: string; now: number };
|
|
24
|
+
}
|
|
25
|
+
export interface CutoverStepResults {
|
|
26
|
+
abort: boolean;
|
|
27
|
+
activate: void;
|
|
28
|
+
completeLegacy: void;
|
|
29
|
+
completeTransfer: void;
|
|
30
|
+
reverse: void;
|
|
31
|
+
clearClaims: number;
|
|
32
|
+
acquire: { acquiredEpoch?: number };
|
|
33
|
+
renew: boolean;
|
|
34
|
+
closeGate: boolean;
|
|
35
|
+
transition: void;
|
|
36
|
+
finishAbort: void;
|
|
37
|
+
}
|
|
38
|
+
function owned(state: CutoverWriterState, epoch: number, now: number): TimerMessageCutoverRow {
|
|
39
|
+
const row = readCutover();
|
|
40
|
+
if (!row || row.epoch !== epoch || row.leaseOwner !== state.ownerId || (row.leaseExpiresAt ?? 0) <= now) {
|
|
41
|
+
throw new Error(`cutover epoch ${epoch} is not owned by ${state.ownerId}`);
|
|
42
|
+
}
|
|
43
|
+
return row;
|
|
44
|
+
}
|
|
45
|
+
function deadlineGuard(state: CutoverWriterState, at: number): TimerMessageDeadlineGuard {
|
|
46
|
+
const deadlines = timerMessageDeadlineSnapshot();
|
|
47
|
+
const slackMs = deadlines.earliestDeadline === null ? null : deadlines.earliestDeadline - at;
|
|
48
|
+
return { safe: slackMs === null || slackMs >= state.worstCaseMs, slackMs, requiredMs: state.worstCaseMs, deadlines };
|
|
49
|
+
}
|
|
50
|
+
const steps = {
|
|
51
|
+
abort: (state: CutoverWriterState, { now, reason }: CutoverStepArgs["abort"]) => {
|
|
52
|
+
const row = readCutover();
|
|
53
|
+
if (!row || normalizePhase(row.phase) === "idle") return false;
|
|
54
|
+
return getDb().query(`UPDATE timer_message_cutover
|
|
55
|
+
SET abort_requested = 1, phase_journal = ?, updated_at = ? WHERE id = 1`)
|
|
56
|
+
.run(appendJournal(row, { epoch: row.epoch, phase: "aborting", at: now, reason }), now).changes === 1;
|
|
57
|
+
|
|
58
|
+
},
|
|
59
|
+
activate: (state: CutoverWriterState, { now, epoch }: CutoverStepArgs["activate"]) => {
|
|
60
|
+
const row = owned(state, epoch, now);
|
|
61
|
+
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed") {
|
|
62
|
+
throw new Error("ingress routing activation requires the live quiesced gate owner");
|
|
63
|
+
}
|
|
64
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET ingress_policy = 'ingress',
|
|
65
|
+
phase_journal = ?, updated_at = ?
|
|
66
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
|
|
67
|
+
AND phase = 'quiesced' AND message_mutation_gate = 'closed'`)
|
|
68
|
+
.run(appendJournal(row, { epoch, phase: "quiesced", at: now, reason: "authoritative ingress routing activated under fence" }),
|
|
69
|
+
now, epoch, state.ownerId, now);
|
|
70
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost before ingress routing activation");
|
|
71
|
+
|
|
72
|
+
},
|
|
73
|
+
completeLegacy: (state: CutoverWriterState, { now, epoch }: CutoverStepArgs["completeLegacy"]) => {
|
|
74
|
+
const row = owned(state, epoch, now);
|
|
75
|
+
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed" || row.ingressPolicy !== "ingress") {
|
|
76
|
+
throw new Error("G1 completion requires fenced authoritative ingress routing");
|
|
77
|
+
}
|
|
78
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
79
|
+
protocol_version = MAX(COALESCE(protocol_version, 1), 2),
|
|
80
|
+
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
81
|
+
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
82
|
+
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
83
|
+
phase_journal = ?, updated_at = ?
|
|
84
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
|
|
85
|
+
AND phase = 'quiesced' AND message_mutation_gate = 'closed' AND ingress_policy = 'ingress'`)
|
|
86
|
+
.run(appendJournal(row, { epoch, phase: "idle", at: now, reason: "G1 proven; gate reopened under legacy authority" }),
|
|
87
|
+
now, epoch, state.ownerId, now);
|
|
88
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost before G1 gate reopen");
|
|
89
|
+
|
|
90
|
+
},
|
|
91
|
+
completeTransfer: (state: CutoverWriterState, { now, epoch, mode, expectedBackend }: CutoverStepArgs["completeTransfer"]) => {
|
|
92
|
+
const row = owned(state, epoch, now);
|
|
93
|
+
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed"
|
|
94
|
+
|| row.ingressPolicy !== "ingress") {
|
|
95
|
+
throw new Error("authority-transfer completion requires the live quiesced ingress gate owner");
|
|
96
|
+
}
|
|
97
|
+
// Only ACTIVE (scheduled) rows must converge on the target backend — terminal rows never
|
|
98
|
+
// dispatch, so their backend is irrelevant and must not block completion (an ordinary kernel-mode
|
|
99
|
+
// cancel terminalizes a kernel-owned row that a reverse would otherwise be unable to move).
|
|
100
|
+
const wrongOwners = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority
|
|
101
|
+
WHERE lifecycle='scheduled' AND backend != ?`).get(expectedBackend) as { n: number }).n;
|
|
102
|
+
if (wrongOwners !== 0) throw new Error(`${wrongOwners} scheduled MESSAGE owners are not ${expectedBackend}`);
|
|
103
|
+
const expectedFrozen = expectedBackend === "kernel" ? 1 : 0;
|
|
104
|
+
const projectionMismatch = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority a
|
|
105
|
+
JOIN scheduled_timers s ON s.id=a.timer_id
|
|
106
|
+
WHERE a.lifecycle='scheduled' AND (s.command IS NOT NULL OR s.message_authority_frozen != ?)`)
|
|
107
|
+
.get(expectedFrozen) as { n: number }).n;
|
|
108
|
+
if (projectionMismatch !== 0) throw new Error(`${projectionMismatch} MESSAGE compatibility projections disagree with ${mode}`);
|
|
109
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
110
|
+
desired_mode = ?, observed_mode = ?, phase = 'idle', lease_owner = NULL,
|
|
111
|
+
lease_expires_at = NULL, quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
112
|
+
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
113
|
+
phase_journal = ?, updated_at = ?
|
|
114
|
+
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
|
|
115
|
+
AND phase='quiesced' AND message_mutation_gate='closed' AND ingress_policy='ingress'`)
|
|
116
|
+
.run(mode, mode, appendJournal(row, {
|
|
117
|
+
epoch, phase: "idle", at: now,
|
|
118
|
+
reason: `${mode} per-row ownership proven; MESSAGE mutation gate reopened`,
|
|
119
|
+
}), now, epoch, state.ownerId, now);
|
|
120
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost before authority-transfer completion");
|
|
121
|
+
|
|
122
|
+
},
|
|
123
|
+
reverse: (state: CutoverWriterState, { now, epoch, targetMode, sourceObservedMode }: CutoverStepArgs["reverse"]) => {
|
|
124
|
+
const row = owned(state, epoch, now);
|
|
125
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET desired_mode=?,
|
|
126
|
+
phase_journal=?, updated_at=?
|
|
127
|
+
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
|
|
128
|
+
AND phase='quiesced' AND message_mutation_gate='closed' AND observed_mode=?`)
|
|
129
|
+
.run(targetMode, appendJournal(row, {
|
|
130
|
+
epoch, phase: "quiesced", at: now,
|
|
131
|
+
reason: `${targetMode} rollback desired; kernel decision commits quiesced for reverse projection`,
|
|
132
|
+
}), now, epoch, state.ownerId, now, sourceObservedMode);
|
|
133
|
+
if (changed.changes !== 1) throw new Error(`reverse transfer requires observed ${sourceObservedMode} under the live fence`);
|
|
134
|
+
|
|
135
|
+
},
|
|
136
|
+
clearClaims: (state: CutoverWriterState, { now }: CutoverStepArgs["clearClaims"]) => {return clearExpiredLegacyMessageClaimsInTx(currentWriteTransactionContext()!, now);
|
|
137
|
+
},
|
|
138
|
+
acquire: (state: CutoverWriterState, { now, create }: CutoverStepArgs["acquire"]) => {let acquiredEpoch: number | undefined; let takeover = false;
|
|
139
|
+
(() => {
|
|
140
|
+
// Stage-10 pins the resolved leg before it asks this independent coordinator to close the
|
|
141
|
+
// global gate. Check that pin's operation lease under THIS acquisition transaction: if an
|
|
142
|
+
// abort won after pin but before requestQuiesce, it cannot be followed by a stale gate close.
|
|
143
|
+
const fence = state.operationLeaseFence;
|
|
144
|
+
if (fence) {
|
|
145
|
+
const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
|
|
146
|
+
WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
|
|
147
|
+
AND lease_owner=? AND lease_expires_at > ?`)
|
|
148
|
+
.get(fence.requestId, fence.ownerId, now);
|
|
149
|
+
if (!live) return;
|
|
150
|
+
}
|
|
151
|
+
const row = readCutover();
|
|
152
|
+
if (!row) {
|
|
153
|
+
if (!create) return;
|
|
154
|
+
acquiredEpoch = 1;
|
|
155
|
+
const journal: TimerMessageCutoverJournalEntry[] = [{ epoch: 1, phase: "leased", at: now, reason: "initial lease acquired" }];
|
|
156
|
+
getDb().query(`INSERT INTO timer_message_cutover
|
|
157
|
+
(id, protocol_version, epoch, desired_mode, observed_mode, phase, lease_owner, lease_expires_at,
|
|
158
|
+
quiesce_started_at, quiesce_deadline, last_completed_timer, ingress_policy,
|
|
159
|
+
message_mutation_gate, gate_epoch, abort_requested, phase_journal, updated_at)
|
|
160
|
+
VALUES (1, 1, 1, 'shadow', 'shadow', 'leased', ?, ?, NULL, NULL, NULL, 'legacy', 'open', NULL, 0, ?, ?)`)
|
|
161
|
+
.run(state.ownerId, now + state.leaseMs, JSON.stringify(journal), now);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const rowPhase = normalizePhase(row.phase);
|
|
165
|
+
const liveForeign = row.leaseOwner !== null && row.leaseOwner !== state.ownerId && (row.leaseExpiresAt ?? 0) > now;
|
|
166
|
+
if (liveForeign) return;
|
|
167
|
+
if (row.leaseOwner === state.ownerId && (row.leaseExpiresAt ?? 0) > now && state.epoch === row.epoch) {
|
|
168
|
+
acquiredEpoch = row.epoch;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const nextEpoch = row.epoch + 1;
|
|
172
|
+
takeover = row.leaseOwner !== null || rowPhase !== "idle" || row.messageMutationGate === "closed";
|
|
173
|
+
const nextPhase: TimerMessageCutoverPhase = rowPhase === "idle" ? "leased" : rowPhase;
|
|
174
|
+
const entry: TimerMessageCutoverJournalEntry = {
|
|
175
|
+
epoch: nextEpoch,
|
|
176
|
+
phase: takeover ? "takeover" : "leased",
|
|
177
|
+
at: now,
|
|
178
|
+
reason: takeover ? `expired epoch ${row.epoch} taken over in phase ${rowPhase}` : "lease acquired",
|
|
179
|
+
};
|
|
180
|
+
const result = getDb().query(`UPDATE timer_message_cutover SET
|
|
181
|
+
protocol_version = COALESCE(protocol_version, 1), epoch = ?, phase = ?, lease_owner = ?, lease_expires_at = ?,
|
|
182
|
+
gate_epoch = CASE WHEN message_mutation_gate = 'closed' THEN ? ELSE NULL END,
|
|
183
|
+
phase_journal = ?, updated_at = ?
|
|
184
|
+
WHERE id = 1 AND (lease_owner IS NULL OR lease_expires_at <= ? OR lease_owner = ?)`)
|
|
185
|
+
.run(nextEpoch, nextPhase, state.ownerId, now + state.leaseMs, nextEpoch,
|
|
186
|
+
appendJournal(row, entry), now, now, state.ownerId);
|
|
187
|
+
if (result.changes === 1) acquiredEpoch = nextEpoch;
|
|
188
|
+
})();
|
|
189
|
+
return { acquiredEpoch };
|
|
190
|
+
},
|
|
191
|
+
renew: (state: CutoverWriterState, { now, epoch }: CutoverStepArgs["renew"]) => {
|
|
192
|
+
const row = readCutover();
|
|
193
|
+
if (!row || row.epoch !== epoch || row.leaseOwner !== state.ownerId || (row.leaseExpiresAt ?? 0) <= now) return false;
|
|
194
|
+
const deadlineCap = row.messageMutationGate === "closed" && row.quiesceDeadline !== null
|
|
195
|
+
? row.quiesceDeadline
|
|
196
|
+
: Number.MAX_SAFE_INTEGER;
|
|
197
|
+
const nextExpiry = Math.min(now + state.leaseMs, deadlineCap);
|
|
198
|
+
return getDb().query(`UPDATE timer_message_cutover SET lease_expires_at = ?, phase_journal = ?, updated_at = ?
|
|
199
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
200
|
+
.run(nextExpiry, appendJournal(row, { epoch, phase: "renewed", at: now }), now,
|
|
201
|
+
epoch, state.ownerId, now).changes === 1;
|
|
202
|
+
|
|
203
|
+
},
|
|
204
|
+
closeGate: (state: CutoverWriterState, { epoch, now }: CutoverStepArgs["closeGate"]) => {
|
|
205
|
+
const row = owned(state, epoch, now);
|
|
206
|
+
// `requestQuiesce` acquires the coordinator lease before the following tick closes the gate.
|
|
207
|
+
// Recheck the Stage-10 operation lease in this close transaction: an abort (or expiry) that
|
|
208
|
+
// won in that interval must leave this independent coordinator open and unleased, never
|
|
209
|
+
// permit stale authority to close the MESSAGE gate after the abort's release observed it open.
|
|
210
|
+
const fence = state.operationLeaseFence;
|
|
211
|
+
if (fence) {
|
|
212
|
+
const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
|
|
213
|
+
WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
|
|
214
|
+
AND lease_owner=? AND lease_expires_at > ?`)
|
|
215
|
+
.get(fence.requestId, fence.ownerId, now);
|
|
216
|
+
if (!live) {
|
|
217
|
+
const released = getDb().query(`UPDATE timer_message_cutover SET
|
|
218
|
+
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
219
|
+
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
220
|
+
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
221
|
+
desired_mode = observed_mode, phase_journal = ?, updated_at = ?
|
|
222
|
+
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>? AND phase='leased'`)
|
|
223
|
+
.run(appendJournal(row, {
|
|
224
|
+
epoch, phase: "idle", at: now,
|
|
225
|
+
reason: "Stage-10 operation lease no longer authorizes MESSAGE gate close",
|
|
226
|
+
}), now, epoch, state.ownerId, now);
|
|
227
|
+
if (released.changes !== 1) throw new Error("cutover lease lost while releasing stale Stage-10 gate request");
|
|
228
|
+
state.epoch = undefined;
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Re-read all three deadline sources under the SAME writer lock that closes the gate. A
|
|
233
|
+
// deadline mutation that won the race is therefore observed here and refuses entry; one that
|
|
234
|
+
// loses cannot commit until after the closed marker and is caught by the next continuous tick.
|
|
235
|
+
const guard = deadlineGuard(state, now);
|
|
236
|
+
if (!guard.safe) {
|
|
237
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'aborting', abort_requested = 1,
|
|
238
|
+
phase_journal = ?, updated_at = ? WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
|
|
239
|
+
.run(appendJournal(row, {
|
|
240
|
+
epoch, phase: "aborting", at: now,
|
|
241
|
+
reason: `deadline guard refused atomic gate close (slack=${guard.slackMs}, required=${guard.requiredMs})`,
|
|
242
|
+
}), now, epoch, state.ownerId);
|
|
243
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost while refusing gate close");
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const deadline = Math.min(
|
|
247
|
+
now + state.quiesceTimeoutMs,
|
|
248
|
+
guard.deadlines.earliestDeadline === null ? Number.MAX_SAFE_INTEGER : guard.deadlines.earliestDeadline - 1,
|
|
249
|
+
);
|
|
250
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'quiescing',
|
|
251
|
+
message_mutation_gate = 'closed', gate_epoch = ?, quiesce_started_at = ?, quiesce_deadline = ?,
|
|
252
|
+
ingress_policy = CASE WHEN ? THEN 'activating' ELSE ingress_policy END,
|
|
253
|
+
phase_journal = ?, updated_at = ?
|
|
254
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
|
|
255
|
+
.run(epoch, now, deadline, state.authoritativeActivation ? 1 : 0,
|
|
256
|
+
appendJournal(row, { epoch, phase: "quiescing", at: now, reason: "MESSAGE mutation gate closed" }),
|
|
257
|
+
now, epoch, state.ownerId);
|
|
258
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost before gate close");
|
|
259
|
+
return true;
|
|
260
|
+
|
|
261
|
+
},
|
|
262
|
+
transition: (state: CutoverWriterState, { epoch, phase, reason, now }: CutoverStepArgs["transition"]) => {
|
|
263
|
+
const row = owned(state, epoch, now);
|
|
264
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = ?, phase_journal = ?, updated_at = ?
|
|
265
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
266
|
+
.run(phase, appendJournal(row, { epoch, phase, at: now, reason }), now, epoch, state.ownerId, now);
|
|
267
|
+
if (changed.changes !== 1) throw new Error(`cutover lease lost before phase ${phase}`);
|
|
268
|
+
|
|
269
|
+
},
|
|
270
|
+
finishAbort: (state: CutoverWriterState, { epoch, reason, now }: CutoverStepArgs["finishAbort"]) => {
|
|
271
|
+
const row = owned(state, epoch, now);
|
|
272
|
+
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
273
|
+
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
274
|
+
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
275
|
+
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
276
|
+
phase_journal = ?, updated_at = ?
|
|
277
|
+
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
278
|
+
.run(appendJournal(row, { epoch, phase: "idle", at: now, reason: `gate reopened: ${reason}` }),
|
|
279
|
+
now, epoch, state.ownerId, now);
|
|
280
|
+
if (changed.changes !== 1) throw new Error("cutover lease lost before abort release");
|
|
281
|
+
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
export const TIMER_CUTOVER_COORDINATOR_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
|
|
285
|
+
"timerCutover.coordinatorStep": { offLoop: true, idempotent: false,
|
|
286
|
+
fn: (input: { step: keyof CutoverStepArgs; state: CutoverWriterState; args: CutoverStepArgs[keyof CutoverStepArgs] }) => {
|
|
287
|
+
const epoch = input.state.epoch;
|
|
288
|
+
const body = steps[input.step] as (state: CutoverWriterState, args: typeof input.args) => unknown;
|
|
289
|
+
const value = body(input.state, input.args);
|
|
290
|
+
return { value: value ?? null, ...(input.state.epoch !== epoch ? { clearEpoch: true } : {}) };
|
|
291
|
+
} },
|
|
292
|
+
});
|