@galaxyproject/orbit 0.0.0 → 0.9.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/CHANGELOG.md +123 -0
- package/LICENSE +21 -0
- package/README.md +760 -5
- package/bin/legacy-mcp-config.js +363 -0
- package/bin/orbit.js +810 -0
- package/bin/pi-extension-path.d.ts +4 -0
- package/bin/pi-extension-path.js +44 -0
- package/bin/provider-auth.d.ts +22 -0
- package/bin/provider-auth.js +77 -0
- package/bin/thinking-pref.js +38 -0
- package/bin/update-check.d.ts +15 -0
- package/bin/update-check.js +137 -0
- package/extensions/cli-update/index.ts +32 -0
- package/extensions/orbit/activity-hooks.ts +141 -0
- package/extensions/orbit/activity.ts +116 -0
- package/extensions/orbit/agent-dir.ts +11 -0
- package/extensions/orbit/auto-resume.ts +352 -0
- package/extensions/orbit/config.ts +3 -0
- package/extensions/orbit/confusables.ts +1 -0
- package/extensions/orbit/context.ts +957 -0
- package/extensions/orbit/dashboard-commands.ts +191 -0
- package/extensions/orbit/dashboard-store.ts +473 -0
- package/extensions/orbit/dashboard-tools.ts +1185 -0
- package/extensions/orbit/empty-completion.ts +99 -0
- package/extensions/orbit/evidence-gate.ts +1276 -0
- package/extensions/orbit/evidence-override-command.ts +294 -0
- package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
- package/extensions/orbit/exec-guard/gate.ts +181 -0
- package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
- package/extensions/orbit/exec-guard/index.ts +1 -0
- package/extensions/orbit/exec-guard/model-tier.ts +36 -0
- package/extensions/orbit/exec-guard/path-jail.ts +54 -0
- package/extensions/orbit/exec-guard/policy.ts +342 -0
- package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
- package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
- package/extensions/orbit/exec-guard/types.ts +42 -0
- package/extensions/orbit/execution-commands.ts +134 -0
- package/extensions/orbit/feedback-command.ts +81 -0
- package/extensions/orbit/feedback.ts +96 -0
- package/extensions/orbit/galaxy-api.test.ts +122 -0
- package/extensions/orbit/galaxy-api.ts +606 -0
- package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
- package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
- package/extensions/orbit/galaxy-enrich.ts +1108 -0
- package/extensions/orbit/galaxy-failure.ts +101 -0
- package/extensions/orbit/galaxy-fixture.ts +126 -0
- package/extensions/orbit/galaxy-history-guard.ts +233 -0
- package/extensions/orbit/galaxy-job-block.ts +347 -0
- package/extensions/orbit/galaxy-job-ops.ts +288 -0
- package/extensions/orbit/galaxy-live-source.ts +929 -0
- package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
- package/extensions/orbit/galaxy-op-policy.ts +332 -0
- package/extensions/orbit/galaxy-ops-tools.ts +326 -0
- package/extensions/orbit/galaxy-output-store.ts +80 -0
- package/extensions/orbit/galaxy-output.ts +346 -0
- package/extensions/orbit/galaxy-page-binding.ts +222 -0
- package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
- package/extensions/orbit/galaxy-page-ops.ts +159 -0
- package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
- package/extensions/orbit/galaxy-page-sync.ts +318 -0
- package/extensions/orbit/galaxy-pages-api.ts +203 -0
- package/extensions/orbit/galaxy-pages-sync.ts +409 -0
- package/extensions/orbit/galaxy-poll-guard.ts +189 -0
- package/extensions/orbit/galaxy-poller.ts +706 -0
- package/extensions/orbit/galaxy-provenance.ts +623 -0
- package/extensions/orbit/galaxy-reconcile.ts +956 -0
- package/extensions/orbit/galaxy-recovery.ts +176 -0
- package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
- package/extensions/orbit/galaxy-submission-capture.ts +707 -0
- package/extensions/orbit/galaxy-submission.ts +502 -0
- package/extensions/orbit/galaxy-tool-source.ts +78 -0
- package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
- package/extensions/orbit/galaxy-udt-block.ts +127 -0
- package/extensions/orbit/galaxy-upload-tus.ts +334 -0
- package/extensions/orbit/galaxy-upload.ts +300 -0
- package/extensions/orbit/galaxy-version-capture.ts +65 -0
- package/extensions/orbit/git.ts +173 -0
- package/extensions/orbit/harness-block-fields.ts +597 -0
- package/extensions/orbit/index.ts +593 -0
- package/extensions/orbit/init-gate.ts +284 -0
- package/extensions/orbit/instructions-command.ts +136 -0
- package/extensions/orbit/invocation-failure-hint.ts +57 -0
- package/extensions/orbit/iwc-plan-check.ts +94 -0
- package/extensions/orbit/lesson-command.ts +303 -0
- package/extensions/orbit/lesson-hint.ts +245 -0
- package/extensions/orbit/lesson-nudge.ts +106 -0
- package/extensions/orbit/lessons/compose.ts +177 -0
- package/extensions/orbit/lessons/counters.ts +120 -0
- package/extensions/orbit/lessons/enabled.ts +116 -0
- package/extensions/orbit/lessons/matcher.ts +346 -0
- package/extensions/orbit/lessons/paths.ts +286 -0
- package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
- package/extensions/orbit/lessons/propose-replay.ts +103 -0
- package/extensions/orbit/lessons/propose-tool.ts +222 -0
- package/extensions/orbit/lessons/propose.ts +502 -0
- package/extensions/orbit/lessons/replay.ts +89 -0
- package/extensions/orbit/lessons/reproduction-index.ts +83 -0
- package/extensions/orbit/lessons/search-tool.ts +99 -0
- package/extensions/orbit/lessons/search.ts +136 -0
- package/extensions/orbit/lessons/store.ts +415 -0
- package/extensions/orbit/lessons/text.ts +36 -0
- package/extensions/orbit/lessons/types.ts +69 -0
- package/extensions/orbit/lessons/user-dir.ts +24 -0
- package/extensions/orbit/lessons/user-lesson.ts +88 -0
- package/extensions/orbit/lessons/wrapper.ts +61 -0
- package/extensions/orbit/lessons-command.ts +147 -0
- package/extensions/orbit/local-exec.ts +39 -0
- package/extensions/orbit/mcp-output.ts +179 -0
- package/extensions/orbit/mcp-servers.test.ts +24 -0
- package/extensions/orbit/mcp-servers.ts +25 -0
- package/extensions/orbit/model-script.ts +351 -0
- package/extensions/orbit/notebook-anchors.ts +342 -0
- package/extensions/orbit/notebook-writer.ts +863 -0
- package/extensions/orbit/observation-allowlists.ts +1109 -0
- package/extensions/orbit/observation-replay.ts +148 -0
- package/extensions/orbit/observation-triggers.ts +563 -0
- package/extensions/orbit/observation-ui.ts +175 -0
- package/extensions/orbit/observations-command.ts +421 -0
- package/extensions/orbit/observations-config.ts +200 -0
- package/extensions/orbit/observations.ts +1381 -0
- package/extensions/orbit/profiles.ts +270 -0
- package/extensions/orbit/progress-updates.ts +95 -0
- package/extensions/orbit/proposal-commands.ts +882 -0
- package/extensions/orbit/provenance-fs-file.ts +59 -0
- package/extensions/orbit/provenance-fs.ts +32 -0
- package/extensions/orbit/provenance-memory-fs.ts +59 -0
- package/extensions/orbit/record-store-file.ts +177 -0
- package/extensions/orbit/record-store-page.ts +77 -0
- package/extensions/orbit/record-store-seam.ts +47 -0
- package/extensions/orbit/record-store.ts +149 -0
- package/extensions/orbit/registry-carrier.ts +107 -0
- package/extensions/orbit/registry-eligibility.ts +95 -0
- package/extensions/orbit/registry-evaluation.ts +403 -0
- package/extensions/orbit/registry-evaluator.ts +519 -0
- package/extensions/orbit/registry-import.ts +91 -0
- package/extensions/orbit/registry-lock.ts +221 -0
- package/extensions/orbit/registry-memory-fs.ts +42 -0
- package/extensions/orbit/registry-page-carrier.ts +103 -0
- package/extensions/orbit/registry-proposal.ts +1348 -0
- package/extensions/orbit/registry-runtime.ts +234 -0
- package/extensions/orbit/registry-schema.ts +735 -0
- package/extensions/orbit/registry-submitter.ts +59 -0
- package/extensions/orbit/registry-templates.ts +346 -0
- package/extensions/orbit/registry.ts +476 -0
- package/extensions/orbit/sandbox/index.ts +120 -0
- package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
- package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
- package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
- package/extensions/orbit/secret-redaction.ts +247 -0
- package/extensions/orbit/session-index/cwd.ts +18 -0
- package/extensions/orbit/session-index/db.ts +56 -0
- package/extensions/orbit/session-index/indexer.ts +154 -0
- package/extensions/orbit/session-index/is-enabled.ts +17 -0
- package/extensions/orbit/session-index/parse.ts +254 -0
- package/extensions/orbit/session-index/query.ts +209 -0
- package/extensions/orbit/session-index/schema.ts +72 -0
- package/extensions/orbit/session-index/tools.ts +152 -0
- package/extensions/orbit/session-lifecycle.ts +321 -0
- package/extensions/orbit/skill-triggers.ts +159 -0
- package/extensions/orbit/skills-command.ts +48 -0
- package/extensions/orbit/skills-discovery.ts +435 -0
- package/extensions/orbit/skills.ts +84 -0
- package/extensions/orbit/sra-import-gate.ts +77 -0
- package/extensions/orbit/state.ts +265 -0
- package/extensions/orbit/submission-replay.ts +147 -0
- package/extensions/orbit/sync-command.ts +157 -0
- package/extensions/orbit/teams/critic-parser.ts +93 -0
- package/extensions/orbit/teams/dispatcher.ts +210 -0
- package/extensions/orbit/teams/is-enabled.ts +22 -0
- package/extensions/orbit/teams/tool.ts +187 -0
- package/extensions/orbit/teams/types.ts +57 -0
- package/extensions/orbit/teams/validate.ts +42 -0
- package/extensions/orbit/tester-id-command.ts +133 -0
- package/extensions/orbit/tools-sync.ts +279 -0
- package/extensions/orbit/tools.ts +1300 -0
- package/extensions/orbit/types.ts +41 -0
- package/extensions/orbit/ui-bridge.ts +55 -0
- package/extensions/orbit/ulid.ts +60 -0
- package/extensions/orbit/user-instructions.ts +388 -0
- package/extensions/orbit/vendor/skills/_catalog.json +68 -0
- package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor-skills.ts +283 -0
- package/extensions/orbit/workspace-state-dir.ts +46 -0
- package/extensions/orbit-handoff/index.ts +73 -0
- package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
- package/extensions/whats-new/index.ts +98 -0
- package/lessons/LICENSE +17 -0
- package/lessons/README.md +117 -0
- package/lessons/SCHEMA.md +187 -0
- package/lessons/build-snapshot.mjs +311 -0
- package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
- package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
- package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
- package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
- package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
- package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
- package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
- package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
- package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
- package/lessons/log.md +42 -0
- package/lessons/reproduction/input-population-mismatch.md +59 -0
- package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
- package/lessons/snapshot.json +1004 -0
- package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
- package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
- package/lessons/validate.mjs +1219 -0
- package/package.json +103 -5
- package/shared/agent-prompt.d.ts +8 -0
- package/shared/agent-prompt.js +15 -0
- package/shared/approval-prompt.d.ts +4 -0
- package/shared/approval-prompt.js +61 -0
- package/shared/brain-env.d.ts +14 -0
- package/shared/brain-env.js +108 -0
- package/shared/brain-exit.d.ts +20 -0
- package/shared/brain-exit.js +149 -0
- package/shared/capability-profile.d.ts +16 -0
- package/shared/capability-profile.js +75 -0
- package/shared/config-source.d.ts +13 -0
- package/shared/config-source.js +17 -0
- package/shared/confusables.d.ts +3 -0
- package/shared/confusables.js +93 -0
- package/shared/custom-provider.d.ts +45 -0
- package/shared/custom-provider.js +169 -0
- package/shared/dashboard-contract.d.ts +123 -0
- package/shared/dashboard-contract.js +560 -0
- package/shared/dashboard-layout-store.d.ts +44 -0
- package/shared/dashboard-layout-store.js +281 -0
- package/shared/feedback-contract.d.ts +61 -0
- package/shared/feedback-contract.js +146 -0
- package/shared/galaxy-artifact-links.d.ts +30 -0
- package/shared/galaxy-artifact-links.js +166 -0
- package/shared/galaxy-credentials.d.ts +26 -0
- package/shared/galaxy-credentials.js +71 -0
- package/shared/galaxy-destructive.d.ts +21 -0
- package/shared/galaxy-destructive.js +269 -0
- package/shared/galaxy-guidance.d.ts +2 -0
- package/shared/galaxy-guidance.js +53 -0
- package/shared/galaxy-live-contract.d.ts +92 -0
- package/shared/galaxy-live-contract.js +180 -0
- package/shared/galaxy-page-cells.d.ts +2 -0
- package/shared/galaxy-page-cells.js +22 -0
- package/shared/galaxy-tool-family.d.ts +5 -0
- package/shared/galaxy-tool-family.js +27 -0
- package/shared/galaxy-url.d.ts +1 -0
- package/shared/galaxy-url.js +48 -0
- package/shared/gtn.d.ts +31 -0
- package/shared/gtn.js +253 -0
- package/shared/handled-prompt.d.ts +3 -0
- package/shared/handled-prompt.js +26 -0
- package/shared/known-provider.d.ts +11 -0
- package/shared/known-provider.js +89 -0
- package/shared/lesson-hint-marker.d.ts +7 -0
- package/shared/lesson-hint-marker.js +32 -0
- package/shared/lesson-rules.d.ts +150 -0
- package/shared/lesson-rules.js +203 -0
- package/shared/malformed-object-ids.d.ts +1 -0
- package/shared/malformed-object-ids.js +37 -0
- package/shared/markdown-sections.d.ts +10 -0
- package/shared/markdown-sections.js +118 -0
- package/shared/notebook-fences.d.ts +12 -0
- package/shared/notebook-fences.js +53 -0
- package/shared/observation-contract.d.ts +91 -0
- package/shared/observation-contract.js +544 -0
- package/shared/orbit-config.d.ts +226 -0
- package/shared/orbit-config.js +189 -0
- package/shared/orbit-env.d.ts +13 -0
- package/shared/orbit-env.js +124 -0
- package/shared/orbit-shell-contract.d.ts +74 -0
- package/shared/orbit-shell-contract.js +30 -0
- package/shared/page-content-hash.d.ts +1 -0
- package/shared/page-content-hash.js +13 -0
- package/shared/pi-default-model.d.ts +7 -0
- package/shared/pi-default-model.js +41 -0
- package/shared/prompt-blocks.d.ts +87 -0
- package/shared/prompt-blocks.js +898 -0
- package/shared/provider-auth-caps.d.ts +22 -0
- package/shared/provider-auth-caps.js +57 -0
- package/shared/random.d.ts +2 -0
- package/shared/random.js +15 -0
- package/shared/record-excerpt.d.ts +4 -0
- package/shared/record-excerpt.js +16 -0
- package/shared/redirect-guard.d.ts +36 -0
- package/shared/redirect-guard.js +235 -0
- package/shared/skills-pin.d.ts +8 -0
- package/shared/skills-pin.js +9 -0
- package/shared/skills-router.d.ts +17 -0
- package/shared/skills-router.js +63 -0
- package/shared/sra-import.d.ts +34 -0
- package/shared/sra-import.js +206 -0
- package/shared/state-dir.d.ts +21 -0
- package/shared/state-dir.js +240 -0
- package/shared/team-dispatch-contract.d.ts +28 -0
- package/shared/team-dispatch-contract.js +1 -0
- package/shared/truncate-middle.d.ts +11 -0
- package/shared/truncate-middle.js +68 -0
- package/shared/version-compare.d.ts +10 -0
- package/shared/version-compare.js +74 -0
- package/shared/whats-new.d.ts +31 -0
- package/shared/whats-new.js +83 -0
- package/shared/wsl.d.ts +10 -0
- package/shared/wsl.js +24 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Galaxy history snapshot for the dashboard's Galaxy panel.
|
|
3
|
+
*
|
|
4
|
+
* Why the brain and not a shell: the brain is the only process that can obtain
|
|
5
|
+
* GALAXY_URL/GALAXY_API_KEY from all three places they come from -- an env var
|
|
6
|
+
* Orbit injects after decrypting safeStorage, an env var a developer exported,
|
|
7
|
+
* and a plaintext profile on disk. A shell-side proxy would have to be written
|
|
8
|
+
* once per shell and would still be dead in whichever one the user happened to
|
|
9
|
+
* be in. Reading here and pushing the projection over the existing
|
|
10
|
+
* `ctx.ui.setWidget` channel costs no new credential surface and no new IPC.
|
|
11
|
+
*
|
|
12
|
+
* Cost control, measured against usegalaxy.org and usegalaxy.eu on 2026-09-18:
|
|
13
|
+
* 1. The summary probe is 179 bytes and already carries the headline -- name,
|
|
14
|
+
* update_time, overall state and the active/hidden/deleted counts. A tick
|
|
15
|
+
* where nothing moved never touches the contents endpoint.
|
|
16
|
+
* 2. The contents index IGNORES `keys=` and `view=`: it returns the full
|
|
17
|
+
* serialization of every item including deleted and hidden ones (547 KB
|
|
18
|
+
* for one real history). The `q`/`qv` filters are what actually work
|
|
19
|
+
* (deleted=False, visible=True -> 17.5 KB), and `order=hid-dsc` puts the
|
|
20
|
+
* newest first so `limit` caps at the server rather than after half a
|
|
21
|
+
* megabyte is already on the wire.
|
|
22
|
+
* 3. Contents are refetched only when Galaxy's own update_time moves.
|
|
23
|
+
*
|
|
24
|
+
* Galaxy 26.1 is the floor. The `v=dev` behaviour above was not checked against
|
|
25
|
+
* anything older; on a server that ignores `v=dev` the endpoint fails the other
|
|
26
|
+
* way (the deprecated filters apply and `limit` does not), so the result is
|
|
27
|
+
* over-fetching rather than showing deleted rows as live.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import * as fsp from "node:fs/promises";
|
|
31
|
+
|
|
32
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import {
|
|
34
|
+
GALAXY_LIVE_MAX_ITEMS,
|
|
35
|
+
GALAXY_LIVE_MAX_TOKEN,
|
|
36
|
+
GALAXY_LIVE_SCHEMA_VERSION,
|
|
37
|
+
clampText,
|
|
38
|
+
normalizeState,
|
|
39
|
+
} from "../../shared/galaxy-live-contract.js";
|
|
40
|
+
import type {
|
|
41
|
+
GalaxyLiveHistory,
|
|
42
|
+
GalaxyLiveItem,
|
|
43
|
+
GalaxyLivePayload,
|
|
44
|
+
GalaxyLiveState,
|
|
45
|
+
} from "../../shared/galaxy-live-contract.js";
|
|
46
|
+
import { OrbitWidgetKey, encodeJsonWidget } from "../../shared/orbit-shell-contract.js";
|
|
47
|
+
import {
|
|
48
|
+
GalaxyApiError,
|
|
49
|
+
galaxyGet,
|
|
50
|
+
galaxyGetMostRecentHistory,
|
|
51
|
+
getGalaxyConfig,
|
|
52
|
+
sameGalaxyServer,
|
|
53
|
+
type GalaxyHistorySummary,
|
|
54
|
+
} from "./galaxy-api.js";
|
|
55
|
+
import { findGalaxyPageBlocks } from "./galaxy-page-binding.js";
|
|
56
|
+
import { findJobBlocks } from "./galaxy-job-block.js";
|
|
57
|
+
import { findInvocationBlocks } from "./notebook-writer.js";
|
|
58
|
+
import { setPollTickHook } from "./galaxy-poller.js";
|
|
59
|
+
import { getDashboardPath, readDashboardDocument } from "./dashboard-store.js";
|
|
60
|
+
import { capabilityProfile } from "../../shared/capability-profile.js";
|
|
61
|
+
|
|
62
|
+
/** The only Galaxy reads this source is allowed to make. Anything not built by
|
|
63
|
+
* these two functions never leaves the module, so there is no path from a
|
|
64
|
+
* renderer-supplied string to an arbitrary Galaxy path. */
|
|
65
|
+
const SUMMARY_KEYS = "name,update_time,state,contents_active";
|
|
66
|
+
|
|
67
|
+
function historySummaryPath(historyId: string): string {
|
|
68
|
+
return `/histories/${encodeURIComponent(historyId)}?keys=${SUMMARY_KEYS}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask for one row more than we intend to keep. Without the extra row a history
|
|
73
|
+
* with exactly `limit` items and a history with ten thousand are
|
|
74
|
+
* indistinguishable, and `truncated` silently reports 0 on the second -- which
|
|
75
|
+
* is the panel telling the user there is nothing more to see.
|
|
76
|
+
*/
|
|
77
|
+
function historyContentsPath(historyId: string, limit: number): string {
|
|
78
|
+
// Every value here is a literal. Nothing a renderer or a model supplies ever
|
|
79
|
+
// reaches the query string -- the only caller-supplied value is historyId,
|
|
80
|
+
// which is checked against ENCODED_ID_RE first.
|
|
81
|
+
const query = [
|
|
82
|
+
"v=dev",
|
|
83
|
+
"q=deleted&qv=False",
|
|
84
|
+
"q=visible&qv=True",
|
|
85
|
+
"order=hid-dsc",
|
|
86
|
+
`limit=${limit}`,
|
|
87
|
+
].join("&");
|
|
88
|
+
return `/histories/${encodeURIComponent(historyId)}/contents?${query}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Galaxy encoded ids are hex, and are a multiple of 16 characters.
|
|
93
|
+
*
|
|
94
|
+
* The guard is not decoration. A value like `.` survives encodeURIComponent,
|
|
95
|
+
* and URL normalization turns the SUMMARY path `/api/histories/.` into
|
|
96
|
+
* `/api/histories/` -- the *collection* endpoint, which answers 200 with `[]`
|
|
97
|
+
* (verified against usegalaxy.org). Without this check the probe would take
|
|
98
|
+
* that 200 as a real history and the panel would draw an empty "Untitled
|
|
99
|
+
* history" for an id that does not exist. The contents path behaves
|
|
100
|
+
* differently -- `/api/histories/./contents` is a 400 -- which is exactly why
|
|
101
|
+
* checking only the second call would miss it.
|
|
102
|
+
*
|
|
103
|
+
* The length bound keeps a megabyte of hex from being pasted into a URL;
|
|
104
|
+
* Galaxy rejects a non-multiple-of-16 id itself, so this only avoids asking.
|
|
105
|
+
*/
|
|
106
|
+
const ENCODED_ID_RE = /^[0-9a-fA-F]{16,64}$/;
|
|
107
|
+
|
|
108
|
+
interface HistorySummary {
|
|
109
|
+
name?: unknown;
|
|
110
|
+
update_time?: unknown;
|
|
111
|
+
state?: unknown;
|
|
112
|
+
/** {active, hidden, deleted} over the history's items. Used ONLY to decide
|
|
113
|
+
* whether rows were left behind, never to count states -- see projectHistory. */
|
|
114
|
+
contents_active?: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface ContentsRow {
|
|
118
|
+
id?: unknown;
|
|
119
|
+
hid?: unknown;
|
|
120
|
+
name?: unknown;
|
|
121
|
+
state?: unknown;
|
|
122
|
+
extension?: unknown;
|
|
123
|
+
deleted?: unknown;
|
|
124
|
+
visible?: unknown;
|
|
125
|
+
history_content_type?: unknown;
|
|
126
|
+
element_count?: unknown;
|
|
127
|
+
populated_state?: unknown;
|
|
128
|
+
job_state_summary?: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Host only -- the panel shows "usegalaxy.org", never a URL that could carry a
|
|
132
|
+
* query string, and never the key. */
|
|
133
|
+
export function serverHostOf(url: string | undefined): string | null {
|
|
134
|
+
if (!url) return null;
|
|
135
|
+
try {
|
|
136
|
+
return new URL(url).host;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Collapse a collection's per-element job states into one state for the row.
|
|
144
|
+
* Galaxy reports `job_state_summary` counts; the panel wants the worst thing
|
|
145
|
+
* happening inside. Error beats paused beats running beats queued beats ok,
|
|
146
|
+
* because the point of the row is "is this going well".
|
|
147
|
+
*/
|
|
148
|
+
function collectionState(row: ContentsRow): GalaxyLiveState {
|
|
149
|
+
const summary = row.job_state_summary;
|
|
150
|
+
if (summary && typeof summary === "object") {
|
|
151
|
+
const counts = summary as Record<string, unknown>;
|
|
152
|
+
const n = (k: string): number => (typeof counts[k] === "number" ? (counts[k] as number) : 0);
|
|
153
|
+
if (n("error") > 0 || n("failed") > 0) return "error";
|
|
154
|
+
if (n("paused") > 0) return "paused";
|
|
155
|
+
if (n("running") > 0) return "running";
|
|
156
|
+
if (n("new") > 0 || n("queued") > 0) return "queued";
|
|
157
|
+
}
|
|
158
|
+
// populated_state is the collection's own build state; "new" means Galaxy is
|
|
159
|
+
// still filling it in.
|
|
160
|
+
if (row.populated_state === "failed") return "error";
|
|
161
|
+
if (row.populated_state === "new") return "new";
|
|
162
|
+
return "ok";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Project one Galaxy contents row into a panel row. Returns null for rows the
|
|
167
|
+
* panel never shows (deleted, hidden) so the caller's cap applies to visible
|
|
168
|
+
* rows rather than being eaten by invisible ones.
|
|
169
|
+
*/
|
|
170
|
+
export function projectRow(raw: unknown): GalaxyLiveItem | null {
|
|
171
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
172
|
+
const row = raw as ContentsRow;
|
|
173
|
+
if (row.deleted === true) return null;
|
|
174
|
+
if (row.visible === false) return null;
|
|
175
|
+
if (typeof row.id !== "string" || !row.id) return null;
|
|
176
|
+
const isCollection = row.history_content_type === "dataset_collection";
|
|
177
|
+
const item: GalaxyLiveItem = {
|
|
178
|
+
// Clamped like every other Galaxy-supplied string. Capping the row count is
|
|
179
|
+
// not capping the payload: a server answering with a hundred-thousand-
|
|
180
|
+
// character id per row turns 200 rows into twenty megabytes on one line.
|
|
181
|
+
id: clampText(row.id, GALAXY_LIVE_MAX_TOKEN),
|
|
182
|
+
hid: typeof row.hid === "number" ? row.hid : 0,
|
|
183
|
+
name: clampText(row.name),
|
|
184
|
+
state: isCollection ? collectionState(row) : normalizeState(row.state),
|
|
185
|
+
extension: clampText(typeof row.extension === "string" ? row.extension : "", 32),
|
|
186
|
+
kind: isCollection ? "collection" : "dataset",
|
|
187
|
+
};
|
|
188
|
+
if (isCollection && typeof row.element_count === "number") {
|
|
189
|
+
item.elementCount = row.element_count;
|
|
190
|
+
}
|
|
191
|
+
return item;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A JSON object, as opposed to null, an array, or a string a proxy sent. */
|
|
195
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
196
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function activeCount(raw: unknown): number | null {
|
|
200
|
+
if (!isPlainObject(raw)) return null;
|
|
201
|
+
const active = raw.active;
|
|
202
|
+
return typeof active === "number" && active >= 0 ? active : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A positive, finite row cap. `slice(0, NaN)` keeps nothing, which would turn a
|
|
206
|
+
* full history into an empty panel. */
|
|
207
|
+
function keepCount(keep: number): number {
|
|
208
|
+
return Number.isFinite(keep) && keep >= 1 ? Math.floor(keep) : GALAXY_LIVE_MAX_ITEMS;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build the history projection from the two raw responses. Pure, so the shape
|
|
213
|
+
* the renderer sees is testable without a network.
|
|
214
|
+
*
|
|
215
|
+
* **Counts are computed from the rows, deliberately.** Galaxy hands you a
|
|
216
|
+
* ready-made histogram in `contents_states`, and using it was the first thing
|
|
217
|
+
* this did; it is wrong here. `contents_states` scores a dataset collection by
|
|
218
|
+
* the collection's own state, while the row below it is scored by the worst job
|
|
219
|
+
* inside it -- so on a real usegalaxy.org history the header read "10 failed"
|
|
220
|
+
* above a list showing 19 red rows, 9 of them collections. A summary that
|
|
221
|
+
* disagrees with the list under it is worse than one that counts a smaller set
|
|
222
|
+
* correctly. So the header counts exactly the rows that were fetched,
|
|
223
|
+
* `countsComplete` says whether that was all of them, and `contents_active` is
|
|
224
|
+
* used for nothing except working out how many were left behind.
|
|
225
|
+
*/
|
|
226
|
+
export function projectHistory(
|
|
227
|
+
historyId: string,
|
|
228
|
+
summary: unknown,
|
|
229
|
+
contents: unknown,
|
|
230
|
+
keep: number = GALAXY_LIVE_MAX_ITEMS,
|
|
231
|
+
): GalaxyLiveHistory {
|
|
232
|
+
const s = (
|
|
233
|
+
summary && typeof summary === "object" && !Array.isArray(summary) ? summary : {}
|
|
234
|
+
) as HistorySummary;
|
|
235
|
+
const rows = Array.isArray(contents) ? contents : [];
|
|
236
|
+
const items: GalaxyLiveItem[] = [];
|
|
237
|
+
for (const raw of rows) {
|
|
238
|
+
const item = projectRow(raw);
|
|
239
|
+
if (item) items.push(item);
|
|
240
|
+
}
|
|
241
|
+
// Galaxy is asked for hid-descending, but an older server that ignores
|
|
242
|
+
// `order` would hand back ascending; sort so the newest row is first either
|
|
243
|
+
// way. The thing a user is waiting on is the thing they just launched.
|
|
244
|
+
items.sort((a, b) => b.hid - a.hid);
|
|
245
|
+
const capped = items.slice(0, keepCount(keep));
|
|
246
|
+
|
|
247
|
+
const counts: Partial<Record<GalaxyLiveState, number>> = {};
|
|
248
|
+
for (const item of capped) counts[item.state] = (counts[item.state] ?? 0) + 1;
|
|
249
|
+
|
|
250
|
+
// Two independent ways to know rows were left behind, and the answer is the
|
|
251
|
+
// larger of them rather than whichever one happens to be present.
|
|
252
|
+
// `contents_active.active` is exact but absent on a server that silently
|
|
253
|
+
// drops the `keys=` we asked for (it answers 200 either way). The overflow
|
|
254
|
+
// row -- we request keep+1 and keep `keep` -- is always available but only
|
|
255
|
+
// proves "at least one more". Preferring the server's count outright was
|
|
256
|
+
// wrong: the summary and the contents are two requests a second apart, so a
|
|
257
|
+
// history that grew in between returns more rows than the count admits to,
|
|
258
|
+
// and the panel then reports nothing hidden while rows are missing.
|
|
259
|
+
const active = activeCount(s.contents_active);
|
|
260
|
+
const overflow = Math.max(0, items.length - capped.length);
|
|
261
|
+
const fromServer = active !== null ? Math.max(0, active - capped.length) : 0;
|
|
262
|
+
const truncated = Math.max(fromServer, overflow);
|
|
263
|
+
const truncatedExact = active !== null ? fromServer >= overflow : overflow === 0;
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
id: historyId,
|
|
267
|
+
name: clampText(s.name) || "Untitled history",
|
|
268
|
+
updateTime: clampText(s.update_time, GALAXY_LIVE_MAX_TOKEN),
|
|
269
|
+
counts,
|
|
270
|
+
countsComplete: truncated === 0,
|
|
271
|
+
items: capped,
|
|
272
|
+
truncated,
|
|
273
|
+
truncatedExact,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Per-tick wall-clock bound. Comfortably above the ~1 s a cold contents fetch
|
|
278
|
+
* took against usegalaxy.org, well under the 15 s poller interval. */
|
|
279
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* How many contents rows we will project, whatever Galaxy sends.
|
|
283
|
+
*
|
|
284
|
+
* We request `GALAXY_LIVE_MAX_ITEMS + 1`; a server that honours `limit` never
|
|
285
|
+
* reaches this. One that ignores it hands back the whole history, and then
|
|
286
|
+
* `projectRow` and the sort run over the lot on the brain's 15 s timer, every
|
|
287
|
+
* 15 s, for as long as the session lasts.
|
|
288
|
+
*
|
|
289
|
+
* Extra rows are dropped rather than refused. Refusing pins the panel on
|
|
290
|
+
* "Galaxy did not answer" for the rest of the session -- the next tick gets the
|
|
291
|
+
* same oversized answer and the one after that too -- while Galaxy is in fact
|
|
292
|
+
* answering, and the user can see their history perfectly well in Galaxy's own
|
|
293
|
+
* UI. Ten of what we asked for is enough slack that this only fires on a server
|
|
294
|
+
* that is genuinely ignoring us.
|
|
295
|
+
*
|
|
296
|
+
* This bounds the work, NOT the transfer -- by the time it is consulted the
|
|
297
|
+
* body has already been buffered and parsed, because `galaxyGet` ends in
|
|
298
|
+
* `resp.json()`. A real byte bound has to go there, which is outside this file.
|
|
299
|
+
*/
|
|
300
|
+
const MAX_CONTENTS_ROWS = GALAXY_LIVE_MAX_ITEMS * 10;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The rows worth projecting, from the end of the list that holds them.
|
|
304
|
+
*
|
|
305
|
+
* We ask for `order=hid-dsc`, so the newest are at the head -- but an older
|
|
306
|
+
* server that ignored `order` hands back ascending, and `projectHistory` only
|
|
307
|
+
* sorts what it is given. Cutting the head there would hand it the oldest rows
|
|
308
|
+
* in the history and the panel would confidently show a user work they finished
|
|
309
|
+
* last year. Read the direction off the rows instead of trusting the query.
|
|
310
|
+
*/
|
|
311
|
+
function boundContentsRows(rows: unknown[]): unknown[] {
|
|
312
|
+
if (rows.length <= MAX_CONTENTS_ROWS) return rows;
|
|
313
|
+
const hid = (row: unknown): number | null => {
|
|
314
|
+
const value = (row as { hid?: unknown } | null)?.hid;
|
|
315
|
+
return typeof value === "number" ? value : null;
|
|
316
|
+
};
|
|
317
|
+
const first = hid(rows[0]);
|
|
318
|
+
const last = hid(rows[rows.length - 1]);
|
|
319
|
+
const ascending = first !== null && last !== null && last > first;
|
|
320
|
+
return ascending ? rows.slice(-MAX_CONTENTS_ROWS) : rows.slice(0, MAX_CONTENTS_ROWS);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Which failure the user is looking at. The split that matters is 401 from
|
|
325
|
+
* 403: Galaxy answers 401 for a key it rejects, and 403 for a good key on a
|
|
326
|
+
* history that is not yours. Collapsing them sends someone to rotate a
|
|
327
|
+
* credential that is fine.
|
|
328
|
+
*/
|
|
329
|
+
function classifyError(err: unknown): NonNullable<GalaxyLivePayload["unavailable"]> {
|
|
330
|
+
if (!(err instanceof GalaxyApiError)) return "unreachable";
|
|
331
|
+
if (err.status === 401) return "unauthorized";
|
|
332
|
+
if (err.status === 403) return "forbidden";
|
|
333
|
+
if (err.status === 400 || err.status === 404) return "no-history";
|
|
334
|
+
return "unreachable";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface GalaxyLiveDeps {
|
|
338
|
+
/** Injected so the whole source is testable without a network or env. */
|
|
339
|
+
get: <T>(path: string, signal?: AbortSignal) => Promise<T>;
|
|
340
|
+
config: () => { url: string; apiKey: string } | null;
|
|
341
|
+
now: () => Date;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const defaultDeps: GalaxyLiveDeps = {
|
|
345
|
+
get: galaxyGet,
|
|
346
|
+
config: getGalaxyConfig,
|
|
347
|
+
now: () => new Date(),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export interface SnapshotOptions {
|
|
351
|
+
/** The update_time the caller already has. When it still matches, contents
|
|
352
|
+
* are not refetched and the result carries no payload. */
|
|
353
|
+
knownUpdateTime?: string;
|
|
354
|
+
signal?: AbortSignal;
|
|
355
|
+
/**
|
|
356
|
+
* Wall-clock bound per tick. `galaxyGet` is a bare fetch with no timeout of
|
|
357
|
+
* its own, and this runs on a repeating timer -- without a bound, one hung
|
|
358
|
+
* Galaxy stacks in-flight requests until something falls over.
|
|
359
|
+
*/
|
|
360
|
+
timeoutMs?: number;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface SnapshotResult {
|
|
364
|
+
/**
|
|
365
|
+
* null means "do not push anything" -- either Galaxy said nothing moved, or
|
|
366
|
+
* the tick was aborted. Making it null rather than a history-less payload is
|
|
367
|
+
* the point: the alternative shape renders as "No history yet.", so a caller
|
|
368
|
+
* that forwarded an unchanged result would blank a populated panel and tell
|
|
369
|
+
* the user their analysis has no history.
|
|
370
|
+
*/
|
|
371
|
+
payload: GalaxyLivePayload | null;
|
|
372
|
+
/** Galaxy answered and nothing had changed since `knownUpdateTime`. */
|
|
373
|
+
unchanged: boolean;
|
|
374
|
+
/** The caller's signal fired. Not a Galaxy failure and must not be shown as one. */
|
|
375
|
+
aborted: boolean;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* One read: probe, and refetch contents only if Galaxy says the history moved.
|
|
380
|
+
*
|
|
381
|
+
* Never throws. A Galaxy failure becomes an `unavailable` payload, because the
|
|
382
|
+
* panel's job is to say "I cannot see Galaxy right now" rather than to vanish.
|
|
383
|
+
*/
|
|
384
|
+
export async function fetchGalaxyLiveSnapshot(
|
|
385
|
+
historyId: string,
|
|
386
|
+
opts: SnapshotOptions = {},
|
|
387
|
+
deps: GalaxyLiveDeps = defaultDeps,
|
|
388
|
+
): Promise<SnapshotResult> {
|
|
389
|
+
const cfg = deps.config();
|
|
390
|
+
const host = serverHostOf(cfg?.url);
|
|
391
|
+
|
|
392
|
+
// Stamped when the payload is built, not when the tick started, so
|
|
393
|
+
// `updatedAt` means what the contract says it means.
|
|
394
|
+
const bare = (unavailable: GalaxyLivePayload["unavailable"]): SnapshotResult => ({
|
|
395
|
+
payload: {
|
|
396
|
+
version: GALAXY_LIVE_SCHEMA_VERSION,
|
|
397
|
+
serverHost: host,
|
|
398
|
+
history: null,
|
|
399
|
+
unavailable,
|
|
400
|
+
updatedAt: deps.now().toISOString(),
|
|
401
|
+
},
|
|
402
|
+
unchanged: false,
|
|
403
|
+
aborted: false,
|
|
404
|
+
});
|
|
405
|
+
const quiet = (aborted: boolean): SnapshotResult => ({
|
|
406
|
+
payload: null,
|
|
407
|
+
unchanged: !aborted,
|
|
408
|
+
aborted,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!cfg) return bare("not-configured");
|
|
412
|
+
if (!historyId || !ENCODED_ID_RE.test(historyId)) return bare("no-history");
|
|
413
|
+
|
|
414
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
415
|
+
const timer = AbortSignal.timeout(timeoutMs);
|
|
416
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, timer]) : timer;
|
|
417
|
+
/** A caller-cancelled tick is not a Galaxy failure; a timeout is. */
|
|
418
|
+
const cancelled = (): boolean => Boolean(opts.signal?.aborted);
|
|
419
|
+
|
|
420
|
+
let raw: unknown;
|
|
421
|
+
try {
|
|
422
|
+
raw = await deps.get<unknown>(historySummaryPath(historyId), signal);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (cancelled()) return quiet(true);
|
|
425
|
+
return bare(classifyError(err));
|
|
426
|
+
}
|
|
427
|
+
// A 200 is not an answer. Galaxy returning JSON `null` here -- which a proxy
|
|
428
|
+
// in front of it can do too -- used to throw straight out of a function whose
|
|
429
|
+
// whole contract is that it does not, so the tick died in the poller's catch
|
|
430
|
+
// with no payload and no backoff. Anything that is not an object is the same
|
|
431
|
+
// problem: unreadable, retry, say so.
|
|
432
|
+
if (!isPlainObject(raw)) return bare("unreachable");
|
|
433
|
+
const summary = raw as HistorySummary;
|
|
434
|
+
|
|
435
|
+
const updateTime = typeof summary.update_time === "string" ? summary.update_time : "";
|
|
436
|
+
if (opts.knownUpdateTime && updateTime && opts.knownUpdateTime === updateTime) {
|
|
437
|
+
return quiet(false);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let contents: unknown;
|
|
441
|
+
try {
|
|
442
|
+
contents = await deps.get<unknown>(
|
|
443
|
+
historyContentsPath(historyId, GALAXY_LIVE_MAX_ITEMS + 1),
|
|
444
|
+
signal,
|
|
445
|
+
);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
if (cancelled()) return quiet(true);
|
|
448
|
+
return bare(classifyError(err));
|
|
449
|
+
}
|
|
450
|
+
// `projectHistory` turns a non-array into no rows, which is indistinguishable
|
|
451
|
+
// from an empty history -- so a server answering 200 with an error object, or
|
|
452
|
+
// an HTML login page a proxy substituted, drew a confident "No datasets yet"
|
|
453
|
+
// over an analysis that has them. Refuse the shape instead.
|
|
454
|
+
if (!Array.isArray(contents)) return bare("unreachable");
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
payload: {
|
|
458
|
+
version: GALAXY_LIVE_SCHEMA_VERSION,
|
|
459
|
+
serverHost: host,
|
|
460
|
+
history: projectHistory(historyId, summary, boundContentsRows(contents)),
|
|
461
|
+
updatedAt: deps.now().toISOString(),
|
|
462
|
+
},
|
|
463
|
+
unchanged: false,
|
|
464
|
+
aborted: false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Which history ────────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The history this analysis is attached to, as Orbit already tracks it. No
|
|
472
|
+
* user-typed ids: the widget cannot ask for one anyway (the widget channel is
|
|
473
|
+
* push-only) and every id that reaches a request has to be one Orbit wrote down.
|
|
474
|
+
*
|
|
475
|
+
* The notebook's `loom-galaxy-page` binding wins, and only when it names the
|
|
476
|
+
* server we are actually talking to -- a notebook carried over from another
|
|
477
|
+
* Galaxy would otherwise have us probe a stranger's id and render "this history
|
|
478
|
+
* belongs to another account" forever.
|
|
479
|
+
*
|
|
480
|
+
* `getCurrentHistoryId()` from state.ts is deliberately not used: it follows
|
|
481
|
+
* the last create_history, not the history the notebook is bound to.
|
|
482
|
+
*/
|
|
483
|
+
export function historyIdFromNotebook(content: string | null, serverUrl: string): string | null {
|
|
484
|
+
if (!content) return null;
|
|
485
|
+
const bindings = findGalaxyPageBlocks(content).filter((b) =>
|
|
486
|
+
sameGalaxyServer(b.galaxyServerUrl, serverUrl),
|
|
487
|
+
);
|
|
488
|
+
const last = bindings[bindings.length - 1];
|
|
489
|
+
return last?.historyId || null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Is anything Orbit launched still going? Drives the poll cadence, nothing else. */
|
|
493
|
+
export function notebookHasLiveWork(content: string | null): boolean {
|
|
494
|
+
if (!content) return false;
|
|
495
|
+
if (findInvocationBlocks(content).some((b) => b.status === "in_progress")) return true;
|
|
496
|
+
return findJobBlocks(content).some((b) => b.status === "in_progress");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── Cadence ──────────────────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Cadences, in milliseconds. The ticker does not own a timer: it is driven by
|
|
503
|
+
* the existing Galaxy poller's 15 s tick and these are minimum spacings
|
|
504
|
+
* measured against that tick, so there is one interval in the process, one
|
|
505
|
+
* place that knows whether anything is in flight, and one thing to stop.
|
|
506
|
+
*/
|
|
507
|
+
const ACTIVE_INTERVAL_MS = 15_000;
|
|
508
|
+
/** Nothing Orbit launched is running. The history can still move -- a user can
|
|
509
|
+
* upload in Galaxy's own UI -- so we keep looking, just not four times a minute. */
|
|
510
|
+
const IDLE_INTERVAL_MS = 60_000;
|
|
511
|
+
/** Consecutive failures widen the gap from ACTIVE up to this. */
|
|
512
|
+
const MAX_BACKOFF_MS = 300_000;
|
|
513
|
+
/** Re-push an unchanged payload this often so the panel's "checked N ago" line
|
|
514
|
+
* cannot claim we have stopped asking when we have not. */
|
|
515
|
+
const STALE_REFRESH_MS = 60_000;
|
|
516
|
+
/** How long a resolved most-recently-used history is reused before re-asking.
|
|
517
|
+
* Only consulted when the notebook carries no binding. */
|
|
518
|
+
const HISTORY_RESOLVE_TTL_MS = 300_000;
|
|
519
|
+
|
|
520
|
+
export interface GalaxyLiveTickerDeps {
|
|
521
|
+
snapshot: typeof fetchGalaxyLiveSnapshot;
|
|
522
|
+
config: () => { url: string; apiKey: string } | null;
|
|
523
|
+
mostRecentHistory: (signal?: AbortSignal) => Promise<GalaxyHistorySummary | null>;
|
|
524
|
+
/** Hand a payload to the shell. Must not throw. */
|
|
525
|
+
push: (payload: GalaxyLivePayload) => void;
|
|
526
|
+
now: () => number;
|
|
527
|
+
/**
|
|
528
|
+
* Is there a panel to draw this? Checked at the top of every tick, before
|
|
529
|
+
* the cadence, so a session whose dashboards never include the live history
|
|
530
|
+
* asks Galaxy nothing at all. Absent means always.
|
|
531
|
+
*/
|
|
532
|
+
wanted?: () => Promise<boolean>;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Everything except `updatedAt`, which moves on every read and would defeat
|
|
536
|
+
* the comparison it is supposed to take part in. */
|
|
537
|
+
function payloadFingerprint(payload: GalaxyLivePayload): string {
|
|
538
|
+
const { updatedAt: _ignored, ...rest } = payload;
|
|
539
|
+
return JSON.stringify(rest);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Decides when to ask Galaxy and what to push, given the notebook the poller
|
|
544
|
+
* already read. Separate from the fetch so the cadence rules can be tested
|
|
545
|
+
* with a clock and no network.
|
|
546
|
+
*/
|
|
547
|
+
export class GalaxyLiveTicker {
|
|
548
|
+
private running = false;
|
|
549
|
+
private lastAttemptAt = 0;
|
|
550
|
+
private lastPushAt = 0;
|
|
551
|
+
private lastFingerprint: string | null = null;
|
|
552
|
+
/**
|
|
553
|
+
* The last payload that actually carried a history. Deliberately NOT "the
|
|
554
|
+
* last payload": an `unchanged` tick re-stamps this to refresh the staleness
|
|
555
|
+
* clock, and re-stamping an error payload wedged the panel on "Galaxy did not
|
|
556
|
+
* answer" for the rest of the session. One dropped packet, and a settled
|
|
557
|
+
* history whose `update_time` never moves again could never clear it.
|
|
558
|
+
*/
|
|
559
|
+
private lastHistoryPayload: GalaxyLivePayload | null = null;
|
|
560
|
+
private knownUpdateTime: string | null = null;
|
|
561
|
+
private historyId: string | null = null;
|
|
562
|
+
private failures = 0;
|
|
563
|
+
private resolved: { id: string; at: number; server: string } | null = null;
|
|
564
|
+
/** Whether the previous tick found a panel. Starts false so the first tick
|
|
565
|
+
* that finds one is treated as the panel appearing. */
|
|
566
|
+
private panelWanted = false;
|
|
567
|
+
/**
|
|
568
|
+
* Everything this ticker asks Galaxy hangs off this, so `stop()` abandons
|
|
569
|
+
* whatever is in flight. Without it a `session_shutdown` landing on a read
|
|
570
|
+
* held the loop open for as long as the read took -- and `galaxyGet` is a
|
|
571
|
+
* bare fetch with no timeout of its own, which against a stalling server
|
|
572
|
+
* measured 301 seconds before it threw.
|
|
573
|
+
*/
|
|
574
|
+
private readonly abort = new AbortController();
|
|
575
|
+
private stopped = false;
|
|
576
|
+
|
|
577
|
+
constructor(private deps: GalaxyLiveTickerDeps) {}
|
|
578
|
+
|
|
579
|
+
/** Stop ticking and abandon anything already in flight. Not reversible. */
|
|
580
|
+
stop(): void {
|
|
581
|
+
this.stopped = true;
|
|
582
|
+
this.abort.abort();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* A signal for one tick, aborted when the session is.
|
|
587
|
+
*
|
|
588
|
+
* Deliberately NOT `AbortSignal.any([this.abort.signal, ...])`: `any`
|
|
589
|
+
* registers the composite on each source for as long as that source lives and
|
|
590
|
+
* Node never takes it off again, so hanging one off a signal that lives for
|
|
591
|
+
* the whole session grows a list nothing empties -- measured at one retained
|
|
592
|
+
* entry per call. This holds a single listener instead, and `done()` removes
|
|
593
|
+
* it. Anything downstream may compose onto the returned signal freely: it
|
|
594
|
+
* dies with the tick.
|
|
595
|
+
*/
|
|
596
|
+
private tickSignal(): { signal: AbortSignal; done: () => void } {
|
|
597
|
+
const controller = new AbortController();
|
|
598
|
+
const onStop = (): void => controller.abort();
|
|
599
|
+
this.abort.signal.addEventListener("abort", onStop, { once: true });
|
|
600
|
+
return {
|
|
601
|
+
signal: controller.signal,
|
|
602
|
+
done: () => this.abort.signal.removeEventListener("abort", onStop),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Minimum spacing for the next attempt, given what the notebook says. */
|
|
607
|
+
private interval(live: boolean): number {
|
|
608
|
+
const base = live ? ACTIVE_INTERVAL_MS : IDLE_INTERVAL_MS;
|
|
609
|
+
if (this.failures === 0) return base;
|
|
610
|
+
// Doubling from the active cadence, not from the idle one: a server that is
|
|
611
|
+
// down should be asked about at the same widening rate either way.
|
|
612
|
+
const backoff = Math.min(MAX_BACKOFF_MS, ACTIVE_INTERVAL_MS * 2 ** this.failures);
|
|
613
|
+
return Math.max(base, backoff);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private emit(payload: GalaxyLivePayload, now: number): void {
|
|
617
|
+
const fingerprint = payloadFingerprint(payload);
|
|
618
|
+
// Only a payload the panel draws a "checked N ago" line under is worth
|
|
619
|
+
// re-sending unchanged. An `unavailable` sentence has no clock on it, so
|
|
620
|
+
// re-pushing it every minute would be a wire message that changes nothing.
|
|
621
|
+
const stale = Boolean(payload.history) && now - this.lastPushAt >= STALE_REFRESH_MS;
|
|
622
|
+
if (fingerprint === this.lastFingerprint && !stale) return;
|
|
623
|
+
this.lastFingerprint = fingerprint;
|
|
624
|
+
if (payload.history) this.lastHistoryPayload = payload;
|
|
625
|
+
this.lastPushAt = now;
|
|
626
|
+
this.deps.push(payload);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private async resolveHistoryId(
|
|
630
|
+
content: string | null,
|
|
631
|
+
serverUrl: string,
|
|
632
|
+
signal: AbortSignal,
|
|
633
|
+
): Promise<string | null> {
|
|
634
|
+
const bound = historyIdFromNotebook(content, serverUrl);
|
|
635
|
+
if (bound) return bound;
|
|
636
|
+
const cached = this.resolved;
|
|
637
|
+
if (
|
|
638
|
+
cached &&
|
|
639
|
+
cached.server === serverUrl &&
|
|
640
|
+
this.deps.now() - cached.at < HISTORY_RESOLVE_TTL_MS
|
|
641
|
+
) {
|
|
642
|
+
return cached.id;
|
|
643
|
+
}
|
|
644
|
+
// Deliberately not caught here. Returning null for a failed lookup would
|
|
645
|
+
// report "this analysis is not attached to a Galaxy history yet" when the
|
|
646
|
+
// truth is that Galaxy did not answer or rejected the key -- two sentences
|
|
647
|
+
// that send the user somewhere completely different. Galaxy answering
|
|
648
|
+
// "you have no histories" is the only real no-history, and that is the
|
|
649
|
+
// null below.
|
|
650
|
+
//
|
|
651
|
+
// The resolve goes straight to `galaxyGet`, which is a bare fetch with no
|
|
652
|
+
// timeout of its own, so the wall clock has to come from here. Composing
|
|
653
|
+
// onto the tick's signal rather than the session's is what keeps it from
|
|
654
|
+
// accumulating; both sources die with the tick.
|
|
655
|
+
const summary = await this.deps.mostRecentHistory(
|
|
656
|
+
AbortSignal.any([signal, AbortSignal.timeout(DEFAULT_TIMEOUT_MS)]),
|
|
657
|
+
);
|
|
658
|
+
if (!summary?.id) return null;
|
|
659
|
+
this.resolved = { id: summary.id, at: this.deps.now(), server: serverUrl };
|
|
660
|
+
return summary.id;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* One poller tick. `content` is the notebook as the poller just read it, or
|
|
665
|
+
* null when there is no notebook to read. Never throws.
|
|
666
|
+
*/
|
|
667
|
+
async tick(content: string | null): Promise<void> {
|
|
668
|
+
// The poller fires this without awaiting it, so nothing outside stops two
|
|
669
|
+
// ticks overlapping while Galaxy is slow. This does.
|
|
670
|
+
if (this.running || this.stopped) return;
|
|
671
|
+
this.running = true;
|
|
672
|
+
try {
|
|
673
|
+
await this.runTick(content);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.error("[galaxy-live] tick failed:", err);
|
|
676
|
+
} finally {
|
|
677
|
+
this.running = false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The payload for "Galaxy answered, and there is nothing to draw". */
|
|
682
|
+
private unavailable(
|
|
683
|
+
reason: NonNullable<GalaxyLivePayload["unavailable"]>,
|
|
684
|
+
serverUrl: string | undefined,
|
|
685
|
+
now: number,
|
|
686
|
+
): GalaxyLivePayload {
|
|
687
|
+
return {
|
|
688
|
+
version: GALAXY_LIVE_SCHEMA_VERSION,
|
|
689
|
+
serverHost: serverHostOf(serverUrl),
|
|
690
|
+
history: null,
|
|
691
|
+
unavailable: reason,
|
|
692
|
+
updatedAt: new Date(now).toISOString(),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private async runTick(content: string | null): Promise<void> {
|
|
697
|
+
const wanted = this.deps.wanted ? await this.deps.wanted() : true;
|
|
698
|
+
if (!wanted) {
|
|
699
|
+
this.panelWanted = false;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!this.panelWanted) {
|
|
703
|
+
// The panel just appeared, or this is the first tick. Ask now rather than
|
|
704
|
+
// up to a minute from now, and push whatever comes back even if it is
|
|
705
|
+
// what was pushed before the panel went away: the widget that was just
|
|
706
|
+
// added has never seen it.
|
|
707
|
+
this.panelWanted = true;
|
|
708
|
+
this.lastAttemptAt = 0;
|
|
709
|
+
this.lastFingerprint = null;
|
|
710
|
+
}
|
|
711
|
+
const now = this.deps.now();
|
|
712
|
+
const cfg = this.deps.config();
|
|
713
|
+
if (!cfg) {
|
|
714
|
+
// No credentials is a steady state, not a failure: say it once and stop
|
|
715
|
+
// asking. The fingerprint check means a disconnected session costs one
|
|
716
|
+
// push for the whole session.
|
|
717
|
+
this.emit(this.unavailable("not-configured", undefined, now), now);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const live = notebookHasLiveWork(content);
|
|
722
|
+
if (now - this.lastAttemptAt < this.interval(live)) return;
|
|
723
|
+
this.lastAttemptAt = now;
|
|
724
|
+
|
|
725
|
+
// One signal for everything this tick asks, released when it is done.
|
|
726
|
+
const tick = this.tickSignal();
|
|
727
|
+
try {
|
|
728
|
+
await this.askGalaxy(content, cfg, now, tick.signal);
|
|
729
|
+
} finally {
|
|
730
|
+
tick.done();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** The half of a tick that talks to Galaxy, once the cadence has allowed it. */
|
|
735
|
+
private async askGalaxy(
|
|
736
|
+
content: string | null,
|
|
737
|
+
cfg: { url: string; apiKey: string },
|
|
738
|
+
now: number,
|
|
739
|
+
signal: AbortSignal,
|
|
740
|
+
): Promise<void> {
|
|
741
|
+
let historyId: string | null;
|
|
742
|
+
try {
|
|
743
|
+
historyId = await this.resolveHistoryId(content, cfg.url, signal);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
// A stop that aborted the request mid-flight is not something to report
|
|
746
|
+
// or to back off from; the session is over.
|
|
747
|
+
if (this.stopped) return;
|
|
748
|
+
// Asking Galaxy which history is current failed. That is the same class
|
|
749
|
+
// of problem as the read below failing, and it reads the same way.
|
|
750
|
+
this.failures++;
|
|
751
|
+
this.emit(this.unavailable(classifyError(err), cfg.url, this.deps.now()), this.deps.now());
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
// The resolve may have come back after the session ended, either because it
|
|
755
|
+
// was aborted or because it finished first. Either way there is nothing to
|
|
756
|
+
// draw on and nothing worth asking Galaxy for.
|
|
757
|
+
if (this.stopped) return;
|
|
758
|
+
if (historyId !== this.historyId) {
|
|
759
|
+
// A different history: the update_time we were comparing against belongs
|
|
760
|
+
// to the old one, and reusing it would suppress the first real read.
|
|
761
|
+
this.knownUpdateTime = null;
|
|
762
|
+
this.historyId = historyId;
|
|
763
|
+
}
|
|
764
|
+
if (!historyId) {
|
|
765
|
+
// Galaxy answered and said there is no history here. Not a failure, so
|
|
766
|
+
// no backoff: the user creating one should show up on the next tick.
|
|
767
|
+
this.failures = 0;
|
|
768
|
+
this.emit(this.unavailable("no-history", cfg.url, now), now);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const result = await this.deps.snapshot(historyId, {
|
|
773
|
+
knownUpdateTime: this.knownUpdateTime ?? undefined,
|
|
774
|
+
// The snapshot applies its own timeout and reads this one as "the caller
|
|
775
|
+
// gave up", which is the right reading only for a stop.
|
|
776
|
+
signal,
|
|
777
|
+
});
|
|
778
|
+
if (result.aborted || this.stopped) return;
|
|
779
|
+
|
|
780
|
+
if (result.unchanged) {
|
|
781
|
+
this.failures = 0;
|
|
782
|
+
// Galaxy answered and nothing moved. Re-stamp the history we already have
|
|
783
|
+
// so the panel's staleness line reflects when we last asked, not when the
|
|
784
|
+
// history last changed -- otherwise a quiet run looks abandoned.
|
|
785
|
+
if (this.lastHistoryPayload) {
|
|
786
|
+
this.emit(
|
|
787
|
+
{ ...this.lastHistoryPayload, updatedAt: new Date(this.deps.now()).toISOString() },
|
|
788
|
+
this.deps.now(),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const payload = result.payload;
|
|
795
|
+
if (!payload) return;
|
|
796
|
+
if (payload.unavailable) {
|
|
797
|
+
this.failures++;
|
|
798
|
+
// Forget what we were comparing against. Otherwise the next healthy tick
|
|
799
|
+
// sees an unmoved `update_time`, takes the cheap "nothing changed" path,
|
|
800
|
+
// and never refetches the contents that would put the history back on
|
|
801
|
+
// screen in place of the error.
|
|
802
|
+
this.knownUpdateTime = null;
|
|
803
|
+
} else {
|
|
804
|
+
this.failures = 0;
|
|
805
|
+
this.knownUpdateTime = payload.history?.updateTime || null;
|
|
806
|
+
}
|
|
807
|
+
this.emit(payload, this.deps.now());
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ── Arming ───────────────────────────────────────────────────────────────────
|
|
812
|
+
|
|
813
|
+
const GALAXY_HISTORY_WIDGET = "galaxy-history";
|
|
814
|
+
|
|
815
|
+
/** The last answer, keyed on the layout file's identity, so a tick that finds
|
|
816
|
+
* the file unchanged pays one lstat and no parse. */
|
|
817
|
+
let wantedCache: { key: string; wanted: boolean } | null = null;
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Does any dashboard in this analysis hold the live-history panel?
|
|
821
|
+
*
|
|
822
|
+
* Neither shipped preset does, so for most sessions the answer is no and the
|
|
823
|
+
* ticker never talks to Galaxy. A layout the brain cannot read -- a symlink,
|
|
824
|
+
* over the cap -- counts as no: nothing it would draw for can be trusted to
|
|
825
|
+
* exist. Read through the brain's own store, so the file is read the way every
|
|
826
|
+
* other reader reads it.
|
|
827
|
+
*/
|
|
828
|
+
export async function layoutWantsGalaxyLive(): Promise<boolean> {
|
|
829
|
+
const filePath = getDashboardPath();
|
|
830
|
+
if (!filePath) return false;
|
|
831
|
+
let key: string;
|
|
832
|
+
try {
|
|
833
|
+
const st = await fsp.lstat(filePath);
|
|
834
|
+
key = `${filePath}|${st.ino}|${st.size}|${st.mtimeMs}`;
|
|
835
|
+
} catch (err) {
|
|
836
|
+
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") return false;
|
|
837
|
+
key = `${filePath}|absent`;
|
|
838
|
+
}
|
|
839
|
+
if (wantedCache?.key === key) return wantedCache.wanted;
|
|
840
|
+
const read = await readDashboardDocument();
|
|
841
|
+
const wanted =
|
|
842
|
+
read.ok &&
|
|
843
|
+
read.document.dashboards.some((d) =>
|
|
844
|
+
d.panels.some((panel) => panel.widget === GALAXY_HISTORY_WIDGET),
|
|
845
|
+
);
|
|
846
|
+
wantedCache = { key, wanted };
|
|
847
|
+
return wanted;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** For tests: forget the cached answer. */
|
|
851
|
+
export function resetGalaxyLiveWantedCache(): void {
|
|
852
|
+
wantedCache = null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
let ticker: GalaxyLiveTicker | null = null;
|
|
856
|
+
/** Bumped by every arm and every disarm, so a push from a tick that was already
|
|
857
|
+
* in flight when the session ended lands nowhere. */
|
|
858
|
+
let armGeneration = 0;
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Shells that draw a dashboard. In the terminal a widget push collapses to
|
|
862
|
+
* nothing, so arming there would spend real Galaxy requests on a panel that
|
|
863
|
+
* does not exist -- the CLI must cost exactly zero, which it does by never
|
|
864
|
+
* registering the hook at all.
|
|
865
|
+
*/
|
|
866
|
+
function shellDrawsDashboard(): boolean {
|
|
867
|
+
return capabilityProfile().ui === "desktop";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Start pushing the live Galaxy history to the shell. Called from
|
|
872
|
+
* `session-lifecycle.ts`, which owns startup and shutdown; idempotent, so a
|
|
873
|
+
* brain restart that fires `session_start` twice does not leave two tickers.
|
|
874
|
+
*/
|
|
875
|
+
export function armGalaxyLivePanel(ctx: ExtensionContext): void {
|
|
876
|
+
if (!shellDrawsDashboard()) {
|
|
877
|
+
disarmGalaxyLivePanel();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
// A fresh ticker per arming is deliberate: `session_start` means a new
|
|
881
|
+
// session, and a new session wants the panel filled now rather than up to a
|
|
882
|
+
// minute later. The generation is what makes shutdown mean shutdown -- the
|
|
883
|
+
// hook is fired and not awaited, so a tick already in flight can still be
|
|
884
|
+
// holding this closure after the session ends.
|
|
885
|
+
const generation = ++armGeneration;
|
|
886
|
+
const push = (payload: GalaxyLivePayload): void => {
|
|
887
|
+
if (generation !== armGeneration) return;
|
|
888
|
+
try {
|
|
889
|
+
ctx.ui.setWidget(OrbitWidgetKey.GalaxyLive, encodeJsonWidget(payload));
|
|
890
|
+
} catch (err) {
|
|
891
|
+
// Same guard as the notebook widget: a tick that lands after session
|
|
892
|
+
// teardown fires against a ctx pi has invalidated, and touching ctx.ui
|
|
893
|
+
// throws "ctx is stale after session replacement or reload". That one is
|
|
894
|
+
// terminal for this arming, so stop. Anything else is surfaced and the
|
|
895
|
+
// panel keeps trying -- one transient throw must not quietly cost the
|
|
896
|
+
// user their panel for the rest of the session.
|
|
897
|
+
if (err instanceof Error && /ctx is stale/i.test(err.message)) {
|
|
898
|
+
disarmGalaxyLivePanel();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
console.error("[galaxy-live] widget push failed:", err);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
// A ticker being replaced must let go of whatever it is waiting on; the new
|
|
905
|
+
// one is about to ask the same questions.
|
|
906
|
+
ticker?.stop();
|
|
907
|
+
ticker = new GalaxyLiveTicker({
|
|
908
|
+
snapshot: fetchGalaxyLiveSnapshot,
|
|
909
|
+
config: getGalaxyConfig,
|
|
910
|
+
mostRecentHistory: galaxyGetMostRecentHistory,
|
|
911
|
+
push,
|
|
912
|
+
now: () => Date.now(),
|
|
913
|
+
wanted: layoutWantsGalaxyLive,
|
|
914
|
+
});
|
|
915
|
+
const own = ticker;
|
|
916
|
+
setPollTickHook((content) => (own === ticker ? own.tick(content) : Promise.resolve()));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Stop pushing. Called from `session_shutdown`, beside `stopGalaxyPoller()`. */
|
|
920
|
+
export function disarmGalaxyLivePanel(): void {
|
|
921
|
+
armGeneration++;
|
|
922
|
+
// The generation guard already drops a late push. This is the other half:
|
|
923
|
+
// stop waiting for the answer at all. Shutdown never blocked on it -- the
|
|
924
|
+
// poller fires the hook without awaiting it -- but an in-flight bare fetch
|
|
925
|
+
// holds the event loop, which is what the controller on the ticker is for.
|
|
926
|
+
ticker?.stop();
|
|
927
|
+
ticker = null;
|
|
928
|
+
setPollTickHook(null);
|
|
929
|
+
}
|