toolroll 0.5.0
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/LICENSE +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, chmodSync, copyFileSync, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { backup, DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { bundleHash, readDesktopBundle, verifyDesktopUpdateBundles } from "./desktop-bundle.js";
|
|
6
|
+
import { readDesktopConfig, desktopServiceCommand, desktopServiceDefinition, verifyDesktopLiveness } from "./desktop-host.js";
|
|
7
|
+
import { daemonStatus } from "./daemon.js";
|
|
8
|
+
import { run } from "./exec.js";
|
|
9
|
+
import { readSchemaVersion, SCHEMA_VERSION, Store } from "./store.js";
|
|
10
|
+
import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate, updateAdmissionPaused } from "./desktop-update-gate.js";
|
|
11
|
+
import { currentDesktopAccess } from "./desktop-access.js";
|
|
12
|
+
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists } from "./coding-update.js";
|
|
13
|
+
export const updateTerminal = (phase) => ["complete", "restored", "cancelled"].includes(phase);
|
|
14
|
+
const terminal = updateTerminal;
|
|
15
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
const fingerprint = (config) => digest(config);
|
|
17
|
+
const fileHash = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
18
|
+
const journalPath = (state) => join(state, "desktop-update.json");
|
|
19
|
+
const payload = (j) => join(j.workDir, "Updater.app");
|
|
20
|
+
const standby = (j) => join(j.workDir, "Standby.app");
|
|
21
|
+
const atInstalled = (bundle, j) => ({ ...bundle, path: j.old.path });
|
|
22
|
+
/** Durable receipts are private and independent of the app being replaced. */
|
|
23
|
+
export function durableJson(file, value) {
|
|
24
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
25
|
+
const fd = openSync(temp, "wx", 0o600);
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(fd, JSON.stringify(value, null, 2));
|
|
28
|
+
fsyncSync(fd);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
closeSync(fd);
|
|
32
|
+
}
|
|
33
|
+
renameSync(temp, file);
|
|
34
|
+
if (process.platform !== "win32") {
|
|
35
|
+
const directory = openSync(dirname(file), "r");
|
|
36
|
+
try {
|
|
37
|
+
fsyncSync(directory);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
closeSync(directory);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function save(j, phase, detail) {
|
|
45
|
+
j.phase = phase;
|
|
46
|
+
j.detail = detail;
|
|
47
|
+
j.updatedAt = new Date().toISOString();
|
|
48
|
+
durableJson(join(j.workDir, "receipt.json"), j);
|
|
49
|
+
durableJson(journalPath(j.stateDir), j);
|
|
50
|
+
}
|
|
51
|
+
function privateFile(file) {
|
|
52
|
+
const stat = lstatSync(file);
|
|
53
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
54
|
+
throw Error("The update record must be an owner-only regular file.");
|
|
55
|
+
}
|
|
56
|
+
export function readUpdateJournal(stateDir, retainedReceipt) {
|
|
57
|
+
const file = retainedReceipt ?? journalPath(stateDir);
|
|
58
|
+
if (!existsSync(file))
|
|
59
|
+
return null;
|
|
60
|
+
privateFile(file);
|
|
61
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
62
|
+
if (j.version !== 1 || !/^[a-f0-9-]{36}$/.test(j.id) || j.stateDir !== resolve(stateDir) || !isAbsolute(j.old?.path ?? "") || j.workDir !== join(dirname(j.old.path), ".standing-orders-updates", j.id) || !/^[a-zA-Z0-9.-]+$/.test(j.label) || !["install", "restore"].includes(j.intended) || !["prepared", "draining", "backing-up", "stopping", "installing", "verifying", "rolling-back", "releasing", "complete", "restored", "cancelled", "needs-attention"].includes(j.phase))
|
|
63
|
+
throw Error("The saved update record is invalid. Preserve it and the app backups; nothing was changed.");
|
|
64
|
+
if (retainedReceipt && retainedReceipt !== join(j.workDir, "receipt.json"))
|
|
65
|
+
throw Error("The retained recovery receipt is at the wrong path.");
|
|
66
|
+
const validBundle = (b) => b && isAbsolute(b.path) && b.path.endsWith(".app") && /^[a-f0-9]{64}$/.test(b.hash) && /^[a-f0-9-]{36}$/.test(b.buildId) && /^\d+\.\d+\.\d+$/.test(b.version) && b.schemaVersion === SCHEMA_VERSION && b.bundleId === (b.development === true ? "com.standing-orders.desktop.development" : "com.standing-orders.desktop") && typeof b.providerBin === "string";
|
|
67
|
+
if (!validBundle(j.old) || !validBundle(j.next) || j.old.bundleId !== j.next.bundleId || !isAbsolute(j.databaseFile) || !/^[a-f0-9]{64}$/.test(j.configHash) || typeof j.wasRunning !== "boolean" || (j.backupPath && (dirname(j.backupPath) !== j.workDir || !/^orders\.backup(?:\.[a-f0-9-]{36})?\.db$/.test(basename(j.backupPath)))) || (j.backupHash && !/^[a-f0-9]{64}$/.test(j.backupHash)))
|
|
68
|
+
throw Error("The saved update paths or build identities are invalid. Nothing was changed.");
|
|
69
|
+
if (j.replacementOccurred !== undefined && typeof j.replacementOccurred !== "boolean")
|
|
70
|
+
throw Error("The recorded replacement state is invalid. Nothing was changed.");
|
|
71
|
+
if (j.codingCatalogExpected !== undefined && typeof j.codingCatalogExpected !== "boolean")
|
|
72
|
+
throw Error("The recorded coding catalog presence is invalid. Nothing was changed.");
|
|
73
|
+
if ((j.codingBackupPath === undefined) !== (j.codingBackupHash === undefined) || (j.codingBackupPath && (dirname(j.codingBackupPath) !== j.workDir || !/^coding\.backup(?:\.[a-f0-9-]{36})?\.sqlite$/.test(basename(j.codingBackupPath)))) || (j.codingBackupHash && !/^[a-f0-9]{64}$/.test(j.codingBackupHash)))
|
|
74
|
+
throw Error("The retained coding backup paths or identity are invalid. Nothing was changed.");
|
|
75
|
+
for (const directory of [dirname(j.workDir), j.workDir]) {
|
|
76
|
+
const stat = lstatSync(directory);
|
|
77
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
78
|
+
throw Error("The update folder must be a private, owner-controlled directory.");
|
|
79
|
+
}
|
|
80
|
+
return j;
|
|
81
|
+
}
|
|
82
|
+
function connect(file, readOnly = false) {
|
|
83
|
+
if (!existsSync(file) || lstatSync(file).isSymbolicLink())
|
|
84
|
+
throw Error("The task database is missing or linked. It was not recreated.");
|
|
85
|
+
const db = new DatabaseSync(file, { readOnly });
|
|
86
|
+
db.exec("PRAGMA busy_timeout=1000");
|
|
87
|
+
const schema = readSchemaVersion(db);
|
|
88
|
+
if (!schema.ok || schema.version !== SCHEMA_VERSION) {
|
|
89
|
+
db.close();
|
|
90
|
+
throw Error("This update needs the current database schema. Use the separate verified migration procedure; the installed app is unchanged.");
|
|
91
|
+
}
|
|
92
|
+
return db;
|
|
93
|
+
}
|
|
94
|
+
const resources = (app) => join(app.path, "Contents", "Resources");
|
|
95
|
+
const serviceArgs = (app, j) => ["--node", join(resources(app), "runtime", "node"), "--helper", join(resources(app), "dist", "desktop-host.js"), "--label", j.label, "--bundle-id", app.bundleId, "--provider-bin", app.providerBin, "--build-id", app.buildId, ...(j.id ? ["--update-id", j.id] : [])];
|
|
96
|
+
async function serviceState(bundle, j) {
|
|
97
|
+
return await desktopServiceCommand("service-status", j.stateDir, readDesktopConfig(j.stateDir), serviceArgs(bundle, j));
|
|
98
|
+
}
|
|
99
|
+
async function service(action, bundle, j) {
|
|
100
|
+
await desktopServiceCommand(`service-${action}`, j.stateDir, readDesktopConfig(j.stateDir), serviceArgs(bundle, j));
|
|
101
|
+
}
|
|
102
|
+
async function healthy(bundle, j, since) {
|
|
103
|
+
const config = readDesktopConfig(j.stateDir);
|
|
104
|
+
const definition = desktopServiceDefinition(j.stateDir, { node: join(resources(bundle), "runtime", "node"), helper: join(resources(bundle), "dist", "desktop-host.js"), label: j.label, bundleId: bundle.bundleId, buildId: bundle.buildId, providerBin: bundle.providerBin, ...(config.containment ? { containment: config.containment } : {}) });
|
|
105
|
+
const status = await daemonStatus(definition, run);
|
|
106
|
+
if (status.state !== "running" || status.stale || !(await verifyDesktopLiveness(config)).alive)
|
|
107
|
+
return false;
|
|
108
|
+
try {
|
|
109
|
+
const supervisor = JSON.parse(readFileSync(join(j.stateDir, "controller-supervisor.json"), "utf8"));
|
|
110
|
+
const access = JSON.parse(readFileSync(join(j.stateDir, "project-access.json"), "utf8"));
|
|
111
|
+
if (supervisor.buildId !== bundle.buildId || supervisor.updatedAt < since || access.checkedAt < since || !currentDesktopAccess(access, supervisor, config.repos, readFileSync(join(j.stateDir, "project-access-request"), "utf8")).verified)
|
|
112
|
+
return false;
|
|
113
|
+
const db = connect(j.databaseFile, true);
|
|
114
|
+
try {
|
|
115
|
+
return config.repos.length > 0 && config.repos.every(repo => db.prepare("SELECT 1 FROM watch_lease WHERE runner=? AND repo=? AND expires_at>?").get(config.runnerName ?? "", repo, new Date().toISOString()));
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
db.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function swap(j) {
|
|
126
|
+
const result = await run(join(payload(j), "Contents", "Resources", "runtime", "bundle-swap"), [j.old.path, standby(j)], { timeoutMs: 10_000, maxBuffer: 8192 });
|
|
127
|
+
if (result.code !== 0)
|
|
128
|
+
throw Error((result.stderr || "The volume refused an atomic app swap. Neither bundle was deleted.").slice(0, 1000));
|
|
129
|
+
}
|
|
130
|
+
async function otherControllers(app, stateDir) {
|
|
131
|
+
const result = await run("/bin/ps", ["-axo", "command="], { timeoutMs: 5000, maxBuffer: 4_194_304 });
|
|
132
|
+
if (result.code !== 0)
|
|
133
|
+
throw Error("Other app controllers could not be checked. No app swap was attempted.");
|
|
134
|
+
const command = `${join(resources(app), "runtime", "node")} ${join(resources(app), "dist", "desktop-host.js")} serve --state `;
|
|
135
|
+
return result.stdout.split("\n").some(line => line.trim().startsWith(command) && line.trim() !== command + resolve(stateDir));
|
|
136
|
+
}
|
|
137
|
+
function lingeringWork(db) {
|
|
138
|
+
const store = new Store(db);
|
|
139
|
+
for (const row of db.prepare("SELECT DISTINCT run FROM run_process WHERE exited_at IS NULL").all()) {
|
|
140
|
+
const problem = store.stopQuiescenceProblem(Number(row.run));
|
|
141
|
+
if (problem)
|
|
142
|
+
return problem;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
export async function previewDesktopUpdate(stateDir, installed, candidate, label, hooks = {}) {
|
|
147
|
+
const config = readDesktopConfig(stateDir), old = readDesktopBundle(resolve(installed)), next = readDesktopBundle(resolve(candidate));
|
|
148
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(label))
|
|
149
|
+
throw Error("Invalid service label.");
|
|
150
|
+
const inside = (parent, child) => { const rel = relative(parent, child); return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); };
|
|
151
|
+
if (inside(old.path, next.path) || inside(next.path, old.path) || [old.path, next.path].some(app => inside(app, resolve(stateDir)) || inside(app, resolve(config.databaseFile))))
|
|
152
|
+
throw Error("The installed app, candidate and saved state must be separate paths.");
|
|
153
|
+
if (old.buildId === next.buildId || old.hash === next.hash)
|
|
154
|
+
throw Error("This build is already installed.");
|
|
155
|
+
if (old.bundleId !== next.bundleId)
|
|
156
|
+
throw Error("A development preview cannot replace a release identity.");
|
|
157
|
+
if (old.schemaVersion !== SCHEMA_VERSION || next.schemaVersion !== SCHEMA_VERSION)
|
|
158
|
+
throw Error("This release needs a database migration. Use the separate verified migration procedure; the installed app is unchanged.");
|
|
159
|
+
if (next.recoveryProtocol !== 1)
|
|
160
|
+
throw Error("This candidate predates automatic update recovery. Choose a current build; the installed app is unchanged.");
|
|
161
|
+
const codingCatalogExpected = codingCatalogExists(`${config.databaseFile}.coding.sqlite`);
|
|
162
|
+
const db = connect(config.databaseFile, true);
|
|
163
|
+
let active;
|
|
164
|
+
try {
|
|
165
|
+
if (updateAdmissionPaused(db))
|
|
166
|
+
throw Error("An update still owns the admission pause. Open Update status to recover it before starting another update.");
|
|
167
|
+
active = activeUpdateWork(db);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
db.close();
|
|
171
|
+
}
|
|
172
|
+
const existing = readUpdateJournal(stateDir);
|
|
173
|
+
if (existing && !terminal(existing.phase))
|
|
174
|
+
throw Error("An update is already recorded. Open Update status to continue or cancel it.");
|
|
175
|
+
await (hooks.verify ?? verifyDesktopUpdateBundles)(old, next);
|
|
176
|
+
if (await (hooks.otherControllers ?? otherControllers)(old, stateDir))
|
|
177
|
+
throw Error("Another installation is using this app. Stop its background service before updating this shared app.");
|
|
178
|
+
const status = await (hooks.serviceStatus ?? serviceState)(old, { stateDir, label });
|
|
179
|
+
if (status.stale && ["running", "loaded"].includes(status.state))
|
|
180
|
+
throw Error("The service does not match the installed app. Reconnect it with Start background service before updating.");
|
|
181
|
+
const plan = { old, next, configHash: fingerprint(config), label, stateDir: resolve(stateDir), databaseFile: config.databaseFile, codingCatalogExpected, wasRunning: ["running", "loaded"].includes(status.state) };
|
|
182
|
+
return { ...plan, digest: digest(plan), active, message: "New work will wait while current work finishes. A verified private backup and the previous app will be kept. The database will not be migrated or restored automatically." };
|
|
183
|
+
}
|
|
184
|
+
export async function prepareDesktopUpdate(stateDir, installed, candidate, label, expected, hooks = {}) {
|
|
185
|
+
const plan = await previewDesktopUpdate(stateDir, installed, candidate, label, hooks);
|
|
186
|
+
if (plan.digest !== expected)
|
|
187
|
+
throw Error("The app, configuration or update changed since preview. Review the update again.");
|
|
188
|
+
const id = randomUUID(), workDir = join(dirname(plan.old.path), ".standing-orders-updates", id);
|
|
189
|
+
mkdirSync(workDir, { recursive: true, mode: 0o700 });
|
|
190
|
+
chmodSync(dirname(workDir), 0o700);
|
|
191
|
+
chmodSync(workDir, 0o700);
|
|
192
|
+
// The staged updater stays put while Standby and the installation exchange.
|
|
193
|
+
cpSync(plan.next.path, join(workDir, "Updater.app"), { recursive: true, errorOnExist: true, force: false });
|
|
194
|
+
cpSync(plan.next.path, join(workDir, "Standby.app"), { recursive: true, errorOnExist: true, force: false });
|
|
195
|
+
if (bundleHash(join(workDir, "Updater.app")) !== plan.next.hash || bundleHash(join(workDir, "Standby.app")) !== plan.next.hash)
|
|
196
|
+
throw Error("The staged update changed during copying. The installed app is unchanged.");
|
|
197
|
+
const journal = { version: 1, id, workDir, ...plan, phase: "prepared", intended: "install", startedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), detail: "Update prepared; waiting to pause new work." };
|
|
198
|
+
// Two starts may finish copying together. Reserve the installation's one
|
|
199
|
+
// active journal atomically; never replace another pending update.
|
|
200
|
+
const reservation = installationLock(journal);
|
|
201
|
+
if (!reservation)
|
|
202
|
+
throw Error("This app is already being updated. Open Update status in its controlling installation.");
|
|
203
|
+
try {
|
|
204
|
+
reservation.exec("CREATE TABLE IF NOT EXISTS owner (slot INTEGER PRIMARY KEY CHECK(slot=1), state TEXT NOT NULL)");
|
|
205
|
+
const owner = reservation.prepare("SELECT state FROM owner WHERE slot=1").get();
|
|
206
|
+
const pending = owner ? readUpdateJournal(String(owner.state)) : null;
|
|
207
|
+
if (pending && !terminal(pending.phase))
|
|
208
|
+
throw Error("Another installation already has an update prepared for this app. Continue or cancel it there first.");
|
|
209
|
+
const existing = readUpdateJournal(stateDir);
|
|
210
|
+
if (existing && !terminal(existing.phase))
|
|
211
|
+
throw Error("Another update was started. Its record was not replaced.");
|
|
212
|
+
reservation.prepare("INSERT INTO owner(slot,state) VALUES(1,?) ON CONFLICT(slot) DO UPDATE SET state=excluded.state").run(resolve(stateDir));
|
|
213
|
+
save(journal, "prepared", journal.detail);
|
|
214
|
+
reservation.exec("COMMIT");
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
reservation.close();
|
|
218
|
+
}
|
|
219
|
+
return journal;
|
|
220
|
+
}
|
|
221
|
+
export function sqliteLock(file, busyTimeoutMs = 0) {
|
|
222
|
+
if (existsSync(file))
|
|
223
|
+
privateFile(file);
|
|
224
|
+
const db = new DatabaseSync(file);
|
|
225
|
+
chmodSync(file, 0o600);
|
|
226
|
+
try {
|
|
227
|
+
db.exec(`PRAGMA busy_timeout=${busyTimeoutMs}; BEGIN EXCLUSIVE`);
|
|
228
|
+
return db;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
db.close();
|
|
232
|
+
if (String(error).includes("locked") || [5, 6].includes(error.errcode ?? 0))
|
|
233
|
+
return null;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const workerLock = (j, busyTimeoutMs = 0) => sqliteLock(join(j.workDir, "worker.sqlite"), busyTimeoutMs);
|
|
238
|
+
export const installationLock = (j) => sqliteLock(join(dirname(j.workDir), `installation-${digest(j.old.path).slice(0, 24)}.sqlite`));
|
|
239
|
+
export function desktopUpdateStatus(stateDir) {
|
|
240
|
+
const j = readUpdateJournal(stateDir);
|
|
241
|
+
if (!j)
|
|
242
|
+
return { active: false, phase: "none", detail: "No update is in progress.", running: false, canResume: false, canCancel: false };
|
|
243
|
+
const lock = workerLock(j), running = lock === null;
|
|
244
|
+
lock?.close();
|
|
245
|
+
let recovery = null;
|
|
246
|
+
try {
|
|
247
|
+
privateFile(join(j.workDir, "recovery.json"));
|
|
248
|
+
recovery = JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8"));
|
|
249
|
+
}
|
|
250
|
+
catch { /* A legacy/manual update has no guardian. */ }
|
|
251
|
+
const wantsAutomatic = recovery?.id === j.id && ["armed", "running", "backoff"].includes(recovery.state ?? "") && !terminal(j.phase);
|
|
252
|
+
const guardianLock = wantsAutomatic ? sqliteLock(join(j.workDir, "guardian.sqlite")) : undefined;
|
|
253
|
+
const automatic = wantsAutomatic && (guardianLock === null || Date.now() - Date.parse(recovery?.updatedAt ?? "") < 45_000);
|
|
254
|
+
guardianLock?.close();
|
|
255
|
+
const detail = wantsAutomatic && !automatic ? "Automatic recovery has not restarted. Work remains paused; use Retry safely in Update status." : recovery?.state === "attention" || (automatic && (!running || j.phase === "needs-attention")) ? recovery?.detail ?? j.detail : j.detail;
|
|
256
|
+
return { active: !terminal(j.phase), phase: j.phase, detail, error: j.error ?? null, running, automaticRecovery: automatic, recoveryAttempts: recovery?.attempts ?? 0, recoveryRepairs: recovery?.repairs ?? 0, canResume: !running && !automatic && !terminal(j.phase), canCancel: !terminal(j.phase), wasRunning: j.wasRunning && !updateStopRequested(j), backupPath: j.backupPath ?? null, updateId: j.id, version: j.next.version, buildId: j.next.buildId, workDir: j.workDir };
|
|
257
|
+
}
|
|
258
|
+
export function updateStopRequested(j) {
|
|
259
|
+
const file = join(j.workDir, "stop-request.json");
|
|
260
|
+
if (!existsSync(file))
|
|
261
|
+
return false;
|
|
262
|
+
privateFile(file);
|
|
263
|
+
const request = JSON.parse(readFileSync(file, "utf8"));
|
|
264
|
+
if (request.id !== j.id || request.action !== "stop")
|
|
265
|
+
throw Error("The saved stop request is invalid. Automatic starts are refused.");
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
export function requestUpdateStop(stateDir) {
|
|
269
|
+
const j = readUpdateJournal(stateDir);
|
|
270
|
+
if (!j || terminal(j.phase))
|
|
271
|
+
return;
|
|
272
|
+
durableJson(join(j.workDir, "stop-request.json"), { id: j.id, action: "stop" });
|
|
273
|
+
requestUpdateRestore(stateDir);
|
|
274
|
+
}
|
|
275
|
+
export function requestUpdateRestore(stateDir) {
|
|
276
|
+
const j = readUpdateJournal(stateDir);
|
|
277
|
+
if (!j || terminal(j.phase))
|
|
278
|
+
throw Error("There is no pending update to cancel.");
|
|
279
|
+
durableJson(join(j.workDir, "request.json"), { id: j.id, action: "restore" });
|
|
280
|
+
}
|
|
281
|
+
function restoreRequested(j) {
|
|
282
|
+
try {
|
|
283
|
+
const request = JSON.parse(readFileSync(join(j.workDir, "request.json"), "utf8"));
|
|
284
|
+
return request.id === j.id && request.action === "restore";
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (error.code === "ENOENT")
|
|
288
|
+
return false;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export async function launchDesktopUpdate(stateDir, retry = false) {
|
|
293
|
+
const { armUpdateRecovery } = await import("./desktop-update-recovery.js");
|
|
294
|
+
await armUpdateRecovery(stateDir, retry);
|
|
295
|
+
}
|
|
296
|
+
export function pauseUpdateRecovery(stateDir, id, detail) {
|
|
297
|
+
const j = readUpdateJournal(stateDir);
|
|
298
|
+
if (!j || j.id !== id || terminal(j.phase))
|
|
299
|
+
return;
|
|
300
|
+
const lock = workerLock(j, 1000);
|
|
301
|
+
if (!lock)
|
|
302
|
+
throw Error("An updater still owns this operation; its state was not replaced.");
|
|
303
|
+
try {
|
|
304
|
+
j.retryableRecovery = false;
|
|
305
|
+
save(j, "needs-attention", detail);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
lock.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
class RetryableUpdateError extends Error {
|
|
312
|
+
}
|
|
313
|
+
function assertConfig(j) {
|
|
314
|
+
const config = readDesktopConfig(j.stateDir);
|
|
315
|
+
if (config.databaseFile !== j.databaseFile || fingerprint(config) !== j.configHash)
|
|
316
|
+
throw Error("Project or installation settings changed during the update. Keep the current app and review a fresh update.");
|
|
317
|
+
}
|
|
318
|
+
function snapshot(db) {
|
|
319
|
+
const result = {};
|
|
320
|
+
for (const table of ["task", "task_scope", "approver", "run", "artifact", "mate_message", "project"]) {
|
|
321
|
+
const rows = db.prepare(`SELECT * FROM "${table}" ORDER BY rowid`).all();
|
|
322
|
+
result[table] = rows;
|
|
323
|
+
}
|
|
324
|
+
return digest(result);
|
|
325
|
+
}
|
|
326
|
+
async function verifiedBackup(j) {
|
|
327
|
+
assertCodingCatalogPresent(j);
|
|
328
|
+
const file = join(j.workDir, "orders.backup.db");
|
|
329
|
+
if (j.backupHash) {
|
|
330
|
+
if (!j.backupPath || fileHash(j.backupPath) !== j.backupHash)
|
|
331
|
+
throw Error("The retained backup changed. It was not overwritten.");
|
|
332
|
+
await verifiedCodingBackup(j);
|
|
333
|
+
save(j, "backing-up", "Private database backups verified; saved tasks and coding sessions are retained.");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// A crashed partial attempt gets a fresh filename; no backup is overwritten.
|
|
337
|
+
const backupPath = existsSync(file) ? join(j.workDir, `orders.backup.${randomUUID()}.db`) : file;
|
|
338
|
+
const db = connect(j.databaseFile);
|
|
339
|
+
try {
|
|
340
|
+
db.exec("BEGIN IMMEDIATE");
|
|
341
|
+
// The write reservation prevents a concurrent writer; a separate read
|
|
342
|
+
// connection is required because SQLite cannot back up a write transaction.
|
|
343
|
+
const source = connect(j.databaseFile, true);
|
|
344
|
+
let before;
|
|
345
|
+
try {
|
|
346
|
+
before = snapshot(source);
|
|
347
|
+
await backup(source, backupPath);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
source.close();
|
|
351
|
+
}
|
|
352
|
+
chmodSync(backupPath, 0o600);
|
|
353
|
+
const copied = connect(backupPath);
|
|
354
|
+
try {
|
|
355
|
+
if (copied.prepare("PRAGMA integrity_check").get()?.integrity_check !== "ok" || copied.prepare("PRAGMA foreign_key_check").all().length !== 0 || snapshot(copied) !== before)
|
|
356
|
+
throw Error("Backup verification failed. The installed app is unchanged.");
|
|
357
|
+
removeUpdateGate(copied, j.id);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
copied.close();
|
|
361
|
+
}
|
|
362
|
+
await verifiedCodingBackup(j);
|
|
363
|
+
db.exec("COMMIT");
|
|
364
|
+
for (const [source, name] of [[join(j.stateDir, "desktop.json"), "desktop.json"], [join(dirname(j.databaseFile), "repos.json"), "repos.json"], [join(dirname(j.databaseFile), "up-login.txt"), "up-login.txt"]]) {
|
|
365
|
+
if (source && name && existsSync(source)) {
|
|
366
|
+
if (!lstatSync(source).isFile() || lstatSync(source).isSymbolicLink())
|
|
367
|
+
throw Error("A saved configuration file is linked or not a regular file. Nothing was replaced.");
|
|
368
|
+
if (name === "up-login.txt")
|
|
369
|
+
privateFile(source);
|
|
370
|
+
copyFileSync(source, join(j.workDir, name));
|
|
371
|
+
chmodSync(join(j.workDir, name), 0o600);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const fd = openSync(backupPath, "r");
|
|
375
|
+
try {
|
|
376
|
+
fsyncSync(fd);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
closeSync(fd);
|
|
380
|
+
}
|
|
381
|
+
j.backupPath = backupPath;
|
|
382
|
+
j.backupHash = fileHash(backupPath);
|
|
383
|
+
save(j, "backing-up", "Private database backups verified; saved tasks, evidence and coding sessions match.");
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
db.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function assertCodingCatalogPresent(j) {
|
|
390
|
+
const present = codingCatalogExists(`${j.databaseFile}.coding.sqlite`);
|
|
391
|
+
if ((j.codingCatalogExpected || j.codingBackupPath || j.codingBackupHash) && !present)
|
|
392
|
+
throw Error("The live coding catalog disappeared during this update. Keep the app stopped and restore the catalog before continuing.");
|
|
393
|
+
if (present && !j.codingCatalogExpected) {
|
|
394
|
+
j.codingCatalogExpected = true;
|
|
395
|
+
save(j, j.phase, j.detail);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function assertDesktopCodingStopped(j, db) {
|
|
399
|
+
assertCodingCatalogPresent(j);
|
|
400
|
+
assertCodingUpdateStopped(db);
|
|
401
|
+
}
|
|
402
|
+
function assertCodingBackup(j) {
|
|
403
|
+
assertCodingCatalogPresent(j);
|
|
404
|
+
if (j.codingBackupPath || j.codingBackupHash) {
|
|
405
|
+
const stat = j.codingBackupPath && existsSync(j.codingBackupPath) ? lstatSync(j.codingBackupPath) : null;
|
|
406
|
+
if (!stat?.isFile() || stat.isSymbolicLink() || !j.codingBackupPath || fileHash(j.codingBackupPath) !== j.codingBackupHash)
|
|
407
|
+
throw Error("The retained coding backup is missing, linked or changed. It was not overwritten; the update remains paused.");
|
|
408
|
+
}
|
|
409
|
+
else if (j.codingCatalogExpected)
|
|
410
|
+
throw Error("The coding catalog has no verified update backup. The update remains paused.");
|
|
411
|
+
}
|
|
412
|
+
async function verifiedCodingBackup(j) {
|
|
413
|
+
assertCodingCatalogPresent(j);
|
|
414
|
+
if (j.codingBackupPath || j.codingBackupHash) {
|
|
415
|
+
assertCodingBackup(j);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const source = `${j.databaseFile}.coding.sqlite`;
|
|
419
|
+
if (!codingCatalogExists(source))
|
|
420
|
+
return;
|
|
421
|
+
const normal = join(j.workDir, 'coding.backup.sqlite');
|
|
422
|
+
const target = existsSync(normal) ? join(j.workDir, `coding.backup.${randomUUID()}.sqlite`) : normal;
|
|
423
|
+
await backupCodingCatalog(source, target, j.id);
|
|
424
|
+
assertCodingCatalogPresent(j);
|
|
425
|
+
j.codingBackupPath = target;
|
|
426
|
+
j.codingBackupHash = fileHash(target);
|
|
427
|
+
assertCodingBackup(j);
|
|
428
|
+
save(j, j.phase, j.detail);
|
|
429
|
+
}
|
|
430
|
+
/** A SQLite OS lock releases on updater death. Each irreversible step has a
|
|
431
|
+
* preceding durable intent; recovery determines the actual side by hashes. */
|
|
432
|
+
export async function runDesktopUpdate(stateDir, hooks = {}) {
|
|
433
|
+
let j = readUpdateJournal(stateDir);
|
|
434
|
+
if (!j || terminal(j.phase))
|
|
435
|
+
return;
|
|
436
|
+
// A status probe briefly tries this same lock. Give it time to finish;
|
|
437
|
+
// treating its millisecond lock as a live updater stranded resume/start.
|
|
438
|
+
const lock = workerLock(j, 1000);
|
|
439
|
+
if (!lock)
|
|
440
|
+
return;
|
|
441
|
+
const latest = readUpdateJournal(stateDir);
|
|
442
|
+
if (!latest || latest.id !== j.id || terminal(latest.phase)) {
|
|
443
|
+
lock.close();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
j = latest;
|
|
447
|
+
let installation;
|
|
448
|
+
try {
|
|
449
|
+
installation = installationLock(j);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
lock.close();
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
if (!installation) {
|
|
456
|
+
lock.close();
|
|
457
|
+
throw Error("Another updater is working on this app. Its operation was not interrupted.");
|
|
458
|
+
}
|
|
459
|
+
const sleep = hooks.sleep ?? (ms => new Promise(done => setTimeout(done, ms)));
|
|
460
|
+
const control = async (action, bundle, journal) => {
|
|
461
|
+
if (action === "start" && updateStopRequested(journal))
|
|
462
|
+
throw Error("You stopped the background service. Automatic restart is disabled for this update.");
|
|
463
|
+
try {
|
|
464
|
+
await (hooks.service ?? service)(action, bundle, journal);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
throw new RetryableUpdateError(error instanceof Error ? error.message : String(error));
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const status = hooks.serviceStatus ?? serviceState;
|
|
471
|
+
const checkpoint = (phase, text) => { save(j, phase, text); hooks.checkpoint?.(phase); };
|
|
472
|
+
const waitHealthy = async (bundle) => {
|
|
473
|
+
const since = new Date().toISOString();
|
|
474
|
+
writeFileSync(join(j.stateDir, "project-access-request"), randomUUID(), { mode: 0o600 });
|
|
475
|
+
assertCodingCatalogPresent(j);
|
|
476
|
+
await control("start", bundle, j);
|
|
477
|
+
assertCodingCatalogPresent(j);
|
|
478
|
+
const deadline = Date.now() + (hooks.healthTimeoutMs ?? 60_000);
|
|
479
|
+
do {
|
|
480
|
+
if (updateStopRequested(j) || (j.intended === "install" && restoreRequested(j)))
|
|
481
|
+
throw Error("The operator requested the previous app or stopped the service.");
|
|
482
|
+
assertCodingCatalogPresent(j);
|
|
483
|
+
const ready = await (hooks.healthy ?? healthy)(bundle, j, since);
|
|
484
|
+
assertCodingCatalogPresent(j);
|
|
485
|
+
if (ready) {
|
|
486
|
+
j.checkedAt = new Date().toISOString();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await sleep(500);
|
|
490
|
+
} while (Date.now() < deadline);
|
|
491
|
+
throw new RetryableUpdateError("The worker did not confirm its build, project access and console connection. The update cannot be marked complete.");
|
|
492
|
+
};
|
|
493
|
+
const recordReplacement = () => {
|
|
494
|
+
if (!j.replacementOccurred && bundleHash(j.old.path) === j.next.hash) {
|
|
495
|
+
j.replacementOccurred = true;
|
|
496
|
+
save(j, j.phase, j.detail);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const release = async (phase, message) => {
|
|
500
|
+
checkpoint("releasing", message);
|
|
501
|
+
const needsBackup = phase === "complete" || j.replacementOccurred;
|
|
502
|
+
if (needsBackup)
|
|
503
|
+
await verifiedCodingBackup(j);
|
|
504
|
+
if ((needsBackup || j.serviceInterrupted) && (!j.wasRunning || updateStopRequested(j)))
|
|
505
|
+
checkSwapQuiescence();
|
|
506
|
+
const db = connect(j.databaseFile);
|
|
507
|
+
try {
|
|
508
|
+
if (needsBackup)
|
|
509
|
+
assertCodingBackup(j);
|
|
510
|
+
else
|
|
511
|
+
assertCodingCatalogPresent(j);
|
|
512
|
+
removeUpdateGate(db, j.id);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
db.close();
|
|
516
|
+
}
|
|
517
|
+
checkpoint(phase, message);
|
|
518
|
+
};
|
|
519
|
+
const checkSwapQuiescence = () => {
|
|
520
|
+
const db = connect(j.databaseFile, true);
|
|
521
|
+
try {
|
|
522
|
+
if (Object.values(activeUpdateWork(db)).some(n => n !== 0) || db.prepare("SELECT 1 FROM watch_lease WHERE expires_at>? LIMIT 1").get(new Date().toISOString()))
|
|
523
|
+
throw Error("A worker or active operation still uses this database. The update remains paused.");
|
|
524
|
+
const problem = lingeringWork(db);
|
|
525
|
+
if (problem)
|
|
526
|
+
throw Error(problem);
|
|
527
|
+
assertDesktopCodingStopped(j, db);
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
db.close();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const finishStopped = async (bundle) => {
|
|
534
|
+
await control("stop", bundle, j);
|
|
535
|
+
if (["running", "loaded"].includes((await status(bundle, j)).state))
|
|
536
|
+
throw Error("The background service did not stop. Work remains paused.");
|
|
537
|
+
checkSwapQuiescence();
|
|
538
|
+
};
|
|
539
|
+
const restore = async () => {
|
|
540
|
+
recordReplacement();
|
|
541
|
+
j.intended = "restore";
|
|
542
|
+
checkpoint("rolling-back", "Restoring the previous app. The current task database will be kept.");
|
|
543
|
+
assertConfig(j);
|
|
544
|
+
assertCodingCatalogPresent(j);
|
|
545
|
+
if (await (hooks.otherControllers ?? otherControllers)(j.old, j.stateDir))
|
|
546
|
+
throw Error("Another installation is using this app. Stop that service before resuming recovery.");
|
|
547
|
+
assertCodingCatalogPresent(j);
|
|
548
|
+
const db = connect(j.databaseFile);
|
|
549
|
+
try {
|
|
550
|
+
installUpdateGate(db, j.id);
|
|
551
|
+
if (!freezeUpdateGate(db, j.id))
|
|
552
|
+
throw Error("Work is still active. The app will not be swapped until it finishes.");
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
db.close();
|
|
556
|
+
}
|
|
557
|
+
const actual = bundleHash(j.old.path);
|
|
558
|
+
if (actual === j.next.hash) {
|
|
559
|
+
await control("stop", atInstalled(j.next, j), j);
|
|
560
|
+
if (["running", "loaded"].includes((await status(atInstalled(j.next, j), j)).state))
|
|
561
|
+
throw Error("The candidate service is still running. The previous app was not swapped in.");
|
|
562
|
+
const checking = connect(j.databaseFile, true);
|
|
563
|
+
try {
|
|
564
|
+
const problem = lingeringWork(checking);
|
|
565
|
+
if (problem)
|
|
566
|
+
throw Error(problem);
|
|
567
|
+
assertDesktopCodingStopped(j, checking);
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
checking.close();
|
|
571
|
+
}
|
|
572
|
+
if (bundleHash(standby(j)) !== j.old.hash)
|
|
573
|
+
throw Error("The previous app backup changed. Nothing was replaced.");
|
|
574
|
+
await verifiedCodingBackup(j);
|
|
575
|
+
checkSwapQuiescence();
|
|
576
|
+
assertCodingBackup(j);
|
|
577
|
+
await (hooks.swap ?? swap)(j);
|
|
578
|
+
}
|
|
579
|
+
else if (actual !== j.old.hash)
|
|
580
|
+
throw Error("The installed app no longer matches either recorded build. Nothing was replaced.");
|
|
581
|
+
if (j.wasRunning && !updateStopRequested(j))
|
|
582
|
+
await waitHealthy(j.old);
|
|
583
|
+
else
|
|
584
|
+
await finishStopped(j.old);
|
|
585
|
+
await release("restored", "Previous app restored. Your current tasks and evidence were preserved." + (j.error ? ` Update stopped because: ${j.error}` : ""));
|
|
586
|
+
};
|
|
587
|
+
const cancel = async () => {
|
|
588
|
+
// A resumed operation may already have swapped or stopped the service,
|
|
589
|
+
// even though its new drain pass is now showing "draining".
|
|
590
|
+
if (bundleHash(j.old.path) === j.old.hash && !j.serviceInterrupted)
|
|
591
|
+
await release("cancelled", "Update cancelled. The installed app and current work are unchanged; any verified backup was kept.");
|
|
592
|
+
else
|
|
593
|
+
await restore();
|
|
594
|
+
};
|
|
595
|
+
try {
|
|
596
|
+
recordReplacement();
|
|
597
|
+
const owner = installation.prepare("SELECT state FROM owner WHERE slot=1").get();
|
|
598
|
+
if (owner?.state !== j.stateDir)
|
|
599
|
+
throw Error("Another installation owns this app update. Its work was not interrupted.");
|
|
600
|
+
assertConfig(j);
|
|
601
|
+
if (bundleHash(payload(j)) !== j.next.hash)
|
|
602
|
+
throw Error("The staged updater changed. Preserve the update folder and recover with a verified release.");
|
|
603
|
+
const db = connect(j.databaseFile);
|
|
604
|
+
try {
|
|
605
|
+
installUpdateGate(db, j.id);
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
db.close();
|
|
609
|
+
}
|
|
610
|
+
assertCodingCatalogPresent(j);
|
|
611
|
+
if (j.intended === "restore" || restoreRequested(j) || updateStopRequested(j)) {
|
|
612
|
+
if (j.intended !== "restore") {
|
|
613
|
+
await cancel();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
await restore();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
checkpoint("draining", "Waiting for current work to finish. New work is paused; no task will be killed for this update.");
|
|
620
|
+
for (;;) {
|
|
621
|
+
if (restoreRequested(j) || updateStopRequested(j)) {
|
|
622
|
+
await cancel();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
assertConfig(j);
|
|
626
|
+
assertCodingCatalogPresent(j);
|
|
627
|
+
const checking = connect(j.databaseFile);
|
|
628
|
+
try {
|
|
629
|
+
const active = activeUpdateWork(checking);
|
|
630
|
+
const lingering = Object.values(active).every(n => n === 0) ? lingeringWork(checking) : null;
|
|
631
|
+
if (!lingering && Object.values(active).every(n => n === 0) && freezeUpdateGate(checking, j.id))
|
|
632
|
+
break;
|
|
633
|
+
save(j, "draining", lingering ? `Waiting for a worker to finish shutdown: ${lingering}. No process is being killed.` : `Waiting for current work: ${active.runs} runs, ${active.claims} leases, ${active.conversations} chat requests, ${active.sessions} sessions, ${active.stopping} shutdowns, ${active.coding} coding sessions, ${active.codingDeliveries} unconfirmed messages. Nothing is being cancelled.`);
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
checking.close();
|
|
637
|
+
}
|
|
638
|
+
assertCodingCatalogPresent(j);
|
|
639
|
+
await sleep(1000);
|
|
640
|
+
}
|
|
641
|
+
checkpoint("backing-up", "Creating and verifying a private backup of tasks, approvals and evidence.");
|
|
642
|
+
await verifiedBackup(j);
|
|
643
|
+
if (restoreRequested(j)) {
|
|
644
|
+
await cancel();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
assertConfig(j);
|
|
648
|
+
if (await (hooks.otherControllers ?? otherControllers)(j.old, j.stateDir))
|
|
649
|
+
throw Error("Another installation is using this app. Stop its background service before continuing.");
|
|
650
|
+
if (restoreRequested(j)) {
|
|
651
|
+
await cancel();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const actual = bundleHash(j.old.path);
|
|
655
|
+
if (actual !== j.old.hash && actual !== j.next.hash)
|
|
656
|
+
throw Error("The installed app changed after preview. Nothing was replaced.");
|
|
657
|
+
const current = actual === j.old.hash ? j.old : atInstalled(j.next, j);
|
|
658
|
+
j.serviceInterrupted = true;
|
|
659
|
+
checkpoint("stopping", "Current work is finished. Stopping the background service for the update.");
|
|
660
|
+
await control("stop", current, j);
|
|
661
|
+
const stopped = await status(current, j);
|
|
662
|
+
if (["running", "loaded"].includes(stopped.state))
|
|
663
|
+
throw Error("The background service did not stop. No app swap was attempted.");
|
|
664
|
+
const afterStop = connect(j.databaseFile, true);
|
|
665
|
+
try {
|
|
666
|
+
if (Object.values(activeUpdateWork(afterStop)).some(n => n !== 0) || afterStop.prepare("SELECT 1 FROM watch_lease WHERE expires_at>? LIMIT 1").get(new Date().toISOString()))
|
|
667
|
+
throw Error("A worker or active operation still uses this database. Stop the other controller before continuing the update.");
|
|
668
|
+
assertDesktopCodingStopped(j, afterStop);
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
afterStop.close();
|
|
672
|
+
}
|
|
673
|
+
assertConfig(j);
|
|
674
|
+
if (await (hooks.otherControllers ?? otherControllers)(j.old, j.stateDir))
|
|
675
|
+
throw Error("Another installation is using this app. Stop its background service before continuing.");
|
|
676
|
+
if (restoreRequested(j)) {
|
|
677
|
+
await restore();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
checkpoint("installing", "Installing the verified app with an atomic swap. The previous app will be retained.");
|
|
681
|
+
if (actual === j.old.hash) {
|
|
682
|
+
const next = readDesktopBundle(standby(j));
|
|
683
|
+
if (next.hash !== j.next.hash)
|
|
684
|
+
throw Error("The staged app changed. The installed app was not replaced.");
|
|
685
|
+
await (hooks.verify ?? verifyDesktopUpdateBundles)(j.old, next);
|
|
686
|
+
await verifiedCodingBackup(j);
|
|
687
|
+
checkSwapQuiescence();
|
|
688
|
+
assertCodingBackup(j);
|
|
689
|
+
await (hooks.swap ?? swap)(j);
|
|
690
|
+
}
|
|
691
|
+
else if (bundleHash(standby(j)) !== j.old.hash)
|
|
692
|
+
throw Error("The retained previous app no longer matches the update record.");
|
|
693
|
+
if (bundleHash(j.old.path) !== j.next.hash)
|
|
694
|
+
throw Error("The installed app did not match the approved update.");
|
|
695
|
+
recordReplacement();
|
|
696
|
+
checkpoint("verifying", "Checking the new worker, project access and console connection. New work remains paused.");
|
|
697
|
+
await waitHealthy(atInstalled(j.next, j));
|
|
698
|
+
if (!j.wasRunning)
|
|
699
|
+
await finishStopped(atInstalled(j.next, j));
|
|
700
|
+
await release("complete", j.wasRunning ? "Update complete. The new worker is verified and queued work can resume." : "Update complete and verified. The service remains stopped, as it was before the update.");
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
if (error.simulatedCrash)
|
|
704
|
+
throw error;
|
|
705
|
+
j.error = (error instanceof Error ? error.message : String(error)).slice(0, 1500);
|
|
706
|
+
const sqliteCode = Number(error.errcode) & 255;
|
|
707
|
+
if ([5, 6].includes(sqliteCode)) {
|
|
708
|
+
j.retryableRecovery = true;
|
|
709
|
+
save(j, "needs-attention", "The task database is temporarily busy. Automatic recovery will retry the saved operation; no app or database was overwritten.");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
if (j.phase === "releasing")
|
|
714
|
+
throw Error("Finishing the update record was interrupted. Resume to verify the installed app; no database rollback was attempted.");
|
|
715
|
+
if (j.serviceInterrupted || bundleHash(j.old.path) === j.next.hash || ["stopping", "installing", "verifying", "rolling-back"].includes(j.phase))
|
|
716
|
+
await restore();
|
|
717
|
+
else {
|
|
718
|
+
const db = connect(j.databaseFile);
|
|
719
|
+
try {
|
|
720
|
+
assertCodingCatalogPresent(j);
|
|
721
|
+
removeUpdateGate(db, j.id);
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
db.close();
|
|
725
|
+
}
|
|
726
|
+
checkpoint("cancelled", "The update stopped before installation. The previous app and task database are unchanged. " + j.error);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch (recoveryError) {
|
|
730
|
+
if (recoveryError.simulatedCrash)
|
|
731
|
+
throw recoveryError;
|
|
732
|
+
j.retryableRecovery = recoveryError instanceof RetryableUpdateError;
|
|
733
|
+
save(j, "needs-attention", `${j.error} Recovery: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)} Open Update status to retry recovery. The database and app backups were kept.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
installation.close();
|
|
738
|
+
lock.close();
|
|
739
|
+
}
|
|
740
|
+
}
|