@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,956 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile: ask Galaxy what is in the history, and make the notebook account
|
|
3
|
+
* for all of it.
|
|
4
|
+
*
|
|
5
|
+
* The submission hook records what it watches happen. What it cannot watch:
|
|
6
|
+
* a turn that died between the submit and the result, a run started from
|
|
7
|
+
* code mode or from Galaxy's own UI while Orbit was open, a session resumed
|
|
8
|
+
* after a crash. Galaxy is the source of truth and the notebook is a
|
|
9
|
+
* reconciled projection of it, so this lists the bound history's invocations
|
|
10
|
+
* and jobs and writes a block for every one no block claims -- as
|
|
11
|
+
* `submitted_by: unknown` and `notebook_anchor: unattributed`, because the
|
|
12
|
+
* harness did not see who made it or for which step, and saying so is the
|
|
13
|
+
* point. A `reconcile.unattributed` row goes with each.
|
|
14
|
+
*
|
|
15
|
+
* The other direction too: a block whose id Galaxy now answers "no such
|
|
16
|
+
* thing" for is downgraded to `server_verified: false`, with a warning row.
|
|
17
|
+
* Only a downgrade -- a fresh Galaxy answer is the only thing that sets it
|
|
18
|
+
* true, and that stays the poller's job.
|
|
19
|
+
*
|
|
20
|
+
* Terminal states seen in the listing are not written here. A block that is
|
|
21
|
+
* in flight on disk and finished on Galaxy is handed to the poller's own
|
|
22
|
+
* transition path (`pollGalaxyNow`), so there is one place that decides a run
|
|
23
|
+
* ended, toasts it, and queues the follow-up.
|
|
24
|
+
*
|
|
25
|
+
* Runs on session start (the reconnect path: a session ending never meant the
|
|
26
|
+
* remote work stopped), every RECONCILE_EVERY_N_TICKS poller ticks, and on
|
|
27
|
+
* `/reconcile`. Idempotent: a second run over the same
|
|
28
|
+
* history finds every id already claimed and writes nothing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
import { appendActivityEvent } from "./activity";
|
|
34
|
+
import {
|
|
35
|
+
INVOCATION_SCHEDULING_DONE,
|
|
36
|
+
galaxyGetInvocation,
|
|
37
|
+
galaxyListHistoryInvocations,
|
|
38
|
+
galaxyListHistoryJobs,
|
|
39
|
+
getGalaxyConfig,
|
|
40
|
+
isGalaxyEncodedId,
|
|
41
|
+
isGalaxyFetchOverridden,
|
|
42
|
+
sameGalaxyServer,
|
|
43
|
+
verifyGalaxyRun,
|
|
44
|
+
type GalaxyJobListing,
|
|
45
|
+
type GalaxyRunVerification,
|
|
46
|
+
type InvocationDetail,
|
|
47
|
+
} from "./galaxy-api";
|
|
48
|
+
import { resetEnrichmentState, runEnrichmentPass, walkInvocationJobs } from "./galaxy-enrich";
|
|
49
|
+
import {
|
|
50
|
+
findJobBlocks,
|
|
51
|
+
isTerminalJobState,
|
|
52
|
+
jobStatusFromGalaxyState,
|
|
53
|
+
locateJobBlock,
|
|
54
|
+
upsertJobBlock,
|
|
55
|
+
type JobYaml,
|
|
56
|
+
} from "./galaxy-job-block";
|
|
57
|
+
import { findGalaxyPageBlocks } from "./galaxy-page-binding";
|
|
58
|
+
import { pollGalaxyNow, setCaptureTickHook } from "./galaxy-poller";
|
|
59
|
+
import {
|
|
60
|
+
UNKNOWN,
|
|
61
|
+
ensureAttemptRecord,
|
|
62
|
+
listAttemptRecords,
|
|
63
|
+
readReconcileState,
|
|
64
|
+
type AttemptRecord,
|
|
65
|
+
writeReconcileState,
|
|
66
|
+
type ReconcileState,
|
|
67
|
+
} from "./galaxy-provenance";
|
|
68
|
+
import { idToken } from "./galaxy-submission";
|
|
69
|
+
import { UNATTRIBUTED } from "./galaxy-submission-capture";
|
|
70
|
+
import type { HarnessBlockFields } from "./harness-block-fields";
|
|
71
|
+
import {
|
|
72
|
+
NotebookChangedError,
|
|
73
|
+
findInvocationBlocks,
|
|
74
|
+
locateInvocationBlock,
|
|
75
|
+
readNotebook,
|
|
76
|
+
statNotebook,
|
|
77
|
+
upsertInvocationBlock,
|
|
78
|
+
withNotebookLock,
|
|
79
|
+
writeNotebook,
|
|
80
|
+
type InvocationYaml,
|
|
81
|
+
} from "./notebook-writer";
|
|
82
|
+
import { evaluateRegistryAttempts, type EvaluationPass } from "./registry-evaluator";
|
|
83
|
+
import { getNotebookPath } from "./state";
|
|
84
|
+
import { ulid } from "./ulid";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every 20th poller tick: five minutes at the poller's 15 s cadence. A reconcile
|
|
88
|
+
* is two listings plus a walk of any invocation still scheduling, so this keeps
|
|
89
|
+
* it to a twentieth of the poll budget while work started in Galaxy's own UI
|
|
90
|
+
* still shows up in the notebook within minutes rather than at the next restart.
|
|
91
|
+
*/
|
|
92
|
+
export const RECONCILE_EVERY_N_TICKS = 20;
|
|
93
|
+
const JOB_PAGE_SIZE = 500;
|
|
94
|
+
const MAX_JOB_PAGES = 10;
|
|
95
|
+
/**
|
|
96
|
+
* Galaxy's own ceiling on `GET /api/invocations?limit=` (`le=100`); anything
|
|
97
|
+
* higher is a 400 and the whole reconcile fails. The listing comes back
|
|
98
|
+
* newest first by default, and galaxy-ops passes no offset, so a full page
|
|
99
|
+
* means "maybe more" and is treated as incomplete.
|
|
100
|
+
*/
|
|
101
|
+
export const INVOCATION_LISTING_LIMIT = 100;
|
|
102
|
+
/**
|
|
103
|
+
* The stamp is the newest `create_time` seen, in Galaxy's own clock, and the
|
|
104
|
+
* next window starts this far before it: a job's row can commit after a
|
|
105
|
+
* later-created one was already listed.
|
|
106
|
+
*/
|
|
107
|
+
const STAMP_MARGIN_MS = 10 * 60_000;
|
|
108
|
+
/** Direct existence checks per run, so a long notebook costs a bounded number of GETs. */
|
|
109
|
+
const MAX_VERIFY_PER_RUN = 100;
|
|
110
|
+
|
|
111
|
+
export type ReconcileTrigger = "session_start" | "tick" | "command" | "page_pull" | "page_resume";
|
|
112
|
+
|
|
113
|
+
export interface ReconcileDeps {
|
|
114
|
+
listJobs: typeof galaxyListHistoryJobs;
|
|
115
|
+
listInvocations: (
|
|
116
|
+
historyId: string,
|
|
117
|
+
limit: number,
|
|
118
|
+
) => Promise<
|
|
119
|
+
{
|
|
120
|
+
id: string;
|
|
121
|
+
create_time?: string;
|
|
122
|
+
state?: string;
|
|
123
|
+
workflow_id?: string;
|
|
124
|
+
history_id?: string;
|
|
125
|
+
}[]
|
|
126
|
+
>;
|
|
127
|
+
getInvocation: (id: string) => Promise<InvocationDetail>;
|
|
128
|
+
verify: (kind: "invocation" | "job", id: string) => Promise<GalaxyRunVerification>;
|
|
129
|
+
now: () => number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultDeps: ReconcileDeps = {
|
|
133
|
+
listJobs: (params) => galaxyListHistoryJobs(params),
|
|
134
|
+
listInvocations: (historyId, limit) => galaxyListHistoryInvocations(historyId, limit),
|
|
135
|
+
getInvocation: (id) => galaxyGetInvocation(id),
|
|
136
|
+
verify: (kind, id) => verifyGalaxyRun(kind, id),
|
|
137
|
+
now: () => Date.now(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Helpers (pure)
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Galaxy writes naive UTC timestamps (`2026-10-07T10:00:00.123456`). Read one
|
|
146
|
+
* as UTC; NaN when it is not a time at all.
|
|
147
|
+
*/
|
|
148
|
+
export function parseGalaxyTime(value: string | undefined): number {
|
|
149
|
+
if (!value) return NaN;
|
|
150
|
+
const hasZone = /(Z|[+-]\d\d:?\d\d)$/i.test(value);
|
|
151
|
+
return Date.parse(hasZone ? value : `${value}Z`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function iso(ms: number): string {
|
|
155
|
+
return new Date(ms).toISOString();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The history this analysis is bound to, and where that answer came from.
|
|
160
|
+
*
|
|
161
|
+
* First choice is the history the harness itself last submitted into, read
|
|
162
|
+
* from the protected attempt records -- notebook text can be edited to name
|
|
163
|
+
* any history the account can see, and reconcile would then write that
|
|
164
|
+
* history's work into this analysis. Only when the harness has submitted
|
|
165
|
+
* nothing yet does the notebook's page binding decide, and the row says so.
|
|
166
|
+
* Never the account's most-recently-used history: a guess here would pull
|
|
167
|
+
* somebody's unrelated work in as this analysis's own.
|
|
168
|
+
*/
|
|
169
|
+
export function boundHistoryId(
|
|
170
|
+
content: string,
|
|
171
|
+
serverUrl: string,
|
|
172
|
+
records: readonly AttemptRecord[],
|
|
173
|
+
): { historyId: string; source: "submission" | "page_binding" } | null {
|
|
174
|
+
const submitted = records
|
|
175
|
+
.filter((r) => r.origin === "submission" && !r.fixture)
|
|
176
|
+
.filter((r) => r.history_id !== UNKNOWN && isGalaxyEncodedId(r.history_id))
|
|
177
|
+
.filter((r) => sameGalaxyServer(r.galaxy_server_url, serverUrl))
|
|
178
|
+
.sort((a, b) => a.created_at.localeCompare(b.created_at))
|
|
179
|
+
.pop();
|
|
180
|
+
if (submitted) return { historyId: submitted.history_id, source: "submission" };
|
|
181
|
+
const binding = findGalaxyPageBlocks(content)
|
|
182
|
+
.filter((b) => sameGalaxyServer(b.galaxyServerUrl, serverUrl))
|
|
183
|
+
.pop();
|
|
184
|
+
if (binding?.historyId && isGalaxyEncodedId(binding.historyId)) {
|
|
185
|
+
return { historyId: binding.historyId, source: "page_binding" };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* When this analysis started, for a reconcile with no cursor yet. The cursor
|
|
192
|
+
* is written on the first session start, so in the normal course this only
|
|
193
|
+
* runs then and the answer is "now". An analysis that predates the cursor
|
|
194
|
+
* starts at its earliest protected submission record. Never from the notebook
|
|
195
|
+
* or the activity log: both are the agent's to edit, and an earlier start
|
|
196
|
+
* widens the window to work that was never this analysis's.
|
|
197
|
+
*/
|
|
198
|
+
export function inferAnalysisStart(records: readonly AttemptRecord[], now: number): number {
|
|
199
|
+
let earliest = now;
|
|
200
|
+
for (const r of records) {
|
|
201
|
+
if (r.origin !== "submission") continue;
|
|
202
|
+
const t = Date.parse(r.created_at);
|
|
203
|
+
if (Number.isFinite(t) && t < earliest) earliest = t;
|
|
204
|
+
}
|
|
205
|
+
return earliest;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A label for a run nobody named. Galaxy's tool id when it is one clean token. */
|
|
209
|
+
function jobLabel(job: GalaxyJobListing): string {
|
|
210
|
+
const tool = idToken(job.tool_id);
|
|
211
|
+
return tool
|
|
212
|
+
? `${tool.split("/").slice(-2, -1)[0] ?? tool} (found on Galaxy)`
|
|
213
|
+
: "job found on Galaxy";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
217
|
+
// The reconcile
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export interface ReconcileResult {
|
|
221
|
+
historyId: string | null;
|
|
222
|
+
since: string | null;
|
|
223
|
+
unattributed: { kind: "invocation" | "job"; id: string; attemptId: string }[];
|
|
224
|
+
unverified: { kind: "invocation" | "job"; id: string }[];
|
|
225
|
+
/** In-flight blocks Galaxy says have ended; handed to the poller. */
|
|
226
|
+
terminalSeen: number;
|
|
227
|
+
/** Blocks recorded before enrichment existed, now given an attempt and a pending state. */
|
|
228
|
+
adopted?: number;
|
|
229
|
+
/** Why the listing half did not run, when it didn't. */
|
|
230
|
+
skipped?: string;
|
|
231
|
+
/** Standalone jobs were held back; the cursor stays where it was. */
|
|
232
|
+
incomplete?: boolean;
|
|
233
|
+
error?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ids checked this process; a block is verified once per session, not every run. */
|
|
237
|
+
const verifiedThisSession = new Set<string>();
|
|
238
|
+
const reportedIncomplete = new Set<string>();
|
|
239
|
+
|
|
240
|
+
export function resetReconcileState(): void {
|
|
241
|
+
verifiedThisSession.clear();
|
|
242
|
+
reportedIncomplete.clear();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Ids already on the record: every block's own id, and every job an attempt's
|
|
247
|
+
* protected record names. Not the blocks' `jobs` summaries -- those are
|
|
248
|
+
* notebook text, and a planted summary would otherwise hide a real run from
|
|
249
|
+
* reconcile for good.
|
|
250
|
+
*/
|
|
251
|
+
function claimedIds(
|
|
252
|
+
content: string,
|
|
253
|
+
records: readonly AttemptRecord[],
|
|
254
|
+
serverUrl: string,
|
|
255
|
+
): { invocations: Set<string>; jobs: Set<string> } {
|
|
256
|
+
const invocations = new Set<string>();
|
|
257
|
+
const jobs = new Set<string>();
|
|
258
|
+
for (const block of findInvocationBlocks(content)) invocations.add(block.invocationId);
|
|
259
|
+
for (const block of findJobBlocks(content)) jobs.add(block.jobId);
|
|
260
|
+
for (const r of records) {
|
|
261
|
+
if (!sameGalaxyServer(r.galaxy_server_url, serverUrl)) continue;
|
|
262
|
+
if (r.ids.invocation_id) invocations.add(r.ids.invocation_id);
|
|
263
|
+
for (const id of r.ids.job_ids ?? []) jobs.add(id);
|
|
264
|
+
for (const id of Object.keys(r.jobs)) jobs.add(id);
|
|
265
|
+
}
|
|
266
|
+
return { invocations, jobs };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* List what Galaxy has, decide what is unclaimed. Pure apart from the deps,
|
|
271
|
+
* so the decisions are testable without a notebook on disk.
|
|
272
|
+
*/
|
|
273
|
+
export async function surveyHistory(
|
|
274
|
+
content: string,
|
|
275
|
+
serverUrl: string,
|
|
276
|
+
historyId: string,
|
|
277
|
+
sinceMs: number,
|
|
278
|
+
deps: ReconcileDeps,
|
|
279
|
+
records: readonly AttemptRecord[] = [],
|
|
280
|
+
): Promise<{
|
|
281
|
+
newInvocations: { id: string; create_time?: string; workflow_id?: string }[];
|
|
282
|
+
newJobs: GalaxyJobListing[];
|
|
283
|
+
seen: { invocations: Map<string, string | undefined>; jobs: Map<string, string | undefined> };
|
|
284
|
+
newestCreate: number;
|
|
285
|
+
/** Set when step jobs could not be told apart from standalone ones. */
|
|
286
|
+
jobsWithheld?: string;
|
|
287
|
+
/** Set when a listing may have been cut short; the cursor must not move past it. */
|
|
288
|
+
incomplete?: string;
|
|
289
|
+
}> {
|
|
290
|
+
const claimed = claimedIds(content, records, serverUrl);
|
|
291
|
+
// A row Galaxy says belongs to another history is not this history's work,
|
|
292
|
+
// whatever list it arrived in.
|
|
293
|
+
const inHistory = (h: string | undefined) => !h || h === historyId;
|
|
294
|
+
let newestCreate = NaN;
|
|
295
|
+
const note = (t: string | undefined) => {
|
|
296
|
+
const ms = parseGalaxyTime(t);
|
|
297
|
+
if (Number.isFinite(ms) && !(ms <= newestCreate)) newestCreate = ms;
|
|
298
|
+
};
|
|
299
|
+
const inWindow = (t: string | undefined) => {
|
|
300
|
+
const ms = parseGalaxyTime(t);
|
|
301
|
+
// No usable time: keep it. Dropping it would hide work, and the block-id
|
|
302
|
+
// check still stops it being written twice.
|
|
303
|
+
return !Number.isFinite(ms) || ms >= sinceMs;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const rawInvocations = await deps.listInvocations(historyId, INVOCATION_LISTING_LIMIT);
|
|
307
|
+
let incomplete: string | undefined;
|
|
308
|
+
// galaxy-ops' listing has no offset, so a full page may not be all of them.
|
|
309
|
+
if (rawInvocations.length >= INVOCATION_LISTING_LIMIT) {
|
|
310
|
+
incomplete = `the history has at least ${INVOCATION_LISTING_LIMIT} invocations; only the newest were listed`;
|
|
311
|
+
}
|
|
312
|
+
const listed = rawInvocations.filter((i) => isGalaxyEncodedId(i.id) && inHistory(i.history_id));
|
|
313
|
+
const seenInvocations = new Map<string, string | undefined>();
|
|
314
|
+
for (const inv of listed) {
|
|
315
|
+
seenInvocations.set(inv.id, inv.state);
|
|
316
|
+
note(inv.create_time);
|
|
317
|
+
}
|
|
318
|
+
const newInvocations = listed.filter(
|
|
319
|
+
(i) => inWindow(i.create_time) && !claimed.invocations.has(i.id),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const jobs: GalaxyJobListing[] = [];
|
|
323
|
+
const sinceDay = iso(sinceMs).slice(0, 10);
|
|
324
|
+
for (let page = 0; ; page++) {
|
|
325
|
+
if (page >= MAX_JOB_PAGES) {
|
|
326
|
+
incomplete = `more than ${MAX_JOB_PAGES * JOB_PAGE_SIZE} jobs since ${sinceDay}`;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
const rows = await deps.listJobs({
|
|
330
|
+
historyId,
|
|
331
|
+
sinceDay,
|
|
332
|
+
limit: JOB_PAGE_SIZE,
|
|
333
|
+
offset: page * JOB_PAGE_SIZE,
|
|
334
|
+
});
|
|
335
|
+
jobs.push(...rows.filter((r) => inHistory(r.history_id)));
|
|
336
|
+
if (rows.length < JOB_PAGE_SIZE) break;
|
|
337
|
+
}
|
|
338
|
+
const seenJobs = new Map<string, string | undefined>();
|
|
339
|
+
for (const job of jobs) {
|
|
340
|
+
if (!isGalaxyEncodedId(job.id)) continue;
|
|
341
|
+
seenJobs.set(job.id, job.state);
|
|
342
|
+
note(job.create_time);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// A workflow's step jobs are in the job listing too, and they belong to the
|
|
346
|
+
// invocation, not to a standalone block of their own. Every invocation that
|
|
347
|
+
// could own a job in this window is walked: the new ones, the ones still
|
|
348
|
+
// scheduling, and the recorded ones whose job list isn't on the block yet.
|
|
349
|
+
const knownJobs = new Map(
|
|
350
|
+
records.filter((r) => r.ids.invocation_id).map((r) => [r.ids.invocation_id!, r]),
|
|
351
|
+
);
|
|
352
|
+
const recordedInvocations = findInvocationBlocks(content).filter((b) => {
|
|
353
|
+
if (!sameGalaxyServer(b.galaxyServerUrl, serverUrl)) return false;
|
|
354
|
+
if (b.status === "in_progress") return true;
|
|
355
|
+
// Its jobs are already claimed once its protected record lists them.
|
|
356
|
+
const r = knownJobs.get(b.invocationId);
|
|
357
|
+
return !r || Object.keys(r.jobs).length === 0;
|
|
358
|
+
});
|
|
359
|
+
const toWalk = new Set<string>([
|
|
360
|
+
...newInvocations.map((i) => i.id),
|
|
361
|
+
...listed.filter((i) => !INVOCATION_SCHEDULING_DONE.has(i.state ?? "")).map((i) => i.id),
|
|
362
|
+
...recordedInvocations.map((b) => b.invocationId).filter((id) => isGalaxyEncodedId(id)),
|
|
363
|
+
]);
|
|
364
|
+
const stepJobs = new Set<string>(claimed.jobs);
|
|
365
|
+
let jobsWithheld: string | undefined;
|
|
366
|
+
for (const id of toWalk) {
|
|
367
|
+
try {
|
|
368
|
+
const inv = await deps.getInvocation(id);
|
|
369
|
+
if (!inv || inv.id !== id) throw new Error(`Galaxy answered for a different invocation`);
|
|
370
|
+
for (const job of await walkInvocationJobs(inv, deps.getInvocation)) stepJobs.add(job.jobId);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// Without this invocation's jobs, a job in the listing could be one of
|
|
373
|
+
// its steps, and writing that as a standalone unattributed run would be
|
|
374
|
+
// a wrong record. Hold the jobs back until the walk succeeds.
|
|
375
|
+
jobsWithheld = `could not list the jobs of invocation ${id}: ${err instanceof Error ? err.message : String(err)}`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// A cut-short listing means an invocation we did not see could own any of
|
|
380
|
+
// these, so none of them is written as standalone this run.
|
|
381
|
+
const newJobs =
|
|
382
|
+
jobsWithheld || incomplete
|
|
383
|
+
? []
|
|
384
|
+
: jobs.filter(
|
|
385
|
+
(j) =>
|
|
386
|
+
isGalaxyEncodedId(j.id) &&
|
|
387
|
+
inWindow(j.create_time) &&
|
|
388
|
+
!claimed.jobs.has(j.id) &&
|
|
389
|
+
!stepJobs.has(j.id),
|
|
390
|
+
);
|
|
391
|
+
return {
|
|
392
|
+
newInvocations,
|
|
393
|
+
newJobs,
|
|
394
|
+
seen: { invocations: seenInvocations, jobs: seenJobs },
|
|
395
|
+
newestCreate,
|
|
396
|
+
...(jobsWithheld ? { jobsWithheld } : {}),
|
|
397
|
+
...(incomplete ? { incomplete } : {}),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function record(dir: string, kind: string, payload: Record<string, unknown>): void {
|
|
402
|
+
appendActivityEvent(dir, {
|
|
403
|
+
timestamp: new Date().toISOString(),
|
|
404
|
+
kind,
|
|
405
|
+
source: "galaxy-reconcile",
|
|
406
|
+
payload,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Read-modify-write the notebook under the lock, retrying a lost compare-and-swap. */
|
|
411
|
+
async function casNotebook(nbPath: string, apply: (content: string) => string): Promise<void> {
|
|
412
|
+
await withNotebookLock(nbPath, async () => {
|
|
413
|
+
for (let attempt = 0; ; attempt++) {
|
|
414
|
+
const stamp = await statNotebook(nbPath);
|
|
415
|
+
const content = await readNotebook(nbPath);
|
|
416
|
+
const next = apply(content);
|
|
417
|
+
if (next === content) return;
|
|
418
|
+
try {
|
|
419
|
+
await writeNotebook(nbPath, next, stamp ?? undefined);
|
|
420
|
+
return;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (!(err instanceof NotebookChangedError) || attempt >= 2) throw err;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Reconcile the notebook against one history, since `sinceMs`. Exported as the
|
|
430
|
+
* unit the brief names: `reconcile(historyId, since)`.
|
|
431
|
+
*/
|
|
432
|
+
export async function reconcile(
|
|
433
|
+
historyId: string,
|
|
434
|
+
sinceMs: number,
|
|
435
|
+
options: {
|
|
436
|
+
trigger: ReconcileTrigger;
|
|
437
|
+
deps?: Partial<ReconcileDeps>;
|
|
438
|
+
historySource?: "submission" | "page_binding";
|
|
439
|
+
},
|
|
440
|
+
): Promise<ReconcileResult & { newestCreate: number }> {
|
|
441
|
+
const deps: ReconcileDeps = { ...defaultDeps, ...options.deps };
|
|
442
|
+
const nbPath = getNotebookPath();
|
|
443
|
+
const server = getGalaxyConfig()?.url;
|
|
444
|
+
const result: ReconcileResult & { newestCreate: number } = {
|
|
445
|
+
historyId,
|
|
446
|
+
since: iso(sinceMs),
|
|
447
|
+
unattributed: [],
|
|
448
|
+
unverified: [],
|
|
449
|
+
terminalSeen: 0,
|
|
450
|
+
newestCreate: NaN,
|
|
451
|
+
};
|
|
452
|
+
if (!nbPath || !server) return { ...result, skipped: "no notebook or no Galaxy credentials" };
|
|
453
|
+
const dir = path.dirname(nbPath);
|
|
454
|
+
|
|
455
|
+
result.adopted = await adoptRecordedBlocks(nbPath, server, options.trigger);
|
|
456
|
+
const content = await readNotebook(nbPath);
|
|
457
|
+
const records = await listAttemptRecords(dir);
|
|
458
|
+
const survey = await surveyHistory(content, server, historyId, sinceMs, deps, records);
|
|
459
|
+
result.newestCreate = survey.newestCreate;
|
|
460
|
+
// Blocks written from fixture answers make no claim a server never made,
|
|
461
|
+
// the same rule the submission replay seam follows.
|
|
462
|
+
const fromFixture = isGalaxyFetchOverridden();
|
|
463
|
+
|
|
464
|
+
// What Galaxy says has ended for a block still in flight on disk goes to the
|
|
465
|
+
// poller, which owns transitions.
|
|
466
|
+
for (const block of findJobBlocks(content)) {
|
|
467
|
+
const state = survey.seen.jobs.get(block.jobId);
|
|
468
|
+
if (block.status === "in_progress" && isTerminalJobState(state)) result.terminalSeen++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Write every unclaimed run in one locked compare-and-swap. The presence
|
|
472
|
+
// checks are repeated against the bytes being replaced, so a submission
|
|
473
|
+
// the hook recorded in the meantime is not written a second time.
|
|
474
|
+
const pending = [
|
|
475
|
+
...survey.newInvocations.map((inv) => ({
|
|
476
|
+
kind: "invocation" as const,
|
|
477
|
+
inv,
|
|
478
|
+
attemptId: ulid(),
|
|
479
|
+
})),
|
|
480
|
+
...survey.newJobs.map((job) => ({ kind: "job" as const, job, attemptId: ulid() })),
|
|
481
|
+
];
|
|
482
|
+
const written: typeof pending = [];
|
|
483
|
+
if (pending.length > 0) {
|
|
484
|
+
await casNotebook(nbPath, (current) => {
|
|
485
|
+
written.length = 0;
|
|
486
|
+
let next = current;
|
|
487
|
+
for (const item of pending) {
|
|
488
|
+
const harness: HarnessBlockFields = {
|
|
489
|
+
attemptId: item.attemptId,
|
|
490
|
+
historyId,
|
|
491
|
+
submittedBy: "unknown",
|
|
492
|
+
enrichment: "pending",
|
|
493
|
+
enrichmentAttempts: 0,
|
|
494
|
+
};
|
|
495
|
+
if (item.kind === "invocation") {
|
|
496
|
+
if (locateInvocationBlock(next, item.inv.id).present) continue;
|
|
497
|
+
const created = parseGalaxyTime(item.inv.create_time);
|
|
498
|
+
const workflow = idToken(item.inv.workflow_id);
|
|
499
|
+
const block: InvocationYaml = {
|
|
500
|
+
invocationId: item.inv.id,
|
|
501
|
+
galaxyServerUrl: server,
|
|
502
|
+
notebookAnchor: UNATTRIBUTED,
|
|
503
|
+
label: workflow ? `workflow ${workflow} (found on Galaxy)` : "workflow found on Galaxy",
|
|
504
|
+
submittedAt: Number.isFinite(created) ? iso(created) : iso(deps.now()),
|
|
505
|
+
// Whether it has ended is the poller's call, not a listing's.
|
|
506
|
+
status: "in_progress",
|
|
507
|
+
...(fromFixture ? {} : { serverVerified: true }),
|
|
508
|
+
};
|
|
509
|
+
next = upsertInvocationBlock(next, block, harness);
|
|
510
|
+
} else {
|
|
511
|
+
if (locateJobBlock(next, item.job.id).present) continue;
|
|
512
|
+
const created = parseGalaxyTime(item.job.create_time);
|
|
513
|
+
const toolId = idToken(item.job.tool_id);
|
|
514
|
+
const terminal = isTerminalJobState(item.job.state);
|
|
515
|
+
const block: JobYaml = {
|
|
516
|
+
jobId: item.job.id,
|
|
517
|
+
galaxyServerUrl: server,
|
|
518
|
+
notebookAnchor: UNATTRIBUTED,
|
|
519
|
+
label: jobLabel(item.job),
|
|
520
|
+
...(toolId ? { toolId } : {}),
|
|
521
|
+
submittedAt: Number.isFinite(created) ? iso(created) : iso(deps.now()),
|
|
522
|
+
// Already over when we found it: written as such, the way capture
|
|
523
|
+
// writes a job that finished before its call returned, so the
|
|
524
|
+
// poller does not "discover" it ended and wake the agent for it.
|
|
525
|
+
...(terminal
|
|
526
|
+
? { status: jobStatusFromGalaxyState(item.job.state), galaxyState: item.job.state }
|
|
527
|
+
: { status: "in_progress" as const }),
|
|
528
|
+
...(fromFixture ? {} : { serverVerified: true }),
|
|
529
|
+
};
|
|
530
|
+
const version = idToken(item.job.tool_version);
|
|
531
|
+
next = upsertJobBlock(next, block, {
|
|
532
|
+
...harness,
|
|
533
|
+
jobs: [
|
|
534
|
+
{
|
|
535
|
+
jobId: item.job.id,
|
|
536
|
+
...(toolId ? { toolId } : {}),
|
|
537
|
+
...(version ? { toolVersion: version } : {}),
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
written.push(item);
|
|
543
|
+
}
|
|
544
|
+
return next;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
for (const item of written) {
|
|
549
|
+
const id = item.kind === "invocation" ? item.inv.id : item.job.id;
|
|
550
|
+
try {
|
|
551
|
+
await ensureAttemptRecord(dir, {
|
|
552
|
+
attemptId: item.attemptId,
|
|
553
|
+
kind: item.kind === "invocation" ? "invocation" : "jobs",
|
|
554
|
+
galaxyServerUrl: server,
|
|
555
|
+
historyId,
|
|
556
|
+
submittedBy: "unknown",
|
|
557
|
+
ids: item.kind === "invocation" ? { invocation_id: id } : { job_ids: [id] },
|
|
558
|
+
origin: "reconcile",
|
|
559
|
+
...(item.kind === "job"
|
|
560
|
+
? {
|
|
561
|
+
seeds: {
|
|
562
|
+
[id]: {
|
|
563
|
+
...(idToken(item.job.tool_id) ? { tool_id: idToken(item.job.tool_id) } : {}),
|
|
564
|
+
...(idToken(item.job.tool_version)
|
|
565
|
+
? { tool_version: idToken(item.job.tool_version) }
|
|
566
|
+
: {}),
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
}
|
|
570
|
+
: {}),
|
|
571
|
+
});
|
|
572
|
+
} catch (err) {
|
|
573
|
+
// Enrichment creates it later from the block; the run is recorded either way.
|
|
574
|
+
console.error("[reconcile] provenance record not written:", err);
|
|
575
|
+
}
|
|
576
|
+
result.unattributed.push({ kind: item.kind, id, attemptId: item.attemptId });
|
|
577
|
+
record(dir, "reconcile.unattributed", {
|
|
578
|
+
block_kind: item.kind,
|
|
579
|
+
id,
|
|
580
|
+
attempt_id: item.attemptId,
|
|
581
|
+
history_id: historyId,
|
|
582
|
+
history_source: options.historySource ?? null,
|
|
583
|
+
trigger: options.trigger,
|
|
584
|
+
...(item.kind === "job"
|
|
585
|
+
? { tool_id: idToken(item.job.tool_id) ?? null, galaxy_state: item.job.state ?? null }
|
|
586
|
+
: { workflow_id: idToken(item.inv.workflow_id) ?? null }),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (survey.jobsWithheld || survey.incomplete) {
|
|
591
|
+
result.error = survey.jobsWithheld ?? survey.incomplete;
|
|
592
|
+
result.incomplete = true;
|
|
593
|
+
}
|
|
594
|
+
// Said once per session per reason: a history past galaxy-ops' listing cap
|
|
595
|
+
// stays that way, and a row every five minutes would bury everything else.
|
|
596
|
+
if (result.error && !reportedIncomplete.has(result.error)) {
|
|
597
|
+
reportedIncomplete.add(result.error);
|
|
598
|
+
record(dir, "reconcile.incomplete", {
|
|
599
|
+
history_id: historyId,
|
|
600
|
+
trigger: options.trigger,
|
|
601
|
+
reason: result.error,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// The existence check: once per block per session, on the triggers where a
|
|
606
|
+
// person or a reconnect is asking, not on the background cadence.
|
|
607
|
+
if (options.trigger !== "tick") {
|
|
608
|
+
result.unverified = await downgradeAbsent(nbPath, server, survey.seen, deps, options.trigger);
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Bring blocks that predate enrichment into it: an agent-recorded block, or
|
|
615
|
+
* one from before capture, has no `enrichment` and no attempt id, so nothing
|
|
616
|
+
* would ever fetch its details -- and without its job list on the block,
|
|
617
|
+
* every reconcile would have to walk it again to tell its step jobs from
|
|
618
|
+
* standalone ones. It gets a fresh attempt id and `enrichment: pending`.
|
|
619
|
+
* `submitted_by` is left as it is: who submitted it is not something this
|
|
620
|
+
* learns.
|
|
621
|
+
*/
|
|
622
|
+
async function adoptRecordedBlocks(
|
|
623
|
+
nbPath: string,
|
|
624
|
+
server: string,
|
|
625
|
+
trigger: ReconcileTrigger,
|
|
626
|
+
): Promise<number> {
|
|
627
|
+
const adopted: { kind: "invocation" | "job"; id: string; attemptId: string }[] = [];
|
|
628
|
+
await casNotebook(nbPath, (current) => {
|
|
629
|
+
adopted.length = 0;
|
|
630
|
+
let next = current;
|
|
631
|
+
for (const b of findInvocationBlocks(current)) {
|
|
632
|
+
if (b.enrichment || b.attemptId || !sameGalaxyServer(b.galaxyServerUrl, server)) continue;
|
|
633
|
+
const attemptId = ulid();
|
|
634
|
+
next = upsertInvocationBlock(next, b, {
|
|
635
|
+
attemptId,
|
|
636
|
+
enrichment: "pending",
|
|
637
|
+
enrichmentAttempts: 0,
|
|
638
|
+
});
|
|
639
|
+
adopted.push({ kind: "invocation", id: b.invocationId, attemptId });
|
|
640
|
+
}
|
|
641
|
+
for (const b of findJobBlocks(current)) {
|
|
642
|
+
if (b.enrichment || b.attemptId || !sameGalaxyServer(b.galaxyServerUrl, server)) continue;
|
|
643
|
+
const attemptId = ulid();
|
|
644
|
+
next = upsertJobBlock(next, b, { attemptId, enrichment: "pending", enrichmentAttempts: 0 });
|
|
645
|
+
adopted.push({ kind: "job", id: b.jobId, attemptId });
|
|
646
|
+
}
|
|
647
|
+
return next;
|
|
648
|
+
});
|
|
649
|
+
const dir = path.dirname(nbPath);
|
|
650
|
+
for (const a of adopted) {
|
|
651
|
+
record(dir, "reconcile.adopted", {
|
|
652
|
+
block_kind: a.kind,
|
|
653
|
+
id: a.id,
|
|
654
|
+
attempt_id: a.attemptId,
|
|
655
|
+
trigger,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return adopted.length;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function downgradeAbsent(
|
|
662
|
+
nbPath: string,
|
|
663
|
+
server: string,
|
|
664
|
+
seen: { invocations: Map<string, unknown>; jobs: Map<string, unknown> },
|
|
665
|
+
deps: ReconcileDeps,
|
|
666
|
+
trigger: ReconcileTrigger,
|
|
667
|
+
): Promise<{ kind: "invocation" | "job"; id: string }[]> {
|
|
668
|
+
const content = await readNotebook(nbPath);
|
|
669
|
+
const candidates: { kind: "invocation" | "job"; id: string }[] = [];
|
|
670
|
+
for (const b of findInvocationBlocks(content)) {
|
|
671
|
+
if (b.serverVerified === false || seen.invocations.has(b.invocationId)) continue;
|
|
672
|
+
if (!sameGalaxyServer(b.galaxyServerUrl, server)) continue;
|
|
673
|
+
candidates.push({ kind: "invocation", id: b.invocationId });
|
|
674
|
+
}
|
|
675
|
+
for (const b of findJobBlocks(content)) {
|
|
676
|
+
if (b.serverVerified === false || seen.jobs.has(b.jobId)) continue;
|
|
677
|
+
if (!sameGalaxyServer(b.galaxyServerUrl, server)) continue;
|
|
678
|
+
candidates.push({ kind: "job", id: b.jobId });
|
|
679
|
+
}
|
|
680
|
+
const absent: { kind: "invocation" | "job"; id: string; detail: string }[] = [];
|
|
681
|
+
let checked = 0;
|
|
682
|
+
for (const c of candidates) {
|
|
683
|
+
const key = `${c.kind}:${c.id}`;
|
|
684
|
+
if (verifiedThisSession.has(key)) continue;
|
|
685
|
+
if (checked >= MAX_VERIFY_PER_RUN) break;
|
|
686
|
+
checked++;
|
|
687
|
+
const verdict = await deps.verify(c.kind, c.id);
|
|
688
|
+
// Unreachable is not a verdict: leave the block and ask next session.
|
|
689
|
+
if (verdict.outcome === "unreachable") continue;
|
|
690
|
+
verifiedThisSession.add(key);
|
|
691
|
+
if (verdict.outcome === "absent") absent.push({ ...c, detail: verdict.detail });
|
|
692
|
+
}
|
|
693
|
+
if (absent.length === 0) return [];
|
|
694
|
+
|
|
695
|
+
const downgraded: typeof absent = [];
|
|
696
|
+
await casNotebook(nbPath, (current) => {
|
|
697
|
+
downgraded.length = 0;
|
|
698
|
+
let next = current;
|
|
699
|
+
for (const a of absent) {
|
|
700
|
+
if (a.kind === "invocation") {
|
|
701
|
+
const rec = locateInvocationBlock(next, a.id).record;
|
|
702
|
+
if (!rec || rec.serverVerified === false) continue;
|
|
703
|
+
next = upsertInvocationBlock(next, { ...rec, serverVerified: false });
|
|
704
|
+
} else {
|
|
705
|
+
const rec = locateJobBlock(next, a.id).record;
|
|
706
|
+
if (!rec || rec.serverVerified === false) continue;
|
|
707
|
+
next = upsertJobBlock(next, { ...rec, serverVerified: false });
|
|
708
|
+
}
|
|
709
|
+
downgraded.push(a);
|
|
710
|
+
}
|
|
711
|
+
return next;
|
|
712
|
+
});
|
|
713
|
+
const dir = path.dirname(nbPath);
|
|
714
|
+
for (const a of downgraded) {
|
|
715
|
+
record(dir, "reconcile.unverified", {
|
|
716
|
+
block_kind: a.kind,
|
|
717
|
+
id: a.id,
|
|
718
|
+
trigger,
|
|
719
|
+
detail: a.detail,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return downgraded.map(({ kind, id }) => ({ kind, id }));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
726
|
+
// Running it
|
|
727
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Resolve the history and the window, reconcile, and move the cursor. The
|
|
731
|
+
* cursor only moves forward, and only on a run whose listing succeeded.
|
|
732
|
+
*/
|
|
733
|
+
export async function runReconcile(
|
|
734
|
+
trigger: ReconcileTrigger,
|
|
735
|
+
deps?: Partial<ReconcileDeps>,
|
|
736
|
+
): Promise<ReconcileResult> {
|
|
737
|
+
const nbPath = getNotebookPath();
|
|
738
|
+
const server = getGalaxyConfig()?.url;
|
|
739
|
+
const empty: ReconcileResult = {
|
|
740
|
+
historyId: null,
|
|
741
|
+
since: null,
|
|
742
|
+
unattributed: [],
|
|
743
|
+
unverified: [],
|
|
744
|
+
terminalSeen: 0,
|
|
745
|
+
};
|
|
746
|
+
if (!nbPath || !server) return { ...empty, skipped: "no notebook or no Galaxy credentials" };
|
|
747
|
+
const dir = path.dirname(nbPath);
|
|
748
|
+
const now = (deps?.now ?? defaultDeps.now)();
|
|
749
|
+
|
|
750
|
+
let content: string;
|
|
751
|
+
try {
|
|
752
|
+
content = await readNotebook(nbPath);
|
|
753
|
+
} catch {
|
|
754
|
+
return { ...empty, skipped: "notebook unreadable" };
|
|
755
|
+
}
|
|
756
|
+
const records = await listAttemptRecords(dir);
|
|
757
|
+
let bound = boundHistoryId(content, server, records);
|
|
758
|
+
|
|
759
|
+
let state: ReconcileState | null = await readReconcileState(dir);
|
|
760
|
+
let stateDirty = false;
|
|
761
|
+
if (!state) {
|
|
762
|
+
state = {
|
|
763
|
+
schema: 1,
|
|
764
|
+
analysis_started_at: iso(inferAnalysisStart(records, now)),
|
|
765
|
+
histories: {},
|
|
766
|
+
};
|
|
767
|
+
stateDirty = true;
|
|
768
|
+
}
|
|
769
|
+
// A page binding is notebook text, so it gets one say: the first history it
|
|
770
|
+
// names is sealed into the cursor, and an edit after that cannot point
|
|
771
|
+
// reconcile somewhere else. A harness submission still moves the binding,
|
|
772
|
+
// since that is a protected record of where the work actually went.
|
|
773
|
+
if (bound?.source === "page_binding") {
|
|
774
|
+
const sealed = state.bound_histories?.[server];
|
|
775
|
+
if (sealed) bound = { historyId: sealed, source: "page_binding" };
|
|
776
|
+
else {
|
|
777
|
+
state.bound_histories = { ...state.bound_histories, [server]: bound.historyId };
|
|
778
|
+
stateDirty = true;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (stateDirty) {
|
|
782
|
+
try {
|
|
783
|
+
await writeReconcileState(dir, state);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.error("[reconcile] cursor not written:", err);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const historyId = bound?.historyId ?? null;
|
|
789
|
+
|
|
790
|
+
if (!historyId) {
|
|
791
|
+
// Nothing to list, but the existence half still applies to what is recorded.
|
|
792
|
+
const unverified =
|
|
793
|
+
trigger === "tick"
|
|
794
|
+
? []
|
|
795
|
+
: await downgradeAbsent(
|
|
796
|
+
nbPath,
|
|
797
|
+
server,
|
|
798
|
+
{ invocations: new Map(), jobs: new Map() },
|
|
799
|
+
{ ...defaultDeps, ...deps },
|
|
800
|
+
trigger,
|
|
801
|
+
);
|
|
802
|
+
return { ...empty, unverified, skipped: "no history is bound to this analysis yet" };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const key = `${server}|${historyId}`;
|
|
806
|
+
const start = Date.parse(state.analysis_started_at);
|
|
807
|
+
const stamp = Date.parse(state.histories[key]?.stamp ?? "");
|
|
808
|
+
const sinceMs = Number.isFinite(stamp) ? Math.max(start, stamp - STAMP_MARGIN_MS) : start;
|
|
809
|
+
|
|
810
|
+
let result: ReconcileResult & { newestCreate: number };
|
|
811
|
+
try {
|
|
812
|
+
result = await reconcile(historyId, sinceMs, {
|
|
813
|
+
trigger,
|
|
814
|
+
deps,
|
|
815
|
+
historySource: bound?.source,
|
|
816
|
+
});
|
|
817
|
+
} catch (err) {
|
|
818
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
819
|
+
record(dir, "reconcile.failed", { history_id: historyId, trigger, error: message });
|
|
820
|
+
return { ...empty, historyId, since: iso(sinceMs), error: message };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (Number.isFinite(result.newestCreate) && !result.error && !result.incomplete) {
|
|
824
|
+
const prior = Number.isFinite(stamp) ? stamp : -Infinity;
|
|
825
|
+
if (result.newestCreate > prior) {
|
|
826
|
+
state.histories[key] = { stamp: iso(result.newestCreate) };
|
|
827
|
+
try {
|
|
828
|
+
await writeReconcileState(dir, state);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.error("[reconcile] cursor not written:", err);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (result.terminalSeen > 0) void pollGalaxyNow();
|
|
836
|
+
const { newestCreate: _newest, ...rest } = result;
|
|
837
|
+
return rest;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Everything capture does after the submission, in one queue: reconcile, then
|
|
842
|
+
* enrichment. One queue rather than independent timers because the two read
|
|
843
|
+
* and write the same blocks, and because a `/reconcile` arriving while the
|
|
844
|
+
* session-start run is still going must run after it, not race it.
|
|
845
|
+
*/
|
|
846
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
847
|
+
let queued = 0;
|
|
848
|
+
|
|
849
|
+
function enqueue<T>(work: () => Promise<T>): Promise<T> {
|
|
850
|
+
queued++;
|
|
851
|
+
const run = queue.then(work, work).finally(() => {
|
|
852
|
+
queued--;
|
|
853
|
+
});
|
|
854
|
+
queue = run.catch(() => undefined);
|
|
855
|
+
return run;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function followThrough(trigger: ReconcileTrigger | "enrich"): Promise<{
|
|
859
|
+
reconcile?: ReconcileResult;
|
|
860
|
+
enrichment: Awaited<ReturnType<typeof runEnrichmentPass>>;
|
|
861
|
+
evaluation?: EvaluationPass;
|
|
862
|
+
}> {
|
|
863
|
+
return enqueue(async () => {
|
|
864
|
+
const reconciled = trigger === "enrich" ? undefined : await runReconcile(trigger);
|
|
865
|
+
const enrichment = await runEnrichmentPass({ force: trigger === "command" });
|
|
866
|
+
// Last, so it reads what this pass just learned. A failure here must not
|
|
867
|
+
// cost the reconcile and enrichment results above.
|
|
868
|
+
let evaluation: EvaluationPass | undefined;
|
|
869
|
+
try {
|
|
870
|
+
evaluation = await evaluateRegistryAttempts(trigger);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
console.error("[registry] evaluation pass failed:", err);
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
...(reconciled ? { reconcile: reconciled } : {}),
|
|
876
|
+
enrichment,
|
|
877
|
+
...(evaluation ? { evaluation } : {}),
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function summarize(result: {
|
|
883
|
+
reconcile?: ReconcileResult;
|
|
884
|
+
enrichment: Awaited<ReturnType<typeof runEnrichmentPass>>;
|
|
885
|
+
evaluation?: EvaluationPass;
|
|
886
|
+
}): string {
|
|
887
|
+
const r = result.reconcile;
|
|
888
|
+
const lines: string[] = [];
|
|
889
|
+
if (r?.skipped) lines.push(`Reconcile: ${r.skipped}.`);
|
|
890
|
+
else if (r?.error && r.unattributed.length === 0)
|
|
891
|
+
lines.push(`Reconcile did not finish: ${r.error}`);
|
|
892
|
+
else if (r) {
|
|
893
|
+
lines.push(
|
|
894
|
+
r.unattributed.length > 0
|
|
895
|
+
? `Found ${r.unattributed.length} run(s) in history ${r.historyId} that no block recorded; written as unattributed.`
|
|
896
|
+
: `Every run in history ${r.historyId} since ${r.since} is in the notebook.`,
|
|
897
|
+
);
|
|
898
|
+
if (r.error) lines.push(`Some jobs were held back: ${r.error}`);
|
|
899
|
+
}
|
|
900
|
+
if (r && r.unverified.length > 0) {
|
|
901
|
+
lines.push(
|
|
902
|
+
`${r.unverified.length} recorded run(s) are not on Galaxy any more; marked unverified.`,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
const e = result.enrichment;
|
|
906
|
+
const enriched = [
|
|
907
|
+
e.completed.length > 0 ? `${e.completed.length} enriched` : "",
|
|
908
|
+
e.retried.length > 0 ? `${e.retried.length} will retry` : "",
|
|
909
|
+
e.unavailable.length > 0 ? `${e.unavailable.length} unavailable` : "",
|
|
910
|
+
].filter(Boolean);
|
|
911
|
+
if (enriched.length > 0) lines.push(`Run details: ${enriched.join(", ")}.`);
|
|
912
|
+
const v = result.evaluation;
|
|
913
|
+
if (v && v.evaluated.length > 0) {
|
|
914
|
+
lines.push(`Re-evaluated ${v.evaluated.length} approved run(s) against Galaxy.`);
|
|
915
|
+
}
|
|
916
|
+
if (v && v.unreachable.length > 0) {
|
|
917
|
+
lines.push(`${v.unreachable.length} approved run(s) couldn't be checked; will try again.`);
|
|
918
|
+
}
|
|
919
|
+
return lines.join("\n");
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Register reconcile and enrichment: session start, the poller tick, and
|
|
924
|
+
* `/reconcile`.
|
|
925
|
+
*/
|
|
926
|
+
export function registerCaptureFollowThrough(pi: ExtensionAPI): void {
|
|
927
|
+
pi.on("session_start", async () => {
|
|
928
|
+
resetEnrichmentState();
|
|
929
|
+
resetReconcileState();
|
|
930
|
+
// Not awaited: startup does not wait on Galaxy. `/reconcile` queues
|
|
931
|
+
// behind it, so a person asking right away sees its result.
|
|
932
|
+
void followThrough("session_start").catch((err) => {
|
|
933
|
+
console.error("[reconcile] session start failed:", err);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
setCaptureTickHook(async (tickNumber) => {
|
|
938
|
+
// A tick never piles onto work already queued; the next one will come.
|
|
939
|
+
if (queued > 0) return;
|
|
940
|
+
await followThrough(tickNumber % RECONCILE_EVERY_N_TICKS === 0 ? "tick" : "enrich");
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
pi.registerCommand("reconcile", {
|
|
944
|
+
description:
|
|
945
|
+
"Check the bound Galaxy history against the notebook: record runs no block claims, and fetch details for finished runs now",
|
|
946
|
+
handler: async (_args, ctx) => {
|
|
947
|
+
const result = await followThrough("command");
|
|
948
|
+
const text = summarize(result) || "Nothing to reconcile.";
|
|
949
|
+
try {
|
|
950
|
+
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
951
|
+
} catch {
|
|
952
|
+
/* stale/headless context -- the activity rows still say what happened */
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
}
|