@humwick/server 0.130.1 → 0.130.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- 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-administration.md +4 -0
- package/docs/guides/tools-coordination.md +2 -0
- package/docs/guides/tools-tasks.md +8 -0
- package/docs/guides/workflows.md +74 -0
- package/docs/openapi.json +486 -4
- package/package.json +10 -18
- package/public/assets/{MessageBubble-BWpoBjl4.js → MessageBubble-Bq9kr21N.js} +2 -2
- package/public/assets/{PlanGraphPanel-C0auck3n.js → PlanGraphPanel-Dqiroy06.js} +2 -2
- package/public/assets/{TraceView-DBnOx7AQ.js → TraceView-DYU962-H.js} +2 -2
- package/public/assets/{VaultDecisionCard-0kp2oNBC.js → VaultDecisionCard-DhPfzioE.js} +2 -2
- package/public/assets/{activity-yLztXEBt.js → activity-DzCcQTee.js} +2 -2
- package/public/assets/{agent-lineage-BbJDR1dW.js → agent-lineage-SgDt9i3T.js} +2 -2
- package/public/assets/agent-liveness-Bq-s73jW.js +2 -0
- package/public/assets/{agent-name-CTZBrSD9.js → agent-name-CcY3Aijy.js} +2 -2
- package/public/assets/agent-profiles-cxtRBn6O.js +2 -0
- package/public/assets/{agent-project-badge-9TqzUUYE.js → agent-project-badge-DGYbIhaI.js} +2 -2
- package/public/assets/{agent-tools-D4n40pn8.js → agent-tools-DHbftV0N.js} +2 -2
- package/public/assets/{agent-type-icon-D6FUarOf.js → agent-type-icon-CY113sp-.js} +2 -2
- package/public/assets/{agents-BYDslkJc.js → agents-BPzY7IlQ.js} +2 -2
- package/public/assets/{analytics-B-XSbEMC.js → analytics-DENFPv_a.js} +2 -2
- package/public/assets/{artifact-viewer-DdWIfV6M.js → artifact-viewer-KRvSc7sl.js} +2 -2
- package/public/assets/{artifacts-DPN1q4pA.js → artifacts-C9gSmL3s.js} +2 -2
- package/public/assets/{artifacts-oBOqks-2.js → artifacts-DT-HkI5O.js} +2 -2
- package/public/assets/{automation-4KcRFXEu.js → automation-BsmjQhKD.js} +2 -2
- package/public/assets/{backlog-Cw9xPTCF.js → backlog-8NYH_62t.js} +2 -2
- package/public/assets/{busy-health-CTu32MZt.js → busy-health-D3hwvYb4.js} +2 -2
- package/public/assets/{channels-BiXzg1G9.js → channels-DCJ5WL7O.js} +2 -2
- package/public/assets/{chat-Ctk2qwvA.js → chat-DAIl357U.js} +3 -3
- package/public/assets/{code-preview-Bm1wWqAp.js → code-preview-CzrZfr9U.js} +2 -2
- package/public/assets/{connectors-C36ax4Vw.js → connectors-xTxtlyou.js} +2 -2
- package/public/assets/{cosmos-Jj9LK2Wf.js → cosmos-BpebFLK2.js} +2 -2
- package/public/assets/{crate-agent-H6S7-B9T.js → crate-agent-BHvI5wXC.js} +2 -2
- package/public/assets/{dependency-graph-Cd1E23wa.js → dependency-graph-DGT0o3wJ.js} +2 -2
- package/public/assets/{deploys-Bwfq146q.js → deploys-hqWOHSHE.js} +2 -2
- package/public/assets/{display-CfO7rTuF.js → display-CDeH4I8T.js} +2 -2
- package/public/assets/{files-DhhZI2Ih.js → files-BTeGVmTg.js} +2 -2
- package/public/assets/{fleet-B_rFEbnL.js → fleet-DgXtNgCa.js} +2 -2
- package/public/assets/{fleet-metrics-BkHP-P3R.js → fleet-metrics-BHEt5yc_.js} +2 -2
- package/public/assets/{formatted-body-Cm1RrzmN.js → formatted-body-MLtodyPp.js} +3 -3
- package/public/assets/{formatted-body-impl-DQiOuOD1.js → formatted-body-impl-CAe664Z8.js} +2 -2
- package/public/assets/{host-providers-dialog-CHqwXTov.js → host-providers-dialog-mapF4Fj9.js} +2 -2
- package/public/assets/index-CqqgOI5R.js +15 -0
- package/public/assets/index-MYAIwYTX.css +2 -0
- package/public/assets/{insights-fH1EL-qZ.js → insights-DNyf7IzF.js} +2 -2
- package/public/assets/{integrations-C5Oix6ng.js → integrations-DlYOHq7T.js} +2 -2
- package/public/assets/{issue-ref-chip-ODyondIa.js → issue-ref-chip-CeWBNWn7.js} +2 -2
- package/public/assets/{log-viewer-Cvfy6PW7.js → log-viewer-Cib8nqdE.js} +2 -2
- package/public/assets/{maintenance-D8CDH5wZ.js → maintenance-DSn1cPm7.js} +2 -2
- package/public/assets/{managed-agents-egllbpij.js → managed-agents-C3CkM81m.js} +2 -2
- package/public/assets/{markdown-preview-CYSFgVgK.js → markdown-preview-W4VQQPAv.js} +3 -3
- package/public/assets/{markdown-preview-impl-BteVIjX4.js → markdown-preview-impl-CMsTdhjq.js} +2 -2
- package/public/assets/{memory-C8WmXsyr.js → memory-BV17qtO4.js} +2 -2
- package/public/assets/{messages-BzQdqlO6.js → messages-Bm_1AqSA.js} +2 -2
- package/public/assets/{orchestrators-B1HxzkNw.js → orchestrators-Dg-vfP09.js} +2 -2
- package/public/assets/{overview-DCUnI4Z7.js → overview-DcYCrvlr.js} +2 -2
- package/public/assets/{pairs-ftCd2XcD.js → pairs-8o9nuG3V.js} +2 -2
- package/public/assets/{profile-icon-B2zH8DMH.js → profile-icon-BsRyBMea.js} +2 -2
- package/public/assets/project-profile-instructions-DETKFn_E.js +2 -0
- package/public/assets/projects-M_m6AHws.js +2 -0
- package/public/assets/{prompt-settings-Bftq9czX.js → prompt-settings-C_mNCgeL.js} +2 -2
- package/public/assets/provider-display-BEeLXC_D.js +2 -0
- package/public/assets/{registry-DWCU5c8q.js → registry-DDHHHE5N.js} +2 -2
- package/public/assets/run-card-Dt4I4jaP.js +2 -0
- package/public/assets/scroll-text-BKXS1ahr.js +2 -0
- package/public/assets/{secrets-C7GIUATz.js → secrets-BkVwxI5O.js} +2 -2
- package/public/assets/{service-access-settings-C3aujqTG.js → service-access-settings-Crq-6a-B.js} +2 -2
- package/public/assets/{settings-C6sZ9eBa.js → settings-CYcavMFu.js} +2 -2
- package/public/assets/shipping-H6gzEeZB.js +2 -0
- package/public/assets/{stats-V9Z-GxUd.js → stats-BJVeWkpV.js} +2 -2
- package/public/assets/{status-dot-CVv0iGHL.js → status-dot-BcHyw1Vw.js} +2 -2
- package/public/assets/store-DKPDIZBe.js +9 -0
- package/public/assets/{tasks-CXU75HRF.js → tasks-qJjlQPpx.js} +2 -2
- package/public/assets/{teams-BPf976zb.js → teams-Cd5Sk1uX.js} +2 -2
- package/public/assets/{terminal-viewer-DE9BOe-8.js → terminal-viewer-CpY5_m7W.js} +3 -3
- package/public/assets/{terminal-viewer-impl-D04pjmGh.js → terminal-viewer-impl-DI_2llFR.js} +2 -2
- package/public/assets/token-estimate-Co-Jdphh.js +2 -0
- package/public/assets/{use-agent-terminal-B1yR456R.js → use-agent-terminal-0yJA_buM.js} +2 -2
- package/public/assets/{use-decisions-D-S4tpDf.js → use-decisions-uJXHf34W.js} +2 -2
- package/public/assets/use-project-list-CpWl6eHQ.js +2 -0
- package/public/assets/{use-selectors-CnrAk2YP.js → use-selectors-BdxBW8mL.js} +2 -2
- package/public/assets/{user-avatar-nBFIcR4M.js → user-avatar-3IndfpOc.js} +2 -2
- package/public/assets/versioned-prompt-editor-CIVg7XB1.js +8 -0
- package/public/assets/{work-queue-TCjJxeT-.js → work-queue-B9rnLwHZ.js} +2 -2
- package/public/assets/{workflow-marker-Cf3Hf2fN.js → workflow-marker-X8wxyhzC.js} +2 -2
- package/public/assets/{workflows-CXQX20Qj.js → workflows-o5YyMFW7.js} +2 -2
- package/public/assets/{workspaces-qSYzKYMA.js → workspaces-CzEGoAng.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/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/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/adapters/provider-quota-harvest.ts +20 -71
- 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 +2 -4
- package/runner/src/agent-runner/liveness-exchange.ts +104 -0
- 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/claude-system-prompt.ts +49 -0
- package/runner/src/index.ts +5 -2
- 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 +35 -0
- package/runner/src/providers.ts +2 -1
- package/runner/src/runner-core.ts +32 -38
- package/runner/src/runner-helpers.ts +0 -8
- package/runner/src/session-scratch.ts +6 -2
- package/runner/src/spawn-input.ts +2 -1
- package/runner/src/test-run-dedup.ts +59 -56
- package/runner/test-support/provisioning-context.ts +4 -2
- package/runner/test-support/runner-harness.ts +2 -1
- package/scripts/cd-config.ts +8 -0
- package/scripts/gate-run-lease.ts +10 -7
- package/scripts/gate.sh +5 -1
- package/scripts/steward-policy-cleanup.ts +2 -2
- package/src/agent-authz.ts +2 -2
- package/src/agent-drive-arbiter.ts +3 -8
- package/src/agent-exit-cleanup-async.ts +2 -2
- package/src/agent-exit-cleanup.ts +23 -19
- package/src/agent-lifecycle-events.ts +112 -85
- package/src/agent-ref.ts +3 -2
- package/src/agent-status-events.ts +43 -14
- package/src/auth/password.ts +4 -3
- package/src/auth/session-refresh.ts +1 -1
- package/src/auth/session.test-fixture.ts +2 -2
- package/src/auth/session.ts +7 -7
- package/src/automations/crud.ts +8 -81
- package/src/automations/dispatch.ts +33 -58
- package/src/automations/index.ts +1 -1
- package/src/automations/persistence.ts +65 -0
- package/src/automations/precheck.ts +18 -16
- package/src/automations/read-async.ts +11 -0
- package/src/automations/reconcile.ts +30 -39
- package/src/automations/storage.ts +33 -10
- package/src/automations/write-async.ts +33 -0
- package/src/branch-land-notice.ts +93 -0
- package/src/branch-landed.ts +31 -98
- package/src/bus.ts +40 -39
- package/src/cd-deploy-notify.ts +32 -4
- 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/notifications.ts +4 -4
- package/src/ci/root-missing.ts +4 -9
- package/src/ci/transition-notify.ts +12 -44
- package/src/claude-app/oauth-http.ts +3 -3
- 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 +19 -4
- package/src/commands-db.ts +7 -7
- package/src/commands-write.ts +8 -0
- package/src/compaction-watch.ts +10 -8
- package/src/config-store-write.ts +13 -1
- package/src/config-store.ts +25 -14
- package/src/config.ts +4 -11
- package/src/connectors.ts +10 -8
- package/src/context-advisory.ts +22 -21
- 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-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/agent-search.ts +1 -15
- package/src/db/agents-async.ts +59 -9
- package/src/db/agents-write-ops.ts +12 -3
- package/src/db/agents.ts +95 -65
- 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/ci-incidents-async.ts +5 -5
- package/src/db/ci-incidents-write-ops.ts +2 -2
- 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/command-row.ts +10 -2
- package/src/db/commands-list-query.ts +2 -0
- package/src/db/commands-planner-stats.test-fixture.ts +8 -0
- package/src/db/commands-read-ops.ts +16 -0
- package/src/db/commands-spawn-read.ts +1 -1
- package/src/db/config-store-write-ops.ts +7 -2
- package/src/db/connection.ts +1 -182
- package/src/db/context-carry-write.ts +74 -0
- package/src/db/continuation-archives-async.ts +5 -0
- package/src/db/continuation-archives-write-ops.ts +7 -0
- package/src/db/continuations.ts +6 -6
- package/src/db/db-error-registry.ts +12 -0
- 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/gateway.ts +26 -133
- package/src/db/history-retention-ops.ts +13 -0
- package/src/db/hosts-registration.ts +10 -2
- package/src/db/hosts.ts +9 -6
- package/src/db/inbox-async.ts +2 -2
- package/src/db/inbox-read-ops.ts +1 -1
- package/src/db/inbox.ts +9 -17
- 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-associations.ts +2 -2
- package/src/db/issue-lifecycle.ts +3 -4
- package/src/db/issue-refs-async.ts +23 -0
- package/src/db/issue-refs-read-ops.ts +6 -0
- package/src/db/issue-refs-write-ops.ts +29 -0
- package/src/db/issue-refs.ts +15 -16
- 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/long-running-work.ts +2 -2
- package/src/db/main-connection-mode.ts +11 -0
- package/src/db/main-connection-writes.ts +41 -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/maintenance-pauses.ts +2 -2
- package/src/db/mappers.ts +2 -14
- package/src/db/merge-lease.ts +1 -15
- package/src/db/message-send.ts +4 -0
- package/src/db/messages.ts +31 -25
- package/src/db/migrations.ts +25 -16
- package/src/db/notification-write-ops.ts +19 -0
- package/src/db/operator-answer-write.ts +19 -0
- package/src/db/orchestrators.ts +14 -76
- package/src/db/pairs-async.ts +22 -1
- package/src/db/pairs-write-ops.ts +8 -3
- package/src/db/pairs.ts +42 -61
- 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-profile-instructions.ts +50 -0
- package/src/db/project-trust-approvals.ts +2 -2
- package/src/db/prompt-templates.ts +4 -4
- 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 +23 -0
- package/src/db/read-ops.ts +8 -0
- package/src/db/retention.ts +13 -2
- package/src/db/review-publish-write.ts +8 -0
- package/src/db/routing-policy.ts +2 -3
- 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 +13 -4
- package/src/db/spawn-budget.ts +5 -0
- package/src/db/spawn-delegation-state.ts +11 -4
- package/src/db/spawn-delegation.ts +1 -11
- package/src/db/sql-audit-indexes.ts +3 -0
- package/src/db/stats-async.ts +9 -6
- package/src/db/task-entity.ts +28 -6
- package/src/db/task-list.ts +3 -1
- package/src/db/tasks.ts +1 -17
- 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/timer-ingress.ts +7 -1
- package/src/db/token-db-write-ops.ts +19 -0
- package/src/db/token-standing-async.ts +12 -0
- package/src/db/token-standing.ts +21 -20
- 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-contact.ts +1 -1
- package/src/db/workflow-task-read-ops.ts +30 -0
- package/src/db/workspaces.ts +8 -5
- package/src/db/write-ops.ts +37 -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 +58 -56
- package/src/dev.ts +8 -3
- 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/event-loop-lag.ts +4 -2
- package/src/event-loop-yield.ts +2 -2
- package/src/events.ts +10 -8
- package/src/host/disk-alert.ts +121 -0
- package/src/host-provider-store.ts +5 -4
- package/src/hub-metrics-report.ts +5 -1
- package/src/hub-metrics.ts +38 -18
- package/src/injection-sweep-pacing.ts +6 -8
- package/src/land-gate-notifications.ts +11 -10
- package/src/land-gate-queue-progress.ts +3 -3
- package/src/land-settle-dispatch.ts +52 -92
- package/src/lifecycle-context-pressure.ts +11 -10
- package/src/lifecycle-manager.ts +107 -100
- package/src/maintenance/breaker.ts +18 -16
- package/src/maintenance/cd-watcher.ts +3 -3
- package/src/maintenance/checkout-refresh.ts +18 -1
- package/src/maintenance/critical-job-watchdog.ts +19 -20
- package/src/maintenance/events.ts +3 -3
- package/src/maintenance/history-retention-job.ts +30 -0
- package/src/maintenance/index.ts +1 -0
- package/src/maintenance/jobs.ts +32 -69
- 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 +98 -137
- package/src/maintenance/stalled-delivery-pager.ts +10 -9
- package/src/maintenance/teams.ts +19 -30
- package/src/maintenance/workspaces.ts +18 -28
- package/src/managed-policy-escalation.ts +7 -7
- package/src/managed-policy.ts +32 -28
- package/src/managed-provider-gate.ts +5 -4
- package/src/managed-spawn-reservation.ts +5 -4
- package/src/mcp/index.ts +12 -9
- package/src/mcp/output-descriptions.ts +8 -8
- package/src/mcp/presentation.ts +6 -5
- package/src/mcp/project-profile-instructions.ts +32 -0
- 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 +4 -3
- package/src/mcp/tool-roles.ts +81 -0
- package/src/mcp/tools-spawn.ts +4 -3
- package/src/mcp/tools-workspace.ts +1 -1
- package/src/mcp-agent-action.ts +1 -2
- 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 +13 -12
- package/src/mcp-maintenance-tools.ts +2 -2
- 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 +5 -5
- package/src/mcp-project-journal-tools.ts +4 -12
- package/src/mcp-project-tools.ts +17 -18
- package/src/mcp-provider-catalog.ts +2 -2
- package/src/mcp-routing-policy-tools.ts +3 -2
- package/src/mcp-task-tools.ts +11 -5
- package/src/mcp-team-template-tools.ts +3 -3
- package/src/mcp-team-tools.ts +6 -6
- package/src/mcp-workflow-tools.ts +13 -4
- 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/notification-types.ts +2 -1
- package/src/notify.ts +5 -5
- package/src/observed-reset-refresh.ts +4 -4
- package/src/operator-alarm.ts +23 -11
- package/src/project-ingest.ts +6 -6
- package/src/project-instructions.ts +12 -1
- package/src/project-profile-instruction-text.ts +6 -0
- package/src/project-profile-instructions.ts +20 -0
- package/src/project-trust.ts +12 -11
- package/src/prompt-resolver.ts +3 -3
- 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-async.ts +5 -7
- package/src/provider-quota-config-store.ts +5 -4
- package/src/provider-secrets.test-fixture.ts +3 -3
- package/src/provider-timeline-event.ts +4 -7
- package/src/quota-band-delivery.ts +2 -2
- package/src/recipe-runner.ts +6 -5
- package/src/repo-config-source.ts +10 -4
- 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-profiles.ts +6 -6
- 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 +2 -2
- package/src/routes/agents.ts +19 -121
- package/src/routes/auth.ts +14 -15
- package/src/routes/authority-audit.ts +2 -2
- package/src/routes/automations.ts +6 -5
- package/src/routes/channel-binding-delete.ts +1 -1
- package/src/routes/channel-output.ts +3 -3
- package/src/routes/claude-api-budget.ts +5 -5
- package/src/routes/commands-listing.ts +5 -1
- package/src/routes/commands.ts +31 -29
- package/src/routes/context-resets.ts +2 -2
- package/src/routes/continuation-archives.ts +2 -2
- package/src/routes/coordinator-teams.ts +1 -1
- package/src/routes/decisions.ts +2 -2
- package/src/routes/host-secret-files.ts +1 -1
- package/src/routes/hosts.ts +11 -11
- package/src/routes/inbox.ts +3 -2
- package/src/routes/index.ts +12 -2
- package/src/routes/integrations.ts +32 -56
- package/src/routes/issue-activity.ts +2 -2
- package/src/routes/issue-associations.ts +4 -4
- package/src/routes/issue-refs.ts +9 -9
- 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 +2 -2
- package/src/routes/orchestrator-host-registration.ts +4 -2
- package/src/routes/orchestrator-host-utilization.ts +5 -3
- package/src/routes/orchestrator-proxy.ts +4 -4
- package/src/routes/orchestrator.ts +28 -19
- package/src/routes/pairs.ts +8 -8
- package/src/routes/project-profile-instructions.ts +27 -0
- package/src/routes/project-trust.ts +3 -3
- package/src/routes/projects.ts +16 -16
- package/src/routes/prompt-templates.ts +12 -12
- package/src/routes/provider-config.ts +6 -6
- package/src/routes/provider-quota-config.ts +2 -2
- package/src/routes/provider-quotas.ts +10 -10
- package/src/routes/run-command.ts +1 -1
- package/src/routes/secrets-backup.ts +2 -2
- package/src/routes/secrets-import.ts +1 -1
- package/src/routes/secrets-vault.ts +7 -7
- package/src/routes/settings.ts +6 -6
- 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/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 +12 -12
- package/src/routes/teams-config.ts +2 -2
- package/src/routes/teams.ts +10 -10
- package/src/routes/tokens.ts +30 -22
- 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 +22 -7
- package/src/routes/workspaces.ts +17 -16
- package/src/routing-policy-normalize.ts +1 -2
- package/src/routing-policy-push.ts +6 -6
- package/src/routing-step-defaults.ts +0 -2
- package/src/runner-token-mint-limit.ts +63 -0
- package/src/runtime-grant.ts +21 -13
- package/src/runtime-tokens.ts +59 -35
- package/src/secrets-broker/config.ts +1 -1
- package/src/secrets-broker/service.ts +2 -2
- package/src/security.ts +16 -21
- 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 +2 -2
- package/src/server.ts +26 -24
- package/src/services/agent-action.ts +12 -31
- package/src/services/agent-effort.ts +7 -32
- package/src/services/agent-hang-watchdog-job.ts +2 -2
- package/src/services/agent-heartbeat.ts +166 -0
- package/src/services/agent-reporting.ts +1 -1
- package/src/services/browser-inventory.ts +3 -3
- package/src/services/browser-session-state.ts +3 -2
- package/src/services/browser-session.ts +21 -27
- package/src/services/bus-command-update.ts +9 -9
- package/src/services/checkout-refresh-state.ts +1 -0
- package/src/services/checkout-refresh.test-fixture.ts +3 -2
- package/src/services/command-host.ts +7 -2
- package/src/services/coordinator-digest.ts +3 -2
- package/src/services/dispatch-command.ts +12 -13
- package/src/services/effective-launch-driver.ts +14 -9
- package/src/services/host-changed-paths.test-fixture.ts +5 -6
- package/src/services/host-project-readiness.ts +2 -2
- package/src/services/host-providers.ts +1 -1
- package/src/services/host-utilization.ts +27 -4
- package/src/services/idle-agent-digest.ts +17 -12
- package/src/services/idle-epics.ts +9 -10
- 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 +69 -72
- package/src/services/issue-refs.ts +10 -15
- package/src/services/kernel-timers-cutover-chaos-worker.ts +11 -8
- package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
- package/src/services/kernel-timers-ingress.ts +4 -4
- 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 +25 -8
- 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 +17 -53
- 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 +57 -0
- package/src/services/plan-ci-recovery.ts +2 -55
- 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 +9 -3
- package/src/services/profile-trust.ts +6 -5
- package/src/services/project-config-read.ts +70 -0
- package/src/services/project-profile-instructions.ts +54 -0
- package/src/services/project.ts +11 -10
- package/src/services/prompt-templates.ts +6 -5
- package/src/services/provider-hold-nudge.ts +9 -9
- package/src/services/provider-hold-record.ts +13 -12
- package/src/services/provider-state-audit.ts +19 -18
- package/src/services/provisioning-harness/budget-baseline.json +4 -4
- 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 +74 -61
- 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-care-state.ts +2 -1
- package/src/services/self-effort-grant.ts +5 -25
- package/src/services/self-effort.ts +5 -5
- package/src/services/send-message.ts +21 -34
- package/src/services/shutdown-agent.ts +17 -14
- package/src/services/spawn-agent.ts +25 -43
- package/src/services/spawn-chain-inspector.ts +7 -14
- 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-policy.ts +68 -0
- 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 +8 -9
- 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-dispatch.ts +3 -0
- 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 +8 -17
- package/src/services/task-validation.ts +2 -2
- package/src/services/team-templates.ts +12 -11
- 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 +25 -20
- 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 +26 -13
- package/src/services/workflow-caller.ts +61 -4
- package/src/services/workflow-child-proof-worker.ts +1 -1
- package/src/services/workflow-ci-main.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 +5 -3
- package/src/services/workflow-effect-driver.ts +42 -17
- package/src/services/workflow-effects.ts +58 -37
- package/src/services/workflow-land-notice.ts +10 -0
- package/src/services/workflow-pause-text.ts +43 -0
- package/src/services/workflow-pins.ts +7 -4
- package/src/services/workflow-reconcile.ts +14 -11
- 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 +6 -3
- package/src/services/workflow-run.test-fixture.ts +1 -1
- package/src/services/workflow-runs.ts +61 -33
- package/src/services/workflow-supervisor-state.ts +26 -1
- package/src/services/workflow-supervisor.ts +11 -6
- package/src/services/workspace-recovery.ts +4 -3
- package/src/settings/registry.ts +1 -2
- package/src/spawn-admission.ts +10 -2
- package/src/spawn-command.ts +4 -4
- package/src/spawn-models.ts +10 -12
- package/src/spawn-placement.ts +28 -10
- package/src/spawn-targets.ts +9 -9
- package/src/sse-agent-status.ts +15 -6
- package/src/sse.ts +49 -24
- package/src/startup-context-injection.ts +62 -57
- 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-handoff.ts +4 -3
- package/src/steward-policy-cleanup.ts +9 -8
- package/src/steward.ts +9 -8
- package/src/task-dispatch-live.ts +6 -3
- package/src/task-dispatcher.ts +3 -3
- package/src/teams-config-store.ts +4 -3
- package/src/token-db-write.ts +10 -0
- package/src/token-db.ts +2 -2
- package/src/token-usage-async.ts +2 -4
- package/src/user-awaiting-reply.ts +5 -5
- package/src/workspace-actions.ts +24 -29
- package/src/workspace-auto-merge.ts +14 -12
- package/src/workspace-changed-paths-client.ts +16 -5
- package/src/workspace-cleanup-stale.ts +30 -7
- package/src/workspace-command-settle.ts +26 -17
- 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 +4 -4
- 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/shipping-Bu8p1gEw.js +0 -2
- package/public/assets/store-BvtFdlGX.js +0 -9
- package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
- package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
- package/runner/src/agent-runner/http-liveness.ts +0 -85
- package/runner/src/gate-run-lease-probe.ts +0 -38
- package/src/db/claude-catalog-reconcile.ts +0 -27
- package/src/db/on-loop-write-transaction.ts +0 -34
- package/src/routing-model-aliases.ts +0 -21
|
@@ -1,938 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OpError,
|
|
3
|
-
type Clock,
|
|
4
|
-
type JsonValue,
|
|
5
|
-
type ModuleCtx,
|
|
6
|
-
type RetryPolicy,
|
|
7
|
-
type Rng,
|
|
8
|
-
type StepResult,
|
|
9
|
-
type WaitCondition,
|
|
10
|
-
type WorkflowClient,
|
|
11
|
-
type WorkflowDecl,
|
|
12
|
-
type WorkflowRun,
|
|
13
|
-
type WorkflowStartOptions,
|
|
14
|
-
type WorkflowStatusName,
|
|
15
|
-
} from "./contract";
|
|
16
|
-
import type { AsyncEntitySession } from "./async-entity-persistence";
|
|
17
|
-
import type { AsyncEventSession } from "./async-event-log";
|
|
18
|
-
import { MessageAuthorityCommitRefusedError, type MessageAuthoritySession } from "./message-authority";
|
|
19
|
-
import {
|
|
20
|
-
foldWorkflowJournal,
|
|
21
|
-
workflowInstanceFromRow,
|
|
22
|
-
workflowJournalRecordFromRow,
|
|
23
|
-
type WorkflowResumePoint,
|
|
24
|
-
type WorkflowWaitCondition,
|
|
25
|
-
} from "./workflow-journal-codec";
|
|
26
|
-
import {
|
|
27
|
-
WorkflowContendedError,
|
|
28
|
-
WorkflowLeaseLostError,
|
|
29
|
-
WorkflowStepConflictError,
|
|
30
|
-
type WorkflowInstanceRow,
|
|
31
|
-
} from "./workflow-journal";
|
|
32
|
-
import { type WorkflowStepCommitFaults } from "./workflow-step-commit";
|
|
33
|
-
import type { KernelPersistence } from "./kernel-persistence";
|
|
34
|
-
import { recoverIndeterminate } from "./kernel-persistence";
|
|
35
|
-
import { armCompatibilityStepFaults } from "./sqlite-kernel-persistence";
|
|
36
|
-
import { createWorkflowLeaseOwner } from "./workflow-lease-owner";
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The predicate the M3 wakeup pass consults to decide whether a `waiting` instance's
|
|
40
|
-
* {@link WaitCondition} is satisfied. A sleeper's dueness is a pure clock comparison the runtime
|
|
41
|
-
* owns; a waiter's dueness depends on events/commands the kernel journal does NOT own, so the host
|
|
42
|
-
* injects this seam (backed by the event log / command status). It must be a pure, side-effect-free
|
|
43
|
-
* read: the runtime calls it while scanning and again under the acquired lease before firing, so it
|
|
44
|
-
* may be evaluated more than once. Defaults to "never satisfied" when the host wires no evaluator.
|
|
45
|
-
*/
|
|
46
|
-
export type WaitEvaluator = (waitFor: WaitCondition, instanceId: string) => boolean | Promise<boolean>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Compute the durable backoff delay (ms) to wait before the NEXT attempt of a step whose attempt
|
|
50
|
-
* `failedAttempt` (1-based) just failed. `fixed` is constant; `exponential` grows
|
|
51
|
-
* `initialMs · multiplier^(failedAttempt-1)` capped at `maxMs` (default multiplier 2). Optional
|
|
52
|
-
* jitter rides the injected {@link Rng} so it stays deterministic under a seeded/controllable rng.
|
|
53
|
-
* A retry that must wait becomes a `sleeping` row the wakeup pass fires — never a busy-loop.
|
|
54
|
-
*/
|
|
55
|
-
export function retryBackoffMs(policy: RetryPolicy, failedAttempt: number, rng: Rng): number {
|
|
56
|
-
const backoff = policy.backoff;
|
|
57
|
-
let base: number;
|
|
58
|
-
if (backoff.kind === "fixed") {
|
|
59
|
-
base = backoff.delayMs;
|
|
60
|
-
} else {
|
|
61
|
-
const multiplier = backoff.multiplier ?? 2;
|
|
62
|
-
const grown = backoff.initialMs * multiplier ** Math.max(0, failedAttempt - 1);
|
|
63
|
-
base = Math.min(grown, backoff.maxMs);
|
|
64
|
-
}
|
|
65
|
-
if (policy.jitter !== undefined && policy.jitter > 0) base = rng.jitter(base, policy.jitter);
|
|
66
|
-
return Math.max(0, Math.round(base));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The default executor lease TTL. A driver acquires a lease before running any step body and
|
|
71
|
-
* renews it every iteration while it makes progress, so this window only needs to comfortably
|
|
72
|
-
* outlast a single step's run+commit. It is injectable ({@link WorkflowRuntimeDeps.leaseTtlMs}) so
|
|
73
|
-
* tests can shrink it to drive expiry/reclaim deterministically under a controllable clock.
|
|
74
|
-
*/
|
|
75
|
-
const DEFAULT_EXECUTOR_LEASE_TTL_MS = 3_600_000;
|
|
76
|
-
|
|
77
|
-
/** Discard a step's buffered sessions after a failed/aborted commit, tolerating a session the
|
|
78
|
-
* rolled-back flush already finalized (double-discard would otherwise throw). Nothing reached
|
|
79
|
-
* storage — the transaction rolled back — so this only drops in-memory buffers. */
|
|
80
|
-
function discardSessions(entitySession: AsyncEntitySession<unknown>, eventSession: AsyncEventSession<unknown>, messageAuthoritySession?: MessageAuthoritySession): void {
|
|
81
|
-
for (const session of [entitySession, eventSession, messageAuthoritySession]) {
|
|
82
|
-
if (!session) continue;
|
|
83
|
-
try {
|
|
84
|
-
session.discard();
|
|
85
|
-
} catch {
|
|
86
|
-
// The session was already finalized by the (rolled-back) flush; its buffer is moot.
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** The fresh per-step context and the sessions that carry that step's K4/K5 effects to commit. */
|
|
92
|
-
export interface WorkflowStepContext {
|
|
93
|
-
readonly ctx: ModuleCtx;
|
|
94
|
-
readonly entitySession: AsyncEntitySession<unknown>; readonly eventSession: AsyncEventSession<unknown>;
|
|
95
|
-
readonly messageAuthoritySession: MessageAuthoritySession;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface WorkflowRuntimeDeps {
|
|
99
|
-
readonly persistence: KernelPersistence;
|
|
100
|
-
/** Module catalog install barrier; every operation awaits it before touching storage. */
|
|
101
|
-
readonly ready: Promise<void>;
|
|
102
|
-
readonly clock: Clock;
|
|
103
|
-
readonly rng: Rng;
|
|
104
|
-
readonly moduleName: string;
|
|
105
|
-
/** The creator's actor (from `ctx.principal.actor`), stamped into `workflow_instances.created_by`
|
|
106
|
-
* when this runtime seeds a new instance at {@link WorkflowRuntime.start}. The generic owner column
|
|
107
|
-
* (KAPI-1 / decision 3) that unlocks a per-creator filter. Absent for system-driven runtimes → the
|
|
108
|
-
* instance lands `created_by = NULL` (accepted creator-invisible divergence). */
|
|
109
|
-
readonly createdBy?: string | null;
|
|
110
|
-
/** The declaring module's workflows, keyed by name (from the manifest). */
|
|
111
|
-
readonly workflows: Readonly<Record<string, WorkflowDecl>>;
|
|
112
|
-
/** Build a fresh `ModuleCtx` + its entity/event sessions for one step attempt, bound to the
|
|
113
|
-
* declaring module's principal/config (the same assembly the op path uses). */
|
|
114
|
-
readonly beginStepContext: () => WorkflowStepContext;
|
|
115
|
-
/** Executor lease TTL in ms; defaults to {@link DEFAULT_EXECUTOR_LEASE_TTL_MS}. */
|
|
116
|
-
readonly leaseTtlMs?: number;
|
|
117
|
-
/** Decides whether a `waiting` instance's {@link WaitCondition} is satisfied; see
|
|
118
|
-
* {@link WaitEvaluator}. Defaults to "never satisfied" (waits only fire once the host wires one). */
|
|
119
|
-
readonly isWaitSatisfied?: WaitEvaluator;
|
|
120
|
-
/** Crash/fault-injection seam at the step-commit boundary (the M0 fault hooks, wired through to
|
|
121
|
-
* the executor). Returns the faults to arm for a given attempt, or `undefined` for none. M4's
|
|
122
|
-
* kill-9 chaos harness drives this; M3 uses it only to prove per-instance loop isolation. */
|
|
123
|
-
readonly commitFaults?: (instanceId: string, stepName: string) => WorkflowStepCommitFaults | undefined;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** How {@link WorkflowRuntime.#drive} treats a PARKED (`sleeping`/`waiting`) resume point: a normal
|
|
127
|
-
* forward driver leaves parked work alone, whereas the M3 wakeup pass fires it once it is due. */
|
|
128
|
-
type DriveMode = "forward" | "wake";
|
|
129
|
-
|
|
130
|
-
type AnyWorkflowDecl = WorkflowDecl<unknown, JsonValue>;
|
|
131
|
-
|
|
132
|
-
interface StepTransition {
|
|
133
|
-
readonly state: JsonValue;
|
|
134
|
-
readonly next: string;
|
|
135
|
-
readonly nextStatus: WorkflowStatusName;
|
|
136
|
-
readonly sleepUntil?: number;
|
|
137
|
-
readonly waitFor?: WaitCondition;
|
|
138
|
-
readonly output?: unknown;
|
|
139
|
-
readonly hasOutput: boolean;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function errMessage(error: unknown): string {
|
|
143
|
-
return error instanceof Error ? error.message : String(error);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The stored wait condition (validated at write time by `#validateWaitCondition`) as its contract
|
|
147
|
-
* type, for handing to the injected {@link WaitEvaluator}. */
|
|
148
|
-
function waitConditionOf(value: WorkflowWaitCondition): WaitCondition {
|
|
149
|
-
return value as unknown as WaitCondition;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* A `start` whose seed/drive are SPLIT so a host can enlist the seed in an op's atomic commit
|
|
154
|
-
* (`WorkflowStartOptions.enlist: "op-commit"`, T-0 hardening #1409 finding 1). Input/state are
|
|
155
|
-
* already validated (a schema failure threw a typed `invalid_input` {@link OpError} at prepare
|
|
156
|
-
* time), so `seed` is a pure idempotent insert the enclosing transaction owns.
|
|
157
|
-
*/
|
|
158
|
-
export interface PreparedWorkflowStart {
|
|
159
|
-
/** The projection handed back to the enlisted caller at prepare time: the LIVE run when the
|
|
160
|
-
* idempotency key already names an instance, else a `pending` stub carrying the id the seed
|
|
161
|
-
* envelope proposes (a concurrent same-key seed dedupes inside the writer job). */
|
|
162
|
-
readonly provisional: WorkflowRun;
|
|
163
|
-
/** Clone-safe seed data. The persistence writer executes it inside the enlisting op commit and
|
|
164
|
-
* returns the authoritative id (including an idempotency-key dedupe result). */
|
|
165
|
-
readonly seed: import("./workflow-journal").WorkflowInstanceSeed;
|
|
166
|
-
/** Drive the seeded instance forward under the single-writer lease (a contended or terminal
|
|
167
|
-
* instance drives zero step bodies). Call only AFTER the enlisting transaction committed. */
|
|
168
|
-
readonly drive: (instanceId: string) => Promise<void>;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Whether a step-body throw is a transient (retryable) failure rather than a permanent one. The
|
|
173
|
-
* ratified contract has no error taxonomy to change, so the kernel applies a conservative rule: a
|
|
174
|
-
* `TypeError` signals a deterministic programming/contract bug (retrying re-throws it), and a step
|
|
175
|
-
* may explicitly opt a specific error out of retry by tagging it `retryable: false`. Everything else
|
|
176
|
-
* is treated as transient and retried within the step's `RetryPolicy` bound. (Retries are still
|
|
177
|
-
* gated by `maxAttempts`, so even a mis-classified permanent error terminates after exhaustion.)
|
|
178
|
-
*/
|
|
179
|
-
function isRetryableError(error: unknown): boolean {
|
|
180
|
-
if (error instanceof TypeError) return false;
|
|
181
|
-
if (error !== null && typeof error === "object" && (error as { retryable?: unknown }).retryable === false) return false;
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function failureText(failure: unknown): string {
|
|
186
|
-
if (typeof failure === "string") return failure;
|
|
187
|
-
if (failure !== null && typeof failure === "object" && !Array.isArray(failure)) {
|
|
188
|
-
const error = (failure as { error?: unknown }).error;
|
|
189
|
-
if (typeof error === "string") return error;
|
|
190
|
-
}
|
|
191
|
-
return JSON.stringify(failure) ?? "unknown error";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* The workflow executor + `WorkflowClient` (start/status/cancel) plus the M2 recovery path. Given
|
|
196
|
-
* a started instance, it acquires a fresh single-writer lease, then drives the `WorkflowDecl` step
|
|
197
|
-
* graph: fold the journal for the resume state, run the next un-memoized step to a `StepResult`,
|
|
198
|
-
* validate it, then commit the step's K4/K5 effects AND its journal memo row atomically through
|
|
199
|
-
* M0's {@link commitStepAttempt}. It transitions per the result (`next`/`sleepUntil`/`waitFor`/
|
|
200
|
-
* done) and continues until the instance completes or suspends. A step whose memo row already
|
|
201
|
-
* exists is never re-run — the fold advances past it, so a same-key re-`start` and a re-drive both
|
|
202
|
-
* execute zero steps.
|
|
203
|
-
*
|
|
204
|
-
* M2 single-writer semantics: a driver must {@link WorkflowJournalPersistence.acquireLease | win a
|
|
205
|
-
* lease} before running any step body. Two concurrent same-key drivers therefore serialize — one
|
|
206
|
-
* acquires and runs, the second fails to acquire and runs ZERO step bodies. The holder
|
|
207
|
-
* {@link WorkflowJournalPersistence.renewLease | renews} each iteration so a slow drive cannot lose
|
|
208
|
-
* the lease mid-progress. {@link WorkflowRuntime.recover} clears confirmed-dead owner incarnations
|
|
209
|
-
* immediately, or reclaims expired leases, then resumes from the memoized state. If a commit
|
|
210
|
-
* loses the fence (a zombie driver whose lease was reclaimed), the buffered sessions are discarded
|
|
211
|
-
* and the driver relinquishes cleanly, leaving the instance reclaimable rather than marking it
|
|
212
|
-
* failed for merely losing a race.
|
|
213
|
-
*/
|
|
214
|
-
export class WorkflowRuntime implements WorkflowClient<unknown> {
|
|
215
|
-
readonly #deps: WorkflowRuntimeDeps;
|
|
216
|
-
readonly #leaseTtlMs: number;
|
|
217
|
-
readonly #owner: string;
|
|
218
|
-
readonly #isWaitSatisfied: WaitEvaluator;
|
|
219
|
-
/** Leases this driver is actively holding mid-drive (instanceId → token), so {@link heartbeat}
|
|
220
|
-
* can renew a long-running body's lease from the injected clock without a live JS timer. */
|
|
221
|
-
readonly #active = new Map<string, string>();
|
|
222
|
-
|
|
223
|
-
constructor(deps: WorkflowRuntimeDeps) {
|
|
224
|
-
this.#deps = deps;
|
|
225
|
-
this.#leaseTtlMs = deps.leaseTtlMs ?? DEFAULT_EXECUTOR_LEASE_TTL_MS;
|
|
226
|
-
this.#isWaitSatisfied = deps.isWaitSatisfied ?? (() => false);
|
|
227
|
-
// The owner records process incarnation for recovery; the per-drive token fences commits.
|
|
228
|
-
this.#owner = createWorkflowLeaseOwner(`${deps.moduleName}:${deps.rng.uuid()}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async start<N extends string>(name: N, input: unknown, options: WorkflowStartOptions): Promise<WorkflowRun> {
|
|
232
|
-
// Enlistment needs an op commit to join; only the module host's op invocation path provides one
|
|
233
|
-
// (it routes enlisted starts through prepareStart itself, never here). Fail closed everywhere
|
|
234
|
-
// else — a step body or standalone client asking for atomicity that does not exist must not get
|
|
235
|
-
// a silent immediate commit instead.
|
|
236
|
-
if (options?.enlist === "op-commit") {
|
|
237
|
-
throw new Error("workflow start enlist:'op-commit' is only available inside an op invocation");
|
|
238
|
-
}
|
|
239
|
-
const prepared = await this.prepareStart(name, input, options);
|
|
240
|
-
|
|
241
|
-
// Own commit (the ratified default), deduped on UNIQUE(module, workflow, idempotency_key): the
|
|
242
|
-
// same key returns the same run and never seeds a second instance. The instance is seeded
|
|
243
|
-
// UNLEASED — the driver below acquires its own fresh lease, so two concurrent same-key starts
|
|
244
|
-
// contend on the acquire (one wins, one runs zero step bodies).
|
|
245
|
-
await this.#deps.ready;
|
|
246
|
-
const requestKey = this.#deps.rng.uuid(), capturedAt = this.#deps.clock.now();
|
|
247
|
-
const seed = structuredClone(prepared.seed);
|
|
248
|
-
const { instance } = await recoverIndeterminate(() => this.#deps.persistence.seedWorkflow(requestKey, seed, capturedAt));
|
|
249
|
-
const instanceId = instance.id;
|
|
250
|
-
|
|
251
|
-
// In-process there is no separate executor yet, so `start` IS the driver: acquire the lease and
|
|
252
|
-
// drive until the instance completes or parks on its first suspension, then return. A dedupe hit
|
|
253
|
-
// that is already terminal (or held live by a concurrent driver) drives zero steps. Durable
|
|
254
|
-
// firing of a parked suspension is M3.
|
|
255
|
-
await prepared.drive(instanceId);
|
|
256
|
-
return (await this.status(instanceId))!;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Validate + stage a `start` WITHOUT committing anything: the returned {@link PreparedWorkflowStart}
|
|
261
|
-
* splits the idempotent instance SEED envelope (which the writer executes inside the caller's
|
|
262
|
-
* T-0 enlisted-op-commit job) from the post-commit DRIVE. All validation happens HERE, eagerly:
|
|
263
|
-
* a workflow-input/state schema failure throws a typed `invalid_input` {@link OpError} (#1409
|
|
264
|
-
* finding 2 — a deterministic validation failure must be classifiable by callers, never a raw
|
|
265
|
-
* ZodError that reads as transient), so an enlisting op aborts before buffering a seed at all.
|
|
266
|
-
*/
|
|
267
|
-
async prepareStart(name: string, input: unknown, options: WorkflowStartOptions): Promise<PreparedWorkflowStart> {
|
|
268
|
-
const decl = this.#deps.workflows[name] as AnyWorkflowDecl | undefined;
|
|
269
|
-
if (!decl) throw new Error(`module '${this.#deps.moduleName}' declares no workflow '${name}'`);
|
|
270
|
-
const idempotencyKey = options?.idempotencyKey;
|
|
271
|
-
if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
|
|
272
|
-
throw new TypeError("workflow start requires a non-empty idempotencyKey");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const qualified = `${this.#deps.moduleName}.${name}`;
|
|
276
|
-
const parsedInput = this.#parseAsInvalidInput(() => decl.input.parse(input), `workflow '${qualified}' input`) as unknown;
|
|
277
|
-
const initialState = this.#parseAsInvalidInput(
|
|
278
|
-
() => decl.state.parse(decl.init(parsedInput)),
|
|
279
|
-
`workflow '${qualified}' initial state`,
|
|
280
|
-
) as JsonValue;
|
|
281
|
-
|
|
282
|
-
const seedId = this.#deps.rng.uuid();
|
|
283
|
-
await this.#deps.ready;
|
|
284
|
-
const existing = await this.#deps.persistence.getWorkflowInstanceByIdentity(this.#deps.moduleName, name, idempotencyKey);
|
|
285
|
-
const now = this.#deps.clock.now();
|
|
286
|
-
const provisional: WorkflowRun = existing
|
|
287
|
-
? await this.#projectRun(existing)
|
|
288
|
-
: { id: seedId, status: "pending", attempts: 0, createdAt: now, updatedAt: now };
|
|
289
|
-
return {
|
|
290
|
-
provisional,
|
|
291
|
-
seed: {
|
|
292
|
-
id: seedId, moduleName: this.#deps.moduleName, workflowName: name, idempotencyKey,
|
|
293
|
-
input: parsedInput as JsonValue, initialState,
|
|
294
|
-
startStep: decl.start, status: "pending", createdBy: this.#deps.createdBy ?? null,
|
|
295
|
-
},
|
|
296
|
-
drive: async (instanceId: string) => {
|
|
297
|
-
await this.#driveWithLease(instanceId, decl);
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Run a schema parse, converting a deterministic validation failure (ZodError) into the typed
|
|
303
|
-
* `invalid_input` {@link OpError} the op/apply layers classify on. Non-validation throws (a
|
|
304
|
-
* buggy `init`, an exotic schema) propagate unchanged — they are not input classifications. */
|
|
305
|
-
#parseAsInvalidInput<T>(parse: () => T, what: string): T {
|
|
306
|
-
try {
|
|
307
|
-
return parse();
|
|
308
|
-
} catch (error) {
|
|
309
|
-
if (error instanceof Error && error.name === "ZodError") {
|
|
310
|
-
throw new OpError("invalid_input", 400, `${what} failed validation: ${errMessage(error)}`);
|
|
311
|
-
}
|
|
312
|
-
throw error;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* The M2 recovery entry point for ONE instance: reclaim its lease (if unleased/expired) and
|
|
318
|
-
* resume driving from the folded journal to completion or its next suspension. Committed steps
|
|
319
|
-
* are never re-run. A supervisor calls this (or {@link WorkflowRuntime.recover}) to pick up an
|
|
320
|
-
* instance orphaned by a crashed driver. Returns the projected run, or `undefined` if the
|
|
321
|
-
* instance is absent. If the lease is currently held live by another driver, the acquire fails
|
|
322
|
-
* and this resumes zero steps — the live driver keeps ownership.
|
|
323
|
-
*/
|
|
324
|
-
async resume(id: string): Promise<WorkflowRun | undefined> {
|
|
325
|
-
await this.#deps.ready;
|
|
326
|
-
const row = await this.#deps.persistence.getWorkflowInstance(id);
|
|
327
|
-
if (!row) return undefined;
|
|
328
|
-
const decl = this.#deps.workflows[row.workflow_name] as AnyWorkflowDecl | undefined;
|
|
329
|
-
// A missing decl means this runtime does not own that workflow; leave it for a runtime that does.
|
|
330
|
-
if (decl) await this.#driveWithLease(id, decl);
|
|
331
|
-
return this.status(id);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* The M2 recovery driver: clear leases of confirmed-dead local owners, then scan for open,
|
|
336
|
-
* drivable instances whose lease is absent or expired. This is the boot + interval job of S4 §6.4
|
|
337
|
-
* — a supervisor invokes it on startup and on a tick to reclaim orphaned work. Returns the ids it
|
|
338
|
-
* drove. Parked `sleeping`/`waiting` instances are intentionally excluded (their wakeup is M3).
|
|
339
|
-
*/
|
|
340
|
-
async recover(): Promise<string[]> {
|
|
341
|
-
await this.#deps.ready;
|
|
342
|
-
const now = this.#deps.clock.now();
|
|
343
|
-
await recoverIndeterminate(() => this.#deps.persistence.reclaimDeadWorkflowLeases(this.#deps.moduleName, now));
|
|
344
|
-
return this.#drivePass(await this.#deps.persistence.listResumableWorkflows(this.#deps.moduleName, this.#deps.clock.now()), "forward");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* A SINGLE-snapshot classification of this module's residual OPEN work for a boot/OFF reconcile that has
|
|
349
|
-
* no ticker (#1447 finding C, round-7). Samples the injected clock EXACTLY ONCE and issues ONE journal
|
|
350
|
-
* query ({@link WorkflowJournalPersistence.snapshotReconcileResidual}), so the reclaimable-now count and
|
|
351
|
-
* the live-lease recovery delay are computed against ONE `now` and CANNOT disagree across the
|
|
352
|
-
* lease-expiry boundary — the round-6 false-convergence (a reclaimable read at E-1 = 0 AND an
|
|
353
|
-
* independent live-lease read at E = undefined, from two separate clock samples, wrongly declaring
|
|
354
|
-
* "converged" and stranding an acknowledged close) is impossible by construction. READ-ONLY (no lease
|
|
355
|
-
* mutation).
|
|
356
|
-
* • `reclaimableCount` — OPEN instances {@link recover} could drive RIGHT NOW (unleased or expired). If
|
|
357
|
-
* a sweep drove nothing yet this is > 0, the acquire was transiently CONTENDED (not "no work") — the
|
|
358
|
-
* reconcile must retry (a short backoff), not declare a false convergence and strand the close.
|
|
359
|
-
* • `liveLeaseRecoveryDelayMs` — ms (≥ 0) until this module's earliest STILL-LIVE crash lease lapses
|
|
360
|
-
* and becomes plain-{@link recover}-reclaimable, or `undefined` when no open instance holds a live
|
|
361
|
-
* lease. A step killed BEFORE its journal commit leaves a `pending` instance still holding the dead
|
|
362
|
-
* driver's lease. Recovery clears confirmed-dead local incarnations before this read; legacy,
|
|
363
|
-
* remote or unverifiable owners still require a bounded retry at expiry. It never force-clears
|
|
364
|
-
* a lease that might belong to a LIVE concurrent process
|
|
365
|
-
* (round-4: clearing a live lease would double-drive its step body, breaking the single-writer
|
|
366
|
-
* invariant).
|
|
367
|
-
*/
|
|
368
|
-
async classifyReconcileResidual(): Promise<{ reclaimableCount: number; liveLeaseRecoveryDelayMs: number | undefined }> {
|
|
369
|
-
const now = this.#deps.clock.now();
|
|
370
|
-
await this.#deps.ready;
|
|
371
|
-
const { reclaimableCount, nextLiveLeaseExpiry } = await this.#deps.persistence.snapshotWorkflowResidual(this.#deps.moduleName, now);
|
|
372
|
-
return { reclaimableCount, liveLeaseRecoveryDelayMs: nextLiveLeaseExpiry === undefined ? undefined : Math.max(0, nextLiveLeaseExpiry - now) };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* The M3 timer/wait wakeup pass — the firing counterpart to {@link recover}. Driven by the
|
|
377
|
-
* injected clock (there is NO live `setTimeout`), it finds DUE parked instances and drives them
|
|
378
|
-
* forward: `sleeping` rows whose `sleep_until <= now` (durable timers AND retry backoffs both park
|
|
379
|
-
* this way), plus `waiting` rows whose {@link WaitCondition} the injected evaluator reports
|
|
380
|
-
* satisfied. Each is acquired FIRST (same single-writer lease discipline as recovery, so a wakeup
|
|
381
|
-
* and a recover can never double-drive one instance) and then fired. This realizes the timers
|
|
382
|
-
* pilot's deferred `fired` as a sleeping-row a pass claims. Returns the ids it genuinely acquired
|
|
383
|
-
* and drove — a candidate a rival grabbed first, or that is no longer due under the lease, is not
|
|
384
|
-
* reported. A supervisor calls this on a clock tick; before that, it drives ZERO.
|
|
385
|
-
*/
|
|
386
|
-
async wakeup(now = this.#deps.clock.now()): Promise<string[]> {
|
|
387
|
-
await this.#deps.ready;
|
|
388
|
-
const sleepers = await this.#deps.persistence.listDueWorkflows(this.#deps.moduleName, now);
|
|
389
|
-
const waiters: string[] = [];
|
|
390
|
-
for (const id of await this.#deps.persistence.listWaitingWorkflows(this.#deps.moduleName, now)) {
|
|
391
|
-
if (await this.#waiterSatisfied(id, now)) waiters.push(id);
|
|
392
|
-
}
|
|
393
|
-
// Dedup in case a status raced between the two queries; preserve sleeper-first ordering.
|
|
394
|
-
const due = [...new Set([...sleepers, ...waiters])];
|
|
395
|
-
return this.#drivePass(due, "wake");
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Drive a work-list of instance ids under `mode`, ISOLATING each instance (M2 review residual 2):
|
|
400
|
-
* one instance's non-lease failure (e.g. an entity OCC conflict re-thrown from its commit) is
|
|
401
|
-
* caught and the pass continues to the next — it never aborts the whole tick and strands later
|
|
402
|
-
* due work. Only instances this call GENUINELY acquired and drove are reported (residual 4): a
|
|
403
|
-
* failed acquire (contended/terminal) or a mid-drive throw is excluded, so the returned list is an
|
|
404
|
-
* honest record of what this pass advanced, which the recovery/wakeup metrics count.
|
|
405
|
-
*/
|
|
406
|
-
async #drivePass(ids: readonly string[], mode: DriveMode): Promise<string[]> {
|
|
407
|
-
const driven: string[] = [];
|
|
408
|
-
for (const id of ids) {
|
|
409
|
-
const row = await this.#deps.persistence.getWorkflowInstance(id);
|
|
410
|
-
if (!row) continue;
|
|
411
|
-
const decl = this.#deps.workflows[row.workflow_name] as AnyWorkflowDecl | undefined;
|
|
412
|
-
if (!decl) continue;
|
|
413
|
-
try {
|
|
414
|
-
if (await this.#driveWithLease(id, decl, mode)) driven.push(id);
|
|
415
|
-
} catch {
|
|
416
|
-
// Residual 2: a per-instance failure must not abort the pass. The lease was already
|
|
417
|
-
// released in #driveWithLease's finally, so a later tick can retry this instance.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return driven;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Renew every lease this driver is actively holding mid-drive, from the injected clock. This is
|
|
425
|
-
* the residual-1 heartbeat: a step body slower than one lease TTL would otherwise let its OWN
|
|
426
|
-
* lease lapse (and self-fence at commit) — a supervisor calls this on a clock tick to keep a
|
|
427
|
-
* long-running body's lease alive WITHOUT any live `setInterval`. Returns the ids renewed; a lease
|
|
428
|
-
* already reclaimed by a rival (token rotated) simply fails to renew and is omitted.
|
|
429
|
-
*/
|
|
430
|
-
async heartbeat(now = this.#deps.clock.now()): Promise<string[]> {
|
|
431
|
-
const renewed: string[] = [];
|
|
432
|
-
for (const [instanceId, token] of this.#active) {
|
|
433
|
-
const requestKey = this.#deps.rng.uuid();
|
|
434
|
-
if (await recoverIndeterminate(() => this.#deps.persistence.renewWorkflowLease(requestKey, { instanceId, token, now, ttlMs: this.#leaseTtlMs }))) renewed.push(instanceId);
|
|
435
|
-
}
|
|
436
|
-
return renewed;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Pre-screen a `waiting` candidate from its row's `wait_for`, written in the same commit as the
|
|
440
|
-
* journal memo. The full journal fold runs only under the acquired lease in #drive (#3006). */
|
|
441
|
-
async #waiterSatisfied(instanceId: string, _now: number): Promise<boolean> {
|
|
442
|
-
const row = await this.#deps.persistence.getWorkflowInstance(instanceId);
|
|
443
|
-
if (!row || row.status !== "waiting") return false;
|
|
444
|
-
const { waitFor } = workflowInstanceFromRow(row);
|
|
445
|
-
if (waitFor === undefined) return false;
|
|
446
|
-
return this.#isWaitSatisfied(waitConditionOf(waitFor), instanceId);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async status(id: string): Promise<WorkflowRun | undefined> {
|
|
450
|
-
const row = await this.#deps.persistence.getWorkflowInstance(id);
|
|
451
|
-
if (!row) return undefined;
|
|
452
|
-
return this.#projectRun(row);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** Project one instance row to its contract {@link WorkflowRun} (shared by `status` and the
|
|
456
|
-
* prepare-time provisional for an already-existing identity). */
|
|
457
|
-
async #projectRun(row: WorkflowInstanceRow): Promise<WorkflowRun> {
|
|
458
|
-
const instance = workflowInstanceFromRow(row);
|
|
459
|
-
// The cursor counts every memoized row (the fold enforces it); a prune deletes through it (#3006).
|
|
460
|
-
const attempts = instance.journalCursor - instance.journalPrunedThrough;
|
|
461
|
-
return {
|
|
462
|
-
id: instance.id,
|
|
463
|
-
status: instance.status,
|
|
464
|
-
attempts,
|
|
465
|
-
createdAt: instance.createdAt,
|
|
466
|
-
updatedAt: instance.updatedAt,
|
|
467
|
-
...(instance.status === "done" && instance.output !== undefined ? { output: instance.output } : {}),
|
|
468
|
-
...(instance.failure !== undefined ? { error: failureText(instance.failure) } : {}),
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async cancel(id: string): Promise<boolean> {
|
|
473
|
-
return this.control("cancel", id);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async control(operation: "cancel" | "fail" | "reopen", id: string, failure?: unknown): Promise<boolean> {
|
|
477
|
-
const requestKey = this.#deps.rng.uuid();
|
|
478
|
-
const now = this.#deps.clock.now();
|
|
479
|
-
return recoverIndeterminate(() => this.#deps.persistence.controlWorkflow(requestKey, operation, id, now, failure));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Acquire the single-writer lease, then drive, then release. Acquire is the mutual-exclusion
|
|
484
|
-
* point: it succeeds only when the instance is unleased or its lease has expired, so a second
|
|
485
|
-
* concurrent driver of the same instance FAILS HERE and runs zero step bodies. This is also the
|
|
486
|
-
* crash-recovery reclaim — an expired lease left by a dead driver is taken over with a fresh
|
|
487
|
-
* token. When {@link WorkflowRuntime.#drive} returns `true` we still hold the lease and release
|
|
488
|
-
* it (so a parked instance can be reacquired by M3 without waiting out the TTL); when it returns
|
|
489
|
-
* `false` we already lost the fence and there is nothing of ours to release.
|
|
490
|
-
*/
|
|
491
|
-
async #driveWithLease(instanceId: string, decl: AnyWorkflowDecl, mode: DriveMode = "forward"): Promise<boolean> {
|
|
492
|
-
const token = this.#deps.rng.uuid();
|
|
493
|
-
let acquired: boolean;
|
|
494
|
-
try {
|
|
495
|
-
const requestKey = this.#deps.rng.uuid();
|
|
496
|
-
const now = this.#deps.clock.now();
|
|
497
|
-
acquired = await recoverIndeterminate(() => this.#deps.persistence.acquireWorkflowLease(requestKey, {
|
|
498
|
-
instanceId,
|
|
499
|
-
owner: this.#owner,
|
|
500
|
-
token,
|
|
501
|
-
now,
|
|
502
|
-
ttlMs: this.#leaseTtlMs,
|
|
503
|
-
}));
|
|
504
|
-
} catch (error) {
|
|
505
|
-
// A SQLITE_BUSY on the acquire itself is a clean back-off: we did NOT take the lease (the
|
|
506
|
-
// UPDATE never landed), so treat it exactly like a contended/terminal acquire — drive nothing,
|
|
507
|
-
// let a later tick retry. Never leaks as an untyped error that would abort the pass.
|
|
508
|
-
if (error instanceof WorkflowContendedError) return false;
|
|
509
|
-
throw error;
|
|
510
|
-
}
|
|
511
|
-
// Contended (a live lease held by another driver) or terminal (no open row to claim): another
|
|
512
|
-
// writer owns this instance, or there is nothing to drive. Either way, run no step body and
|
|
513
|
-
// report that we did NOT drive it (so a pass never over-reports what it advanced).
|
|
514
|
-
if (!acquired) return false;
|
|
515
|
-
|
|
516
|
-
// Register the live lease so heartbeat() can renew it mid-body from the injected clock.
|
|
517
|
-
this.#active.set(instanceId, token);
|
|
518
|
-
let holdsLease = true;
|
|
519
|
-
try {
|
|
520
|
-
holdsLease = await this.#drive(instanceId, decl, token, mode);
|
|
521
|
-
} catch (error) {
|
|
522
|
-
// A SQLITE_BUSY on a renew or the commit tx propagates here as WorkflowContendedError. Nothing
|
|
523
|
-
// of ours committed (the tx rolled back), and we still hold our lease — so this is a typed
|
|
524
|
-
// relinquish, NOT a failure: fall through to the finally, which RELEASES our lease so a later
|
|
525
|
-
// tick/recovery reclaims and re-runs the same un-memoized step. The instance is never marked
|
|
526
|
-
// failed for merely losing a storage race.
|
|
527
|
-
if (!(error instanceof WorkflowContendedError)) throw error;
|
|
528
|
-
holdsLease = true;
|
|
529
|
-
return false;
|
|
530
|
-
} finally {
|
|
531
|
-
this.#active.delete(instanceId);
|
|
532
|
-
if (holdsLease) {
|
|
533
|
-
const requestKey = this.#deps.rng.uuid();
|
|
534
|
-
const now = this.#deps.clock.now();
|
|
535
|
-
await recoverIndeterminate(() => this.#deps.persistence.releaseWorkflowLease(requestKey, instanceId, token, now));
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Drive the step graph forward under lease `token` until the instance is terminal or parked on a
|
|
543
|
-
* suspension. Returns whether we STILL hold the lease at exit: `true` after a normal
|
|
544
|
-
* terminal/parked stop, `false` if we relinquished because a commit lost the fence (our lease was
|
|
545
|
-
* reclaimed by another driver — a zombie-driver fence-off). The caller releases the lease only in
|
|
546
|
-
* the `true` case.
|
|
547
|
-
*/
|
|
548
|
-
async #drive(instanceId: string, decl: AnyWorkflowDecl, token: string, mode: DriveMode): Promise<boolean> {
|
|
549
|
-
for (;;) {
|
|
550
|
-
const row = await this.#deps.persistence.getWorkflowInstance(instanceId);
|
|
551
|
-
if (!row) throw new Error(`workflow instance '${instanceId}' vanished mid-drive`);
|
|
552
|
-
// Pause TOCTOU (second half): the acquire gated on `paused_at IS NULL`, but a pause can still
|
|
553
|
-
// land AFTER a successful acquire and BEFORE we run the next step — or mid-drive between steps
|
|
554
|
-
// of a forward drive. Re-check here so a paused instance runs NO new step: pause takes effect
|
|
555
|
-
// as "no new step after the current one." We still hold the lease, so return `true` for the
|
|
556
|
-
// caller to release it; the instance simply sits paused until resumed.
|
|
557
|
-
if (row.paused_at !== null) return true;
|
|
558
|
-
const instance = workflowInstanceFromRow(row);
|
|
559
|
-
const records = (await this.#deps.persistence.listWorkflowSteps(instanceId)).map(workflowJournalRecordFromRow);
|
|
560
|
-
const resume = foldWorkflowJournal(instance, records);
|
|
561
|
-
|
|
562
|
-
// A `ready` resume has an un-memoized step to run. `done`/`failed`/`canceled` are terminal.
|
|
563
|
-
// `sleeping`/`waiting` are parked: a forward driver leaves them be, but a `wake` pass fires
|
|
564
|
-
// them ONCE they are DUE (timer elapsed / wait satisfied) — the same clock and predicate the
|
|
565
|
-
// pass pre-screened with, re-checked here under the acquired lease so a "due but un-drivable"
|
|
566
|
-
// gap cannot open. After the parked step runs the instance is `running`, so subsequent loop
|
|
567
|
-
// iterations proceed as a normal forward drive.
|
|
568
|
-
const runnable = resume.status === "ready"
|
|
569
|
-
|| (mode === "wake" && await this.#isDue(resume, this.#deps.clock.now(), instanceId));
|
|
570
|
-
if (!runnable) return true;
|
|
571
|
-
const stepName = resume.stepName;
|
|
572
|
-
if (!stepName) return true;
|
|
573
|
-
|
|
574
|
-
const step = decl.steps[stepName];
|
|
575
|
-
if (!step) {
|
|
576
|
-
await this.#fail(instanceId, `workflow '${instance.workflowName}' has no step '${stepName}'`);
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Renew before running the step so a competitor cannot reclaim while a long body runs, and so
|
|
581
|
-
// progress across steps keeps extending the window. A failed renew means our lease was already
|
|
582
|
-
// reclaimed (we were too slow) — relinquish without running the body; the reclaiming driver
|
|
583
|
-
// owns the instance now.
|
|
584
|
-
if (!await this.#renew(instanceId, token)) {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const { ctx, entitySession, eventSession, messageAuthoritySession } = this.#deps.beginStepContext();
|
|
589
|
-
const startedAt = this.#deps.clock.now();
|
|
590
|
-
let result: StepResult<JsonValue>;
|
|
591
|
-
try {
|
|
592
|
-
result = await step.run(ctx, resume.state as JsonValue);
|
|
593
|
-
} catch (error) {
|
|
594
|
-
// The step body threw. Its buffered effects never commit; classify the failure and either
|
|
595
|
-
// retry (immediately, or parked on a backoff sleeping-row) or go terminal `failed`.
|
|
596
|
-
entitySession.discard();
|
|
597
|
-
eventSession.discard();
|
|
598
|
-
messageAuthoritySession.discard();
|
|
599
|
-
const outcome = await this.#handleStepFailure(instanceId, resume, stepName, step.retry, startedAt, token, error);
|
|
600
|
-
if (outcome === "retry-loop") continue;
|
|
601
|
-
return outcome === "relinquish" ? false : true;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
let transition: StepTransition;
|
|
605
|
-
try {
|
|
606
|
-
transition = this.#validateTransition(decl, stepName, result);
|
|
607
|
-
} catch (error) {
|
|
608
|
-
// A malformed `StepResult` is a deterministic contract violation, never a transient fault —
|
|
609
|
-
// retrying cannot help, so it is always permanent regardless of the step's retry policy.
|
|
610
|
-
entitySession.discard();
|
|
611
|
-
eventSession.discard();
|
|
612
|
-
messageAuthoritySession.discard();
|
|
613
|
-
const outcome = await this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, errMessage(error), { terminal: true });
|
|
614
|
-
return outcome === "relinquish" ? false : true;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// The memoized `result` JSON must round-trip through the journal codec, so it carries the
|
|
618
|
-
// same sleepUntil/waitFor/output the transition columns record.
|
|
619
|
-
const resultJson: Record<string, JsonValue> = { state: transition.state, next: transition.next };
|
|
620
|
-
if (transition.hasOutput) resultJson.output = (transition.output ?? null) as JsonValue;
|
|
621
|
-
if (transition.sleepUntil !== undefined) resultJson.sleepUntil = transition.sleepUntil;
|
|
622
|
-
if (transition.waitFor !== undefined) resultJson.waitFor = transition.waitFor as unknown as JsonValue;
|
|
623
|
-
|
|
624
|
-
// Residual 1 (renew cadence): renew right before the commit using the post-body clock, so a
|
|
625
|
-
// body that alone outran the TTL still fences successfully AS LONG AS no rival actually
|
|
626
|
-
// reclaimed. If the renew fails, a rival already rotated our token — relinquish; nothing of
|
|
627
|
-
// ours committed. This makes the "body slower than the TTL self-fences" failure impossible
|
|
628
|
-
// for a solo holder, while still yielding to a genuine reclaim.
|
|
629
|
-
if (!await this.#renew(instanceId, token)) {
|
|
630
|
-
discardSessions(entitySession, eventSession, messageAuthoritySession);
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// The four-way atomic commit: fence re-check + entity flush + event flush + journal memo,
|
|
635
|
-
// all-or-nothing. No effect reaches storage on any side channel.
|
|
636
|
-
const faults = this.#deps.commitFaults?.(instanceId, stepName);
|
|
637
|
-
try {
|
|
638
|
-
entitySession.seal();
|
|
639
|
-
eventSession.seal();
|
|
640
|
-
messageAuthoritySession.seal();
|
|
641
|
-
const capturedAt = this.#deps.clock.now();
|
|
642
|
-
const requestKey = this.#deps.rng.uuid();
|
|
643
|
-
if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
|
|
644
|
-
const envelope = structuredClone({
|
|
645
|
-
requestKey,
|
|
646
|
-
capturedAt,
|
|
647
|
-
lease: { instanceId, token, now: capturedAt },
|
|
648
|
-
entities: entitySession.mutations(),
|
|
649
|
-
events: eventSession.appends(capturedAt),
|
|
650
|
-
messageAuthority: messageAuthoritySession.operations(),
|
|
651
|
-
record: {
|
|
652
|
-
instanceId,
|
|
653
|
-
stepIndex: resume.stepIndex,
|
|
654
|
-
stepName,
|
|
655
|
-
attempt: resume.nextAttempt,
|
|
656
|
-
outcome: "completed" as const,
|
|
657
|
-
state: transition.state,
|
|
658
|
-
result: resultJson,
|
|
659
|
-
nextStatus: transition.nextStatus,
|
|
660
|
-
startedAt,
|
|
661
|
-
sleepUntil: transition.sleepUntil ?? null,
|
|
662
|
-
waitFor: transition.waitFor ?? null,
|
|
663
|
-
output: transition.nextStatus === "done" && transition.hasOutput ? ((transition.output ?? null) as JsonValue) : null,
|
|
664
|
-
},
|
|
665
|
-
});
|
|
666
|
-
await recoverIndeterminate(() => this.#deps.persistence.stepCommit(envelope));
|
|
667
|
-
} catch (error) {
|
|
668
|
-
// We lost the race for this step: either our lease was reclaimed/expired (fence failed) or
|
|
669
|
-
// another driver already memoized this exact attempt (conflict). The transaction rolled
|
|
670
|
-
// back — no effect of ours reached storage. Discard the buffered sessions and relinquish;
|
|
671
|
-
// do NOT mark the instance failed, because a legitimate driver may be progressing it. Leave
|
|
672
|
-
// it exactly as found so that driver (or a later reclaim) continues from the memoized state.
|
|
673
|
-
if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) {
|
|
674
|
-
discardSessions(entitySession, eventSession, messageAuthoritySession);
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
if (error instanceof MessageAuthorityCommitRefusedError && error.refusal.code === "superseded") {
|
|
678
|
-
// The participant proved that a higher fence/generation owns this timer. Its first
|
|
679
|
-
// transaction rolled back every proposed domain effect; now memoize a domain-free done
|
|
680
|
-
// transition so the obsolete due sleeper is reaped instead of re-driven every tick.
|
|
681
|
-
discardSessions(entitySession, eventSession, messageAuthoritySession);
|
|
682
|
-
const disposition = await this.#commitSupersededAttempt(instanceId, resume, stepName, startedAt, token, error);
|
|
683
|
-
return disposition === "relinquish" ? false : true;
|
|
684
|
-
}
|
|
685
|
-
// Any other commit failure (e.g. an entity OCC conflict) is not a lost-lease condition;
|
|
686
|
-
// drop the buffers and propagate — nothing was committed. The wakeup/recover pass isolates
|
|
687
|
-
// this per-instance (residual 2) so it does not abort the whole tick.
|
|
688
|
-
discardSessions(entitySession, eventSession, messageAuthoritySession);
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Terminal (done) or parked (sleeping/waiting): stop. Only `running` continues the loop, and
|
|
693
|
-
// the next fold advances past the step we just memoized.
|
|
694
|
-
if (transition.nextStatus !== "running") return true;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/** Whether a PARKED resume point is due to fire now: a `sleeping` timer/backoff whose deadline has
|
|
699
|
-
* passed, or a `waiting` condition the injected evaluator reports satisfied. */
|
|
700
|
-
async #isDue(resume: WorkflowResumePoint, now: number, instanceId: string): Promise<boolean> {
|
|
701
|
-
if (resume.status === "sleeping") return resume.sleepUntil !== undefined && resume.sleepUntil <= now;
|
|
702
|
-
if (resume.status === "waiting") {
|
|
703
|
-
return resume.waitFor !== undefined && await this.#isWaitSatisfied(waitConditionOf(resume.waitFor), instanceId);
|
|
704
|
-
}
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Classify a step-body throw and act. With a retry policy and attempts remaining (and a retryable
|
|
710
|
-
* error), journal a `failed` attempt — bumping `attempt` under the UNIQUE(instance, step, attempt)
|
|
711
|
-
* index so each retry is exactly-once — and either retry immediately (`retry-loop`, backoff 0) or
|
|
712
|
-
* park a `sleeping` backoff row the wakeup pass fires. Otherwise (no policy, attempts exhausted, or
|
|
713
|
-
* a non-retryable error) journal the final `failed` attempt and go terminal `failed`. Returns how
|
|
714
|
-
* `#drive` should proceed. A lost lease during the commit yields `relinquish`.
|
|
715
|
-
*/
|
|
716
|
-
async #handleStepFailure(
|
|
717
|
-
instanceId: string,
|
|
718
|
-
resume: WorkflowResumePoint,
|
|
719
|
-
stepName: string,
|
|
720
|
-
retry: RetryPolicy | undefined,
|
|
721
|
-
startedAt: number,
|
|
722
|
-
token: string,
|
|
723
|
-
error: unknown,
|
|
724
|
-
): Promise<"retry-loop" | "stop" | "relinquish"> {
|
|
725
|
-
const failedAttempt = resume.nextAttempt;
|
|
726
|
-
const message = errMessage(error);
|
|
727
|
-
const retryable = retry !== undefined && failedAttempt < retry.maxAttempts && isRetryableError(error);
|
|
728
|
-
if (!retryable) {
|
|
729
|
-
return this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, message, { terminal: true });
|
|
730
|
-
}
|
|
731
|
-
const delayMs = retryBackoffMs(retry, failedAttempt, this.#deps.rng);
|
|
732
|
-
return this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, message, { terminal: false, delayMs });
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Journal one `failed` attempt atomically under the fence and advance the instance: to terminal
|
|
737
|
-
* `failed` (`terminal`), to a `sleeping` backoff row due at `now + delayMs` (a retry that must
|
|
738
|
-
* wait), or straight back to `running` for an immediate retry (`delayMs === 0`). Renews the lease
|
|
739
|
-
* with the post-body clock first (residual 1) so a slow failing body still fences. Returns
|
|
740
|
-
* `retry-loop` to re-run in the same drive, `stop` when parked/terminal, or `relinquish` on a lost
|
|
741
|
-
* lease / attempt conflict (another writer owns the instance).
|
|
742
|
-
*/
|
|
743
|
-
async #commitFailedAttempt(
|
|
744
|
-
instanceId: string,
|
|
745
|
-
resume: WorkflowResumePoint,
|
|
746
|
-
stepName: string,
|
|
747
|
-
startedAt: number,
|
|
748
|
-
token: string,
|
|
749
|
-
message: string,
|
|
750
|
-
disposition: { terminal: true } | { terminal: false; delayMs: number },
|
|
751
|
-
): Promise<"retry-loop" | "stop" | "relinquish"> {
|
|
752
|
-
if (!await this.#renew(instanceId, token)) {
|
|
753
|
-
return "relinquish";
|
|
754
|
-
}
|
|
755
|
-
const failedAt = this.#deps.clock.now();
|
|
756
|
-
const immediate = !disposition.terminal && disposition.delayMs <= 0;
|
|
757
|
-
const nextStatus: WorkflowStatusName = disposition.terminal ? "failed" : immediate ? "running" : "sleeping";
|
|
758
|
-
// M4 chaos seam: a kill point armed here fires while scheduling the backoff/failed row.
|
|
759
|
-
const faults = this.#deps.commitFaults?.(instanceId, stepName);
|
|
760
|
-
try {
|
|
761
|
-
const requestKey = this.#deps.rng.uuid();
|
|
762
|
-
if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
|
|
763
|
-
const envelope = structuredClone({
|
|
764
|
-
requestKey,
|
|
765
|
-
capturedAt: failedAt,
|
|
766
|
-
lease: { instanceId, token, now: failedAt },
|
|
767
|
-
record: {
|
|
768
|
-
instanceId,
|
|
769
|
-
stepIndex: resume.stepIndex,
|
|
770
|
-
stepName,
|
|
771
|
-
attempt: resume.nextAttempt,
|
|
772
|
-
outcome: "failed" as const,
|
|
773
|
-
state: resume.state as JsonValue,
|
|
774
|
-
result: { error: message },
|
|
775
|
-
nextStatus,
|
|
776
|
-
startedAt,
|
|
777
|
-
sleepUntil: disposition.terminal || immediate ? null : failedAt + disposition.delayMs,
|
|
778
|
-
waitFor: null,
|
|
779
|
-
output: null,
|
|
780
|
-
failure: disposition.terminal ? { error: message } : null,
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
await recoverIndeterminate(() => this.#deps.persistence.failedStepCommit(envelope));
|
|
784
|
-
} catch (error) {
|
|
785
|
-
// Same race semantics as a completed commit: a fenced-off zombie or a duplicate attempt row
|
|
786
|
-
// rolls back; relinquish and leave the instance for its rightful driver.
|
|
787
|
-
if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) return "relinquish";
|
|
788
|
-
throw error;
|
|
789
|
-
}
|
|
790
|
-
return immediate ? "retry-loop" : "stop";
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/** Terminalize an exact authority generation that the host proved was superseded. Contention and
|
|
794
|
-
* ordinary refusals never enter here: those retain the sleeping row and remain retryable. */
|
|
795
|
-
async #commitSupersededAttempt(
|
|
796
|
-
instanceId: string,
|
|
797
|
-
resume: WorkflowResumePoint,
|
|
798
|
-
stepName: string,
|
|
799
|
-
startedAt: number,
|
|
800
|
-
token: string,
|
|
801
|
-
refusal: MessageAuthorityCommitRefusedError,
|
|
802
|
-
): Promise<"stop" | "relinquish"> {
|
|
803
|
-
if (!await this.#renew(instanceId, token)) {
|
|
804
|
-
return "relinquish";
|
|
805
|
-
}
|
|
806
|
-
const faults = this.#deps.commitFaults?.(instanceId, stepName);
|
|
807
|
-
try {
|
|
808
|
-
const capturedAt = this.#deps.clock.now();
|
|
809
|
-
const requestKey = this.#deps.rng.uuid();
|
|
810
|
-
if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
|
|
811
|
-
const envelope = structuredClone({
|
|
812
|
-
requestKey,
|
|
813
|
-
capturedAt,
|
|
814
|
-
lease: { instanceId, token, now: capturedAt },
|
|
815
|
-
record: {
|
|
816
|
-
instanceId,
|
|
817
|
-
stepIndex: resume.stepIndex,
|
|
818
|
-
stepName,
|
|
819
|
-
attempt: resume.nextAttempt,
|
|
820
|
-
outcome: "completed" as const,
|
|
821
|
-
state: resume.state as JsonValue,
|
|
822
|
-
result: {
|
|
823
|
-
state: resume.state as JsonValue,
|
|
824
|
-
next: "done",
|
|
825
|
-
superseded: true,
|
|
826
|
-
reason: refusal.refusal.reason,
|
|
827
|
-
},
|
|
828
|
-
nextStatus: "done" as const,
|
|
829
|
-
startedAt,
|
|
830
|
-
sleepUntil: null,
|
|
831
|
-
waitFor: null,
|
|
832
|
-
output: null,
|
|
833
|
-
},
|
|
834
|
-
});
|
|
835
|
-
await recoverIndeterminate(() => this.#deps.persistence.supersededStepCommit(envelope));
|
|
836
|
-
} catch (error) {
|
|
837
|
-
if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) return "relinquish";
|
|
838
|
-
throw error;
|
|
839
|
-
}
|
|
840
|
-
return "stop";
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/** Validate a step's `StepResult` against the decl and project its durable transition. */
|
|
844
|
-
#validateTransition(decl: AnyWorkflowDecl, stepName: string, result: StepResult<JsonValue>): StepTransition {
|
|
845
|
-
if (result === null || typeof result !== "object") {
|
|
846
|
-
throw new TypeError(`step '${stepName}' must return a StepResult object`);
|
|
847
|
-
}
|
|
848
|
-
const next = (result as { next?: unknown }).next;
|
|
849
|
-
if (typeof next !== "string" || next.length === 0) {
|
|
850
|
-
throw new TypeError(`step '${stepName}' must return a non-empty 'next'`);
|
|
851
|
-
}
|
|
852
|
-
if (next !== "done" && !Object.hasOwn(decl.steps, next)) {
|
|
853
|
-
throw new TypeError(`step '${stepName}' returned unknown next step '${next}'`);
|
|
854
|
-
}
|
|
855
|
-
const state = decl.state.parse((result as { state: unknown }).state) as JsonValue;
|
|
856
|
-
|
|
857
|
-
const sleepUntilRaw = (result as { sleepUntil?: unknown }).sleepUntil;
|
|
858
|
-
const waitForRaw = (result as { waitFor?: unknown }).waitFor;
|
|
859
|
-
if (sleepUntilRaw !== undefined && waitForRaw !== undefined) {
|
|
860
|
-
throw new TypeError(`step '${stepName}' cannot return both sleepUntil and waitFor`);
|
|
861
|
-
}
|
|
862
|
-
let sleepUntil: number | undefined;
|
|
863
|
-
if (sleepUntilRaw !== undefined) {
|
|
864
|
-
if (typeof sleepUntilRaw !== "number" || !Number.isFinite(sleepUntilRaw)) {
|
|
865
|
-
throw new TypeError(`step '${stepName}' sleepUntil must be a finite number`);
|
|
866
|
-
}
|
|
867
|
-
sleepUntil = sleepUntilRaw;
|
|
868
|
-
}
|
|
869
|
-
let waitFor: WaitCondition | undefined;
|
|
870
|
-
if (waitForRaw !== undefined) {
|
|
871
|
-
waitFor = this.#validateWaitCondition(stepName, waitForRaw);
|
|
872
|
-
}
|
|
873
|
-
if (next === "done" && (sleepUntil !== undefined || waitFor !== undefined)) {
|
|
874
|
-
throw new TypeError(`step '${stepName}' cannot schedule work after next=done`);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const hasOutput = next === "done" && Object.hasOwn(result, "output");
|
|
878
|
-
const nextStatus: WorkflowStatusName = next === "done" ? "done"
|
|
879
|
-
: sleepUntil !== undefined ? "sleeping"
|
|
880
|
-
: waitFor !== undefined ? "waiting"
|
|
881
|
-
: "running";
|
|
882
|
-
return {
|
|
883
|
-
state,
|
|
884
|
-
next,
|
|
885
|
-
nextStatus,
|
|
886
|
-
...(sleepUntil !== undefined ? { sleepUntil } : {}),
|
|
887
|
-
...(waitFor !== undefined ? { waitFor } : {}),
|
|
888
|
-
...(hasOutput ? { output: (result as { output?: unknown }).output } : {}),
|
|
889
|
-
hasOutput,
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
#validateWaitCondition(stepName: string, value: unknown): WaitCondition {
|
|
894
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
895
|
-
throw new TypeError(`step '${stepName}' waitFor must be a WaitCondition object`);
|
|
896
|
-
}
|
|
897
|
-
const kind = (value as { kind?: unknown }).kind;
|
|
898
|
-
if (kind === "event") {
|
|
899
|
-
const topic = (value as { topic?: unknown }).topic;
|
|
900
|
-
if (typeof topic !== "string" || !topic.includes(".")) {
|
|
901
|
-
throw new TypeError(`step '${stepName}' waitFor event requires a '<module>.<name>' topic`);
|
|
902
|
-
}
|
|
903
|
-
const correlationId = (value as { correlationId?: unknown }).correlationId;
|
|
904
|
-
return {
|
|
905
|
-
kind: "event",
|
|
906
|
-
topic: topic as `${string}.${string}`,
|
|
907
|
-
...(correlationId !== undefined ? { correlationId: String(correlationId) } : {}),
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
if (kind === "command") {
|
|
911
|
-
const commandId = (value as { commandId?: unknown }).commandId;
|
|
912
|
-
if (typeof commandId !== "string" || commandId.length === 0) {
|
|
913
|
-
throw new TypeError(`step '${stepName}' waitFor command requires a commandId`);
|
|
914
|
-
}
|
|
915
|
-
return { kind: "command", commandId };
|
|
916
|
-
}
|
|
917
|
-
throw new TypeError(`step '${stepName}' waitFor kind must be 'event' or 'command'`);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/** Durably transition an open instance to `failed`. M1 has no retry driver (that is M3), so an
|
|
921
|
-
* unhandled step throw or an invalid result terminates the instance rather than looping. */
|
|
922
|
-
async #fail(instanceId: string, message: string): Promise<void> {
|
|
923
|
-
const requestKey = this.#deps.rng.uuid(), now = this.#deps.clock.now();
|
|
924
|
-
await recoverIndeterminate(() => this.#deps.persistence.controlWorkflow(requestKey, "fail", instanceId, now, { error: message }));
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
async #renew(instanceId: string, token: string): Promise<boolean> {
|
|
928
|
-
const requestKey = this.#deps.rng.uuid();
|
|
929
|
-
const now = this.#deps.clock.now();
|
|
930
|
-
return recoverIndeterminate(() => this.#deps.persistence.renewWorkflowLease(requestKey, { instanceId, token, now, ttlMs: this.#leaseTtlMs }));
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
export function createWorkflowRuntime(deps: WorkflowRuntimeDeps): WorkflowRuntime {
|
|
935
|
-
return new WorkflowRuntime(deps);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
export type { WorkflowResumePoint };
|