@humwick/server 0.130.2 → 0.130.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/prompts/manifest.json +1 -0
- package/bundled/prompts/task.hydration.md +1 -1
- package/bundled/roles/builder.md +1 -1
- package/docs/guides/sub-coordinators.md +12 -5
- package/docs/guides/tools-coordination.md +1 -0
- package/docs/guides/workflows.md +16 -0
- package/docs/openapi.json +73 -4
- package/package.json +8 -7
- package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-Bq9kr21N.js} +2 -2
- package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-Dqiroy06.js} +2 -2
- package/public/assets/{TraceView-Wimm0Czv.js → TraceView-DYU962-H.js} +2 -2
- package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DhPfzioE.js} +2 -2
- package/public/assets/{activity-CrSurn-r.js → activity-DzCcQTee.js} +2 -2
- package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-SgDt9i3T.js} +2 -2
- package/public/assets/agent-liveness-Bq-s73jW.js +2 -0
- package/public/assets/{agent-name-DKbiIhBo.js → agent-name-CcY3Aijy.js} +2 -2
- package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-cxtRBn6O.js} +2 -2
- package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-DGYbIhaI.js} +2 -2
- package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-DHbftV0N.js} +2 -2
- package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CY113sp-.js} +2 -2
- package/public/assets/{agents-B-v66C-4.js → agents-BPzY7IlQ.js} +2 -2
- package/public/assets/{analytics-BqCb-LKi.js → analytics-DENFPv_a.js} +2 -2
- package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-KRvSc7sl.js} +2 -2
- package/public/assets/{artifacts-DJevXcvg.js → artifacts-C9gSmL3s.js} +2 -2
- package/public/assets/{artifacts-D32zyw4Y.js → artifacts-DT-HkI5O.js} +2 -2
- package/public/assets/{automation-BpRajx_5.js → automation-BsmjQhKD.js} +2 -2
- package/public/assets/{backlog-D_UjTdzX.js → backlog-8NYH_62t.js} +2 -2
- package/public/assets/{busy-health-N3VEh4ao.js → busy-health-D3hwvYb4.js} +2 -2
- package/public/assets/{channels-9jopungq.js → channels-DCJ5WL7O.js} +2 -2
- package/public/assets/{chat-CSXrTB3C.js → chat-DAIl357U.js} +3 -3
- package/public/assets/{code-preview-p1NnmqK0.js → code-preview-CzrZfr9U.js} +2 -2
- package/public/assets/{connectors-CZdqlDNL.js → connectors-xTxtlyou.js} +2 -2
- package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BpebFLK2.js} +2 -2
- package/public/assets/{crate-agent-30aXTKva.js → crate-agent-BHvI5wXC.js} +2 -2
- package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-DGT0o3wJ.js} +2 -2
- package/public/assets/{deploys-DoN6bAb5.js → deploys-hqWOHSHE.js} +2 -2
- package/public/assets/{display-BrKWFGJz.js → display-CDeH4I8T.js} +2 -2
- package/public/assets/{files-D6XEu8Ng.js → files-BTeGVmTg.js} +2 -2
- package/public/assets/{fleet-C7jQWbw2.js → fleet-DgXtNgCa.js} +2 -2
- package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BHEt5yc_.js} +2 -2
- package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-MLtodyPp.js} +3 -3
- package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-CAe664Z8.js} +2 -2
- package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-mapF4Fj9.js} +2 -2
- package/public/assets/{index-CRHep3M9.js → index-CqqgOI5R.js} +5 -5
- package/public/assets/{insights-CzrtkpOE.js → insights-DNyf7IzF.js} +2 -2
- package/public/assets/{integrations-aVsUmigt.js → integrations-DlYOHq7T.js} +2 -2
- package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-CeWBNWn7.js} +2 -2
- package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-Cib8nqdE.js} +2 -2
- package/public/assets/{maintenance-NfjqFZAk.js → maintenance-DSn1cPm7.js} +2 -2
- package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-C3CkM81m.js} +2 -2
- package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-W4VQQPAv.js} +3 -3
- package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CMsTdhjq.js} +2 -2
- package/public/assets/{memory-DNsCl6UX.js → memory-BV17qtO4.js} +2 -2
- package/public/assets/{messages-DopU1L1X.js → messages-Bm_1AqSA.js} +2 -2
- package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-Dg-vfP09.js} +2 -2
- package/public/assets/{overview-C1DCTzzV.js → overview-DcYCrvlr.js} +2 -2
- package/public/assets/{pairs-DE_m6rrE.js → pairs-8o9nuG3V.js} +2 -2
- package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-BsRyBMea.js} +2 -2
- package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-DETKFn_E.js} +2 -2
- package/public/assets/{projects-Br2Bd2h1.js → projects-M_m6AHws.js} +2 -2
- package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-C_mNCgeL.js} +2 -2
- package/public/assets/provider-display-BEeLXC_D.js +2 -0
- package/public/assets/{registry-Cw38_6gW.js → registry-DDHHHE5N.js} +2 -2
- package/public/assets/run-card-Dt4I4jaP.js +2 -0
- package/public/assets/{secrets-DxI-vzSq.js → secrets-BkVwxI5O.js} +2 -2
- package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-Crq-6a-B.js} +2 -2
- package/public/assets/{settings-CKZTVGIx.js → settings-CYcavMFu.js} +2 -2
- package/public/assets/shipping-H6gzEeZB.js +2 -0
- package/public/assets/{stats-DyUBKyWI.js → stats-BJVeWkpV.js} +2 -2
- package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-BcHyw1Vw.js} +2 -2
- package/public/assets/store-DKPDIZBe.js +9 -0
- package/public/assets/{tasks-Bh7Chr4-.js → tasks-qJjlQPpx.js} +2 -2
- package/public/assets/{teams-bowI7qVY.js → teams-Cd5Sk1uX.js} +2 -2
- package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-CpY5_m7W.js} +3 -3
- package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-DI_2llFR.js} +2 -2
- package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-0yJA_buM.js} +2 -2
- package/public/assets/{use-decisions-samMQhQf.js → use-decisions-uJXHf34W.js} +2 -2
- package/public/assets/{use-project-list-59LheVP7.js → use-project-list-CpWl6eHQ.js} +2 -2
- package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-BdxBW8mL.js} +2 -2
- package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-3IndfpOc.js} +2 -2
- package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-CIVg7XB1.js} +2 -2
- package/public/assets/{work-queue-CSvpuNLz.js → work-queue-B9rnLwHZ.js} +2 -2
- package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-X8wxyhzC.js} +2 -2
- package/public/assets/{workflows-DdmxRSyg.js → workflows-o5YyMFW7.js} +2 -2
- package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CzEGoAng.js} +2 -2
- package/public/cosmos.html +4 -4
- package/public/index.html +28 -28
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-headless.ts +1 -1
- package/runner/src/adapters/claude-launch-exit.ts +26 -0
- package/runner/src/adapters/claude-session-probe.ts +1 -1
- package/runner/src/adapters/claude-tmux.ts +38 -10
- package/runner/src/adapters/claude.ts +28 -75
- package/runner/src/adapters/provider-quota-harvest.ts +20 -71
- package/runner/src/agent-runner/lifecycle-milestones.ts +2 -2
- package/runner/src/agent-runner/liveness-exchange.ts +104 -0
- package/runner/src/claude-system-prompt.ts +49 -0
- package/runner/src/launch-assembly.ts +14 -10
- package/runner/src/profile-home.ts +1 -1
- package/runner/src/project-instructions.ts +2 -2
- package/runner/src/provider-command.ts +18 -0
- package/runner/src/provider-setup.ts +2 -1
- package/runner/src/runner-core.ts +14 -15
- package/runner/src/runner-helpers.ts +0 -8
- package/runner/src/session-scratch.ts +6 -2
- package/runner/test-support/provisioning-context.ts +4 -2
- package/runner/test-support/runner-harness.ts +2 -1
- package/scripts/steward-policy-cleanup.ts +2 -2
- package/src/agent-authz.ts +2 -2
- package/src/agent-drive-arbiter.ts +3 -8
- package/src/agent-exit-cleanup-async.ts +2 -2
- package/src/agent-exit-cleanup.ts +23 -19
- package/src/agent-lifecycle-events.ts +112 -85
- package/src/agent-ref.ts +3 -2
- package/src/agent-status-events.ts +43 -14
- package/src/auth/password.ts +4 -3
- package/src/auth/session-refresh.ts +1 -1
- package/src/auth/session.test-fixture.ts +2 -2
- package/src/auth/session.ts +7 -7
- package/src/automations/crud.ts +8 -81
- package/src/automations/dispatch.ts +33 -58
- package/src/automations/index.ts +1 -1
- package/src/automations/persistence.ts +65 -0
- package/src/automations/precheck.ts +18 -16
- package/src/automations/read-async.ts +11 -0
- package/src/automations/reconcile.ts +30 -39
- package/src/automations/storage.ts +33 -10
- package/src/automations/write-async.ts +33 -0
- package/src/branch-land-notice.ts +3 -3
- package/src/branch-landed.ts +25 -23
- package/src/bus.ts +21 -19
- package/src/cd-deploy-notify.ts +2 -2
- package/src/ci/notifications.ts +4 -4
- package/src/ci/root-missing.ts +3 -3
- package/src/ci/transition-notify.ts +5 -5
- package/src/claude-app/oauth-http.ts +3 -3
- package/src/cli/workflow.ts +12 -3
- package/src/commands-db.ts +4 -4
- package/src/commands-write.ts +8 -0
- package/src/compaction-watch.ts +10 -8
- package/src/config-store-write.ts +2 -2
- package/src/config.ts +4 -11
- package/src/connectors.ts +10 -8
- package/src/context-advisory.ts +22 -21
- package/src/db/agent-action-write.ts +36 -0
- package/src/db/agent-control-async.ts +9 -0
- package/src/db/agent-control-write.ts +49 -0
- package/src/db/agent-effort-observe.ts +22 -0
- package/src/db/agent-meta.ts +14 -3
- package/src/db/agents-async.ts +8 -5
- package/src/db/agents.ts +15 -12
- package/src/db/auth-identities.ts +2 -2
- package/src/db/automation-read-ops.ts +11 -0
- package/src/db/automation-write-ops.ts +46 -0
- package/src/db/blackboard.ts +2 -2
- package/src/db/browser-session-write.ts +32 -0
- package/src/db/bus-outbox-write-ops.ts +3 -1
- package/src/db/checkout-refresh-schema.ts +47 -0
- package/src/db/checkout-refresh.ts +5 -5
- package/src/db/command-agent-profile-backfill.ts +39 -0
- package/src/db/command-agent-profiles.ts +5 -1
- package/src/db/command-retention.ts +126 -0
- package/src/db/commands-planner-stats.test-fixture.ts +8 -0
- package/src/db/commands-read-ops.ts +16 -0
- package/src/db/connection.ts +1 -182
- package/src/db/context-carry-write.ts +74 -0
- package/src/db/continuations.ts +4 -4
- package/src/db/db-error-registry.ts +12 -0
- package/src/db/gateway.ts +26 -133
- package/src/db/history-retention-ops.ts +13 -0
- package/src/db/inbox-async.ts +2 -2
- package/src/db/inbox-read-ops.ts +1 -1
- package/src/db/inbox.ts +9 -3
- package/src/db/issue-associations.ts +2 -2
- package/src/db/issue-refs-async.ts +4 -1
- package/src/db/issue-refs-read-ops.ts +6 -0
- package/src/db/issue-refs.ts +8 -8
- package/src/db/kernel-event-log-schema.ts +2 -0
- package/src/db/land-boundaries.ts +51 -5
- package/src/db/land-gate-attestations.ts +2 -2
- package/src/db/landing-receipts.ts +2 -2
- package/src/db/lifecycle-context-write.ts +5 -0
- package/src/db/lifecycle-tail-async.ts +76 -0
- package/src/db/lifecycle-tail-write-ops.ts +62 -0
- package/src/db/lifecycle-write-window.ts +2 -1
- package/src/db/lineage-pair-dedupe-sql.ts +3 -3
- package/src/db/main-connection-mode.ts +11 -0
- package/src/db/main-connection-writes.ts +20 -2
- package/src/db/maintenance-job-runs-async.ts +2 -2
- package/src/db/maintenance-job-write-ops.ts +1 -1
- package/src/db/maintenance-pauses.ts +2 -2
- package/src/db/message-send.ts +4 -0
- package/src/db/messages.ts +31 -25
- package/src/db/migrations.ts +18 -1
- package/src/db/notification-write-ops.ts +19 -0
- package/src/db/operator-answer-write.ts +19 -0
- package/src/db/orchestrators.ts +10 -4
- package/src/db/pairs-async.ts +22 -1
- package/src/db/pairs-write-ops.ts +8 -3
- package/src/db/pairs.ts +41 -48
- package/src/db/plan-dispatch-schema.ts +5 -1
- package/src/db/plan-ready-notices.ts +43 -0
- package/src/db/plan-tools-include-migration.ts +36 -0
- package/src/db/plans-write-ops.ts +2 -0
- package/src/db/presence-buffer.ts +5 -0
- package/src/db/presence-loop.ts +18 -23
- package/src/db/presence-write.ts +0 -6
- package/src/db/project-entries-async.ts +14 -0
- package/src/db/project-entries-read-ops.ts +5 -1
- package/src/db/project-entries-write-ops.ts +8 -0
- package/src/db/project-entries.ts +15 -12
- package/src/db/project-journal.ts +2 -2
- package/src/db/project-trust-approvals.ts +2 -2
- package/src/db/prompt-templates.ts +4 -4
- package/src/db/read-op-marshaling-fixtures.ts +18 -0
- package/src/db/read-ops.ts +4 -0
- package/src/db/retention.ts +13 -2
- package/src/db/review-publish-write.ts +8 -0
- package/src/db/runtime-admin-async.ts +19 -0
- package/src/db/runtime-admin-write-ops.ts +36 -0
- package/src/db/runtime-tail-async.ts +288 -0
- package/src/db/runtime-tail-write-ops.ts +97 -0
- package/src/db/schema.ts +4 -1
- package/src/db/spawn-budget.ts +5 -0
- package/src/db/spawn-delegation.ts +1 -11
- package/src/db/sql-audit-indexes.ts +3 -0
- package/src/db/stats-async.ts +5 -3
- package/src/db/task-entity.ts +5 -4
- package/src/db/team-claim-policy.ts +60 -0
- package/src/db/teams-async.ts +4 -0
- package/src/db/teams-write-ops.ts +4 -0
- package/src/db/terminal-events.ts +15 -0
- package/src/db/timer-cutover-bodies.ts +292 -0
- package/src/db/timer-cutover-core.ts +37 -278
- package/src/db/timer-cutover-state.ts +40 -0
- package/src/db/timer-cutover-write-ops.ts +1 -40
- package/src/db/token-db-write-ops.ts +2 -0
- package/src/db/token-standing-async.ts +12 -0
- package/src/db/token-standing.ts +6 -9
- package/src/db/token-usage-retention.ts +33 -0
- package/src/db/token-usage-write-ops.ts +1 -1
- package/src/db/token-usage.ts +4 -1
- package/src/db/user-invites-async.ts +13 -0
- package/src/db/user-invites.ts +3 -3
- package/src/db/users.ts +14 -6
- package/src/db/workspaces.ts +8 -5
- package/src/db/write-ops.ts +31 -12
- package/src/db/writer-fault-seam.ts +2 -9
- package/src/db/writer-fault-test-ops.ts +18 -0
- package/src/db/writer-gateway.ts +42 -46
- package/src/dev.ts +8 -3
- package/src/event-loop-lag.ts +4 -2
- package/src/event-loop-yield.ts +2 -2
- package/src/events.ts +10 -8
- package/src/host-provider-store.ts +5 -4
- package/src/hub-metrics-report.ts +5 -1
- package/src/hub-metrics.ts +38 -18
- package/src/injection-sweep-pacing.ts +6 -8
- package/src/land-gate-notifications.ts +11 -10
- package/src/land-gate-queue-progress.ts +3 -3
- package/src/land-settle-dispatch.ts +52 -92
- package/src/lifecycle-context-pressure.ts +11 -10
- package/src/lifecycle-manager.ts +104 -98
- package/src/maintenance/breaker.ts +18 -16
- package/src/maintenance/checkout-refresh.ts +18 -1
- package/src/maintenance/critical-job-watchdog.ts +3 -3
- package/src/maintenance/events.ts +3 -3
- package/src/maintenance/history-retention-job.ts +30 -0
- package/src/maintenance/jobs.ts +29 -67
- package/src/maintenance/long-running-work.ts +3 -1
- package/src/maintenance/offline-agent-prune-job.ts +28 -0
- package/src/maintenance/orphaned-sessions.ts +5 -4
- package/src/maintenance/pauses.ts +4 -10
- package/src/maintenance/plan-ready-notice-job.ts +71 -0
- package/src/maintenance/scheduler.ts +18 -13
- package/src/maintenance/stalled-delivery-pager.ts +4 -4
- package/src/maintenance/teams.ts +19 -30
- package/src/maintenance/workspaces.ts +17 -27
- package/src/managed-policy-escalation.ts +7 -7
- package/src/managed-policy.ts +3 -3
- package/src/managed-provider-gate.ts +5 -4
- package/src/managed-spawn-reservation.ts +5 -4
- package/src/mcp/index.ts +8 -8
- package/src/mcp/output-descriptions.ts +1 -1
- package/src/mcp/presentation.ts +2 -1
- package/src/mcp/runtime-output.ts +6 -1
- package/src/mcp/self-care-visibility.ts +2 -2
- package/src/mcp/spawn-targets-matrix.ts +10 -1
- package/src/mcp/tool-groups.ts +3 -2
- package/src/mcp/tool-roles.ts +81 -0
- package/src/mcp/tools-workspace.ts +1 -1
- package/src/mcp-blackboard-tools.ts +6 -5
- package/src/mcp-decision-tools.ts +7 -7
- package/src/mcp-egress-tools.ts +10 -9
- package/src/mcp-issue-activity-tools.ts +2 -2
- package/src/mcp-issue-association-tools.ts +4 -4
- package/src/mcp-issue-ref-tools.ts +3 -3
- package/src/mcp-merge-tools.ts +5 -5
- package/src/mcp-notification-subscription-tools.ts +14 -15
- package/src/mcp-output-levels.test-fixture.ts +1 -1
- package/src/mcp-profile-trust-tools.ts +1 -1
- package/src/mcp-project-journal-tools.ts +4 -12
- package/src/mcp-project-tools.ts +17 -18
- package/src/mcp-routing-policy-tools.ts +3 -2
- package/src/mcp-team-tools.ts +6 -6
- package/src/mcp-workflow-tools.ts +5 -3
- package/src/memory-area-injection.ts +13 -12
- package/src/memory-boundary-capture.ts +6 -6
- package/src/memory-injection.ts +45 -43
- package/src/merge-pause-maintenance.ts +5 -4
- package/src/notification-router.ts +44 -17
- package/src/notify.ts +5 -5
- package/src/observed-reset-refresh.ts +4 -4
- package/src/operator-alarm.ts +2 -5
- package/src/project-ingest.ts +6 -6
- package/src/project-instructions.ts +8 -1
- package/src/project-trust.ts +12 -11
- package/src/prompt-resolver.ts +3 -3
- package/src/provider-quota-async.ts +5 -7
- package/src/provider-timeline-event.ts +4 -7
- package/src/quota-band-delivery.ts +2 -2
- package/src/recipe-runner.ts +4 -3
- package/src/retention-config-store.ts +4 -3
- package/src/reviewer-pipeline.ts +11 -10
- package/src/routes/_route.ts +4 -4
- package/src/routes/_shared.ts +13 -12
- package/src/routes/agent-effort.ts +5 -5
- package/src/routes/agent-heartbeat-quota.ts +23 -0
- package/src/routes/agent-keep.ts +1 -1
- package/src/routes/agent-reporting.ts +1 -1
- package/src/routes/agent-self-effort.ts +5 -5
- package/src/routes/agent-sessions.ts +24 -24
- package/src/routes/agents-spawn.ts +1 -1
- package/src/routes/agents.ts +17 -119
- package/src/routes/auth.ts +14 -15
- package/src/routes/authority-audit.ts +2 -2
- package/src/routes/automations.ts +6 -5
- package/src/routes/channel-binding-delete.ts +1 -1
- package/src/routes/channel-output.ts +3 -3
- package/src/routes/claude-api-budget.ts +5 -5
- package/src/routes/commands-listing.ts +1 -1
- package/src/routes/commands.ts +29 -27
- package/src/routes/context-resets.ts +2 -2
- package/src/routes/coordinator-teams.ts +1 -1
- package/src/routes/decisions.ts +2 -2
- package/src/routes/host-secret-files.ts +1 -1
- package/src/routes/hosts.ts +11 -11
- package/src/routes/inbox.ts +3 -2
- package/src/routes/index.ts +2 -1
- package/src/routes/integrations.ts +9 -7
- package/src/routes/issue-activity.ts +2 -2
- package/src/routes/issue-associations.ts +4 -4
- package/src/routes/issue-refs.ts +3 -3
- package/src/routes/kernel-timers-operator.ts +1 -1
- package/src/routes/memory.ts +1 -1
- package/src/routes/merges.ts +11 -11
- package/src/routes/messages.ts +23 -17
- package/src/routes/orchestrator-bootstrap.ts +1 -1
- package/src/routes/orchestrator-host-registration.ts +2 -0
- package/src/routes/orchestrator-host-utilization.ts +2 -2
- package/src/routes/orchestrator-proxy.ts +2 -2
- package/src/routes/orchestrator.ts +27 -18
- package/src/routes/pairs.ts +8 -8
- package/src/routes/project-profile-instructions.ts +1 -1
- package/src/routes/project-trust.ts +1 -1
- package/src/routes/projects.ts +16 -16
- package/src/routes/prompt-templates.ts +12 -12
- package/src/routes/provider-quotas.ts +10 -10
- package/src/routes/run-command.ts +1 -1
- package/src/routes/secrets-backup.ts +2 -2
- package/src/routes/secrets-import.ts +1 -1
- package/src/routes/secrets-vault.ts +7 -7
- package/src/routes/settings.ts +1 -1
- package/src/routes/shipping.ts +9 -1
- package/src/routes/spawn-policy.ts +8 -7
- package/src/routes/sse.ts +2 -2
- package/src/routes/team-templates.ts +7 -7
- package/src/routes/teams.ts +10 -10
- package/src/routes/tokens.ts +18 -17
- package/src/routes/users.ts +24 -24
- package/src/routes/viewer-read.ts +3 -3
- package/src/routes/web-tool-calls.ts +4 -4
- package/src/routes/workflows.ts +2 -2
- package/src/routes/workspaces.ts +17 -16
- package/src/routing-policy-push.ts +6 -6
- package/src/runtime-grant.ts +17 -11
- package/src/runtime-tokens.ts +20 -28
- package/src/secrets-broker/config.ts +1 -1
- package/src/secrets-broker/service.ts +2 -2
- package/src/security.ts +3 -1
- package/src/self-resume-carry.ts +21 -110
- package/src/self-resume-supersede.ts +4 -3
- package/src/self-resume.ts +54 -66
- package/src/server-background.ts +1 -1
- package/src/server.ts +21 -22
- package/src/services/agent-action.ts +12 -31
- package/src/services/agent-effort.ts +7 -32
- package/src/services/agent-hang-watchdog-job.ts +2 -2
- package/src/services/agent-heartbeat.ts +166 -0
- package/src/services/agent-reporting.ts +1 -1
- package/src/services/browser-inventory.ts +3 -3
- package/src/services/browser-session-state.ts +3 -2
- package/src/services/browser-session.ts +21 -27
- package/src/services/bus-command-update.ts +9 -9
- package/src/services/checkout-refresh-state.ts +1 -0
- package/src/services/checkout-refresh.test-fixture.ts +3 -2
- package/src/services/command-host.ts +7 -2
- package/src/services/coordinator-digest.ts +3 -2
- package/src/services/dispatch-command.ts +12 -13
- package/src/services/effective-launch-driver.ts +14 -9
- package/src/services/host-changed-paths.test-fixture.ts +5 -6
- package/src/services/host-project-readiness.ts +2 -2
- package/src/services/host-providers.ts +1 -1
- package/src/services/host-utilization.ts +9 -9
- package/src/services/idle-agent-digest.ts +17 -12
- package/src/services/idle-epics.ts +4 -4
- package/src/services/interrupt-agent.ts +12 -9
- package/src/services/issue-activity.ts +3 -3
- package/src/services/issue-associations.ts +15 -18
- package/src/services/issue-lifecycle-comments.ts +3 -2
- package/src/services/issue-lifecycle.ts +14 -13
- package/src/services/issue-refs.ts +4 -5
- package/src/services/kernel-timers-cutover-chaos-worker.ts +1 -1
- package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
- package/src/services/kernel-timers-ingress.ts +3 -3
- package/src/services/kernel-timers-message-authority-chaos-worker.ts +3 -10
- package/src/services/kernel-timers-operator-chaos-worker.ts +3 -2
- package/src/services/kernel-timers-outbox.ts +1 -1
- package/src/services/kernel-timers-stage10-operator.ts +1 -1
- package/src/services/kernel-timers.ts +4 -3
- package/src/services/land-gate-placement.ts +11 -2
- package/src/services/land-issue-relations.ts +5 -4
- package/src/services/land-restart-retry.ts +7 -6
- package/src/services/landed-ancestry.ts +6 -5
- package/src/services/legacy-landing-host.ts +4 -1
- package/src/services/legacy-landing-recovery.ts +8 -6
- package/src/services/managed-running.ts +17 -17
- package/src/services/memory-areas.ts +3 -3
- package/src/services/merge-pause.ts +9 -15
- package/src/services/operator-decisions.ts +37 -41
- package/src/services/orchestrator-registration-recovery.ts +6 -2
- package/src/services/orchestrator-upgrade.ts +14 -12
- package/src/services/permission-decision.ts +4 -3
- package/src/services/plan-ci-notice.ts +1 -1
- package/src/services/plan-conformance.ts +4 -4
- package/src/services/plan-dispatch-policy.ts +10 -3
- package/src/services/plan-dispatch.ts +35 -14
- package/src/services/plan-graph.ts +39 -261
- package/src/services/plan-import.ts +1 -1
- package/src/services/plan-live-bindings.ts +8 -37
- package/src/services/plan-task-create.ts +1 -1
- package/src/services/plan-terminal-refs.ts +2 -12
- package/src/services/plan-wake-digest.ts +8 -3
- package/src/services/profile-trust.ts +2 -2
- package/src/services/project.ts +11 -10
- package/src/services/prompt-templates.ts +6 -5
- package/src/services/provider-hold-nudge.ts +9 -9
- package/src/services/provider-hold-record.ts +13 -12
- package/src/services/provider-state-audit.ts +19 -18
- package/src/services/provisioning-harness/driver.ts +1 -1
- package/src/services/provisioning-harness/fixture-matrix.ts +3 -3
- package/src/services/provisioning-harness/lifecycle.ts +24 -22
- package/src/services/provisioning-harness/prepare.ts +16 -17
- package/src/services/rate-limit-resume.ts +10 -9
- package/src/services/red-main-auto-revert.ts +7 -5
- package/src/services/red-main-controller-deps.ts +20 -3
- package/src/services/red-main-incident.ts +3 -1
- package/src/services/red-main-quarantine.ts +2 -3
- package/src/services/register-agent.ts +22 -22
- package/src/services/restart-project-role.ts +1 -1
- package/src/services/restart-spawn.ts +17 -16
- package/src/services/review-publish.ts +7 -24
- package/src/services/review-ref-reconcile.ts +29 -38
- package/src/services/runtime-dependency-alert.ts +27 -0
- package/src/services/scheduler-command.ts +11 -9
- package/src/services/scheduler.ts +5 -5
- package/src/services/self-effort-grant.ts +5 -25
- package/src/services/send-message.ts +16 -33
- package/src/services/shutdown-agent.ts +17 -14
- package/src/services/spawn-agent.ts +12 -41
- package/src/services/spawn-chain-inspector.ts +1 -1
- package/src/services/spawn-cwd-placement.ts +21 -5
- package/src/services/spawn-issue-activity.ts +8 -6
- package/src/services/spawn-launch.ts +17 -1
- package/src/services/spawn-loss-escalation.ts +1 -1
- package/src/services/spawn-registration-watchdog.ts +2 -2
- package/src/services/spawn-repo-probe.ts +7 -5
- package/src/services/spawn-tree-cleanup.ts +7 -8
- package/src/services/steward-reaper.ts +4 -4
- package/src/services/task-context-hydration.ts +25 -1
- package/src/services/task-delivery.ts +20 -34
- package/src/services/task-entity.ts +40 -203
- package/src/services/task-issue-link.ts +13 -20
- package/src/services/task-issue-weave.ts +12 -11
- package/src/services/task-lifecycle.ts +27 -31
- package/src/services/task-repo-cwd.ts +37 -0
- package/src/services/task-review-hold.ts +4 -17
- package/src/services/task-semantic-events.ts +13 -11
- package/src/services/task-state-reconcile.ts +3 -4
- package/src/services/task-validation.ts +2 -2
- package/src/services/team-templates.ts +5 -5
- package/src/services/team.ts +31 -98
- package/src/services/terminal-provider-exit.ts +6 -5
- package/src/services/timer-message-fire-chaos-worker.ts +7 -3
- package/src/services/timer-message-fire-outbox.ts +1 -1
- package/src/services/transient-agent-reaper.ts +22 -19
- package/src/services/turn-reconcile-audit.ts +9 -9
- package/src/services/user-onboarding.ts +8 -6
- package/src/services/user-profile.ts +6 -9
- package/src/services/work-finality.ts +16 -4
- package/src/services/workflow-auto-start.ts +2 -1
- package/src/services/workflow-caller.ts +61 -4
- package/src/services/workflow-child-proof-worker.ts +1 -1
- package/src/services/workflow-control-proof-worker.ts +1 -1
- package/src/services/workflow-coordinator.ts +8 -2
- package/src/services/workflow-decisions.ts +5 -5
- package/src/services/workflow-deferred-cleanup.ts +1 -1
- package/src/services/workflow-effect-driver.ts +42 -19
- package/src/services/workflow-effects.ts +41 -27
- package/src/services/workflow-pins.ts +7 -4
- package/src/services/workflow-reconcile.ts +1 -0
- package/src/services/workflow-resume-pins.ts +3 -3
- package/src/services/workflow-routing-defaults.ts +18 -0
- package/src/services/workflow-run-view.ts +4 -3
- package/src/services/workflow-run.test-fixture.ts +1 -1
- package/src/services/workflow-runs.ts +34 -22
- package/src/services/workflow-supervisor-state.ts +21 -1
- package/src/services/workflow-supervisor.ts +4 -3
- package/src/services/workspace-recovery.ts +4 -3
- package/src/spawn-admission.ts +10 -2
- package/src/spawn-placement.ts +27 -9
- package/src/spawn-targets.ts +8 -8
- package/src/sse-agent-status.ts +15 -6
- package/src/sse.ts +49 -24
- package/src/startup-context-injection.ts +61 -57
- package/src/steward-handoff.ts +4 -3
- package/src/steward-policy-cleanup.ts +9 -8
- package/src/steward.ts +2 -2
- package/src/task-dispatch-live.ts +5 -2
- package/src/task-dispatcher.ts +2 -2
- package/src/token-db.ts +2 -2
- package/src/token-usage-async.ts +2 -4
- package/src/user-awaiting-reply.ts +5 -5
- package/src/workspace-actions.ts +24 -29
- package/src/workspace-auto-merge.ts +14 -12
- package/src/workspace-changed-paths-client.ts +16 -5
- package/src/workspace-cleanup-stale.ts +30 -7
- package/src/workspace-command-settle.ts +24 -16
- package/src/workspace-config.ts +1 -1
- package/src/workspace-conflict-resolver.ts +9 -7
- package/src/workspace-dead-owner-recovery.ts +5 -4
- package/src/workspace-escalation.ts +12 -11
- package/src/workspace-human-escalation.ts +3 -3
- package/src/workspace-land-gate-watchdog.ts +3 -3
- package/src/workspace-land-refusal.ts +20 -0
- package/src/workspace-land-stopped.ts +1 -0
- package/src/workspace-merge.ts +10 -26
- package/src/workspace-nothing-to-land-notice.ts +2 -2
- package/src/workspace-orphans.ts +18 -37
- package/src/workspace-pr-completion.ts +10 -8
- package/src/workspace-rekey.ts +3 -3
- package/src/workspace-steward-failure.ts +2 -2
- package/src/workspace-steward-wake.ts +2 -2
- package/public/assets/agent-liveness-CaNwK0Zf.js +0 -2
- package/public/assets/provider-display-DsuWMvVL.js +0 -2
- package/public/assets/run-card-CpKqRbhg.js +0 -2
- package/public/assets/shipping-B9YtnwtG.js +0 -2
- package/public/assets/store-DcClXLH0.js +0 -9
- package/runner/src/agent-runner/http-liveness.ts +0 -85
- package/src/db/on-loop-write-transaction.ts +0 -34
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { framed } from "../hub-metrics";
|
|
2
|
+
import { getGateway } from "./gateway";
|
|
3
|
+
import { normalizePhase, readCutover } from "./timer-cutover-state";
|
|
4
|
+
import type { CutoverWriterState, CutoverStepArgs, CutoverStepResults } from "./timer-cutover-bodies";
|
|
1
5
|
import { randomUUID } from "node:crypto";
|
|
2
6
|
import {
|
|
3
7
|
clearExpiredLegacyMessageClaimsInTx,
|
|
@@ -64,48 +68,9 @@ export type TimerMessageCutoverTickResult =
|
|
|
64
68
|
| { state: "absent" | "idle" | "waiting-for-lease" }
|
|
65
69
|
| { state: "advanced" | "holding" | "aborting"; phase: TimerMessageCutoverPhase; epoch: number; liveClaims?: number; guard: TimerMessageDeadlineGuard };
|
|
66
70
|
|
|
67
|
-
function normalizePhase(phase: string): TimerMessageCutoverPhase {
|
|
68
|
-
return phase === "leased" || phase === "quiescing" || phase === "draining" || phase === "quiesced" || phase === "aborting"
|
|
69
|
-
? phase
|
|
70
|
-
: "idle";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
|
|
74
|
-
return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
|
|
78
|
-
return JSON.stringify([...journalOf(row), entry]);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function readCutover(): TimerMessageCutoverRow | null {
|
|
82
|
-
const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
|
|
83
|
-
return row ? {
|
|
84
|
-
id: 1,
|
|
85
|
-
protocolVersion: row.protocol_version,
|
|
86
|
-
epoch: row.epoch,
|
|
87
|
-
desiredMode: row.desired_mode,
|
|
88
|
-
observedMode: row.observed_mode,
|
|
89
|
-
phase: row.phase,
|
|
90
|
-
leaseOwner: row.lease_owner,
|
|
91
|
-
leaseExpiresAt: row.lease_expires_at,
|
|
92
|
-
quiesceStartedAt: row.quiesce_started_at,
|
|
93
|
-
quiesceDeadline: row.quiesce_deadline,
|
|
94
|
-
lastCompletedTimer: row.last_completed_timer,
|
|
95
|
-
ingressPolicy: row.ingress_policy,
|
|
96
|
-
messageMutationGate: row.message_mutation_gate,
|
|
97
|
-
gateEpoch: row.gate_epoch,
|
|
98
|
-
abortRequested: row.abort_requested === 1,
|
|
99
|
-
phaseJournal: JSON.parse(row.phase_journal),
|
|
100
|
-
updatedAt: row.updated_at,
|
|
101
|
-
} : null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
71
|
/** Singleton Stage-6 lease/quiesce state machine. It has no HTTP/MCP/CLI binding: production stays
|
|
105
72
|
* shadow and the only boot wiring is recovery of an already-existing non-idle journal. */
|
|
106
|
-
//
|
|
107
|
-
// writer-op module. It cannot be safely folded into this service in LC-4b: doing so breaches the
|
|
108
|
-
// file-size ratchet; callers with an awaitable batch facade are already migrated here.
|
|
73
|
+
// #1870 — only transaction bodies cross the writer seam; fault hooks and timers stay on main.
|
|
109
74
|
export class TimerMessageCutoverCoordinator {
|
|
110
75
|
readonly ownerId: string;
|
|
111
76
|
readonly #now: () => number;
|
|
@@ -158,7 +123,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
158
123
|
if (!acquired) return false;
|
|
159
124
|
const guard = this.deadlineGuard();
|
|
160
125
|
if (!guard.safe) {
|
|
161
|
-
this.requestAbort(`deadline guard refused entry (slack=${guard.slackMs}, required=${guard.requiredMs})`);
|
|
126
|
+
await this.requestAbort(`deadline guard refused entry (slack=${guard.slackMs}, required=${guard.requiredMs})`);
|
|
162
127
|
await this.tick();
|
|
163
128
|
await this.tick();
|
|
164
129
|
return false;
|
|
@@ -166,15 +131,9 @@ export class TimerMessageCutoverCoordinator {
|
|
|
166
131
|
return true;
|
|
167
132
|
}
|
|
168
133
|
|
|
169
|
-
requestAbort(reason = "abort requested"): boolean {
|
|
134
|
+
async requestAbort(reason = "abort requested"): Promise<boolean> {
|
|
170
135
|
const now = this.#now();
|
|
171
|
-
return
|
|
172
|
-
const row = readCutover();
|
|
173
|
-
if (!row || normalizePhase(row.phase) === "idle") return false;
|
|
174
|
-
return getDb().query(`UPDATE timer_message_cutover
|
|
175
|
-
SET abort_requested = 1, phase_journal = ?, updated_at = ? WHERE id = 1`)
|
|
176
|
-
.run(appendJournal(row, { epoch: row.epoch, phase: "aborting", at: now, reason }), now).changes === 1;
|
|
177
|
-
});
|
|
136
|
+
return await this.#writeStep("abort", { now, reason });
|
|
178
137
|
}
|
|
179
138
|
|
|
180
139
|
/** Stage 9: commit only the durable route marker while the same all-surface gate remains closed. */
|
|
@@ -182,19 +141,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
182
141
|
const now = this.#now();
|
|
183
142
|
const epoch = this.#epoch;
|
|
184
143
|
if (epoch === undefined) throw new Error("cutover lease is not owned");
|
|
185
|
-
|
|
186
|
-
const row = this.#owned(epoch, now);
|
|
187
|
-
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed") {
|
|
188
|
-
throw new Error("ingress routing activation requires the live quiesced gate owner");
|
|
189
|
-
}
|
|
190
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET ingress_policy = 'ingress',
|
|
191
|
-
phase_journal = ?, updated_at = ?
|
|
192
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
|
|
193
|
-
AND phase = 'quiesced' AND message_mutation_gate = 'closed'`)
|
|
194
|
-
.run(appendJournal(row, { epoch, phase: "quiesced", at: now, reason: "authoritative ingress routing activated under fence" }),
|
|
195
|
-
now, epoch, this.ownerId, now);
|
|
196
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost before ingress routing activation");
|
|
197
|
-
});
|
|
144
|
+
await this.#writeStep("activate", { now, epoch });
|
|
198
145
|
await this.#faults.afterRoutingActivation?.(epoch);
|
|
199
146
|
}
|
|
200
147
|
|
|
@@ -205,23 +152,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
205
152
|
const epoch = this.#epoch;
|
|
206
153
|
if (epoch === undefined) throw new Error("cutover lease is not owned");
|
|
207
154
|
await this.#faults.afterG1BeforeReopen?.(epoch);
|
|
208
|
-
|
|
209
|
-
const row = this.#owned(epoch, now);
|
|
210
|
-
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed" || row.ingressPolicy !== "ingress") {
|
|
211
|
-
throw new Error("G1 completion requires fenced authoritative ingress routing");
|
|
212
|
-
}
|
|
213
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
214
|
-
protocol_version = MAX(COALESCE(protocol_version, 1), 2),
|
|
215
|
-
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
216
|
-
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
217
|
-
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
218
|
-
phase_journal = ?, updated_at = ?
|
|
219
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
|
|
220
|
-
AND phase = 'quiesced' AND message_mutation_gate = 'closed' AND ingress_policy = 'ingress'`)
|
|
221
|
-
.run(appendJournal(row, { epoch, phase: "idle", at: now, reason: "G1 proven; gate reopened under legacy authority" }),
|
|
222
|
-
now, epoch, this.ownerId, now);
|
|
223
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost before G1 gate reopen");
|
|
224
|
-
});
|
|
155
|
+
await this.#writeStep("completeLegacy", { now, epoch });
|
|
225
156
|
this.#epoch = undefined;
|
|
226
157
|
}
|
|
227
158
|
|
|
@@ -236,37 +167,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
236
167
|
const now = this.#now();
|
|
237
168
|
const epoch = this.#epoch;
|
|
238
169
|
if (epoch === undefined) throw new Error("cutover lease is not owned");
|
|
239
|
-
|
|
240
|
-
const row = this.#owned(epoch, now);
|
|
241
|
-
if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed"
|
|
242
|
-
|| row.ingressPolicy !== "ingress") {
|
|
243
|
-
throw new Error("authority-transfer completion requires the live quiesced ingress gate owner");
|
|
244
|
-
}
|
|
245
|
-
// Only ACTIVE (scheduled) rows must converge on the target backend — terminal rows never
|
|
246
|
-
// dispatch, so their backend is irrelevant and must not block completion (an ordinary kernel-mode
|
|
247
|
-
// cancel terminalizes a kernel-owned row that a reverse would otherwise be unable to move).
|
|
248
|
-
const wrongOwners = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority
|
|
249
|
-
WHERE lifecycle='scheduled' AND backend != ?`).get(expectedBackend) as { n: number }).n;
|
|
250
|
-
if (wrongOwners !== 0) throw new Error(`${wrongOwners} scheduled MESSAGE owners are not ${expectedBackend}`);
|
|
251
|
-
const expectedFrozen = expectedBackend === "kernel" ? 1 : 0;
|
|
252
|
-
const projectionMismatch = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority a
|
|
253
|
-
JOIN scheduled_timers s ON s.id=a.timer_id
|
|
254
|
-
WHERE a.lifecycle='scheduled' AND (s.command IS NOT NULL OR s.message_authority_frozen != ?)`)
|
|
255
|
-
.get(expectedFrozen) as { n: number }).n;
|
|
256
|
-
if (projectionMismatch !== 0) throw new Error(`${projectionMismatch} MESSAGE compatibility projections disagree with ${mode}`);
|
|
257
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
258
|
-
desired_mode = ?, observed_mode = ?, phase = 'idle', lease_owner = NULL,
|
|
259
|
-
lease_expires_at = NULL, quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
260
|
-
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
261
|
-
phase_journal = ?, updated_at = ?
|
|
262
|
-
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
|
|
263
|
-
AND phase='quiesced' AND message_mutation_gate='closed' AND ingress_policy='ingress'`)
|
|
264
|
-
.run(mode, mode, appendJournal(row, {
|
|
265
|
-
epoch, phase: "idle", at: now,
|
|
266
|
-
reason: `${mode} per-row ownership proven; MESSAGE mutation gate reopened`,
|
|
267
|
-
}), now, epoch, this.ownerId, now);
|
|
268
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost before authority-transfer completion");
|
|
269
|
-
});
|
|
170
|
+
await this.#writeStep("completeTransfer", { now, epoch, mode, expectedBackend });
|
|
270
171
|
this.#epoch = undefined;
|
|
271
172
|
await this.#faults.afterModeCommit?.(epoch, mode);
|
|
272
173
|
}
|
|
@@ -275,22 +176,11 @@ export class TimerMessageCutoverCoordinator {
|
|
|
275
176
|
* benignly refuse while reverse projection owns the quiesced writer window. `targetMode` is the mode
|
|
276
177
|
* the reverse restores (dual-read→shadow or kernel→dual-read); `sourceObservedMode` is the currently
|
|
277
178
|
* observed mode the reverse departs from. */
|
|
278
|
-
markReverseTransferDesired(targetMode: KernelTimersDesiredMode, sourceObservedMode: KernelTimersDesiredMode): void {
|
|
179
|
+
async markReverseTransferDesired(targetMode: KernelTimersDesiredMode, sourceObservedMode: KernelTimersDesiredMode): Promise<void> {
|
|
279
180
|
const now = this.#now();
|
|
280
181
|
const epoch = this.#epoch;
|
|
281
182
|
if (epoch === undefined) throw new Error("cutover lease is not owned");
|
|
282
|
-
|
|
283
|
-
const row = this.#owned(epoch, now);
|
|
284
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET desired_mode=?,
|
|
285
|
-
phase_journal=?, updated_at=?
|
|
286
|
-
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
|
|
287
|
-
AND phase='quiesced' AND message_mutation_gate='closed' AND observed_mode=?`)
|
|
288
|
-
.run(targetMode, appendJournal(row, {
|
|
289
|
-
epoch, phase: "quiesced", at: now,
|
|
290
|
-
reason: `${targetMode} rollback desired; kernel decision commits quiesced for reverse projection`,
|
|
291
|
-
}), now, epoch, this.ownerId, now, sourceObservedMode);
|
|
292
|
-
if (changed.changes !== 1) throw new Error(`reverse transfer requires observed ${sourceObservedMode} under the live fence`);
|
|
293
|
-
});
|
|
183
|
+
await this.#writeStep("reverse", { now, epoch, targetMode, sourceObservedMode });
|
|
294
184
|
}
|
|
295
185
|
|
|
296
186
|
/** One deterministic recovery/coordination step. The deadline guard is recomputed on every call,
|
|
@@ -315,7 +205,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
315
205
|
: boundedOut
|
|
316
206
|
? "bounded quiesce timeout reached"
|
|
317
207
|
: `deadline guard tripped (slack=${guard.slackMs}, required=${guard.requiredMs})`;
|
|
318
|
-
if (normalizePhase(row.phase) !== "aborting") this.#transition(row.epoch, "aborting", reason);
|
|
208
|
+
if (normalizePhase(row.phase) !== "aborting") await this.#transition(row.epoch, "aborting", reason);
|
|
319
209
|
else await this.#finishAbort(row.epoch, reason);
|
|
320
210
|
return { state: "aborting", phase: "aborting", epoch: row.epoch, guard };
|
|
321
211
|
}
|
|
@@ -327,20 +217,20 @@ export class TimerMessageCutoverCoordinator {
|
|
|
327
217
|
|
|
328
218
|
switch (normalizePhase(row.phase)) {
|
|
329
219
|
case "leased":
|
|
330
|
-
if (!this.#closeGate(row.epoch, now)) {
|
|
220
|
+
if (!await this.#closeGate(row.epoch, now)) {
|
|
331
221
|
return { state: "aborting", phase: "aborting", epoch: row.epoch, guard: this.deadlineGuard(now) };
|
|
332
222
|
}
|
|
333
223
|
await this.#faults.afterGateClose?.(row.epoch);
|
|
334
224
|
return { state: "advanced", phase: "quiescing", epoch: row.epoch, guard };
|
|
335
225
|
case "quiescing":
|
|
336
|
-
this.#transition(row.epoch, "draining", "message claims are fenced; begin bounded drain");
|
|
226
|
+
await this.#transition(row.epoch, "draining", "message claims are fenced; begin bounded drain");
|
|
337
227
|
return { state: "advanced", phase: "draining", epoch: row.epoch, guard };
|
|
338
228
|
case "draining": {
|
|
339
|
-
|
|
229
|
+
await this.#writeStep("clearClaims", { now });
|
|
340
230
|
const liveClaims = countLiveLegacyMessageClaims(now);
|
|
341
231
|
await this.#faults.duringClaimDrain?.(row.epoch, liveClaims);
|
|
342
232
|
if (liveClaims === 0) {
|
|
343
|
-
this.#transition(row.epoch, "quiesced", "all live legacy MESSAGE claims drained");
|
|
233
|
+
await this.#transition(row.epoch, "quiesced", "all live legacy MESSAGE claims drained");
|
|
344
234
|
return { state: "advanced", phase: "quiesced", epoch: row.epoch, liveClaims, guard };
|
|
345
235
|
}
|
|
346
236
|
return { state: "holding", phase: "draining", epoch: row.epoch, liveClaims, guard };
|
|
@@ -358,11 +248,22 @@ export class TimerMessageCutoverCoordinator {
|
|
|
358
248
|
/** Boot recovery is deliberately non-creative: absent/idle shadow state stays a production no-op. */
|
|
359
249
|
startRecovery(): void {
|
|
360
250
|
if (this.#ticker) return;
|
|
361
|
-
const pass = () =>
|
|
251
|
+
const pass = framed("timer message-cutover", () => this.tick().catch((error) => console.warn(`timer MESSAGE cutover recovery failed: ${String(error)}`)));
|
|
362
252
|
pass();
|
|
363
253
|
this.#ticker = setInterval(pass, this.#tickMs);
|
|
364
254
|
}
|
|
365
255
|
|
|
256
|
+
async #writeStep<K extends keyof CutoverStepArgs>(step: K, args: CutoverStepArgs[K]): Promise<CutoverStepResults[K]> {
|
|
257
|
+
const epoch = this.#epoch;
|
|
258
|
+
const state: CutoverWriterState = { ownerId: this.ownerId, epoch, leaseMs: this.#leaseMs,
|
|
259
|
+
quiesceTimeoutMs: this.#quiesceTimeoutMs, worstCaseMs: this.#worstCaseMs,
|
|
260
|
+
authoritativeActivation: this.#authoritativeActivation, operationLeaseFence: this.#operationLeaseFence };
|
|
261
|
+
const result = await getGateway().write("timerCutover.coordinatorStep", { step, state, args },
|
|
262
|
+
{ critical: true, idempotencyKey: randomUUID() }) as unknown as { value: CutoverStepResults[K]; clearEpoch?: boolean };
|
|
263
|
+
if (result.clearEpoch && this.#epoch === epoch) this.#epoch = undefined;
|
|
264
|
+
return result.value;
|
|
265
|
+
}
|
|
266
|
+
|
|
366
267
|
stop(): void {
|
|
367
268
|
if (this.#ticker) clearInterval(this.#ticker);
|
|
368
269
|
this.#ticker = undefined;
|
|
@@ -371,57 +272,7 @@ export class TimerMessageCutoverCoordinator {
|
|
|
371
272
|
async #acquire(create: boolean): Promise<boolean> {
|
|
372
273
|
const now = this.#now();
|
|
373
274
|
let acquiredEpoch: number | undefined;
|
|
374
|
-
|
|
375
|
-
writeTransaction(() => {
|
|
376
|
-
// Stage-10 pins the resolved leg before it asks this independent coordinator to close the
|
|
377
|
-
// global gate. Check that pin's operation lease under THIS acquisition transaction: if an
|
|
378
|
-
// abort won after pin but before requestQuiesce, it cannot be followed by a stale gate close.
|
|
379
|
-
const fence = this.#operationLeaseFence;
|
|
380
|
-
if (fence) {
|
|
381
|
-
const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
|
|
382
|
-
WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
|
|
383
|
-
AND lease_owner=? AND lease_expires_at > ?`)
|
|
384
|
-
.get(fence.requestId, fence.ownerId, now);
|
|
385
|
-
if (!live) return;
|
|
386
|
-
}
|
|
387
|
-
const row = readCutover();
|
|
388
|
-
if (!row) {
|
|
389
|
-
if (!create) return;
|
|
390
|
-
acquiredEpoch = 1;
|
|
391
|
-
const journal: TimerMessageCutoverJournalEntry[] = [{ epoch: 1, phase: "leased", at: now, reason: "initial lease acquired" }];
|
|
392
|
-
getDb().query(`INSERT INTO timer_message_cutover
|
|
393
|
-
(id, protocol_version, epoch, desired_mode, observed_mode, phase, lease_owner, lease_expires_at,
|
|
394
|
-
quiesce_started_at, quiesce_deadline, last_completed_timer, ingress_policy,
|
|
395
|
-
message_mutation_gate, gate_epoch, abort_requested, phase_journal, updated_at)
|
|
396
|
-
VALUES (1, 1, 1, 'shadow', 'shadow', 'leased', ?, ?, NULL, NULL, NULL, 'legacy', 'open', NULL, 0, ?, ?)`)
|
|
397
|
-
.run(this.ownerId, now + this.#leaseMs, JSON.stringify(journal), now);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const rowPhase = normalizePhase(row.phase);
|
|
401
|
-
const liveForeign = row.leaseOwner !== null && row.leaseOwner !== this.ownerId && (row.leaseExpiresAt ?? 0) > now;
|
|
402
|
-
if (liveForeign) return;
|
|
403
|
-
if (row.leaseOwner === this.ownerId && (row.leaseExpiresAt ?? 0) > now && this.#epoch === row.epoch) {
|
|
404
|
-
acquiredEpoch = row.epoch;
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const nextEpoch = row.epoch + 1;
|
|
408
|
-
takeover = row.leaseOwner !== null || rowPhase !== "idle" || row.messageMutationGate === "closed";
|
|
409
|
-
const nextPhase: TimerMessageCutoverPhase = rowPhase === "idle" ? "leased" : rowPhase;
|
|
410
|
-
const entry: TimerMessageCutoverJournalEntry = {
|
|
411
|
-
epoch: nextEpoch,
|
|
412
|
-
phase: takeover ? "takeover" : "leased",
|
|
413
|
-
at: now,
|
|
414
|
-
reason: takeover ? `expired epoch ${row.epoch} taken over in phase ${rowPhase}` : "lease acquired",
|
|
415
|
-
};
|
|
416
|
-
const result = getDb().query(`UPDATE timer_message_cutover SET
|
|
417
|
-
protocol_version = COALESCE(protocol_version, 1), epoch = ?, phase = ?, lease_owner = ?, lease_expires_at = ?,
|
|
418
|
-
gate_epoch = CASE WHEN message_mutation_gate = 'closed' THEN ? ELSE NULL END,
|
|
419
|
-
phase_journal = ?, updated_at = ?
|
|
420
|
-
WHERE id = 1 AND (lease_owner IS NULL OR lease_expires_at <= ? OR lease_owner = ?)`)
|
|
421
|
-
.run(nextEpoch, nextPhase, this.ownerId, now + this.#leaseMs, nextEpoch,
|
|
422
|
-
appendJournal(row, entry), now, now, this.ownerId);
|
|
423
|
-
if (result.changes === 1) acquiredEpoch = nextEpoch;
|
|
424
|
-
});
|
|
275
|
+
({ acquiredEpoch } = await this.#writeStep("acquire", { now, create }));
|
|
425
276
|
if (acquiredEpoch === undefined) return false;
|
|
426
277
|
this.#epoch = acquiredEpoch;
|
|
427
278
|
await this.#faults.afterLeaseAcquire?.(acquiredEpoch);
|
|
@@ -430,116 +281,24 @@ export class TimerMessageCutoverCoordinator {
|
|
|
430
281
|
|
|
431
282
|
async #renew(epoch: number): Promise<boolean> {
|
|
432
283
|
const now = this.#now();
|
|
433
|
-
const renewed =
|
|
434
|
-
const row = readCutover();
|
|
435
|
-
if (!row || row.epoch !== epoch || row.leaseOwner !== this.ownerId || (row.leaseExpiresAt ?? 0) <= now) return false;
|
|
436
|
-
const deadlineCap = row.messageMutationGate === "closed" && row.quiesceDeadline !== null
|
|
437
|
-
? row.quiesceDeadline
|
|
438
|
-
: Number.MAX_SAFE_INTEGER;
|
|
439
|
-
const nextExpiry = Math.min(now + this.#leaseMs, deadlineCap);
|
|
440
|
-
return getDb().query(`UPDATE timer_message_cutover SET lease_expires_at = ?, phase_journal = ?, updated_at = ?
|
|
441
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
442
|
-
.run(nextExpiry, appendJournal(row, { epoch, phase: "renewed", at: now }), now,
|
|
443
|
-
epoch, this.ownerId, now).changes === 1;
|
|
444
|
-
});
|
|
284
|
+
const renewed = await this.#writeStep("renew", { now, epoch });
|
|
445
285
|
if (renewed) await this.#faults.afterRenewal?.(epoch);
|
|
446
286
|
return renewed;
|
|
447
287
|
}
|
|
448
288
|
|
|
449
|
-
#closeGate(epoch: number, now: number): boolean {
|
|
450
|
-
return
|
|
451
|
-
const row = this.#owned(epoch, now);
|
|
452
|
-
// `requestQuiesce` acquires the coordinator lease before the following tick closes the gate.
|
|
453
|
-
// Recheck the Stage-10 operation lease in this close transaction: an abort (or expiry) that
|
|
454
|
-
// won in that interval must leave this independent coordinator open and unleased, never
|
|
455
|
-
// permit stale authority to close the MESSAGE gate after the abort's release observed it open.
|
|
456
|
-
const fence = this.#operationLeaseFence;
|
|
457
|
-
if (fence) {
|
|
458
|
-
const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
|
|
459
|
-
WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
|
|
460
|
-
AND lease_owner=? AND lease_expires_at > ?`)
|
|
461
|
-
.get(fence.requestId, fence.ownerId, now);
|
|
462
|
-
if (!live) {
|
|
463
|
-
const released = getDb().query(`UPDATE timer_message_cutover SET
|
|
464
|
-
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
465
|
-
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
466
|
-
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
467
|
-
desired_mode = observed_mode, phase_journal = ?, updated_at = ?
|
|
468
|
-
WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>? AND phase='leased'`)
|
|
469
|
-
.run(appendJournal(row, {
|
|
470
|
-
epoch, phase: "idle", at: now,
|
|
471
|
-
reason: "Stage-10 operation lease no longer authorizes MESSAGE gate close",
|
|
472
|
-
}), now, epoch, this.ownerId, now);
|
|
473
|
-
if (released.changes !== 1) throw new Error("cutover lease lost while releasing stale Stage-10 gate request");
|
|
474
|
-
this.#epoch = undefined;
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
// Re-read all three deadline sources under the SAME writer lock that closes the gate. A
|
|
479
|
-
// deadline mutation that won the race is therefore observed here and refuses entry; one that
|
|
480
|
-
// loses cannot commit until after the closed marker and is caught by the next continuous tick.
|
|
481
|
-
const guard = this.deadlineGuard(now);
|
|
482
|
-
if (!guard.safe) {
|
|
483
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'aborting', abort_requested = 1,
|
|
484
|
-
phase_journal = ?, updated_at = ? WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
|
|
485
|
-
.run(appendJournal(row, {
|
|
486
|
-
epoch, phase: "aborting", at: now,
|
|
487
|
-
reason: `deadline guard refused atomic gate close (slack=${guard.slackMs}, required=${guard.requiredMs})`,
|
|
488
|
-
}), now, epoch, this.ownerId);
|
|
489
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost while refusing gate close");
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
const deadline = Math.min(
|
|
493
|
-
now + this.#quiesceTimeoutMs,
|
|
494
|
-
guard.deadlines.earliestDeadline === null ? Number.MAX_SAFE_INTEGER : guard.deadlines.earliestDeadline - 1,
|
|
495
|
-
);
|
|
496
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'quiescing',
|
|
497
|
-
message_mutation_gate = 'closed', gate_epoch = ?, quiesce_started_at = ?, quiesce_deadline = ?,
|
|
498
|
-
ingress_policy = CASE WHEN ? THEN 'activating' ELSE ingress_policy END,
|
|
499
|
-
phase_journal = ?, updated_at = ?
|
|
500
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
|
|
501
|
-
.run(epoch, now, deadline, this.#authoritativeActivation ? 1 : 0,
|
|
502
|
-
appendJournal(row, { epoch, phase: "quiescing", at: now, reason: "MESSAGE mutation gate closed" }),
|
|
503
|
-
now, epoch, this.ownerId);
|
|
504
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost before gate close");
|
|
505
|
-
return true;
|
|
506
|
-
});
|
|
289
|
+
async #closeGate(epoch: number, now: number): Promise<boolean> {
|
|
290
|
+
return await this.#writeStep("closeGate", { epoch, now });
|
|
507
291
|
}
|
|
508
292
|
|
|
509
|
-
#transition(epoch: number, phase: TimerMessageCutoverPhase, reason: string): void {
|
|
293
|
+
async #transition(epoch: number, phase: TimerMessageCutoverPhase, reason: string): Promise<void> {
|
|
510
294
|
const now = this.#now();
|
|
511
|
-
|
|
512
|
-
const row = this.#owned(epoch, now);
|
|
513
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET phase = ?, phase_journal = ?, updated_at = ?
|
|
514
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
515
|
-
.run(phase, appendJournal(row, { epoch, phase, at: now, reason }), now, epoch, this.ownerId, now);
|
|
516
|
-
if (changed.changes !== 1) throw new Error(`cutover lease lost before phase ${phase}`);
|
|
517
|
-
});
|
|
295
|
+
await this.#writeStep("transition", { epoch, phase, reason, now });
|
|
518
296
|
}
|
|
519
297
|
|
|
520
298
|
async #finishAbort(epoch: number, reason: string): Promise<void> {
|
|
521
299
|
await this.#faults.beforeLeaseRelease?.(epoch);
|
|
522
300
|
const now = this.#now();
|
|
523
|
-
|
|
524
|
-
const row = this.#owned(epoch, now);
|
|
525
|
-
const changed = getDb().query(`UPDATE timer_message_cutover SET
|
|
526
|
-
phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
|
|
527
|
-
quiesce_started_at = NULL, quiesce_deadline = NULL,
|
|
528
|
-
message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
|
|
529
|
-
phase_journal = ?, updated_at = ?
|
|
530
|
-
WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
|
|
531
|
-
.run(appendJournal(row, { epoch, phase: "idle", at: now, reason: `gate reopened: ${reason}` }),
|
|
532
|
-
now, epoch, this.ownerId, now);
|
|
533
|
-
if (changed.changes !== 1) throw new Error("cutover lease lost before abort release");
|
|
534
|
-
});
|
|
301
|
+
await this.#writeStep("finishAbort", { epoch, reason, now });
|
|
535
302
|
this.#epoch = undefined;
|
|
536
303
|
}
|
|
537
|
-
|
|
538
|
-
#owned(epoch: number, now: number): TimerMessageCutoverRow {
|
|
539
|
-
const row = readCutover();
|
|
540
|
-
if (!row || row.epoch !== epoch || row.leaseOwner !== this.ownerId || (row.leaseExpiresAt ?? 0) <= now) {
|
|
541
|
-
throw new Error(`cutover epoch ${epoch} is not owned by ${this.ownerId}`);
|
|
542
|
-
}
|
|
543
|
-
return row;
|
|
544
|
-
}
|
|
545
304
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getDb } from "./connection";
|
|
2
|
+
import type { TimerMessageCutoverRow } from "./timer-message-flip";
|
|
3
|
+
import type { TimerMessageCutoverPhase, TimerMessageCutoverJournalEntry } from "./timer-cutover-core";
|
|
4
|
+
|
|
5
|
+
export function normalizePhase(phase: string): TimerMessageCutoverPhase {
|
|
6
|
+
return phase === "leased" || phase === "quiescing" || phase === "draining" || phase === "quiesced" || phase === "aborting"
|
|
7
|
+
? phase
|
|
8
|
+
: "idle";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
|
|
12
|
+
return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
|
|
16
|
+
return JSON.stringify([...journalOf(row), entry]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readCutover(): TimerMessageCutoverRow | null {
|
|
20
|
+
const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
|
|
21
|
+
return row ? {
|
|
22
|
+
id: 1,
|
|
23
|
+
protocolVersion: row.protocol_version,
|
|
24
|
+
epoch: row.epoch,
|
|
25
|
+
desiredMode: row.desired_mode,
|
|
26
|
+
observedMode: row.observed_mode,
|
|
27
|
+
phase: row.phase,
|
|
28
|
+
leaseOwner: row.lease_owner,
|
|
29
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
30
|
+
quiesceStartedAt: row.quiesce_started_at,
|
|
31
|
+
quiesceDeadline: row.quiesce_deadline,
|
|
32
|
+
lastCompletedTimer: row.last_completed_timer,
|
|
33
|
+
ingressPolicy: row.ingress_policy,
|
|
34
|
+
messageMutationGate: row.message_mutation_gate,
|
|
35
|
+
gateEpoch: row.gate_epoch,
|
|
36
|
+
abortRequested: row.abort_requested === 1,
|
|
37
|
+
phaseJournal: JSON.parse(row.phase_journal),
|
|
38
|
+
updatedAt: row.updated_at,
|
|
39
|
+
} : null;
|
|
40
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readCutover, appendJournal } from "./timer-cutover-state";
|
|
1
2
|
// Narrow cutover recovery write operations. The release is deliberately critical: it reopens the
|
|
2
3
|
// dispatch gate after a crashed transfer; shedding it can leave both dispatchers fenced indefinitely.
|
|
3
4
|
|
|
@@ -19,15 +20,6 @@ interface BatchCheckpoint {
|
|
|
19
20
|
done: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
type TimerMessageCutoverPhase = "idle" | "leased" | "quiescing" | "draining" | "quiesced" | "aborting";
|
|
23
|
-
|
|
24
|
-
interface TimerMessageCutoverJournalEntry {
|
|
25
|
-
epoch: number;
|
|
26
|
-
phase: TimerMessageCutoverPhase | "takeover" | "renewed";
|
|
27
|
-
at: number;
|
|
28
|
-
reason?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
function operationRow(): OperationRow | null {
|
|
32
24
|
return getDb().query("SELECT failures, journal FROM timer_message_cutover_operation WHERE id=1")
|
|
33
25
|
.get() as OperationRow | null;
|
|
@@ -37,37 +29,6 @@ function appendOperationEntry(row: OperationRow, entry: Record<string, unknown>)
|
|
|
37
29
|
return JSON.stringify([...JSON.parse(row.journal) as Array<Record<string, unknown>>, entry]);
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
function readCutover(): TimerMessageCutoverRow | null {
|
|
41
|
-
const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
|
|
42
|
-
return row ? {
|
|
43
|
-
id: 1,
|
|
44
|
-
protocolVersion: row.protocol_version,
|
|
45
|
-
epoch: row.epoch,
|
|
46
|
-
desiredMode: row.desired_mode,
|
|
47
|
-
observedMode: row.observed_mode,
|
|
48
|
-
phase: row.phase,
|
|
49
|
-
leaseOwner: row.lease_owner,
|
|
50
|
-
leaseExpiresAt: row.lease_expires_at,
|
|
51
|
-
quiesceStartedAt: row.quiesce_started_at,
|
|
52
|
-
quiesceDeadline: row.quiesce_deadline,
|
|
53
|
-
lastCompletedTimer: row.last_completed_timer,
|
|
54
|
-
ingressPolicy: row.ingress_policy,
|
|
55
|
-
messageMutationGate: row.message_mutation_gate,
|
|
56
|
-
gateEpoch: row.gate_epoch,
|
|
57
|
-
abortRequested: row.abort_requested === 1,
|
|
58
|
-
phaseJournal: JSON.parse(row.phase_journal),
|
|
59
|
-
updatedAt: row.updated_at,
|
|
60
|
-
} : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
|
|
64
|
-
return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
|
|
68
|
-
return JSON.stringify([...journalOf(row), entry]);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
32
|
/**
|
|
72
33
|
* Operator-abort escape hatch for an interrupted transfer. This is a writer-op body, so the
|
|
73
34
|
* gateway executor already owns its transaction; keeping it here avoids a writer-registry →
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireHandedOffPredecessorBody } from "./token-standing";
|
|
1
2
|
// Token-db writer-op registry (ADR 0001 §3.1/§3.4/§3.7.1, S3 L-F #1898). Domain-owned map; the op
|
|
2
3
|
// BODIES live in `../token-db.ts` and the shared `write-ops.ts` edit is one import + spread.
|
|
3
4
|
//
|
|
@@ -72,6 +73,7 @@ function stripHandoffToken(result: unknown): unknown {
|
|
|
72
73
|
|
|
73
74
|
export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
|
|
74
75
|
// Non-idempotent (mints a new row each apply); the facade supplies `jti` as the ledger key.
|
|
76
|
+
"tokens.retireHandedOffPredecessor": { offLoop: true, idempotent: true, fn: (args: { jti: string }, ctx) => retireHandedOffPredecessorBody(args.jti, ctx.now) },
|
|
75
77
|
"tokens.create": {
|
|
76
78
|
offLoop: true,
|
|
77
79
|
idempotent: false,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getGateway } from "./gateway";
|
|
2
|
+
import { trackRelayEvent } from "../relay-event-drain";
|
|
3
|
+
|
|
4
|
+
// A concurrent authentication must see a predecessor as retired while its writer op is pending.
|
|
5
|
+
const retiring = new Set<string>();
|
|
6
|
+
export function tokenRetirementPending(jti: string): boolean { return retiring.has(jti); }
|
|
7
|
+
export function retireHandedOffPredecessor(jti: string): void {
|
|
8
|
+
if (retiring.has(jti)) return;
|
|
9
|
+
retiring.add(jti);
|
|
10
|
+
const write = getGateway().write("tokens.retireHandedOffPredecessor", { jti });
|
|
11
|
+
trackRelayEvent(write.finally(() => retiring.delete(jti)), "token.handoff.retired");
|
|
12
|
+
}
|
package/src/db/token-standing.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { participatesInSpawnDelegation, spawnDelegationState, spawnTreeStopped }
|
|
|
3
3
|
import { getDb } from "./connection.ts";
|
|
4
4
|
import type { TokenRevokeReason } from "../security.ts";
|
|
5
5
|
|
|
6
|
-
export function isTokenRevoked(jti: string): boolean {
|
|
6
|
+
export function isTokenRevoked(jti: string, retire?: (predecessor: string) => void): boolean {
|
|
7
7
|
let row: { revoked_at: number | null; retire_jti: string | null; agent_id: string | null } | null;
|
|
8
8
|
try {
|
|
9
9
|
// #1245 — the same lookup names a handed-off predecessor this token has not retired yet.
|
|
@@ -24,19 +24,16 @@ export function isTokenRevoked(jti: string): boolean {
|
|
|
24
24
|
const state = spawnDelegationState(row.agent_id);
|
|
25
25
|
if (state.participating && (state.displaced || spawnTreeStopped(row.agent_id))) return true;
|
|
26
26
|
}
|
|
27
|
-
if (row.retire_jti)
|
|
27
|
+
if (row.retire_jti) retire?.(row.retire_jti);
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// A successor's first authentication proves its holder has it, so the predecessor it replaced (token-db.ts
|
|
32
32
|
// handoffTokenInternal) is retired as a rotation. A failed write is retried by the successor's next request.
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch {
|
|
38
|
-
// The successor stays valid; its next authentication retries.
|
|
39
|
-
}
|
|
33
|
+
export function retireHandedOffPredecessorBody(jti: string, now: number): null {
|
|
34
|
+
getDb().query("UPDATE tokens SET revoked_at = COALESCE(revoked_at, ?), revoked_reason = 'rotation' WHERE jti = ? AND (revoked_at IS NULL OR revoked_reason = 'stale')")
|
|
35
|
+
.run(Math.floor(now / 1000), jti);
|
|
36
|
+
return null;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
// Returns undefined if the token is not revoked, otherwise its reason string
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getDb } from "./connection";
|
|
2
|
+
|
|
3
|
+
export const TOKEN_USAGE_RETENTION_SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS retired_token_usage_totals (
|
|
5
|
+
agent_id TEXT PRIMARY KEY, tokens INTEGER NOT NULL, unknown_reports INTEGER NOT NULL);
|
|
6
|
+
CREATE TABLE IF NOT EXISTS token_usage_retention_state (
|
|
7
|
+
id INTEGER PRIMARY KEY CHECK (id = 1), expired_before INTEGER NOT NULL);
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
/** #3619 — old retries must not resurrect reports already folded into permanent budget totals. */
|
|
11
|
+
export function isTokenUsageExpired(createdAt: number): boolean {
|
|
12
|
+
const state = getDb().query("SELECT expired_before FROM token_usage_retention_state WHERE id = 1")
|
|
13
|
+
.get() as { expired_before: number } | null;
|
|
14
|
+
return state !== null && createdAt < state.expired_before;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Writer-op body: fold each deleted report into its agent's permanent spend in the SAME commit. */
|
|
18
|
+
export function pruneTokenUsage(cutoff: number, limit: number): { pruned: number; moreWork: boolean } {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
db.query(`INSERT INTO token_usage_retention_state VALUES (1, ?)
|
|
21
|
+
ON CONFLICT(id) DO UPDATE SET expired_before = max(expired_before, excluded.expired_before)`).run(cutoff);
|
|
22
|
+
const rows = db.query(`SELECT id, agent_id, input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
|
|
23
|
+
+ COALESCE(reasoning_output_tokens, 0) AS tokens, usage_status IS NOT NULL AS unknown_reports
|
|
24
|
+
FROM session_token_usage WHERE created_at < ? ORDER BY created_at, id LIMIT ?`)
|
|
25
|
+
.all(cutoff, limit) as { id: number; agent_id: string; tokens: number; unknown_reports: number }[];
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
db.query(`INSERT INTO retired_token_usage_totals VALUES (?, ?, ?)
|
|
28
|
+
ON CONFLICT(agent_id) DO UPDATE SET tokens = tokens + excluded.tokens,
|
|
29
|
+
unknown_reports = unknown_reports + excluded.unknown_reports`).run(row.agent_id, row.tokens, row.unknown_reports);
|
|
30
|
+
db.query("DELETE FROM session_token_usage WHERE id = ?").run(row.id);
|
|
31
|
+
}
|
|
32
|
+
return { pruned: rows.length, moreWork: rows.length === limit };
|
|
33
|
+
}
|
|
@@ -13,7 +13,7 @@ export const TOKEN_USAGE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Obj
|
|
|
13
13
|
idempotent: true,
|
|
14
14
|
fn: (args: { input: SessionTokenUsageInput }, ctx: WriterOpContext) => {
|
|
15
15
|
const result = recordSessionTokenUsage(args.input);
|
|
16
|
-
const budgetNotices = recordExhaustedSpawnBudgets(result.record.agentId, ctx.now);
|
|
16
|
+
const budgetNotices = result.record ? recordExhaustedSpawnBudgets(result.record.agentId, ctx.now) : [];
|
|
17
17
|
return { ...result, budgetNotices };
|
|
18
18
|
},
|
|
19
19
|
},
|