@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateAgentSession } from "./agents";
|
|
2
|
+
import { recordEffortObservedInternal } from "./agent-effort-state";
|
|
3
|
+
import { findMessageByIdempotencyKey, sendMessageWithResultInternal } from "./messages";
|
|
4
|
+
export class EffortReportFenced extends Error {}
|
|
5
|
+
/** #2984 — the prompt that resumes an agent once its own effort change applied. */
|
|
6
|
+
export function selfEffortContinuation(effort: string): string {
|
|
7
|
+
return `Effort is now ${effort}. Continue your work.`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export function observeEffortBody(input: Parameters<typeof recordEffortObservedInternal>[0]) {
|
|
12
|
+
const session = validateAgentSession(input.agentId, { instanceId: input.instanceId });
|
|
13
|
+
if (!session.ok) throw new EffortReportFenced(session.error ?? "stale agent instance");
|
|
14
|
+
const observed = recordEffortObservedInternal(input);
|
|
15
|
+
const version = observed.outcome !== "stale" ? observed.resumeVersion : undefined;
|
|
16
|
+
const key = `effort-continuation:${input.agentId}:${version}`;
|
|
17
|
+
const continuation = version !== undefined && !findMessageByIdempotencyKey("system", key)
|
|
18
|
+
? sendMessageWithResultInternal({ from: "system", to: input.agentId, kind: "system", subject: "Effort changed",
|
|
19
|
+
body: selfEffortContinuation(input.effort), replyExpected: false, idempotencyKey: key,
|
|
20
|
+
payload: { kind: "agent.effort_continuation", agentId: input.agentId, effortVersion: version, effort: input.effort } }, false, {}, input.now).message : null;
|
|
21
|
+
return { observed, continuation };
|
|
22
|
+
}
|
package/src/db/agent-meta.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { isRecord } from "@humwick/sdk";
|
|
6
6
|
|
|
7
7
|
export const AGENT_META_WARN_BYTES = 64 * 1024;
|
|
8
|
+
export const PROFILE_PROJECTION_WARN_BYTES = 192 * 1024;
|
|
9
|
+
export const COMMAND_PROFILE_WARN_BYTES = 1024 * 1024;
|
|
8
10
|
|
|
9
11
|
// Log dedupe only, so a per-thread copy is correct: the writer thread warns once itself (#1870 S6c).
|
|
10
12
|
const warnedOversizeMeta = new Set<string>();
|
|
@@ -62,13 +64,22 @@ export function jsonBytes(value: string): number {
|
|
|
62
64
|
|
|
63
65
|
/** Runtime tripwire: one warning per agent/source until the payload returns below the ceiling. */
|
|
64
66
|
export function warnIfAgentMetaOversized(agentId: string, json: string, source: string): void {
|
|
65
|
-
|
|
67
|
+
warnIfOversized(`${agentId}:${source}`, json, AGENT_META_WARN_BYTES, `[agent-meta] ${agentId} ${source}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function warnIfOversized(key: string, json: string, threshold: number, label: string): void {
|
|
66
71
|
const bytes = jsonBytes(json);
|
|
67
|
-
if (bytes <=
|
|
72
|
+
if (bytes <= threshold) {
|
|
68
73
|
warnedOversizeMeta.delete(key);
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
71
76
|
if (warnedOversizeMeta.has(key)) return;
|
|
72
77
|
warnedOversizeMeta.add(key);
|
|
73
|
-
console.warn(
|
|
78
|
+
console.warn(`${label} is ${bytes} bytes (alarm threshold ${threshold})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function serializeProfileProjection(agentId: string, projection: unknown): string | null {
|
|
82
|
+
const json = projection == null ? null : JSON.stringify(projection);
|
|
83
|
+
warnIfOversized(`projection:${agentId}`, json ?? "", PROFILE_PROJECTION_WARN_BYTES, `[agent-profile] ${agentId} profile_projection`);
|
|
84
|
+
return json;
|
|
74
85
|
}
|
package/src/db/agents-async.ts
CHANGED
|
@@ -131,7 +131,7 @@ export async function setStatusAsync(id: string, status: AgentCard["status"], gu
|
|
|
131
131
|
const before = await getAgentAsync(id);
|
|
132
132
|
if (!agentSessionGuardResult(before, guard).ok) return false;
|
|
133
133
|
if (!before) return false;
|
|
134
|
-
if (before.status !== status) runBeforeAgentStatusTransitionHandlers(before, status);
|
|
134
|
+
if (before.status !== status) await runBeforeAgentStatusTransitionHandlers(before, status);
|
|
135
135
|
if (status === "offline" || status === "stale") evictPendingPresence(id);
|
|
136
136
|
const now = Date.now();
|
|
137
137
|
// #2111 — buffered into the short lifecycle window and submitted with its peers as ONE batch job.
|
|
@@ -228,8 +228,9 @@ export async function applyAgentLifecycleUpdateAsync(
|
|
|
228
228
|
): Promise<AgentLifecycleUpdateResult> {
|
|
229
229
|
// Allocated first, per the ordinal-timing rule at the top of setStatusAsync.
|
|
230
230
|
const seq = nextLifecycleSeq();
|
|
231
|
-
const before = prepareAgentLifecycleUpdate(id, update, guard);
|
|
231
|
+
const before = prepareAgentLifecycleUpdate(id, update, guard, false);
|
|
232
232
|
if (!before) return { ok: false, changed: false, metaWriteBytes: 0, before: null, after: null };
|
|
233
|
+
if (update.status !== undefined && update.status !== before.status) await runBeforeAgentStatusTransitionHandlers(before, update.status);
|
|
233
234
|
const result = (await getGateway().write("agents.applyLifecycleUpdate", agentLifecycleWriteArgs(before, update, seq, now))) as unknown as Omit<AgentLifecycleUpdateResult, "ok" | "before">;
|
|
234
235
|
return { ok: true, before, ...result };
|
|
235
236
|
}
|
|
@@ -296,7 +297,7 @@ export async function reapStaleAgentsAsync(ttlMs: number = STALE_TTL_MS, lagGrac
|
|
|
296
297
|
*/
|
|
297
298
|
export async function pruneOfflineAgentsAsync(
|
|
298
299
|
maxOfflineMs: number = DAY_MS,
|
|
299
|
-
options: { onFinality?: (result: WorkFinalFlushResult) => void } = {},
|
|
300
|
+
options: { onFinality?: (result: WorkFinalFlushResult) => void; onArchivePurgeFailure?: () => void } = {},
|
|
300
301
|
): Promise<string[]> {
|
|
301
302
|
const cutoff = Date.now() - maxOfflineMs;
|
|
302
303
|
const candidates = (await getGateway().read("agents.offlinePruneCandidates", {
|
|
@@ -304,7 +305,7 @@ export async function pruneOfflineAgentsAsync(
|
|
|
304
305
|
})) as unknown as AgentCard[];
|
|
305
306
|
const candidateIds = candidates.map((candidate) => candidate.id);
|
|
306
307
|
if (candidateIds.length === 0) return [];
|
|
307
|
-
await purgeArchivesBeforeDelete(candidateIds, cutoff);
|
|
308
|
+
if (!await purgeArchivesBeforeDelete(candidateIds, cutoff)) options.onArchivePurgeFailure?.();
|
|
308
309
|
|
|
309
310
|
const finalityQueues = takeDeferredLineageReports(candidateIds);
|
|
310
311
|
const refreshFinalityCandidates = candidates
|
|
@@ -334,11 +335,13 @@ export async function pruneOfflineAgentsAsync(
|
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
/** #3012 — best effort: whatever the off-loop purge leaves, the delete's own FK cascade still removes. */
|
|
337
|
-
async function purgeArchivesBeforeDelete(agentIds: string[], offlineBefore?: number): Promise<
|
|
338
|
+
async function purgeArchivesBeforeDelete(agentIds: string[], offlineBefore?: number): Promise<boolean> {
|
|
338
339
|
try {
|
|
339
340
|
await purgeContinuationArchivesAsync({ agentIds, offlineBefore });
|
|
341
|
+
return true;
|
|
340
342
|
} catch (error) {
|
|
341
343
|
console.warn("[agents] continuation archive purge before delete failed; the cascade will remove them:", error);
|
|
344
|
+
return false;
|
|
342
345
|
}
|
|
343
346
|
}
|
|
344
347
|
|
package/src/db/agents.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trackRelayEvent } from "../relay-event-drain";
|
|
1
2
|
import { mainThreadRegistry } from "./main-thread-state";
|
|
2
3
|
import { spawnTreeParentIds } from "./spawn-tree";
|
|
3
4
|
import { recordSpawnProcessExit } from "./spawn-process-evidence";
|
|
@@ -8,6 +9,7 @@ import { STALE_TTL_MS, DAY_MS } from "../config";
|
|
|
8
9
|
import { withoutAuthzMetaKeys } from "../authz-meta-keys";
|
|
9
10
|
import {
|
|
10
11
|
jsonBytes,
|
|
12
|
+
serializeProfileProjection,
|
|
11
13
|
splitAgentMetaForStorage,
|
|
12
14
|
splitAgentMetaPatchForStorage,
|
|
13
15
|
topLevelJsonPath,
|
|
@@ -48,10 +50,10 @@ import type {
|
|
|
48
50
|
QuotaState,
|
|
49
51
|
} from "../types";
|
|
50
52
|
|
|
51
|
-
type BeforeAgentStatusTransition = (agent: AgentCard, nextStatus: AgentCard["status"]) =>
|
|
53
|
+
type BeforeAgentStatusTransition = (agent: AgentCard, nextStatus: AgentCard["status"]) => unknown;
|
|
52
54
|
const beforeAgentStatusTransitionHandlers = mainThreadRegistry("agents.ts:beforeAgentStatusTransitionHandlers", new Set<BeforeAgentStatusTransition>());
|
|
53
55
|
|
|
54
|
-
/** Register
|
|
56
|
+
/** Register work that must finish before a status transition is persisted. */
|
|
55
57
|
export function onBeforeAgentStatusTransition(handler: BeforeAgentStatusTransition): () => void {
|
|
56
58
|
beforeAgentStatusTransitionHandlers.add(handler);
|
|
57
59
|
return () => beforeAgentStatusTransitionHandlers.delete(handler);
|
|
@@ -62,8 +64,9 @@ export function onBeforeAgentStatusTransition(handler: BeforeAgentStatusTransiti
|
|
|
62
64
|
* a writer-worker's own instance), so this must only ever be invoked main-thread-side: from
|
|
63
65
|
* `setStatus` directly, or from `setStatusAsync`'s pre-write orchestration. Never from the
|
|
64
66
|
* `agents.setStatus` op body. */
|
|
65
|
-
export function runBeforeAgentStatusTransitionHandlers(before: AgentCard, next: AgentCard["status"]): void {
|
|
66
|
-
|
|
67
|
+
export function runBeforeAgentStatusTransitionHandlers(before: AgentCard, next: AgentCard["status"]): Promise<void> {
|
|
68
|
+
const pending = [...beforeAgentStatusTransitionHandlers].map(handler => handler(before, next));
|
|
69
|
+
return trackRelayEvent(Promise.all(pending).then(() => {}), "agent.before-status");
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const CONTEXT_SNAPSHOT_DEBOUNCE_MS = 60_000;
|
|
@@ -161,7 +164,7 @@ export function upsertAgentInternal(
|
|
|
161
164
|
const contextState = mergeRelayContextMarkers(input.id, input.context);
|
|
162
165
|
const storedMeta = splitAgentMetaForStorage(withCanonicalSpawnRequestId(input.meta));
|
|
163
166
|
const metaJson = JSON.stringify(storedMeta.meta);
|
|
164
|
-
const profileProjectionJson = storedMeta.
|
|
167
|
+
const profileProjectionJson = storedMeta.profileProjectionProvided ? serializeProfileProjection(input.id, storedMeta.profileProjection) : null;
|
|
165
168
|
warnIfAgentMetaOversized(input.id, metaJson, "stored meta");
|
|
166
169
|
const stmt = getDb().query(`
|
|
167
170
|
INSERT INTO agents (id, name, kind, label, tags, system_tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, quota_state, profile_projection, meta, lifecycle, spawned_by, team_id, status_seq, ready_seq, last_seen, created_at)
|
|
@@ -502,12 +505,12 @@ export function agentLifecycleWriteArgs(before: AgentCard, update: AgentLifecycl
|
|
|
502
505
|
|
|
503
506
|
/** The main-thread half of a lifecycle delta: guard, `before` snapshot, transition handlers and presence
|
|
504
507
|
* eviction (main-thread state), all before the write is submitted. Null when the guard rejects. */
|
|
505
|
-
export function prepareAgentLifecycleUpdate(id: string, update: AgentLifecycleUpdate, guard?: AgentSessionGuard): AgentCard | null {
|
|
508
|
+
export function prepareAgentLifecycleUpdate(id: string, update: AgentLifecycleUpdate, guard?: AgentSessionGuard, runHandlers = true): AgentCard | null {
|
|
506
509
|
const before = getPersistedAgentRuntimeCard(id);
|
|
507
510
|
if (!before || !agentSessionGuardResult(before, guard).ok) return null;
|
|
508
511
|
const nextStatus = update.status;
|
|
509
512
|
// Pre-transition handlers must run BEFORE the write lands, exactly as `setStatus` does.
|
|
510
|
-
if (nextStatus !== undefined && nextStatus !== before.status) runBeforeAgentStatusTransitionHandlers(before, nextStatus);
|
|
513
|
+
if (runHandlers && nextStatus !== undefined && nextStatus !== before.status) runBeforeAgentStatusTransitionHandlers(before, nextStatus);
|
|
511
514
|
// #1468 — a terminal transition evicts any buffered beat so a coalesced advance cannot later
|
|
512
515
|
// resurrect a gone agent at flush.
|
|
513
516
|
if (nextStatus === "offline" || nextStatus === "stale") evictPendingPresence(id);
|
|
@@ -566,7 +569,7 @@ export function applyAgentLifecycleUpdateInternal(args: AgentLifecycleWriteArgs)
|
|
|
566
569
|
}
|
|
567
570
|
if (patch.profileProjectionProvided) {
|
|
568
571
|
sets.push("profile_projection = $profileProjection");
|
|
569
|
-
params.$profileProjection =
|
|
572
|
+
params.$profileProjection = serializeProfileProjection(id, patch.profileProjection);
|
|
570
573
|
}
|
|
571
574
|
}
|
|
572
575
|
const runtime = update.runtime;
|
|
@@ -615,7 +618,7 @@ export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?
|
|
|
615
618
|
warnIfAgentMetaOversized(id, metaJson, "stored meta");
|
|
616
619
|
const result = getDb()
|
|
617
620
|
.query("UPDATE agents SET meta = ?, profile_projection = ?, last_seen = ? WHERE id = ?")
|
|
618
|
-
.run(metaJson,
|
|
621
|
+
.run(metaJson, serializeProfileProjection(id, stored.profileProjection), now, id);
|
|
619
622
|
return result.changes > 0;
|
|
620
623
|
}
|
|
621
624
|
|
|
@@ -631,7 +634,7 @@ export function markAgentReadyNotified(id: string, at: number = Date.now()): boo
|
|
|
631
634
|
const merged = { ...(agent.meta ?? {}), readyNotifiedAt: at };
|
|
632
635
|
const stored = splitAgentMetaForStorage(merged);
|
|
633
636
|
return getDb().query("UPDATE agents SET meta = ?, profile_projection = ? WHERE id = ?")
|
|
634
|
-
.run(JSON.stringify(stored.meta),
|
|
637
|
+
.run(JSON.stringify(stored.meta), serializeProfileProjection(id, stored.profileProjection), id).changes > 0;
|
|
635
638
|
}
|
|
636
639
|
|
|
637
640
|
/** Delete specific keys from an agent's meta blob. Returns true if anything changed. Used to clear
|
|
@@ -650,7 +653,7 @@ export function clearAgentMetaKeys(id: string, keys: string[]): boolean {
|
|
|
650
653
|
if (!changed) return false;
|
|
651
654
|
const stored = splitAgentMetaForStorage(meta);
|
|
652
655
|
return getDb().query("UPDATE agents SET meta = ?, profile_projection = ? WHERE id = ?")
|
|
653
|
-
.run(JSON.stringify(stored.meta),
|
|
656
|
+
.run(JSON.stringify(stored.meta), serializeProfileProjection(id, stored.profileProjection), id).changes > 0;
|
|
654
657
|
}
|
|
655
658
|
|
|
656
659
|
/**
|
|
@@ -741,7 +744,7 @@ export function recordAgentExitDiagnosticsInternal(
|
|
|
741
744
|
.run({
|
|
742
745
|
$seq: lifecycleSeq,
|
|
743
746
|
$meta: JSON.stringify(stored.meta),
|
|
744
|
-
$profileProjection:
|
|
747
|
+
$profileProjection: serializeProfileProjection(id, stored.profileProjection),
|
|
745
748
|
$id: id,
|
|
746
749
|
} as never);
|
|
747
750
|
return result.changes > 0 ? agent.epoch : null;
|
|
@@ -93,9 +93,9 @@ export function createAuthIdentity(input: {
|
|
|
93
93
|
providerSubject: string;
|
|
94
94
|
secretHash?: string | null;
|
|
95
95
|
metadata?: Record<string, unknown>;
|
|
96
|
-
}): AuthIdentity {
|
|
96
|
+
}, submittedAt: number = Date.now()): AuthIdentity {
|
|
97
97
|
const id = randomUUID();
|
|
98
|
-
const now =
|
|
98
|
+
const now = submittedAt;
|
|
99
99
|
getDb()
|
|
100
100
|
.query(
|
|
101
101
|
`INSERT INTO auth_identities (id, user_id, provider, provider_subject, secret_hash, metadata, created_at)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// #3799 — automation scans (run listing, reconcile/precheck/dispatch ticks) on the reader pool.
|
|
2
|
+
// A cold scan of automation_runs once blocked the hub loop for seconds on swap-bound I/O.
|
|
3
|
+
import { listAutomationRuns, listDispatchableQueuedRuns, listDueAutomations, listPendingPrecheckRuns, listReconcilableRuns } from "../automations/storage.ts";
|
|
4
|
+
|
|
5
|
+
export const AUTOMATION_READ_OPS = {
|
|
6
|
+
"automations.listRuns": (args: { automationId?: string; status?: string; limit?: number }) => listAutomationRuns(args),
|
|
7
|
+
"automations.pendingPrecheckRuns": () => listPendingPrecheckRuns(),
|
|
8
|
+
"automations.reconcilableRuns": () => listReconcilableRuns(),
|
|
9
|
+
"automations.dispatchableQueuedRuns": () => listDispatchableQueuedRuns(),
|
|
10
|
+
"automations.dueAutomations": (args: { now: number }) => listDueAutomations(args.now),
|
|
11
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ValidationError } from "@humwick/sdk";
|
|
2
|
+
import { insertAutomation, updateAutomationNormalized, deleteAutomation } from "../automations/persistence";
|
|
3
|
+
import { nextScheduledAt } from "../automations/cron";
|
|
4
|
+
import { getAutomation, getAutomationRun, hasActiveRun, insertRun, enqueueAutomationRun, updateRun, cancelActiveRuns, rescheduleAutomation } from "../automations/storage";
|
|
5
|
+
import type { Automation } from "../types";
|
|
6
|
+
import type { WriterOpSpec } from "./write-ops";
|
|
7
|
+
|
|
8
|
+
function admitRun(input: { automationId: string; now: number; manual?: boolean; queuedRunId?: string }) {
|
|
9
|
+
const automation = getAutomation(input.automationId);
|
|
10
|
+
if (!automation) return null;
|
|
11
|
+
const active = hasActiveRun(automation.id);
|
|
12
|
+
if (input.queuedRunId) {
|
|
13
|
+
const run = getAutomationRun(input.queuedRunId);
|
|
14
|
+
if (!run || run.automationId !== automation.id || run.status !== "scheduled" || active) return null;
|
|
15
|
+
updateRun(run.id, { status: "dispatching" }, input.now);
|
|
16
|
+
return { automation, run: getAutomationRun(run.id)! };
|
|
17
|
+
}
|
|
18
|
+
if (input.manual) {
|
|
19
|
+
if (active) throw new ValidationError("automation already has an active run");
|
|
20
|
+
} else {
|
|
21
|
+
if (!automation.enabled || automation.nextRunAt === undefined || automation.nextRunAt > input.now) return null;
|
|
22
|
+
if (active && automation.concurrencyPolicy === "replace") cancelActiveRuns(automation.id, input.now, "Replaced by newer scheduled run");
|
|
23
|
+
else if (active && automation.concurrencyPolicy === "queue") enqueueAutomationRun(automation, automation.nextRunAt, input.now);
|
|
24
|
+
const next = automation.enabled ? nextScheduledAt(automation.schedule, automation.timezone, input.now) : undefined;
|
|
25
|
+
rescheduleAutomation(automation, next, input.now);
|
|
26
|
+
if (active && automation.concurrencyPolicy !== "replace") return null;
|
|
27
|
+
}
|
|
28
|
+
const run = insertRun(automation, input.manual ? input.now : automation.nextRunAt ?? input.now, input.now);
|
|
29
|
+
return { automation: getAutomation(automation.id)!, run };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const AUTOMATION_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
|
|
33
|
+
"automations.create": { offLoop: true, idempotent: false, fn: (a: { normalized: Parameters<typeof insertAutomation>[0]; now: number }) => insertAutomation(a.normalized, a.now) },
|
|
34
|
+
"automations.update": { offLoop: true, idempotent: false, fn: (a: { id: string; normalized: Parameters<typeof updateAutomationNormalized>[1]; expected: Automation; now: number }) => {
|
|
35
|
+
const current = getAutomation(a.id);
|
|
36
|
+
if (!current || JSON.stringify(current) !== JSON.stringify(a.expected)) throw new ValidationError("automation changed or was deleted during catalog validation");
|
|
37
|
+
return updateAutomationNormalized(a.id, a.normalized, a.now);
|
|
38
|
+
} },
|
|
39
|
+
"automations.delete": { offLoop: true, idempotent: true, fn: (a: { id: string }) => deleteAutomation(a.id) },
|
|
40
|
+
"automations.admitRun": { offLoop: true, idempotent: false, fn: admitRun },
|
|
41
|
+
"automations.insertRun": { offLoop: true, idempotent: false, fn: (a: Parameters<typeof insertRun>) => insertRun(...a) },
|
|
42
|
+
"automations.enqueueRun": { offLoop: true, idempotent: false, fn: (a: Parameters<typeof enqueueAutomationRun>) => enqueueAutomationRun(...a) },
|
|
43
|
+
"automations.updateRun": { offLoop: true, idempotent: false, fn: (a: Parameters<typeof updateRun>) => updateRun(...a) ?? null },
|
|
44
|
+
"automations.cancelRuns": { offLoop: true, idempotent: false, fn: (a: Parameters<typeof cancelActiveRuns>) => cancelActiveRuns(...a) ?? null },
|
|
45
|
+
"automations.reschedule": { offLoop: true, idempotent: false, fn: (a: Parameters<typeof rescheduleAutomation>) => rescheduleAutomation(...a) ?? null },
|
|
46
|
+
});
|
package/src/db/blackboard.ts
CHANGED
|
@@ -161,11 +161,11 @@ export function searchEntries(
|
|
|
161
161
|
return rows.map(rowToEntry);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export function retractEntry(id: string): BlackboardEntry | null {
|
|
164
|
+
export function retractEntry(id: string, submittedAt: number = Date.now()): BlackboardEntry | null {
|
|
165
165
|
const existing = getEntry(id);
|
|
166
166
|
if (!existing) return null;
|
|
167
167
|
assertStatus("retracted");
|
|
168
168
|
getDb().query("UPDATE blackboard_entries SET status = 'retracted', updated_at = ? WHERE id = ?")
|
|
169
|
-
.run(
|
|
169
|
+
.run(submittedAt, id);
|
|
170
170
|
return getEntry(id);
|
|
171
171
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getAgent } from "./agents";
|
|
2
|
+
import { createCommandInternal } from "../commands-db";
|
|
3
|
+
import { countActiveBrowserSessions, getBrowserSessionRow, insertBrowserSession, transitionBrowserSession } from "./browser-sessions";
|
|
4
|
+
import { getOrchestrator } from "./orchestrators";
|
|
5
|
+
import type { BrowserLimits } from "@humwick/sdk";
|
|
6
|
+
|
|
7
|
+
export interface StartBrowserWrite {
|
|
8
|
+
id: string; commandId: string; agentId: string; instanceId?: string; epoch?: number;
|
|
9
|
+
orchestratorId: string; limits: BrowserLimits;
|
|
10
|
+
}
|
|
11
|
+
export function startBrowserBody(input: StartBrowserWrite, now: number) {
|
|
12
|
+
const agent = getAgent(input.agentId);
|
|
13
|
+
if (!agent || agent.status === "offline" || agent.instanceId !== input.instanceId || agent.epoch !== input.epoch) return { conflict: "agent" as const };
|
|
14
|
+
const host = getOrchestrator(input.orchestratorId);
|
|
15
|
+
if (!host || host.status !== "online") return { conflict: "host" as const };
|
|
16
|
+
if (countActiveBrowserSessions({ agentId: input.agentId }) >= input.limits.perAgent) return { conflict: "agentLimit" as const };
|
|
17
|
+
if (countActiveBrowserSessions({ orchestratorId: host.id }) >= input.limits.perHost) return { conflict: "hostLimit" as const };
|
|
18
|
+
const command = createCommandInternal({ id: input.commandId, input: { type: "browser.start", source: input.agentId, target: host.agentId, correlationId: input.id,
|
|
19
|
+
params: { sessionId: input.id, agentId: input.agentId, orchestratorId: host.id, idleTimeoutMs: input.limits.idleTimeoutMinutes * 60_000, hostSessionLimit: input.limits.perHost } } }, now);
|
|
20
|
+
const row = insertBrowserSession({ id: input.id, agentId: input.agentId, orchestratorId: host.id, startCommandId: command.id, now });
|
|
21
|
+
return { row, command };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function stopBrowserBody(input: { id: string; reason: string; commandId: string }, now: number) {
|
|
25
|
+
const row = getBrowserSessionRow(input.id);
|
|
26
|
+
if (!row || (row.status !== "starting" && row.status !== "running")) return { row };
|
|
27
|
+
const host = getOrchestrator(row.orchestratorId);
|
|
28
|
+
if (!host) return { row: transitionBrowserSession(row.id, ["starting", "running"], { status: "stopped", stoppedAt: now, error: "orchestrator no longer registered" }, now) };
|
|
29
|
+
const command = createCommandInternal({ id: input.commandId, input: { type: "browser.stop", source: "system", target: host.agentId, correlationId: row.id,
|
|
30
|
+
params: { sessionId: row.id, agentId: row.agentId, orchestratorId: host.id, reason: input.reason } } }, now);
|
|
31
|
+
return { row: transitionBrowserSession(row.id, ["starting", "running"], { status: "stopping", stopCommandId: command.id }, now), command };
|
|
32
|
+
}
|
|
@@ -31,8 +31,10 @@ export const BUS_OUTBOX_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Obje
|
|
|
31
31
|
"bus.append": {
|
|
32
32
|
offLoop: true,
|
|
33
33
|
// Appending assigns a new sequence row; retries reconcile by idempotency key rather than
|
|
34
|
-
// inserting a second durable event after a phantom acknowledgement.
|
|
34
|
+
// inserting a second durable event after a phantom acknowledgement. A plain emit never retries,
|
|
35
|
+
// so it goes keyless and writes no ledger row (#3614).
|
|
35
36
|
idempotent: false,
|
|
37
|
+
atMostOnce: true,
|
|
36
38
|
fn: (args: BusAppendOpArgs, ctx: WriterOpContext) => ({
|
|
37
39
|
...appendRelayEventInternal(args.type, args.source, args.data, args.subject, ctx.now)!, replayed: false,
|
|
38
40
|
}),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { getDb } from "./connection";
|
|
2
|
+
|
|
3
|
+
type Database = ReturnType<typeof getDb>;
|
|
4
|
+
|
|
5
|
+
// Keep one referenced command per checkout; generic command retention can expire its history (#3797).
|
|
6
|
+
export const CHECKOUT_REFRESH_SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS checkout_refresh_heads (
|
|
8
|
+
project_id TEXT NOT NULL,
|
|
9
|
+
orchestrator_id TEXT NOT NULL,
|
|
10
|
+
root_path TEXT NOT NULL,
|
|
11
|
+
command_id TEXT NOT NULL REFERENCES commands(id) ON DELETE CASCADE,
|
|
12
|
+
created_at INTEGER NOT NULL,
|
|
13
|
+
PRIMARY KEY (project_id, orchestrator_id, root_path)
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_checkout_refresh_heads_command ON checkout_refresh_heads(command_id);
|
|
16
|
+
CREATE TRIGGER IF NOT EXISTS checkout_refresh_head_insert AFTER INSERT ON commands
|
|
17
|
+
WHEN NEW.type = 'project.checkout.refresh' AND json_valid(NEW.params)
|
|
18
|
+
AND json_type(NEW.params, '$.projectId') = 'text'
|
|
19
|
+
AND json_type(NEW.params, '$.orchestratorId') = 'text'
|
|
20
|
+
AND json_type(NEW.params, '$.rootPath') = 'text'
|
|
21
|
+
BEGIN
|
|
22
|
+
INSERT INTO checkout_refresh_heads VALUES (
|
|
23
|
+
json_extract(NEW.params, '$.projectId'), json_extract(NEW.params, '$.orchestratorId'),
|
|
24
|
+
json_extract(NEW.params, '$.rootPath'), NEW.id, NEW.created_at)
|
|
25
|
+
ON CONFLICT(project_id, orchestrator_id, root_path) DO UPDATE SET
|
|
26
|
+
command_id = excluded.command_id, created_at = excluded.created_at
|
|
27
|
+
WHERE excluded.created_at >= checkout_refresh_heads.created_at;
|
|
28
|
+
END;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export function migrateCheckoutRefreshHeads(db: Database): void {
|
|
32
|
+
db.transaction(() => {
|
|
33
|
+
const installed = db.query("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'checkout_refresh_heads'").get();
|
|
34
|
+
db.run(CHECKOUT_REFRESH_SCHEMA);
|
|
35
|
+
if (installed) return;
|
|
36
|
+
db.run(`INSERT INTO checkout_refresh_heads
|
|
37
|
+
SELECT project_id, orchestrator_id, root_path, id, created_at FROM (
|
|
38
|
+
SELECT id, created_at, json_extract(params, '$.projectId') AS project_id,
|
|
39
|
+
json_extract(params, '$.orchestratorId') AS orchestrator_id, json_extract(params, '$.rootPath') AS root_path,
|
|
40
|
+
row_number() OVER (PARTITION BY json_extract(params, '$.projectId'), json_extract(params, '$.orchestratorId'),
|
|
41
|
+
json_extract(params, '$.rootPath') ORDER BY created_at DESC, rowid DESC) AS ordinal
|
|
42
|
+
FROM commands WHERE type = 'project.checkout.refresh' AND json_valid(params)
|
|
43
|
+
AND json_type(params, '$.projectId') = 'text' AND json_type(params, '$.orchestratorId') = 'text'
|
|
44
|
+
AND json_type(params, '$.rootPath') = 'text'
|
|
45
|
+
) WHERE ordinal = 1`);
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
@@ -4,12 +4,12 @@ import { getOrchestrator } from "./orchestrators";
|
|
|
4
4
|
import { stringValue } from "@humwick/sdk";
|
|
5
5
|
import { getDb } from "./connection";
|
|
6
6
|
|
|
7
|
+
export const CHECKOUT_REFRESH_COMMANDS_SQL = `SELECT c.id, c.status, c.params, c.result, c.error, c.updated_at
|
|
8
|
+
FROM checkout_refresh_heads h CROSS JOIN commands c ON c.id = h.command_id
|
|
9
|
+
WHERE h.project_id = ?`;
|
|
10
|
+
|
|
7
11
|
export function checkoutRefreshCommands(projectId: string): CheckoutRefreshStatus[] {
|
|
8
|
-
const rows = getDb().query(
|
|
9
|
-
SELECT id, status, params, result, error, updated_at,
|
|
10
|
-
row_number() OVER (PARTITION BY json_extract(params, '$.orchestratorId'), json_extract(params, '$.rootPath') ORDER BY created_at DESC, rowid DESC) AS ordinal
|
|
11
|
-
FROM commands WHERE type = 'project.checkout.refresh' AND json_extract(params, '$.projectId') = ?
|
|
12
|
-
) WHERE ordinal = 1`).all(projectId) as Array<{ id: string; status: Command["status"]; params: string; result: string | null; error: string | null; updated_at: number }>;
|
|
12
|
+
const rows = getDb().query(CHECKOUT_REFRESH_COMMANDS_SQL).all(projectId) as Array<{ id: string; status: Command["status"]; params: string; result: string | null; error: string | null; updated_at: number }>;
|
|
13
13
|
return rows.map((row) => {
|
|
14
14
|
const params = JSON.parse(row.params);
|
|
15
15
|
return { orchestratorId: params.orchestratorId, rootPath: params.rootPath, commandId: row.id, status: row.status, updatedAt: row.updated_at,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// #3606/#3610 — spawn commands written before the content-hash store (command-agent-profiles.ts)
|
|
2
|
+
// still carry `params.agentProfile` inline (~0.4 MB each). Retention keeps some of them past the
|
|
3
|
+
// terminal window (managed agents, unconfirmed spawns), so move those profiles into the store in
|
|
4
|
+
// bounded chunks from the retention pass. `updated_at` is untouched: retention windows key off it.
|
|
5
|
+
|
|
6
|
+
import { storeCommandAgentProfile } from "./command-agent-profiles";
|
|
7
|
+
import { TERMINAL_COMMAND_STATUS_SQL_LIST } from "./command-terminal-status";
|
|
8
|
+
import type { getDb } from "./connection";
|
|
9
|
+
|
|
10
|
+
type Database = ReturnType<typeof getDb>;
|
|
11
|
+
const WRITE_TRANSACTION_BUDGET_MS = 25;
|
|
12
|
+
|
|
13
|
+
/** One bounded chunk: returns the same shape as a retention prune chunk (`pruned` = rows slimmed). */
|
|
14
|
+
export function slimSpawnProfileChunk(db: Database, limit: number): { candidates: number; attempted: number; pruned: number } {
|
|
15
|
+
// Only terminal rows: a live spawn is still being claimed. A profile carrying vault ciphertext stays
|
|
16
|
+
// inline (the #3488 settlement trigger prunes it there), so those rows are not candidates.
|
|
17
|
+
const ids = db.query(`
|
|
18
|
+
SELECT id FROM commands
|
|
19
|
+
WHERE type = 'agent.spawn' AND status IN (${TERMINAL_COMMAND_STATUS_SQL_LIST}) AND agent_profile_hash IS NULL
|
|
20
|
+
AND instr(params, '"agentProfile"') > 0 AND json_valid(params) AND json_type(params, '$.agentProfile') = 'object'
|
|
21
|
+
AND instr(params, 'sealed:v2:') = 0
|
|
22
|
+
LIMIT ?
|
|
23
|
+
`).all(limit) as Array<{ id: string }>;
|
|
24
|
+
if (ids.length === 0) return { candidates: 0, attempted: 0, pruned: 0 };
|
|
25
|
+
const read = db.query("SELECT params FROM commands WHERE id = ?");
|
|
26
|
+
const write = db.query("UPDATE commands SET params = ?, agent_profile_hash = ? WHERE id = ? AND agent_profile_hash IS NULL");
|
|
27
|
+
return db.transaction(() => {
|
|
28
|
+
const startedAt = performance.now(); let attempted = 0; let pruned = 0;
|
|
29
|
+
for (const { id } of ids) {
|
|
30
|
+
if (attempted > 0 && performance.now() - startedAt >= WRITE_TRANSACTION_BUDGET_MS) break;
|
|
31
|
+
attempted += 1;
|
|
32
|
+
const row = read.get(id) as { params: string } | null;
|
|
33
|
+
if (!row) continue;
|
|
34
|
+
const stored = storeCommandAgentProfile(JSON.parse(row.params) as Record<string, unknown>, Date.now(), db);
|
|
35
|
+
if (stored.agentProfileHash) pruned += write.run(JSON.stringify(stored.params), stored.agentProfileHash, id).changes;
|
|
36
|
+
}
|
|
37
|
+
return { candidates: ids.length, attempted, pruned };
|
|
38
|
+
})();
|
|
39
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { isRecord } from "@humwick/sdk";
|
|
9
9
|
import { getDb } from "./connection";
|
|
10
|
+
import { COMMAND_PROFILE_WARN_BYTES, warnIfOversized } from "./agent-meta";
|
|
10
11
|
|
|
11
12
|
type Database = ReturnType<typeof getDb>;
|
|
12
13
|
|
|
@@ -41,7 +42,10 @@ export function storeCommandAgentProfile(sealed: Record<string, unknown>, now: n
|
|
|
41
42
|
const body = JSON.stringify(profile);
|
|
42
43
|
if (body.includes("sealed:v2:")) return { params: sealed, agentProfileHash: null };
|
|
43
44
|
const hash = createHash("sha256").update(body).digest("hex");
|
|
44
|
-
db.query("INSERT OR IGNORE INTO command_agent_profiles (hash, profile, created_at) VALUES (?, ?, ?)").run(hash, body, now);
|
|
45
|
+
const inserted = db.query("INSERT OR IGNORE INTO command_agent_profiles (hash, profile, created_at) VALUES (?, ?, ?)").run(hash, body, now);
|
|
46
|
+
if (inserted.changes && Buffer.byteLength(body) > COMMAND_PROFILE_WARN_BYTES) {
|
|
47
|
+
warnIfOversized("command-profile", body, COMMAND_PROFILE_WARN_BYTES, "[commands] stored agentProfile");
|
|
48
|
+
}
|
|
45
49
|
const { agentProfile: _stored, ...params } = sealed;
|
|
46
50
|
return { params, agentProfileHash: hash };
|
|
47
51
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { quoteIdentifier } from "@humwick/kernel/persistence-schema";
|
|
2
|
+
import { getDb } from "./connection";
|
|
3
|
+
import { TERMINAL_COMMAND_STATUS_SQL_LIST } from "./command-terminal-status";
|
|
4
|
+
|
|
5
|
+
type Database = ReturnType<typeof getDb>;
|
|
6
|
+
|
|
7
|
+
const ELIGIBLE = `type != 'agent.spawn' AND status IN (${TERMINAL_COMMAND_STATUS_SQL_LIST})`;
|
|
8
|
+
export const COMMAND_RETENTION_SCHEMA = `
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_commands_terminal_retention ON commands(updated_at, id) WHERE ${ELIGIBLE};
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_commands_refresh_retention ON commands(updated_at, id)
|
|
11
|
+
WHERE type = 'project.checkout.refresh' AND status IN (${TERMINAL_COMMAND_STATUS_SQL_LIST});
|
|
12
|
+
CREATE TABLE IF NOT EXISTS command_retention_cursor (
|
|
13
|
+
id INTEGER PRIMARY KEY CHECK (id = 1), updated_at INTEGER NOT NULL, command_id TEXT NOT NULL);
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// Sampled status stats favor a broad status scan plus sort; retain the bounded age seek (#3619).
|
|
17
|
+
export const TERMINAL_COMMAND_CANDIDATES_SQL = `SELECT id, updated_at FROM commands INDEXED BY idx_commands_terminal_retention
|
|
18
|
+
WHERE ${ELIGIBLE} AND updated_at < ? AND (updated_at, id) > (?, ?)
|
|
19
|
+
ORDER BY updated_at, id LIMIT ?`;
|
|
20
|
+
|
|
21
|
+
export const REFRESH_COMMAND_CANDIDATES_SQL = `SELECT id, updated_at FROM commands INDEXED BY idx_commands_refresh_retention
|
|
22
|
+
WHERE type = 'project.checkout.refresh' AND status IN (${TERMINAL_COMMAND_STATUS_SQL_LIST})
|
|
23
|
+
AND updated_at >= ? AND updated_at < ? AND (updated_at, id) > (?, ?)
|
|
24
|
+
ORDER BY updated_at, id LIMIT ?`;
|
|
25
|
+
|
|
26
|
+
const JSON_REFERENCES = [
|
|
27
|
+
["activity_events", "metadata", ["$.commandId", "$.mergeCommandId"]],
|
|
28
|
+
["task_events", "metadata", ["$.commandId", "$.mergeCommandId"]],
|
|
29
|
+
["workspaces", "metadata", ["$.mergeCommandId", "$.rekeyCommandId", "$.lastStewardRekey.commandId"]],
|
|
30
|
+
["scheduled_timers", "command", ["$.commandId"]],
|
|
31
|
+
["workflow_instances", "wait_for", ["$.commandId"]],
|
|
32
|
+
["workflow_instances", "initial_state", ["$.commandId"]],
|
|
33
|
+
["workflow_step_journal", "wait_for", ["$.commandId"]],
|
|
34
|
+
["workflow_step_journal", "state_snapshot", ["$.commandId"]],
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function jsonReference(column: string, path: string): string {
|
|
38
|
+
return `json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '${path}')`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function commandReferences(db: Database): { tbl: string; col: string }[] {
|
|
42
|
+
return db.query(`SELECT s.name AS tbl, c.name AS col FROM sqlite_schema s, pragma_table_info(s.name) c
|
|
43
|
+
WHERE s.type = 'table' AND s.name NOT LIKE 'sqlite_%' AND s.name != 'command_retention_cursor'
|
|
44
|
+
AND (lower(replace(c.name, '_', '')) LIKE '%commandid' OR lower(replace(c.name, '_', '')) LIKE '%commandids')
|
|
45
|
+
UNION SELECT s.name, f."from" FROM sqlite_schema s, pragma_foreign_key_list(s.name) f
|
|
46
|
+
WHERE s.type = 'table' AND f."table" = 'commands'`).all() as { tbl: string; col: string }[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Equality checks in a DELETE must seek references, never repeatedly scan their history (#3619). */
|
|
50
|
+
export function ensureCommandRetentionReferenceIndexes(db: Database): void {
|
|
51
|
+
for (const { tbl, col } of commandReferences(db)) {
|
|
52
|
+
if (col.replaceAll("_", "").toLowerCase().endsWith("commandids")) continue;
|
|
53
|
+
const indexes = db.query(`PRAGMA index_list(${quoteIdentifier(tbl)})`).all() as { name: string; partial: number }[];
|
|
54
|
+
const covered = indexes.some(index => !index.partial &&
|
|
55
|
+
(db.query(`PRAGMA index_info(${quoteIdentifier(index.name)})`).get() as { name: string } | null)?.name === col);
|
|
56
|
+
if (!covered) db.run(`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`idx_retention_${tbl}_${col}`)} ON ${quoteIdentifier(tbl)}(${quoteIdentifier(col)})`);
|
|
57
|
+
}
|
|
58
|
+
for (const [table, column, paths] of JSON_REFERENCES) {
|
|
59
|
+
if (table === "scheduled_timers") continue; // idx_scheduled_timers_command_created already owns this expression.
|
|
60
|
+
for (const [i, path] of paths.entries()) db.run(`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`idx_retention_${table}_${column}_${i}`)}
|
|
61
|
+
ON ${table}(${jsonReference(column, path)})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const predicates = new WeakMap<Database, { version: number; sql: string }>();
|
|
66
|
+
|
|
67
|
+
/** Resolve each reference index for the whole candidate set, including CASCADE/SET NULL FKs. */
|
|
68
|
+
function referencedCommandIds(db: Database): string {
|
|
69
|
+
const version = (db.query("PRAGMA schema_version").get() as { schema_version: number }).schema_version;
|
|
70
|
+
const cached = predicates.get(db);
|
|
71
|
+
if (cached?.version === version) return cached.sql;
|
|
72
|
+
const refs = commandReferences(db);
|
|
73
|
+
const selections = refs.map(({ tbl, col }) => {
|
|
74
|
+
const table = quoteIdentifier(tbl), column = quoteIdentifier(col);
|
|
75
|
+
return col.replaceAll("_", "").toLowerCase().endsWith("commandids")
|
|
76
|
+
? `SELECT candidate.id FROM ${table} ref, json_each(CASE WHEN json_valid(ref.${column}) THEN ref.${column} ELSE '[]' END) item
|
|
77
|
+
JOIN candidates candidate ON CAST(item.value AS TEXT) = candidate.id`
|
|
78
|
+
: `SELECT candidate.id FROM candidates candidate CROSS JOIN ${table} ref ON ref.${column} = candidate.id`;
|
|
79
|
+
});
|
|
80
|
+
// Strip candidate TEXT affinity so JSON expression indexes can seek (#3619).
|
|
81
|
+
for (const [table, column, paths] of JSON_REFERENCES) {
|
|
82
|
+
for (const path of paths) {
|
|
83
|
+
const expression = table === "scheduled_timers" ? `json_extract(ref.${column}, '${path}')` : jsonReference(`ref.${column}`, path);
|
|
84
|
+
selections.push(`SELECT candidate.id FROM candidates candidate CROSS JOIN ${table} ref ON ${expression} = +candidate.id`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const sql = selections.join(" UNION ");
|
|
88
|
+
predicates.set(db, { version, sql });
|
|
89
|
+
return sql;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** #3619 — advance past referenced rows so they cannot starve unrelated expired commands. */
|
|
93
|
+
export function pruneTerminalCommands(cutoff: number, limit: number, refreshCutoff = cutoff): { pruned: number; scanned: number; moreWork: boolean } {
|
|
94
|
+
const db = getDb();
|
|
95
|
+
const start = performance.now();
|
|
96
|
+
const cursor = db.query("SELECT updated_at, command_id FROM command_retention_cursor WHERE id = 1").get() as
|
|
97
|
+
{ updated_at: number; command_id: string } | null;
|
|
98
|
+
const candidates = db.query(TERMINAL_COMMAND_CANDIDATES_SQL)
|
|
99
|
+
.all(cutoff, cursor?.updated_at ?? -1, cursor?.command_id ?? "", limit) as { id: string; updated_at: number }[];
|
|
100
|
+
// Merge two bounded age seeks; one shared cursor also advances past referenced refresh receipts (#3797).
|
|
101
|
+
if (refreshCutoff > cutoff) candidates.push(...db.query(REFRESH_COMMAND_CANDIDATES_SQL)
|
|
102
|
+
.all(cutoff, refreshCutoff, cursor?.updated_at ?? -1, cursor?.command_id ?? "", limit) as typeof candidates);
|
|
103
|
+
const rows = candidates.sort((a, b) => a.updated_at - b.updated_at || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).slice(0, limit);
|
|
104
|
+
if (!rows.length) {
|
|
105
|
+
db.run("DELETE FROM command_retention_cursor WHERE id = 1");
|
|
106
|
+
return { pruned: 0, scanned: 0, moreWork: false };
|
|
107
|
+
}
|
|
108
|
+
const references = referencedCommandIds(db);
|
|
109
|
+
let pruned = 0, scanned = 0;
|
|
110
|
+
// Visit each reference index once per chunk, rather than switch indexes per command (#3807).
|
|
111
|
+
for (let offset = 0; offset < rows.length; offset += 25) {
|
|
112
|
+
if (scanned > 0 && performance.now() - start >= 25) break;
|
|
113
|
+
const chunk = rows.slice(offset, offset + 25);
|
|
114
|
+
pruned += db.query(`WITH candidates AS MATERIALIZED (
|
|
115
|
+
SELECT id FROM commands WHERE id IN (${chunk.map(() => "?").join(",")}) AND ${ELIGIBLE}
|
|
116
|
+
AND (updated_at < ? OR (type = 'project.checkout.refresh' AND updated_at < ?))),
|
|
117
|
+
referenced(id) AS MATERIALIZED (${references})
|
|
118
|
+
DELETE FROM commands WHERE id IN (SELECT id FROM candidates EXCEPT SELECT id FROM referenced)`)
|
|
119
|
+
.run(...chunk.map(row => row.id), cutoff, refreshCutoff).changes;
|
|
120
|
+
scanned += chunk.length;
|
|
121
|
+
}
|
|
122
|
+
const last = rows[scanned - 1]!;
|
|
123
|
+
db.query(`INSERT INTO command_retention_cursor VALUES (1, ?, ?)
|
|
124
|
+
ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at, command_id = excluded.command_id`).run(last.updated_at, last.id);
|
|
125
|
+
return { pruned, scanned, moreWork: true };
|
|
126
|
+
}
|
|
@@ -39,6 +39,10 @@ export const PROD_SAMPLED_COMMANDS_STATS: CommandsStatsProfile = {
|
|
|
39
39
|
idx_commands_target_updated: "499038 6 1 1",
|
|
40
40
|
idx_commands_correlation: "499038 5",
|
|
41
41
|
idx_commands_type_status_created: "499038 18063 6168 1 1",
|
|
42
|
+
// Undeployed (#3619): conservative full-history upper bound for terminal non-spawn rows.
|
|
43
|
+
idx_commands_terminal_retention: "499038 2 1",
|
|
44
|
+
// Undeployed (#3797): conservative refresh-history census, before the one-hour pruning drains it.
|
|
45
|
+
idx_commands_refresh_retention: "65773 2 1",
|
|
42
46
|
idx_commands_created: "499038 1",
|
|
43
47
|
idx_commands_status: "499038 134",
|
|
44
48
|
idx_commands_target: "499038 6",
|
|
@@ -78,6 +82,10 @@ export const PROD_EXACT_COMMANDS_STATS: CommandsStatsProfile = {
|
|
|
78
82
|
idx_commands_target_updated: "505748 157 1 1",
|
|
79
83
|
idx_commands_correlation: "505748 12",
|
|
80
84
|
idx_commands_type_status_created: "505748 18063 6168 1 1",
|
|
85
|
+
// Undeployed (#3619): conservative full-history upper bound for terminal non-spawn rows.
|
|
86
|
+
idx_commands_terminal_retention: "505748 2 1",
|
|
87
|
+
// Undeployed (#3797): conservative refresh-history census, before the one-hour pruning drains it.
|
|
88
|
+
idx_commands_refresh_retention: "65773 2 1",
|
|
81
89
|
idx_commands_created: "505748 2",
|
|
82
90
|
idx_commands_status: "505748 72250",
|
|
83
91
|
idx_commands_target: "505748 157",
|