@humwick/server 0.130.1 → 0.130.2
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/README.md +5 -0
- package/docs/guides/tools-administration.md +4 -0
- package/docs/guides/tools-coordination.md +1 -0
- package/docs/guides/tools-tasks.md +8 -0
- package/docs/guides/workflows.md +58 -0
- package/docs/openapi.json +414 -1
- package/package.json +8 -17
- package/public/assets/{MessageBubble-BWpoBjl4.js → MessageBubble-BWLqMGKG.js} +2 -2
- package/public/assets/{PlanGraphPanel-C0auck3n.js → PlanGraphPanel-3TlIQpjk.js} +2 -2
- package/public/assets/{TraceView-DBnOx7AQ.js → TraceView-Wimm0Czv.js} +2 -2
- package/public/assets/{VaultDecisionCard-0kp2oNBC.js → VaultDecisionCard-Bau_vNax.js} +2 -2
- package/public/assets/{activity-yLztXEBt.js → activity-CrSurn-r.js} +2 -2
- package/public/assets/{agent-lineage-BbJDR1dW.js → agent-lineage-CknAJKcb.js} +2 -2
- package/public/assets/agent-liveness-CaNwK0Zf.js +2 -0
- package/public/assets/{agent-name-CTZBrSD9.js → agent-name-DKbiIhBo.js} +2 -2
- package/public/assets/agent-profiles-DwF6EQ4R.js +2 -0
- package/public/assets/{agent-project-badge-9TqzUUYE.js → agent-project-badge-Diz4Sjjb.js} +2 -2
- package/public/assets/{agent-tools-D4n40pn8.js → agent-tools-CRhpRQza.js} +2 -2
- package/public/assets/{agent-type-icon-D6FUarOf.js → agent-type-icon-LrUEU3lB.js} +2 -2
- package/public/assets/{agents-BYDslkJc.js → agents-B-v66C-4.js} +2 -2
- package/public/assets/{analytics-B-XSbEMC.js → analytics-BqCb-LKi.js} +2 -2
- package/public/assets/{artifact-viewer-DdWIfV6M.js → artifact-viewer-kf4pGuAR.js} +2 -2
- package/public/assets/{artifacts-oBOqks-2.js → artifacts-D32zyw4Y.js} +2 -2
- package/public/assets/{artifacts-DPN1q4pA.js → artifacts-DJevXcvg.js} +2 -2
- package/public/assets/{automation-4KcRFXEu.js → automation-BpRajx_5.js} +2 -2
- package/public/assets/{backlog-Cw9xPTCF.js → backlog-D_UjTdzX.js} +2 -2
- package/public/assets/{busy-health-CTu32MZt.js → busy-health-N3VEh4ao.js} +2 -2
- package/public/assets/{channels-BiXzg1G9.js → channels-9jopungq.js} +2 -2
- package/public/assets/{chat-Ctk2qwvA.js → chat-CSXrTB3C.js} +2 -2
- package/public/assets/{code-preview-Bm1wWqAp.js → code-preview-p1NnmqK0.js} +2 -2
- package/public/assets/{connectors-C36ax4Vw.js → connectors-CZdqlDNL.js} +2 -2
- package/public/assets/{cosmos-Jj9LK2Wf.js → cosmos-BgdsE4dG.js} +2 -2
- package/public/assets/{crate-agent-H6S7-B9T.js → crate-agent-30aXTKva.js} +2 -2
- package/public/assets/{dependency-graph-Cd1E23wa.js → dependency-graph-B_jvw2GE.js} +2 -2
- package/public/assets/{deploys-Bwfq146q.js → deploys-DoN6bAb5.js} +2 -2
- package/public/assets/{display-CfO7rTuF.js → display-BrKWFGJz.js} +2 -2
- package/public/assets/{files-DhhZI2Ih.js → files-D6XEu8Ng.js} +2 -2
- package/public/assets/{fleet-B_rFEbnL.js → fleet-C7jQWbw2.js} +2 -2
- package/public/assets/{fleet-metrics-BkHP-P3R.js → fleet-metrics-BiDs3MLx.js} +2 -2
- package/public/assets/{formatted-body-Cm1RrzmN.js → formatted-body-CQBwc1fb.js} +3 -3
- package/public/assets/{formatted-body-impl-DQiOuOD1.js → formatted-body-impl-DyG0WsQS.js} +2 -2
- package/public/assets/{host-providers-dialog-CHqwXTov.js → host-providers-dialog-DKaW31dN.js} +2 -2
- package/public/assets/index-CRHep3M9.js +15 -0
- package/public/assets/index-MYAIwYTX.css +2 -0
- package/public/assets/{insights-fH1EL-qZ.js → insights-CzrtkpOE.js} +2 -2
- package/public/assets/{integrations-C5Oix6ng.js → integrations-aVsUmigt.js} +2 -2
- package/public/assets/{issue-ref-chip-ODyondIa.js → issue-ref-chip-BnSlM_95.js} +2 -2
- package/public/assets/{log-viewer-Cvfy6PW7.js → log-viewer-qe7Ee47Z.js} +2 -2
- package/public/assets/{maintenance-D8CDH5wZ.js → maintenance-NfjqFZAk.js} +2 -2
- package/public/assets/{managed-agents-egllbpij.js → managed-agents-BiH0HgaE.js} +2 -2
- package/public/assets/{markdown-preview-CYSFgVgK.js → markdown-preview-BixaT3Xr.js} +3 -3
- package/public/assets/{markdown-preview-impl-BteVIjX4.js → markdown-preview-impl-DNGlWIS3.js} +2 -2
- package/public/assets/{memory-C8WmXsyr.js → memory-DNsCl6UX.js} +2 -2
- package/public/assets/{messages-BzQdqlO6.js → messages-DopU1L1X.js} +2 -2
- package/public/assets/{orchestrators-B1HxzkNw.js → orchestrators-Cq7ckLQT.js} +2 -2
- package/public/assets/{overview-DCUnI4Z7.js → overview-C1DCTzzV.js} +2 -2
- package/public/assets/{pairs-ftCd2XcD.js → pairs-DE_m6rrE.js} +2 -2
- package/public/assets/{profile-icon-B2zH8DMH.js → profile-icon-B3rfh77F.js} +2 -2
- package/public/assets/project-profile-instructions-DQWZE0kA.js +2 -0
- package/public/assets/projects-Br2Bd2h1.js +2 -0
- package/public/assets/{prompt-settings-Bftq9czX.js → prompt-settings-BKvFsvGD.js} +2 -2
- package/public/assets/provider-display-DsuWMvVL.js +2 -0
- package/public/assets/{registry-DWCU5c8q.js → registry-Cw38_6gW.js} +2 -2
- package/public/assets/run-card-CpKqRbhg.js +2 -0
- package/public/assets/scroll-text-BKXS1ahr.js +2 -0
- package/public/assets/{secrets-C7GIUATz.js → secrets-DxI-vzSq.js} +2 -2
- package/public/assets/{service-access-settings-C3aujqTG.js → service-access-settings-D9s_VJbd.js} +2 -2
- package/public/assets/{settings-C6sZ9eBa.js → settings-CKZTVGIx.js} +2 -2
- package/public/assets/{shipping-Bu8p1gEw.js → shipping-B9YtnwtG.js} +2 -2
- package/public/assets/{stats-V9Z-GxUd.js → stats-DyUBKyWI.js} +2 -2
- package/public/assets/{status-dot-CVv0iGHL.js → status-dot-DYqlEUhJ.js} +2 -2
- package/public/assets/{store-BvtFdlGX.js → store-DcClXLH0.js} +3 -3
- package/public/assets/{tasks-CXU75HRF.js → tasks-Bh7Chr4-.js} +2 -2
- package/public/assets/{teams-BPf976zb.js → teams-bowI7qVY.js} +2 -2
- package/public/assets/{terminal-viewer-DE9BOe-8.js → terminal-viewer-BgVLHdsU.js} +3 -3
- package/public/assets/{terminal-viewer-impl-D04pjmGh.js → terminal-viewer-impl-Dz22UPy6.js} +2 -2
- package/public/assets/token-estimate-Co-Jdphh.js +2 -0
- package/public/assets/{use-agent-terminal-B1yR456R.js → use-agent-terminal-DSQQ-iY7.js} +2 -2
- package/public/assets/{use-decisions-D-S4tpDf.js → use-decisions-samMQhQf.js} +2 -2
- package/public/assets/use-project-list-59LheVP7.js +2 -0
- package/public/assets/{use-selectors-CnrAk2YP.js → use-selectors-BZa25xEB.js} +2 -2
- package/public/assets/{user-avatar-nBFIcR4M.js → user-avatar-DHxlvUvV.js} +2 -2
- package/public/assets/versioned-prompt-editor-BmED5DnJ.js +8 -0
- package/public/assets/{work-queue-TCjJxeT-.js → work-queue-CSvpuNLz.js} +2 -2
- package/public/assets/{workflow-marker-Cf3Hf2fN.js → workflow-marker-DAZcj5qt.js} +2 -2
- package/public/assets/{workflows-CXQX20Qj.js → workflows-DdmxRSyg.js} +2 -2
- package/public/assets/{workspaces-qSYzKYMA.js → workspaces-BxeKfVuF.js} +2 -2
- package/public/cosmos.html +4 -4
- package/public/index.html +31 -29
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/hooks/gate-run-guard.provisioned.mjs +31 -1
- package/runner/src/adapters/claude-effort.ts +2 -4
- package/runner/src/adapters/codex-launch-config.ts +4 -5
- package/runner/src/adapters/codex.ts +1 -1
- package/runner/src/adapters/opencode-provider.ts +7 -22
- package/runner/src/adapters/opencode.ts +4 -4
- package/runner/src/agent-runner/credentials.ts +42 -12
- package/runner/src/agent-runner/inbox.ts +27 -0
- package/runner/src/agent-runner/lifecycle-milestones.ts +0 -2
- package/runner/src/bin/claude.ts +4 -0
- package/runner/src/bin/codex.ts +4 -0
- package/runner/src/bin/opencode.ts +4 -0
- package/runner/src/index.ts +5 -2
- package/runner/src/provider-setup.ts +34 -0
- package/runner/src/providers.ts +2 -1
- package/runner/src/runner-core.ts +18 -23
- package/runner/src/spawn-input.ts +2 -1
- package/runner/src/test-run-dedup.ts +59 -56
- package/scripts/cd-config.ts +8 -0
- package/scripts/gate-run-lease.ts +10 -7
- package/scripts/gate.sh +5 -1
- package/src/branch-land-notice.ts +93 -0
- package/src/branch-landed.ts +7 -76
- package/src/bus.ts +19 -20
- package/src/cd-deploy-notify.ts +30 -2
- package/src/cd-host-lag-notify.ts +7 -6
- package/src/ci/incidents.ts +2 -2
- package/src/ci/monitor.ts +5 -8
- package/src/ci/root-missing.ts +2 -7
- package/src/ci/transition-notify.ts +7 -39
- package/src/cli/cd-pauses.ts +19 -0
- package/src/cli/orchestrator.ts +1 -1
- package/src/cli/state.ts +2 -19
- package/src/cli/task-edit.ts +22 -0
- package/src/cli/task.ts +3 -1
- package/src/cli/workflow.ts +8 -2
- package/src/commands-db.ts +3 -3
- package/src/config-store-write.ts +13 -1
- package/src/config-store.ts +25 -14
- package/src/db/activity-async.ts +6 -1
- package/src/db/activity-write-ops.ts +12 -1
- package/src/db/activity.ts +1 -14
- package/src/db/agent-search.ts +1 -15
- package/src/db/agents-async.ts +52 -5
- package/src/db/agents-write-ops.ts +12 -3
- package/src/db/agents.ts +81 -54
- package/src/db/ci-incidents-async.ts +5 -5
- package/src/db/ci-incidents-write-ops.ts +2 -2
- package/src/db/command-row.ts +10 -2
- package/src/db/commands-list-query.ts +2 -0
- package/src/db/commands-spawn-read.ts +1 -1
- package/src/db/config-store-write-ops.ts +7 -2
- package/src/db/continuation-archives-async.ts +5 -0
- package/src/db/continuation-archives-write-ops.ts +7 -0
- package/src/db/continuations.ts +2 -2
- package/src/db/db-writer-worker.ts +8 -1
- package/src/db/deploy-events-async.ts +1 -0
- package/src/db/deploy-events-read-ops.ts +2 -1
- package/src/db/deploy-events.ts +35 -4
- package/src/db/hosts-registration.ts +10 -2
- package/src/db/hosts.ts +9 -6
- package/src/db/inbox.ts +0 -14
- package/src/db/index.ts +2 -0
- package/src/db/integrations-async.ts +8 -0
- package/src/db/integrations-ops.ts +15 -0
- package/src/db/integrations.ts +15 -21
- package/src/db/issue-lifecycle.ts +3 -4
- package/src/db/issue-refs-async.ts +20 -0
- package/src/db/issue-refs-write-ops.ts +29 -0
- package/src/db/issue-refs.ts +7 -8
- package/src/db/long-running-work.ts +2 -2
- package/src/db/main-connection-writes.ts +23 -2
- package/src/db/maintenance-job-reconcile.ts +61 -0
- package/src/db/maintenance-job-runs-async.ts +14 -0
- package/src/db/maintenance-job-write-ops.ts +9 -0
- package/src/db/mappers.ts +2 -14
- package/src/db/merge-lease.ts +1 -15
- package/src/db/migrations.ts +7 -15
- package/src/db/orchestrators.ts +4 -72
- package/src/db/pairs.ts +1 -13
- package/src/db/project-profile-instructions.ts +50 -0
- package/src/db/prompt-versions.ts +9 -2
- package/src/db/provider-config-history.ts +8 -3
- package/src/db/provider-model-seed.ts +34 -0
- package/src/db/read-op-marshaling-fixtures.ts +5 -0
- package/src/db/read-ops.ts +4 -0
- package/src/db/routing-policy.ts +2 -3
- package/src/db/schema.ts +9 -3
- package/src/db/spawn-delegation-state.ts +11 -4
- package/src/db/stats-async.ts +4 -3
- package/src/db/task-entity.ts +23 -2
- package/src/db/task-list.ts +3 -1
- package/src/db/tasks.ts +1 -17
- package/src/db/timer-ingress.ts +7 -1
- package/src/db/token-db-write-ops.ts +17 -0
- package/src/db/token-standing.ts +15 -11
- package/src/db/workflow-contact.ts +1 -1
- package/src/db/workflow-task-read-ops.ts +30 -0
- package/src/db/write-ops.ts +6 -0
- package/src/db/writer-gateway.ts +17 -11
- package/src/dispatch-availability.ts +10 -6
- package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts} +4 -2
- package/src/host/disk-alert.ts +121 -0
- package/src/lifecycle-manager.ts +3 -2
- package/src/maintenance/cd-watcher.ts +3 -3
- package/src/maintenance/critical-job-watchdog.ts +16 -17
- package/src/maintenance/index.ts +1 -0
- package/src/maintenance/jobs.ts +3 -2
- package/src/maintenance/scheduler.ts +84 -128
- package/src/maintenance/stalled-delivery-pager.ts +6 -5
- package/src/maintenance/workspaces.ts +1 -1
- package/src/managed-policy.ts +31 -27
- package/src/mcp/index.ts +5 -2
- package/src/mcp/output-descriptions.ts +8 -8
- package/src/mcp/presentation.ts +4 -4
- package/src/mcp/project-profile-instructions.ts +32 -0
- package/src/mcp/tool-groups.ts +1 -1
- package/src/mcp/tools-spawn.ts +4 -3
- package/src/mcp-agent-action.ts +1 -2
- package/src/mcp-issue-ref-tools.ts +12 -11
- package/src/mcp-maintenance-tools.ts +2 -2
- package/src/mcp-profile-trust-tools.ts +4 -4
- package/src/mcp-provider-catalog.ts +2 -2
- package/src/mcp-task-tools.ts +11 -5
- package/src/mcp-team-template-tools.ts +3 -3
- package/src/mcp-workflow-tools.ts +10 -3
- package/src/notification-types.ts +2 -1
- package/src/operator-alarm.ts +26 -11
- package/src/project-instructions.ts +4 -0
- package/src/project-profile-instruction-text.ts +6 -0
- package/src/project-profile-instructions.ts +20 -0
- package/src/provider-catalog-store.ts +3 -49
- package/src/provider-config-store.ts +12 -11
- package/src/provider-model-seed.ts +1 -18
- package/src/provider-quota-config-store.ts +5 -4
- package/src/provider-secrets.test-fixture.ts +3 -3
- package/src/recipe-runner.ts +2 -2
- package/src/repo-config-source.ts +10 -4
- package/src/routes/agent-profiles.ts +6 -6
- package/src/routes/agents-spawn.ts +1 -1
- package/src/routes/agents.ts +6 -6
- package/src/routes/commands-listing.ts +4 -0
- package/src/routes/commands.ts +2 -2
- package/src/routes/continuation-archives.ts +2 -2
- package/src/routes/index.ts +10 -1
- package/src/routes/integrations.ts +24 -50
- package/src/routes/issue-refs.ts +7 -7
- package/src/routes/orchestrator-bootstrap.ts +1 -1
- package/src/routes/orchestrator-host-registration.ts +2 -2
- package/src/routes/orchestrator-host-utilization.ts +3 -1
- package/src/routes/orchestrator-proxy.ts +2 -2
- package/src/routes/orchestrator.ts +2 -2
- package/src/routes/project-profile-instructions.ts +27 -0
- package/src/routes/project-trust.ts +2 -2
- package/src/routes/provider-config.ts +6 -6
- package/src/routes/provider-quota-config.ts +2 -2
- package/src/routes/settings.ts +5 -5
- package/src/routes/stats.ts +6 -6
- package/src/routes/task-http.ts +15 -1
- package/src/routes/task-issues.ts +11 -26
- package/src/routes/tasks.ts +22 -53
- package/src/routes/team-templates.ts +5 -5
- package/src/routes/teams-config.ts +2 -2
- package/src/routes/tokens.ts +12 -5
- package/src/routes/workflows.ts +20 -5
- package/src/routing-policy-normalize.ts +1 -2
- package/src/routing-step-defaults.ts +0 -2
- package/src/runner-token-mint-limit.ts +63 -0
- package/src/runtime-grant.ts +4 -2
- package/src/runtime-tokens.ts +39 -7
- package/src/security.ts +13 -20
- package/src/server-background.ts +1 -1
- package/src/server.ts +5 -2
- package/src/services/host-utilization.ts +23 -0
- package/src/services/idle-epics.ts +5 -6
- package/src/services/issue-lifecycle.ts +57 -61
- package/src/services/issue-refs.ts +9 -13
- package/src/services/kernel-timers-cutover-chaos-worker.ts +10 -7
- package/src/services/kernel-timers-ingress.ts +1 -1
- package/src/services/landed-ancestry.ts +19 -3
- package/src/services/memory-areas.ts +15 -51
- package/src/services/plan-ci-notice.ts +57 -0
- package/src/services/plan-ci-recovery.ts +2 -55
- package/src/services/plan-wake-digest.ts +1 -0
- package/src/services/profile-trust.ts +4 -3
- package/src/services/project-config-read.ts +70 -0
- package/src/services/project-profile-instructions.ts +54 -0
- package/src/services/provisioning-harness/budget-baseline.json +4 -4
- package/src/services/restart-spawn.ts +61 -49
- package/src/services/self-care-state.ts +2 -1
- package/src/services/self-effort.ts +5 -5
- package/src/services/send-message.ts +5 -1
- package/src/services/spawn-agent.ts +13 -2
- package/src/services/spawn-chain-inspector.ts +6 -13
- package/src/services/spawn-policy.ts +68 -0
- package/src/services/spawn-tree-cleanup.ts +2 -2
- package/src/services/task-dispatch.ts +3 -0
- package/src/services/task-state-reconcile.ts +6 -14
- package/src/services/team-templates.ts +9 -8
- package/src/services/transient-agent-reaper.ts +3 -1
- package/src/services/workflow-auto-start.ts +25 -13
- package/src/services/workflow-ci-main.ts +1 -1
- package/src/services/workflow-deferred-cleanup.ts +4 -2
- package/src/services/workflow-effect-driver.ts +2 -0
- package/src/services/workflow-effects.ts +18 -11
- package/src/services/workflow-land-notice.ts +10 -0
- package/src/services/workflow-pause-text.ts +43 -0
- package/src/services/workflow-reconcile.ts +13 -11
- package/src/services/workflow-run-view.ts +2 -0
- package/src/services/workflow-runs.ts +29 -13
- package/src/services/workflow-supervisor-state.ts +5 -0
- package/src/services/workflow-supervisor.ts +7 -3
- package/src/settings/registry.ts +1 -2
- package/src/spawn-command.ts +4 -4
- package/src/spawn-models.ts +10 -12
- package/src/spawn-placement.ts +1 -1
- package/src/spawn-targets.ts +1 -1
- package/src/startup-context-injection.ts +1 -0
- package/src/state-adoption/cd-cutover.ts +53 -7
- package/src/state-adoption/fleet-cutover.ts +35 -0
- package/src/state-adoption/fleet-plan.ts +36 -0
- package/src/state-adoption/index.ts +2 -2
- package/src/state-adoption/linux-plan.ts +2 -2
- package/src/state-adoption/linux.ts +2 -2
- package/src/state-adoption/macos-plan.ts +2 -2
- package/src/state-adoption/macos.ts +1 -1
- package/src/steward.ts +7 -6
- package/src/task-dispatch-live.ts +1 -1
- package/src/task-dispatcher.ts +1 -1
- package/src/teams-config-store.ts +4 -3
- package/src/token-db-write.ts +10 -0
- package/src/workspace-command-settle.ts +2 -1
- package/src/workspace-steward-wake.ts +2 -2
- package/node_modules/@humwick/kernel/LICENSE +0 -167
- package/node_modules/@humwick/kernel/async-entity-persistence.ts +0 -344
- package/node_modules/@humwick/kernel/async-event-log.ts +0 -107
- package/node_modules/@humwick/kernel/automations.pilot.ts +0 -147
- package/node_modules/@humwick/kernel/config.ts +0 -111
- package/node_modules/@humwick/kernel/contract.ts +0 -572
- package/node_modules/@humwick/kernel/control-authz.ts +0 -281
- package/node_modules/@humwick/kernel/control.ts +0 -645
- package/node_modules/@humwick/kernel/effects.ts +0 -493
- package/node_modules/@humwick/kernel/event-log.ts +0 -319
- package/node_modules/@humwick/kernel/http-transport.ts +0 -384
- package/node_modules/@humwick/kernel/identity-grammar.ts +0 -20
- package/node_modules/@humwick/kernel/identity.ts +0 -248
- package/node_modules/@humwick/kernel/index.ts +0 -28
- package/node_modules/@humwick/kernel/kernel-persistence.ts +0 -181
- package/node_modules/@humwick/kernel/landing-incident.ts +0 -306
- package/node_modules/@humwick/kernel/landing.ts +0 -953
- package/node_modules/@humwick/kernel/manifest-validation.ts +0 -460
- package/node_modules/@humwick/kernel/mcp-transport.ts +0 -109
- package/node_modules/@humwick/kernel/message-authority-chaos-worker.ts +0 -200
- package/node_modules/@humwick/kernel/message-authority.ts +0 -281
- package/node_modules/@humwick/kernel/module-host.ts +0 -722
- package/node_modules/@humwick/kernel/openapi-init.ts +0 -14
- package/node_modules/@humwick/kernel/package.json +0 -39
- package/node_modules/@humwick/kernel/persistence-schema.ts +0 -248
- package/node_modules/@humwick/kernel/persistence.ts +0 -510
- package/node_modules/@humwick/kernel/registry.ts +0 -60
- package/node_modules/@humwick/kernel/sqlite-kernel-persistence.ts +0 -499
- package/node_modules/@humwick/kernel/sqlite-module-host.ts +0 -87
- package/node_modules/@humwick/kernel/timers-chaos-worker.ts +0 -186
- package/node_modules/@humwick/kernel/timers.pilot.ts +0 -1411
- package/node_modules/@humwick/kernel/workflow-activation.ts +0 -25
- package/node_modules/@humwick/kernel/workflow-backstop.ts +0 -40
- package/node_modules/@humwick/kernel/workflow-cancel-tree.ts +0 -20
- package/node_modules/@humwick/kernel/workflow-chaos-worker.ts +0 -213
- package/node_modules/@humwick/kernel/workflow-composition.ts +0 -231
- package/node_modules/@humwick/kernel/workflow-container.ts +0 -72
- package/node_modules/@humwick/kernel/workflow-control.test-fixture.ts +0 -42
- package/node_modules/@humwick/kernel/workflow-control.ts +0 -178
- package/node_modules/@humwick/kernel/workflow-definition.ts +0 -507
- package/node_modules/@humwick/kernel/workflow-dispatch.ts +0 -60
- package/node_modules/@humwick/kernel/workflow-foreach.ts +0 -167
- package/node_modules/@humwick/kernel/workflow-journal-codec.ts +0 -339
- package/node_modules/@humwick/kernel/workflow-journal.ts +0 -922
- package/node_modules/@humwick/kernel/workflow-lane.ts +0 -48
- package/node_modules/@humwick/kernel/workflow-lease-owner.ts +0 -60
- package/node_modules/@humwick/kernel/workflow-recovery.test-fixture.ts +0 -19
- package/node_modules/@humwick/kernel/workflow-runtime.ts +0 -938
- package/node_modules/@humwick/kernel/workflow-script.ts +0 -11
- package/node_modules/@humwick/kernel/workflow-state.ts +0 -299
- package/node_modules/@humwick/kernel/workflow-step-commit.ts +0 -195
- package/node_modules/@humwick/kernel/workflow-transitions.ts +0 -317
- package/node_modules/@humwick/kernel/workflow-values.ts +0 -153
- package/node_modules/@humwick/kernel/workflow.pilot.ts +0 -376
- package/node_modules/@humwick/offload/LICENSE +0 -167
- package/node_modules/@humwick/offload/README.md +0 -57
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
- package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
- package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/envelope.js +0 -80
- package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
- package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
- package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/index.js +0 -5
- package/node_modules/@humwick/offload/dist/index.js.map +0 -1
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
- package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
- package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/types.js +0 -176
- package/node_modules/@humwick/offload/dist/types.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
- package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
- package/node_modules/@humwick/offload/dist/worker.js +0 -1
- package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
- package/node_modules/@humwick/offload/package.json +0 -42
- package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
- package/node_modules/@humwick/offload/src/envelope.ts +0 -92
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
- package/node_modules/@humwick/offload/src/index.ts +0 -4
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
- package/node_modules/@humwick/offload/src/types.ts +0 -235
- package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
- package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
- package/node_modules/@humwick/offload/src/worker.ts +0 -5
- package/public/assets/agent-liveness-DyqxfB-t.js +0 -2
- package/public/assets/agent-profiles-BpI_jTxq.js +0 -2
- package/public/assets/index-B7ZerHLY.css +0 -2
- package/public/assets/index-BU3uWo2t.js +0 -15
- package/public/assets/projects-B_lmcqfO.js +0 -2
- package/public/assets/provider-display-DbLWpP3j.js +0 -2
- package/public/assets/run-card-CO_bguQf.js +0 -2
- package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
- package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
- package/runner/src/gate-run-lease-probe.ts +0 -38
- package/src/db/claude-catalog-reconcile.ts +0 -27
- package/src/routing-model-aliases.ts +0 -21
package/src/db/timer-ingress.ts
CHANGED
|
@@ -104,13 +104,18 @@ function rowToIntent(row: IntentDbRow): TimerIngressIntentRow {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
const installedOn = new WeakSet<object>();
|
|
108
|
+
|
|
107
109
|
/** Idempotent, crash-safe schema install (one transaction; re-running over existing data is a
|
|
108
110
|
* no-op). Bootstrap DDL — it runs ONCE at startup as its own committed statement, never lazily
|
|
109
111
|
* inside an op, so a rolled-back op can never undo the schema it ran against (§4 allowlist). */
|
|
110
112
|
export function installTimerIngressSchema(
|
|
111
|
-
options: { requestKeyMigrationFaults?: TimerIngressRequestKeyMigrationFaults } = {},
|
|
113
|
+
options: { requestKeyMigrationFaults?: TimerIngressRequestKeyMigrationFaults; ifNotInstalled?: boolean } = {},
|
|
112
114
|
): void {
|
|
113
115
|
const db = getDb();
|
|
116
|
+
// #1870 S7: `startServer` installs it before bind (the boot exception); `createTimerIngress` passes
|
|
117
|
+
// `ifNotInstalled` so its later call runs no DDL on the main connection. Explicit calls always install.
|
|
118
|
+
if (options.ifNotInstalled && installedOn.has(db)) return;
|
|
114
119
|
db.transaction(() => {
|
|
115
120
|
// AUTOINCREMENT is deliberate: `seq` must be monotone and NEVER reused (the watermark's meaning
|
|
116
121
|
// depends on it — and it doubles as the per-timer apply FENCE), even if settled rows are pruned.
|
|
@@ -165,6 +170,7 @@ export function installTimerIngressSchema(
|
|
|
165
170
|
db.run("INSERT OR IGNORE INTO timer_ingress_watermark (id, applied_seq, updated_at) VALUES (1, 0, 0)");
|
|
166
171
|
options.requestKeyMigrationFaults?.beforeCommit?.();
|
|
167
172
|
}).immediate();
|
|
173
|
+
installedOn.add(db);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
// ── Reads ───────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type PruneExpiredTokenRecordsInput,
|
|
26
26
|
} from "../token-db.ts";
|
|
27
27
|
import type { TokenRecord } from "../types.ts";
|
|
28
|
+
import { recordLaunchFact, type LaunchFact } from "./launch-facts.ts";
|
|
28
29
|
import type { TokenRevokeReason } from "../security.ts";
|
|
29
30
|
import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
|
|
30
31
|
|
|
@@ -41,6 +42,11 @@ export interface HandoffTokenOpArgs {
|
|
|
41
42
|
readonly newJti: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** Args for `tokens.createRunner`: a runner mint plus, for a fresh launch, its launch fact (#1870 S7). */
|
|
46
|
+
export interface CreateRunnerTokenOpArgs extends CreateTokenOpArgs {
|
|
47
|
+
readonly launch?: { readonly agentId: string; readonly fact: LaunchFact };
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
export interface RevokeTokenOpArgs {
|
|
45
51
|
readonly jti: string;
|
|
46
52
|
readonly reason?: TokenRevokeReason;
|
|
@@ -72,6 +78,17 @@ export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
|
|
|
72
78
|
fn: (args: CreateTokenOpArgs, ctx: WriterOpContext) => createTokenInternal(args.input, args.jti, ctx.now),
|
|
73
79
|
serializeIdempotencyResult: stripCreateToken,
|
|
74
80
|
},
|
|
81
|
+
// A runner mint and the launch fact its successors derive from, in one transaction.
|
|
82
|
+
"tokens.createRunner": {
|
|
83
|
+
offLoop: true,
|
|
84
|
+
idempotent: false,
|
|
85
|
+
fn: (args: CreateRunnerTokenOpArgs, ctx: WriterOpContext) => {
|
|
86
|
+
const issued = createTokenInternal(args.input, args.jti, ctx.now);
|
|
87
|
+
if (args.launch) recordLaunchFact(args.launch.agentId, args.launch.fact, ctx.now);
|
|
88
|
+
return issued;
|
|
89
|
+
},
|
|
90
|
+
serializeIdempotencyResult: stripCreateToken,
|
|
91
|
+
},
|
|
75
92
|
// Non-idempotent (mints and links a successor); folds the eligibility read (ADR §3.4).
|
|
76
93
|
"tokens.handoff": {
|
|
77
94
|
offLoop: true,
|
package/src/db/token-standing.ts
CHANGED
|
@@ -61,24 +61,28 @@ export function tokenRevocationReason(jti: string): string | null | undefined {
|
|
|
61
61
|
/** A rotation retires one token for its successor, so it is never a lineage revocation. */
|
|
62
62
|
export type LineageRevokeReason = Exclude<TokenRevokeReason, "rotation">;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* stale walk stops at stale members: what follows them is an orchestrator recovery. It never enters a `keep` token. */
|
|
68
|
-
export function revokeTokenLineage(seeds: readonly string[], reason: LineageRevokeReason | null, nowSeconds: number, keep: readonly string[] = []): string[] {
|
|
69
|
-
if (seeds.length === 0) return [];
|
|
70
|
-
const rows = getDb().query(`
|
|
64
|
+
// #3776 — each step must be a key lookup whatever sqlite_stat1 says: CROSS JOIN pins the join order and the unary
|
|
65
|
+
// `+` keeps the predecessor index off the backward step, so stale stats cannot make the walk scan per member.
|
|
66
|
+
export const TOKEN_LINEAGE_REVOKE_SQL = `
|
|
71
67
|
WITH RECURSIVE kept(jti) AS (SELECT value FROM json_each(?1)),
|
|
72
68
|
lineage(jti) AS (
|
|
73
69
|
SELECT value FROM json_each(?2) WHERE value NOT IN kept
|
|
74
|
-
UNION SELECT t.jti FROM lineage l JOIN tokens p ON p.jti = l.jti JOIN tokens t ON t.predecessor_jti = p.jti
|
|
70
|
+
UNION SELECT t.jti FROM lineage l CROSS JOIN tokens p ON p.jti = l.jti CROSS JOIN tokens t ON t.predecessor_jti = p.jti
|
|
75
71
|
WHERE t.jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
|
|
76
|
-
UNION SELECT p.predecessor_jti FROM lineage l JOIN tokens p ON p.jti = l.jti
|
|
77
|
-
WHERE p.predecessor_jti IS NOT NULL AND p.predecessor_jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
|
|
72
|
+
UNION SELECT p.predecessor_jti FROM lineage l CROSS JOIN tokens p ON p.jti = l.jti
|
|
73
|
+
WHERE +p.predecessor_jti IS NOT NULL AND +p.predecessor_jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
|
|
78
74
|
)
|
|
79
75
|
UPDATE tokens SET revoked_at = COALESCE(revoked_at, ?3), revoked_reason = ?4
|
|
80
76
|
WHERE jti IN lineage AND (revoked_at IS NULL OR (?4 IS NOT 'stale' AND revoked_reason = 'stale'))
|
|
81
77
|
RETURNING jti
|
|
82
|
-
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
/** #1245 — a token and every token handed off from it (token-db.ts handoffTokenInternal) are one credential, so
|
|
81
|
+
* revoking any of them revokes the whole lineage: unretired predecessors, pending successors and their renewals.
|
|
82
|
+
* A hard reason also hardens stale members, so no auth, renewal or orchestrator recovery reaches the lineage. A
|
|
83
|
+
* stale walk stops at stale members: what follows them is an orchestrator recovery. It never enters a `keep` token. */
|
|
84
|
+
export function revokeTokenLineage(seeds: readonly string[], reason: LineageRevokeReason | null, nowSeconds: number, keep: readonly string[] = []): string[] {
|
|
85
|
+
if (seeds.length === 0) return [];
|
|
86
|
+
const rows = getDb().query(TOKEN_LINEAGE_REVOKE_SQL).all(JSON.stringify(keep), JSON.stringify(seeds), nowSeconds, reason) as Array<{ jti: string }>;
|
|
83
87
|
return rows.map((row) => row.jti);
|
|
84
88
|
}
|
|
@@ -20,7 +20,7 @@ export interface WorkflowHeldContact { runId: string; reason: WorkflowContactRea
|
|
|
20
20
|
|
|
21
21
|
/** Notices that bypass the step-contact hold: the run's own coordinator notices (including GO awaiting an external
|
|
22
22
|
* hold's release), and a step agent held on a provider block, which waits until someone acts (#3647). */
|
|
23
|
-
const COORDINATOR_NOTICE_KINDS = new Set(["workflow.started", "workflow.landed", "workflow.escalated", "workflow.result", "workflow.review-passed", "agent.spawn_failed", "agent.spawn_recovered", "delegation.budget_exhausted", "agent.provider_blocked", "agent.provider_still_held"]);
|
|
23
|
+
const COORDINATOR_NOTICE_KINDS = new Set(["workflow.started", "workflow.landed", "workflow.escalated", "workflow.result", "workflow.review-passed", "workflow.paused", "workflow.unpaused", "agent.spawn_failed", "agent.spawn_recovered", "delegation.budget_exhausted", "agent.provider_blocked", "agent.provider_still_held"]);
|
|
24
24
|
export const WORKFLOW_SCOPE = "local";
|
|
25
25
|
export const WORKFLOW_RUN_TABLE = "kernel_entity__workflow__run";
|
|
26
26
|
const LINK_TABLE = "kernel_entity__workflow__link";
|
|
@@ -185,3 +185,33 @@ export function openWorkflowRunHolders(): { teamIds: Set<string>; coordinatorIds
|
|
|
185
185
|
}
|
|
186
186
|
return holders;
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
/** #3757 — a running run a coordinator paused, with its input Task's team; `pause` is the stored JSON. */
|
|
190
|
+
export interface PausedWorkflowRunRow {
|
|
191
|
+
runId: string; taskId: number; taskTitle: string | null; coordinator: string; callerAgentId: string | null;
|
|
192
|
+
teamId: string | null; owner: string | null; pause: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let pauseColumnOn: unknown;
|
|
196
|
+
/** The column appears when the workflow module first registers after #3757; until then nothing is paused. */
|
|
197
|
+
function pauseColumnReady(): boolean {
|
|
198
|
+
const db = getDb();
|
|
199
|
+
if (pauseColumnOn === db) return true;
|
|
200
|
+
const ready = (db.query(`PRAGMA table_info(${WORKFLOW_RUN_TABLE})`).all() as Array<{ name: string }>).some((column) => column.name === "pause");
|
|
201
|
+
if (ready) pauseColumnOn = db;
|
|
202
|
+
return ready;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function pausedWorkflowRuns(): PausedWorkflowRunRow[] {
|
|
206
|
+
if (!workflowTablesReady() || !pauseColumnReady()) return [];
|
|
207
|
+
return getDb().query(`SELECT run.id AS runId, run.taskId AS taskId, task.title AS taskTitle, run.coordinator AS coordinator, run.callerAgentId AS callerAgentId,
|
|
208
|
+
task.team_id AS teamId, task.owner AS owner, run.pause AS pause
|
|
209
|
+
FROM ${WORKFLOW_RUN_TABLE} run LEFT JOIN tasks task ON task.id = run.taskId
|
|
210
|
+
WHERE run.scope = ? AND run.status = 'running' AND run.pause IS NOT NULL ORDER BY run.id`).all(WORKFLOW_SCOPE) as PausedWorkflowRunRow[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** #3757 — a paused run keeps its idle step agents: the idle reapers leave them alone until it is unpaused. */
|
|
214
|
+
export function workflowRunPaused(runId: string): boolean {
|
|
215
|
+
if (!workflowTablesReady() || !pauseColumnReady()) return false;
|
|
216
|
+
return Boolean(getDb().query(`SELECT 1 FROM ${WORKFLOW_RUN_TABLE} WHERE scope = ? AND id = ? AND status = 'running' AND pause IS NOT NULL`).get(WORKFLOW_SCOPE, runId));
|
|
217
|
+
}
|
package/src/db/write-ops.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { INTEGRATIONS_WRITE_OPS } from "./integrations-ops.ts";
|
|
1
2
|
import { bufferedLastSeen, type PresenceSnapshot } from "./presence-buffer";
|
|
2
3
|
import { ensureDurablePayloadSchema, readRetryReceipt, storeDurablePayload } from "./durable-payloads";
|
|
3
4
|
import { ledgerSlotPredicate, ledgerSlots, WRITER_IDEMPOTENCY_RETENTION_MS } from "./writer-ledger";
|
|
@@ -5,6 +6,7 @@ import { workspaceChangeListeners } from "./workspaces";
|
|
|
5
6
|
import { withWriterOpState } from "./main-thread-state";
|
|
6
7
|
import { importVaultRows } from "./secrets-vault-import";
|
|
7
8
|
import { PROMPT_VERSION_WRITE_OPS } from "./prompt-versions-ops";
|
|
9
|
+
import { PROJECT_PROFILE_INSTRUCTION_WRITE_OPS } from "./project-profile-instructions";
|
|
8
10
|
import { VAULT_WRITE_OPS } from "./secrets-vault-write";
|
|
9
11
|
import { VAULT_EXPIRY_WRITE_OPS } from "./vault-expiry";
|
|
10
12
|
import { VAULT_BACKUP_WRITE_OPS } from "./vault-backup";
|
|
@@ -117,6 +119,7 @@ import { RESTART_EPOCH_WRITE_OPS } from "./restart-epoch-write-ops.ts";
|
|
|
117
119
|
import { TOKEN_USAGE_WRITE_OPS } from "./token-usage-write-ops.ts";
|
|
118
120
|
// #1870 S3 — the 1 s raw writers: maintenance leases/runs, pending issue closes, workspace lifecycle.
|
|
119
121
|
import { MAINTENANCE_JOB_WRITE_OPS } from "./maintenance-job-write-ops.ts";
|
|
122
|
+
import { ISSUE_REFS_WRITE_OPS } from "./issue-refs-write-ops.ts";
|
|
120
123
|
import { LONG_RUNNING_WORK_WRITE_OPS } from "./long-running-work-write-ops.ts";
|
|
121
124
|
import { WORKSPACE_WRITE_OPS } from "./workspace-write-ops.ts";
|
|
122
125
|
|
|
@@ -406,6 +409,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
|
|
|
406
409
|
...ROUTING_POLICY_WRITE_OPS,
|
|
407
410
|
...USER_SETTINGS_WRITE_OPS,
|
|
408
411
|
...PROMPT_VERSION_WRITE_OPS,
|
|
412
|
+
...PROJECT_PROFILE_INSTRUCTION_WRITE_OPS,
|
|
409
413
|
...TOKEN_DB_WRITE_OPS,
|
|
410
414
|
...DELIVERY_WRITE_OPS,
|
|
411
415
|
...TIMER_WRITE_OPS,
|
|
@@ -440,6 +444,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
|
|
|
440
444
|
...SPAWN_TREE_CLEANUP_WRITE_OPS,
|
|
441
445
|
...PROJECTS_WRITE_OPS,
|
|
442
446
|
...CHANNELS_WRITE_OPS,
|
|
447
|
+
...INTEGRATIONS_WRITE_OPS,
|
|
443
448
|
...CI_INCIDENTS_WRITE_OPS,
|
|
444
449
|
...TEAMS_WRITE_OPS,
|
|
445
450
|
...TEAM_SPAWN_DELEGATION_WRITE_OPS,
|
|
@@ -468,6 +473,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
|
|
|
468
473
|
...TOKEN_USAGE_WRITE_OPS,
|
|
469
474
|
...MAINTENANCE_JOB_WRITE_OPS,
|
|
470
475
|
...LONG_RUNNING_WORK_WRITE_OPS,
|
|
476
|
+
...ISSUE_REFS_WRITE_OPS,
|
|
471
477
|
...WORKSPACE_WRITE_OPS,
|
|
472
478
|
...WEB_TOOL_CALL_WRITE_OPS,
|
|
473
479
|
...CLAUDE_API_BUDGET_WRITE_OPS,
|
package/src/db/writer-gateway.ts
CHANGED
|
@@ -174,8 +174,9 @@ export interface WriterStats {
|
|
|
174
174
|
* SQLite lock (H4 §3.6.1). A wedged writer is NOT idle — this is deliberately distinct from
|
|
175
175
|
* `busy:0`, which is how the naive pool-replace path mis-reported the stall. New non-critical
|
|
176
176
|
* writes shed and critical writes are held until the wedged op completes and the FIFO resumes.
|
|
177
|
+
* #3776 — names the op holding the writer and how long it has run, so health says WHICH job; null when not wedged.
|
|
177
178
|
*/
|
|
178
|
-
wedged:
|
|
179
|
+
wedged: { op: string; runningMs: number } | null;
|
|
179
180
|
/**
|
|
180
181
|
* #2276 — how many times the IN-PROCESS drain has yielded the event loop mid-convoy because its
|
|
181
182
|
* time slice was spent. 0 on the worker backend (nothing to slice). A steadily climbing count is
|
|
@@ -259,7 +260,7 @@ let drainYields = 0;
|
|
|
259
260
|
// lock (§3.6.1). Cleared when that in-flight job finally settles (the op completed and released the
|
|
260
261
|
// lock), which re-kicks the FIFO drain. Worker backend only — an in-process op runs synchronously on
|
|
261
262
|
// the main thread, so it cannot be observed mid-stall.
|
|
262
|
-
let
|
|
263
|
+
let wedgedOp: { op: string; startedAt: number } | undefined;
|
|
263
264
|
|
|
264
265
|
// Test-only fault injection for the precise H5 window: the op has COMMITTED, but its
|
|
265
266
|
// acknowledgement is lost before the caller observes the result. One-shot by design so a retry
|
|
@@ -355,14 +356,14 @@ function createFileWriter(dbPath: string): WorkerBackend {
|
|
|
355
356
|
|
|
356
357
|
/** Server shutdown keeps already-admitted writes; test/reset teardown remains immediate. */
|
|
357
358
|
export async function drainWriterOnShutdown(): Promise<void> {
|
|
358
|
-
try { await shutdownDrain.start(() => !draining && !
|
|
359
|
+
try { await shutdownDrain.start(() => !draining && !wedgedOp && pendingWriteDepth() === 0 && inFlightWrites() === 0); }
|
|
359
360
|
finally { await shutdownWriter(); }
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
export async function shutdownWriter(): Promise<void> {
|
|
363
364
|
const current = backend;
|
|
364
365
|
backend = undefined;
|
|
365
|
-
|
|
366
|
+
wedgedOp = undefined;
|
|
366
367
|
// Reject anything still queued at the gateway so no caller hangs across a teardown.
|
|
367
368
|
const stranded = submissionQueue.splice(0, submissionQueue.length);
|
|
368
369
|
for (const entry of stranded) {
|
|
@@ -379,7 +380,8 @@ export function isWriterInitialized(): boolean {
|
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
export function writerStats(): WriterStats {
|
|
382
|
-
if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged:
|
|
383
|
+
if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged: null, drainYields };
|
|
384
|
+
const wedged = wedgedOp ? { op: wedgedOp.op, runningMs: Math.round(performance.now() - wedgedOp.startedAt) } : null;
|
|
383
385
|
if (backend.kind === "in-process") {
|
|
384
386
|
return { backend: "in-process", queued: pendingWriteDepth(), busy: inFlightWrites(), highWater: queueHighWater, wedged, drainYields };
|
|
385
387
|
}
|
|
@@ -480,7 +482,7 @@ function admitOrThrow(op: string, opts: WriteOpts | undefined, count: number): v
|
|
|
480
482
|
const occupancy = pendingWriteDepth() + inFlightWrites();
|
|
481
483
|
if (occupancy + count > queueHardCap) throw new WriterOverloadedError(op, occupancy);
|
|
482
484
|
if (opts?.critical) return;
|
|
483
|
-
const workerWedged =
|
|
485
|
+
const workerWedged = wedgedOp !== undefined;
|
|
484
486
|
if (workerWedged || occupancy + count > queueHighWater) throw new WriterOverloadedError(op, occupancy);
|
|
485
487
|
}
|
|
486
488
|
|
|
@@ -511,7 +513,7 @@ async function drainLoop(): Promise<void> {
|
|
|
511
513
|
// second concurrent drain and anything submitted meanwhile is picked up by this same loop when
|
|
512
514
|
// it resumes — FIFO is preserved, the queue is only PAUSED.
|
|
513
515
|
let sliceStartedAt = performance.now();
|
|
514
|
-
while (submissionQueue.length > 0 && !
|
|
516
|
+
while (submissionQueue.length > 0 && !wedgedOp) {
|
|
515
517
|
const entry = submissionQueue.shift()!;
|
|
516
518
|
const onLoop = backend?.kind === "in-process";
|
|
517
519
|
await dispatchEntry(entry);
|
|
@@ -521,7 +523,7 @@ async function drainLoop(): Promise<void> {
|
|
|
521
523
|
sliceStartedAt = performance.now();
|
|
522
524
|
continue;
|
|
523
525
|
}
|
|
524
|
-
if (submissionQueue.length === 0 ||
|
|
526
|
+
if (submissionQueue.length === 0 || wedgedOp) continue;
|
|
525
527
|
if (performance.now() - sliceStartedAt < drainSliceMs) continue;
|
|
526
528
|
drainYields += 1;
|
|
527
529
|
await yieldEventLoop();
|
|
@@ -529,7 +531,7 @@ async function drainLoop(): Promise<void> {
|
|
|
529
531
|
}
|
|
530
532
|
} finally {
|
|
531
533
|
draining = false;
|
|
532
|
-
shutdownDrain.notify(!
|
|
534
|
+
shutdownDrain.notify(!wedgedOp && pendingWriteDepth() === 0 && inFlightWrites() === 0);
|
|
533
535
|
}
|
|
534
536
|
}
|
|
535
537
|
|
|
@@ -650,6 +652,7 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
|
|
|
650
652
|
|
|
651
653
|
// retrySafe stays false (default): a write must NEVER be auto-re-dispatched (it would repeat its
|
|
652
654
|
// effect). No pool timeout is armed — the deadline is the gateway's own (§3.6.1).
|
|
655
|
+
const startedAt = performance.now();
|
|
653
656
|
const poolPromise = back.pool.run<WriterJobInput, WriterJobResult>(WRITER_TASK_NAME, input, { labels });
|
|
654
657
|
|
|
655
658
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -665,13 +668,16 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
|
|
|
665
668
|
// WEDGED (§3.6.1). The op is still running and still holds the lock; do NOT replace the worker.
|
|
666
669
|
// Surface the caller's INDETERMINATE outcome, hold the FIFO, and resume when the op finally
|
|
667
670
|
// settles (it commits/rolls back and releases the lock on the SAME warm connection).
|
|
668
|
-
wedged
|
|
671
|
+
// #3776 — name the op: "writer wedged" alone left an incident to guess which job held the lock.
|
|
672
|
+
wedgedOp = { op: entry.op, startedAt };
|
|
673
|
+
console.warn(`[writer-gateway] writer op ${entry.op} has held the writer past its ${writerTimeoutMs}ms deadline; writes are held until it settles`);
|
|
669
674
|
rejectIndeterminate(entry);
|
|
670
675
|
poolPromise.then(
|
|
671
676
|
() => {},
|
|
672
677
|
() => {},
|
|
673
678
|
).finally(() => {
|
|
674
|
-
wedged
|
|
679
|
+
console.warn(`[writer-gateway] wedged writer op ${entry.op} settled after ${Math.round(performance.now() - startedAt)}ms`);
|
|
680
|
+
wedgedOp = undefined;
|
|
675
681
|
kickDrain();
|
|
676
682
|
}).catch((error) => console.error(`[writer-gateway] resume after wedge failed: ${errMessage(error)}`));
|
|
677
683
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { errMessage, type HostView, type Orchestrator } from "@humwick/sdk";
|
|
2
|
+
import { resolveProviderSelection, type ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
|
|
3
3
|
import type { DispatchTarget } from "./task-dispatcher";
|
|
4
4
|
import { spawnPlacementRefusal, hostSpawnAdmission, hostIdentitiesById, spawnHostCapAdmission, type SpawnCapOptions } from "./spawn-placement";
|
|
5
5
|
import { liveHostDisabledProviders } from "./host-provider-store";
|
|
@@ -17,13 +17,15 @@ export interface LiveTargetAdmission {
|
|
|
17
17
|
targetUnavailableReason: (target: DispatchTarget) => string | undefined;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function liveTargetAdmission(orchestrators: readonly Orchestrator[], views: readonly HostView[], orchestratorId?: string, capOpts: SpawnCapOptions = {}): LiveTargetAdmission {
|
|
20
|
+
export function liveTargetAdmission(orchestrators: readonly Orchestrator[], views: readonly HostView[], catalog: ProviderCatalogEntry[], orchestratorId?: string, capOpts: SpawnCapOptions = {}): LiveTargetAdmission {
|
|
21
21
|
const identities = hostIdentitiesById(views);
|
|
22
22
|
const byId = new Map(views.map((view) => [view.id, view]));
|
|
23
23
|
const disabled = liveHostDisabledProviders();
|
|
24
24
|
const capBlock = spawnHostCapAdmission(capOpts);
|
|
25
25
|
const hosts = orchestrators.filter((host) => !orchestratorId || host.id === orchestratorId);
|
|
26
26
|
const targetAdmission = ({ provider, model, effort }: DispatchTarget): TargetAdmission => {
|
|
27
|
+
try { resolveProviderSelection({ provider, model, effort }, catalog); }
|
|
28
|
+
catch { return { eligible: false, uncertainties: [] }; }
|
|
27
29
|
let eligible = false;
|
|
28
30
|
const uncertainties: TargetUncertainty[] = [];
|
|
29
31
|
for (const host of hosts) {
|
|
@@ -34,9 +36,11 @@ export function liveTargetAdmission(orchestrators: readonly Orchestrator[], view
|
|
|
34
36
|
}
|
|
35
37
|
return { eligible, uncertainties };
|
|
36
38
|
};
|
|
37
|
-
const targetUnavailableReason = (target: DispatchTarget) =>
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const targetUnavailableReason = (target: DispatchTarget) => {
|
|
40
|
+
try { resolveProviderSelection(target, catalog); }
|
|
41
|
+
catch (error) { return errMessage(error); }
|
|
42
|
+
return spawnPlacementRefusal(orchestrators, views, target.provider, target.model, target.effort, orchestratorId, capOpts);
|
|
43
|
+
};
|
|
40
44
|
return { targetAdmission, targetUnavailableReason };
|
|
41
45
|
}
|
|
42
46
|
|
package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Precondition setup for the packaged-ephemeral landing-ingress Proof of Life.
|
|
2
2
|
* Delivery and both crash boundaries run through the packaged Relay; this fixture only registers
|
|
3
3
|
* the disposable workspace that its real ephemeral orchestrator will probe. */
|
|
4
|
-
import {
|
|
4
|
+
import { closeDb, openExistingDb } from "../db/connection.ts";
|
|
5
5
|
import { upsertWorkspace } from "../db/workspaces.ts";
|
|
6
6
|
|
|
7
7
|
const dbPath = process.env.DB_PATH;
|
|
@@ -13,7 +13,8 @@ if (!dbPath || !worktreePath || !orchestratorId || !baseSha) {
|
|
|
13
13
|
throw new Error("usage: landing-ingress-proof-worker <worktree-path> <orchestrator-id> <base-sha>");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// The installed Relay already owns the schema; a fixture must not migrate its live DB (#3715).
|
|
17
|
+
openExistingDb(dbPath);
|
|
17
18
|
upsertWorkspace({
|
|
18
19
|
id: workspaceId,
|
|
19
20
|
repoRoot: worktreePath,
|
|
@@ -27,4 +28,5 @@ upsertWorkspace({
|
|
|
27
28
|
orchestratorId,
|
|
28
29
|
metadata: {},
|
|
29
30
|
});
|
|
31
|
+
closeDb();
|
|
30
32
|
console.log(JSON.stringify({ workspaceId }));
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// #3762 — warn before a host disk fills. Pure: the single-writer utilization op settles each beat
|
|
2
|
+
// against the stored one (same seam as gate-capacity-alert.ts), so each level change alerts once.
|
|
3
|
+
// Two triggers, one level ladder: used% crossing 85/92/97, or the free-space history projecting
|
|
4
|
+
// full within 24 h (6 h = high). An episode alerts once per worse level and clears with a notice.
|
|
5
|
+
import type { HostDiskWatch, HostMachineSample } from "@humwick/sdk";
|
|
6
|
+
|
|
7
|
+
export type DiskLevel = 1 | 2 | 3;
|
|
8
|
+
|
|
9
|
+
/** Used-share rungs: warn / high / critical. */
|
|
10
|
+
export const DISK_USED_PCT_LEVELS: readonly [number, number, number] = [85, 92, 97];
|
|
11
|
+
/** An episode only clears below this, so a disk hovering on a rung does not flap. */
|
|
12
|
+
export const DISK_CLEAR_USED_PCT = 82;
|
|
13
|
+
export const DISK_ETA_WARN_H = 24;
|
|
14
|
+
export const DISK_ETA_HIGH_H = 6;
|
|
15
|
+
/** Growth needs this much history before it is a rate rather than a blip. */
|
|
16
|
+
export const DISK_MIN_TREND_SPAN_MS = 3 * 3_600_000;
|
|
17
|
+
const HISTORY_SPACING_MS = 30 * 60_000;
|
|
18
|
+
const HISTORY_MAX_SPAN_MS = 25 * 3_600_000;
|
|
19
|
+
const TREND_WINDOW_MS = 24 * 3_600_000;
|
|
20
|
+
|
|
21
|
+
export type DiskAlert =
|
|
22
|
+
| {
|
|
23
|
+
kind: "low"; level: DiskLevel; since: number; freeMb: number; totalMb: number; path?: string;
|
|
24
|
+
/** Free-space decline over the trend window, MB/h; present only while declining. */
|
|
25
|
+
rateMbPerHour?: number; etaHours?: number; spanHours?: number;
|
|
26
|
+
}
|
|
27
|
+
| { kind: "recovered"; since: number; freeMb: number; totalMb: number; path?: string };
|
|
28
|
+
|
|
29
|
+
export interface DiskNotice {
|
|
30
|
+
subject: string;
|
|
31
|
+
body: string;
|
|
32
|
+
priority: "low" | "default" | "high" | "urgent";
|
|
33
|
+
idempotencyKey: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usageLevel(usedPct: number): number {
|
|
37
|
+
return DISK_USED_PCT_LEVELS.filter((rung) => usedPct >= rung).length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function growthLevel(etaHours: number | undefined): number {
|
|
41
|
+
if (etaHours === undefined) return 0;
|
|
42
|
+
return etaHours < DISK_ETA_HIGH_H ? 2 : etaHours < DISK_ETA_WARN_H ? 1 : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nextHistory(prev: HostDiskWatch["history"], at: number, freeMb: number): Array<[number, number]> {
|
|
46
|
+
const kept = (prev ?? []).filter(([t]) => t >= at - HISTORY_MAX_SPAN_MS && t < at);
|
|
47
|
+
const last = kept[kept.length - 1];
|
|
48
|
+
if (!last || at - last[0] >= HISTORY_SPACING_MS) kept.push([at, freeMb]);
|
|
49
|
+
return kept;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Decline rate over the oldest point inside the trend window, or undefined (too little history / not declining). */
|
|
53
|
+
function trend(history: Array<[number, number]>, at: number, freeMb: number): { rateMbPerHour: number; etaHours: number; spanHours: number } | undefined {
|
|
54
|
+
const oldest = history.find(([t]) => t >= at - TREND_WINDOW_MS);
|
|
55
|
+
if (!oldest || at - oldest[0] < DISK_MIN_TREND_SPAN_MS) return undefined;
|
|
56
|
+
const declineMb = oldest[1] - freeMb;
|
|
57
|
+
if (declineMb <= 0) return undefined;
|
|
58
|
+
const spanHours = (at - oldest[0]) / 3_600_000;
|
|
59
|
+
const rateMbPerHour = declineMb / spanHours;
|
|
60
|
+
return { rateMbPerHour, etaHours: freeMb / rateMbPerHour, spanHours };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stamps the relay-owned disk watch onto the stored sample and names the one alert owed, if any.
|
|
65
|
+
* A beat with no disk pair keeps the previous watch (missing telemetry is not a state change).
|
|
66
|
+
*/
|
|
67
|
+
export function settleDiskWatch(prev: HostMachineSample | undefined, next: HostMachineSample): { machine: HostMachineSample; alert?: DiskAlert } {
|
|
68
|
+
const before = prev?.diskWatch;
|
|
69
|
+
const { diskFreeMb: freeMb, diskTotalMb: totalMb } = next;
|
|
70
|
+
if (freeMb === undefined || totalMb === undefined || totalMb <= 0) {
|
|
71
|
+
return { machine: before ? { ...next, diskWatch: before } : next };
|
|
72
|
+
}
|
|
73
|
+
const history = nextHistory(before?.history, next.at, freeMb);
|
|
74
|
+
const projected = trend(history, next.at, freeMb);
|
|
75
|
+
const usedPct = ((totalMb - freeMb) / totalMb) * 100;
|
|
76
|
+
const level = Math.max(usageLevel(usedPct), growthLevel(projected?.etaHours));
|
|
77
|
+
const prevLevel = before?.level ?? 0;
|
|
78
|
+
const where = next.diskPath ? { path: next.diskPath } : {};
|
|
79
|
+
|
|
80
|
+
if (level > prevLevel) {
|
|
81
|
+
const since = before?.since ?? next.at;
|
|
82
|
+
const watch: HostDiskWatch = { level: level as DiskLevel, since, history };
|
|
83
|
+
return {
|
|
84
|
+
machine: { ...next, diskWatch: watch },
|
|
85
|
+
alert: {
|
|
86
|
+
kind: "low", level: level as DiskLevel, since, freeMb, totalMb, ...where,
|
|
87
|
+
...(projected ? { rateMbPerHour: projected.rateMbPerHour, etaHours: projected.etaHours, spanHours: projected.spanHours } : {}),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (prevLevel > 0 && level === 0 && usedPct < DISK_CLEAR_USED_PCT) {
|
|
92
|
+
return { machine: { ...next, diskWatch: { history } }, alert: { kind: "recovered", since: before!.since ?? next.at, freeMb, totalMb, ...where } };
|
|
93
|
+
}
|
|
94
|
+
return { machine: { ...next, diskWatch: { ...(before?.level ? { level: before.level, since: before.since } : {}), history } } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const gb = (mb: number) => `${(mb / 1024).toFixed(mb >= 10_240 ? 0 : 1)} GB`;
|
|
98
|
+
|
|
99
|
+
export function composeDiskNotice(hostId: string, label: string, alert: DiskAlert): DiskNotice {
|
|
100
|
+
const volume = alert.path ? ` (${alert.path})` : "";
|
|
101
|
+
const usedPct = Math.round(((alert.totalMb - alert.freeMb) / alert.totalMb) * 100);
|
|
102
|
+
if (alert.kind === "recovered") {
|
|
103
|
+
return {
|
|
104
|
+
subject: `Disk recovered on ${label}`,
|
|
105
|
+
body: `${label}${volume} has ${gb(alert.freeMb)} free of ${gb(alert.totalMb)} (${usedPct}% used); the low-disk episode is over. (#3762)`,
|
|
106
|
+
priority: "low",
|
|
107
|
+
idempotencyKey: `host-disk:${hostId}:${alert.since}:recovered`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const word = alert.level === 3 ? "CRITICAL" : alert.level === 2 ? "very low" : "low";
|
|
111
|
+
const growth = alert.rateMbPerHour !== undefined && alert.etaHours !== undefined
|
|
112
|
+
? ` Free space fell ~${gb(alert.rateMbPerHour * 24)}/day over the last ${Math.round(alert.spanHours ?? 0)} h; at that rate it is full in ~${Math.max(1, Math.round(alert.etaHours))} h.`
|
|
113
|
+
: "";
|
|
114
|
+
return {
|
|
115
|
+
subject: `Disk ${word} on ${label}: ${gb(alert.freeMb)} free (${usedPct}% used)`,
|
|
116
|
+
body: `${label}${volume} has ${gb(alert.freeMb)} free of ${gb(alert.totalMb)} (${usedPct}% used).${growth} `
|
|
117
|
+
+ "Check workspaces, runtime logs and caches on that host before agents start failing with ENOSPC. (#3762)",
|
|
118
|
+
priority: alert.level === 3 ? "urgent" : alert.level === 2 ? "high" : "default",
|
|
119
|
+
idempotencyKey: `host-disk:${hostId}:${alert.since}:${alert.level}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -53,7 +53,8 @@ const DEFAULT_COMPACT_TARGET = 0.3;
|
|
|
53
53
|
const DEFAULT_MAX_TASKS_BEFORE_COMPACT = 10;
|
|
54
54
|
const DEFAULT_COMPACT_COOLDOWN_MS = 60_000;
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
type ManagedSpawnParamsBuilder = (...args: Parameters<typeof buildManagedSpawnParamsAsync>) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
57
|
+
interface LifecycleManagerOptions { tickMs?: number; now?: () => number; buildManagedSpawnParams?: ManagedSpawnParamsBuilder; }
|
|
57
58
|
|
|
58
59
|
// #1021 A1 — the outcome of trying to wake an on-demand policy via a queued message. A
|
|
59
60
|
// caller (e.g. wakeRepoSteward) must be able to tell "a live agent will handle this" from
|
|
@@ -66,7 +67,7 @@ export class LifecycleManager {
|
|
|
66
67
|
private timer?: Timer;
|
|
67
68
|
private readonly tickMs: number;
|
|
68
69
|
private readonly now: () => number;
|
|
69
|
-
private readonly buildManagedSpawnParams:
|
|
70
|
+
private readonly buildManagedSpawnParams: ManagedSpawnParamsBuilder; private readonly pendingSpawnPreparations = new Map<string, Promise<Command | null>>();
|
|
70
71
|
private readonly updateRestartPending = new Set<string>();
|
|
71
72
|
// #1761 — how many times a policy's stop has been forcefully re-issued while the process refused to
|
|
72
73
|
// confirm dead. Bounds the escalation so a wedged steward is retried, then given up to `failed`,
|
|
@@ -140,7 +140,7 @@ export async function runCdWatcherJob(context: MaintenanceJobContext = { signal:
|
|
|
140
140
|
// `offThread: true` mirrors the #1725 off-thread jobs (old-message-prune, db-maintenance, …): it
|
|
141
141
|
// records in the run's persisted result that the heavy work ran in a subprocess, so an operator
|
|
142
142
|
// reading the last result sees the same proof the run registry acted on for breaker attribution.
|
|
143
|
-
return { ...result, offThread: true, transition: notifyMainCiTransition(result.observation) };
|
|
143
|
+
return { ...result, offThread: true, transition: await notifyMainCiTransition(result.observation) };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/** Best-effort Cosmos deploy hero-shot: read the ledger row this deploy just
|
|
@@ -164,9 +164,9 @@ async function emitDeployHeroShot(sha: string | null): Promise<void> {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/** Best-effort transition alert; never throws into the deploy job. */
|
|
167
|
-
function notifyMainCiTransition(observation: CiObservation): Record<string, unknown
|
|
167
|
+
async function notifyMainCiTransition(observation: CiObservation): Promise<Record<string, unknown>> {
|
|
168
168
|
try {
|
|
169
|
-
const { transition, incidentId, notified, messages } = notifyCiTransition(observation);
|
|
169
|
+
const { transition, incidentId, notified, messages } = await notifyCiTransition(observation);
|
|
170
170
|
return { flip: transition, notified, messages, ...(incidentId ? { incidentId } : {}) };
|
|
171
171
|
} catch (error) {
|
|
172
172
|
const message = errMessage(error);
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
// - the configured fleet-ops agent target, so something that can actually act is woken.
|
|
18
18
|
|
|
19
19
|
import { errMessage } from "@humwick/sdk";
|
|
20
|
-
import { getConfig
|
|
20
|
+
import { getConfig } from "../config-store";
|
|
21
|
+
import { setConfigAsync } from "../config-store-write";
|
|
22
|
+
import { resetExpiredCriticalRunsAsync } from "../db/maintenance-job-runs-async";
|
|
21
23
|
import { stewardFallbackTarget } from "../config";
|
|
22
24
|
import {
|
|
23
25
|
evaluateCriticalMaintenanceJobs,
|
|
24
26
|
legacyHungCriticalMaintenanceRuns,
|
|
25
|
-
resetExpiredCriticalMaintenanceRuns,
|
|
26
27
|
type CriticalJobCondition,
|
|
27
28
|
type LegacyHungCriticalMaintenanceRun,
|
|
28
29
|
type ResetCriticalMaintenanceRun,
|
|
@@ -69,11 +70,11 @@ export interface CriticalJobWatchdogResult {
|
|
|
69
70
|
suppressed?: "unchanged-within-reminder-window" | "awaiting-second-failure";
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWatchdogResult {
|
|
73
|
+
export async function runCriticalJobWatchdog(now: number = Date.now()): Promise<CriticalJobWatchdogResult> {
|
|
73
74
|
// Do this before evaluating health so the durable scheduler latch is recovered even if the
|
|
74
75
|
// notification path is unavailable. A normal timed-out run clears itself; this is only the
|
|
75
76
|
// safety net for a row stranded in `running` after its own deadline lease elapsed.
|
|
76
|
-
const resetRuns =
|
|
77
|
+
const resetRuns = await resetExpiredCriticalRunsAsync(now);
|
|
77
78
|
const legacyHungRuns = legacyHungCriticalMaintenanceRuns(now);
|
|
78
79
|
const conditions = evaluateCriticalMaintenanceJobs(now);
|
|
79
80
|
const errors = conditions.filter((condition) => condition.status === "error");
|
|
@@ -86,9 +87,9 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
|
|
|
86
87
|
state.missedChecks[key] = checks;
|
|
87
88
|
return checks >= 2;
|
|
88
89
|
});
|
|
89
|
-
const persist = () => {
|
|
90
|
+
const persist = async () => {
|
|
90
91
|
if (JSON.stringify(state) !== JSON.stringify(prior)) {
|
|
91
|
-
|
|
92
|
+
await setConfigAsync(STATE_NAMESPACE, STATE_KEY, state, "critical-job-watchdog");
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
const heldRestartEpoch = currentHeldRestartEpoch(now);
|
|
@@ -97,7 +98,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
|
|
|
97
98
|
// Recovered (or never degraded): clear the marker so the NEXT degradation notifies immediately
|
|
98
99
|
// rather than being swallowed by the reminder window opened by the previous one.
|
|
99
100
|
state.lastNotified = null;
|
|
100
|
-
persist();
|
|
101
|
+
await persist();
|
|
101
102
|
return errors.length > 0
|
|
102
103
|
? { status: "error", degradedJobIds: errors.map((condition) => condition.jobId), notified: false, recipients: [], suppressed: "awaiting-second-failure" }
|
|
103
104
|
: { status: conditions.length > 0 ? "warn" : "ok", degradedJobIds: [], notified: false, recipients: [] };
|
|
@@ -115,7 +116,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
|
|
|
115
116
|
...(heldRestartEpoch ? [`epoch:${heldRestartEpoch.id}:${heldRestartEpoch.blockerCode}`] : []),
|
|
116
117
|
].sort().join("|");
|
|
117
118
|
if (state.lastNotified?.fingerprint === fingerprint && now - state.lastNotified.notifiedAt < DEGRADED_REMINDER_MS) {
|
|
118
|
-
persist();
|
|
119
|
+
await persist();
|
|
119
120
|
return {
|
|
120
121
|
status: "error", degradedJobIds,
|
|
121
122
|
...(resetRuns.length > 0 ? { resetJobIds: resetRuns.map((run) => run.jobId) } : {}),
|
|
@@ -154,7 +155,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
state.lastNotified = { fingerprint, notifiedAt: now };
|
|
157
|
-
persist();
|
|
158
|
+
await persist();
|
|
158
159
|
return {
|
|
159
160
|
status: "error", degradedJobIds,
|
|
160
161
|
...(resetRuns.length > 0 ? { resetJobIds: resetRuns.map((run) => run.jobId) } : {}),
|
|
@@ -236,16 +237,14 @@ function composeNotice(
|
|
|
236
237
|
|
|
237
238
|
/** Handler wrapper: a watchdog that throws must not mark itself failed-and-silent, so failures are
|
|
238
239
|
* reported as a result the job record carries. */
|
|
239
|
-
export function criticalJobWatchdogJob(context: { signal?: AbortSignal } = {}): Record<string, unknown
|
|
240
|
-
// #1819 —
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// of critical jobs (one today), so overrun is not a live risk; if that ever changes this is an
|
|
245
|
-
// #1809 offload candidate, not a place for more checks.
|
|
240
|
+
export async function criticalJobWatchdogJob(context: { signal?: AbortSignal } = {}): Promise<Record<string, unknown>> {
|
|
241
|
+
// #1819 — single-unit: one `evaluateCriticalMaintenanceJobs` read then one notify fan-out that must not
|
|
242
|
+
// be split (the `lastNotified` fingerprint is stamped only after the sends, so a half-run would re-page
|
|
243
|
+
// on the next tick). The only safe checkpoint is before it starts. Its two writes (the lease reset and
|
|
244
|
+
// the state persist) are writer ops (#1870 S7).
|
|
246
245
|
if (context.signal?.aborted) return { status: "ok", aborted: true, degradedJobIds: [], notified: false, recipients: [] };
|
|
247
246
|
try {
|
|
248
|
-
return { ...runCriticalJobWatchdog() };
|
|
247
|
+
return { ...(await runCriticalJobWatchdog()) };
|
|
249
248
|
} catch (error) {
|
|
250
249
|
return { status: "error", error: errMessage(error) };
|
|
251
250
|
}
|