@aiwg/cli 2026.10.1 → 2026.10.4
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/README.md +2 -2
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3183 -24
- package/bin/aiwg.mjs +135 -47
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +78 -12
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +218 -5
- package/dist/src/cli/handlers/diagnose.js +13 -5
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +248 -30
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +169 -10
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +28 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +25 -11
- package/dist/src/cli/handlers/script-runner.js +19 -8
- package/dist/src/cli/handlers/session.js +406 -62
- package/dist/src/cli/handlers/sessions.js +243 -76
- package/dist/src/cli/handlers/setup-manifest.js +474 -79
- package/dist/src/cli/handlers/setup.js +180 -39
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +60 -3
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +307 -55
- package/dist/src/cli/handlers/utilities.js +125 -26
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +349 -35
- package/dist/src/config/cli.js +256 -23
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +38 -11
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +14 -7
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +25 -6
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +15 -14
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +942 -0
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +64 -18
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +15 -3
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +95 -5
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +366 -56
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +620 -85
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +17 -27
- package/dist/src/mcp/helpers.mjs +109 -4
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +248 -131
- package/dist/src/mcp/registry.mjs +169 -100
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +127 -82
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +22 -3
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +50 -2
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +647 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +76 -14
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3183 -24
- package/dist/src/models/model-discovery.js +931 -67
- package/dist/src/models/provider-policy.js +719 -11
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +261 -0
- package/dist/src/models/worker-direct-readiness.js +550 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +144 -0
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +986 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +430 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +53 -23
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +47 -12
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +558 -0
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +49 -0
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +43 -9
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +434 -24
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
- package/dist/src/sessions/adapters/muse.js +200 -104
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +30 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +25 -5
- package/dist/src/sessions/batch-import.js +72 -51
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +75 -2
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +84 -46
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +199 -63
- package/dist/src/sessions/workspace-discovery.js +315 -88
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +32 -2
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
- package/tools/_resolve-impl.mjs +39 -2
- package/tools/agents/deploy-agents.mjs +64 -23
- package/tools/agents/providers/base.mjs +153 -166
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +62 -49
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +360 -0
- package/tools/providers/omp-transport.mjs +194 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { signalChild } from '../process/signal-child.mjs';
|
|
2
|
+
import { hasSelectionRelevantConflicts, validateWorkerCatalogDiscovery } from './provider-policy.js';
|
|
1
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
2
5
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join, relative } from 'node:path';
|
|
8
|
+
import { constants } from 'node:fs';
|
|
5
9
|
import { homedir, tmpdir } from 'node:os';
|
|
10
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
6
11
|
export const PROVIDER_DISCOVERY_DECISIONS = {
|
|
7
12
|
claude: {
|
|
8
13
|
provider: 'claude',
|
|
@@ -14,8 +19,8 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
|
|
|
14
19
|
codex: {
|
|
15
20
|
provider: 'codex',
|
|
16
21
|
status: 'native',
|
|
17
|
-
interface: 'codex app-server model/list',
|
|
18
|
-
reason: 'The app-server JSON-RPC model/list method returns models available to the current Codex account without running a model turn.',
|
|
22
|
+
interface: 'codex app-server model/list; codex debug models (version-scoped fixture)',
|
|
23
|
+
reason: 'The app-server JSON-RPC model/list method returns models available to the current Codex account without running a model turn. The version-scoped codex debug models JSON shape normalizes through the same observation contract.',
|
|
19
24
|
documentation: 'https://developers.openai.com/codex/app-server/',
|
|
20
25
|
},
|
|
21
26
|
copilot: {
|
|
@@ -62,9 +67,9 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
|
|
|
62
67
|
},
|
|
63
68
|
muse: {
|
|
64
69
|
provider: 'muse',
|
|
65
|
-
status: '
|
|
66
|
-
interface:
|
|
67
|
-
reason: '
|
|
70
|
+
status: 'native',
|
|
71
|
+
interface: 'model-catalog JSON rows (~/.local/share/muse/model-catalog/*.json)',
|
|
72
|
+
reason: 'Bounded model-catalog JSON rows report configured model facts for the current Muse installation without probing model APIs or consuming tokens. Entitlement stays account-scoped and contributor data-use facts remain config-scoped, never row-derived.',
|
|
68
73
|
documentation: 'https://github.com/jmagly/aiwg/blob/main/docs/architecture/adr-muse-provider-target.md',
|
|
69
74
|
},
|
|
70
75
|
opencode: {
|
|
@@ -88,6 +93,13 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
|
|
|
88
93
|
reason: 'OpenHuman profiles accept semantic model hints but expose no standardized local model-list command.',
|
|
89
94
|
documentation: 'https://github.com/roctinam/openhuman',
|
|
90
95
|
},
|
|
96
|
+
ollama: {
|
|
97
|
+
provider: 'ollama',
|
|
98
|
+
status: 'native',
|
|
99
|
+
interface: 'GET /api/tags on the configured local endpoint',
|
|
100
|
+
reason: 'The read-only HTTP model-list endpoint reports the locally present models and per-model digests for the configured Ollama server without probing a remote registry or consuming inference tokens. Declared here (roctinam/aiwg#2899, B-OG-09); no fetcher is implemented against this map by this task — see the local-http worker-adapter registry entry and its adapter docstring for what is actually dispatchable today.',
|
|
101
|
+
documentation: 'https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models',
|
|
102
|
+
},
|
|
91
103
|
omp: { provider: 'omp', status: 'native', interface: 'omp models --json --no-extensions',
|
|
92
104
|
reason: 'OMP JSON catalog reports credential-available LLM models; ambient extensions are disabled by default.',
|
|
93
105
|
documentation: 'https://github.com/can1357/oh-my-pi/blob/5964a0f7649275bcde818f20073193fd032451f2/packages/coding-agent/src/commands/models.ts' },
|
|
@@ -115,6 +127,12 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
|
|
|
115
127
|
};
|
|
116
128
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
117
129
|
export function classifyDiscoveryError(message) {
|
|
130
|
+
message = stripVTControlCharacters(message);
|
|
131
|
+
if (/\b(?:EROFS|EACCES|EPERM)\b|read-only file system/i.test(message)
|
|
132
|
+
|| (/FileSystem\.open|permission denied|operation not permitted/i.test(message)
|
|
133
|
+
&& /[\\/]opencode[\\/]log[\\/]|[\\/]\.pi[\\/]agent(?:[\\/]|$)/i.test(message))) {
|
|
134
|
+
return 'environment';
|
|
135
|
+
}
|
|
118
136
|
if (/(?:401|403|unauth|forbidden|credential|login|required auth)/i.test(message)) {
|
|
119
137
|
return 'authentication';
|
|
120
138
|
}
|
|
@@ -128,7 +146,7 @@ const SECRET_ASSIGNMENT = /((?:api[_-]?key|authorization|token|secret|password|c
|
|
|
128
146
|
const BEARER_SECRET = /(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
|
|
129
147
|
/** Keep native diagnostics useful without ever echoing credential material. */
|
|
130
148
|
export function redactModelDiscoveryText(value) {
|
|
131
|
-
return value
|
|
149
|
+
return stripVTControlCharacters(value)
|
|
132
150
|
.replace(SECRET_ASSIGNMENT, '$1[REDACTED]')
|
|
133
151
|
.replace(BEARER_SECRET, '$1[REDACTED]')
|
|
134
152
|
.slice(0, 4096);
|
|
@@ -152,7 +170,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
|
|
|
152
170
|
child.stdout.on('data', chunk => { stdout += String(chunk); if (Buffer.byteLength(stdout) > 8 * 1024 * 1024) {
|
|
153
171
|
stdout = '';
|
|
154
172
|
stderr = 'Model catalog exceeded 8 MiB limit';
|
|
155
|
-
child
|
|
173
|
+
signalChild(child, 'SIGKILL');
|
|
156
174
|
finish(1);
|
|
157
175
|
} });
|
|
158
176
|
child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
|
|
@@ -163,7 +181,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
|
|
|
163
181
|
child.on('close', code => finish(code ?? 1));
|
|
164
182
|
const timer = setTimeout(() => {
|
|
165
183
|
stderr = `${stderr}${stderr ? '\n' : ''}${command} timed out`;
|
|
166
|
-
child
|
|
184
|
+
signalChild(child);
|
|
167
185
|
finish(124);
|
|
168
186
|
}, options.timeoutMs ?? 10_000);
|
|
169
187
|
});
|
|
@@ -178,7 +196,7 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
|
|
|
178
196
|
runner(command, ['models', '--pure'], { cwd: tmpdir(), timeoutMs: 15_000 }),
|
|
179
197
|
]);
|
|
180
198
|
if (result.exitCode !== 0) {
|
|
181
|
-
const error = result.stderr.trim() || `OpenCode models exited ${result.exitCode}`;
|
|
199
|
+
const error = redactModelDiscoveryText(result.stderr.trim()) || `OpenCode models exited ${result.exitCode}`;
|
|
182
200
|
return {
|
|
183
201
|
provider: 'opencode',
|
|
184
202
|
source: 'native',
|
|
@@ -190,10 +208,16 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
|
|
|
190
208
|
error,
|
|
191
209
|
};
|
|
192
210
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
211
|
+
const diagnostics = [];
|
|
212
|
+
const models = [...new Set(result.stdout.split(/\r?\n/).flatMap((raw, index) => {
|
|
213
|
+
const line = raw.trim();
|
|
214
|
+
if (!line)
|
|
215
|
+
return [];
|
|
216
|
+
if (/^[a-z0-9][a-z0-9._-]*\/\S+$/i.test(line))
|
|
217
|
+
return [line];
|
|
218
|
+
diagnostics.push(`OpenCode models line ${index + 1}: malformed provider/model row`);
|
|
219
|
+
return [];
|
|
220
|
+
}))].map(id => ({ id }));
|
|
197
221
|
return {
|
|
198
222
|
provider: 'opencode',
|
|
199
223
|
source: 'native',
|
|
@@ -201,8 +225,8 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
|
|
|
201
225
|
...(version ? { runtimeVersion: version } : {}),
|
|
202
226
|
accountScope: 'local-runtime',
|
|
203
227
|
models,
|
|
204
|
-
...(models.length === 0
|
|
205
|
-
? { errorKind: 'invalid-output', error: 'OpenCode returned no normalized provider/model rows.' }
|
|
228
|
+
...(diagnostics.length || models.length === 0
|
|
229
|
+
? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'OpenCode returned no normalized provider/model rows.' }
|
|
206
230
|
: {}),
|
|
207
231
|
};
|
|
208
232
|
}
|
|
@@ -213,22 +237,33 @@ export async function discoverPiModels(command = 'pi', runner = runModelDiscover
|
|
|
213
237
|
runner(command, ['--list-models'], { cwd: tmpdir(), timeoutMs: 15_000 }),
|
|
214
238
|
]);
|
|
215
239
|
if (result.exitCode !== 0) {
|
|
216
|
-
const error = result.stderr.trim() || `Pi --list-models exited ${result.exitCode}`;
|
|
240
|
+
const error = redactModelDiscoveryText(result.stderr.trim()) || `Pi --list-models exited ${result.exitCode}`;
|
|
217
241
|
return { provider: 'pi', source: 'native', observedAt,
|
|
218
242
|
...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime',
|
|
219
243
|
models: [], errorKind: classifyDiscoveryError(error), error };
|
|
220
244
|
}
|
|
221
|
-
const
|
|
222
|
-
const
|
|
245
|
+
const diagnostics = [];
|
|
246
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
247
|
+
let headerSeen = false;
|
|
248
|
+
const models = lines.flatMap((raw, index) => {
|
|
249
|
+
const line = raw.trim();
|
|
250
|
+
if (!line)
|
|
251
|
+
return [];
|
|
252
|
+
if (!headerSeen && /^provider\s+model(?:\s|$)/i.test(line)) {
|
|
253
|
+
headerSeen = true;
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
223
256
|
const columns = line.split(/\s{2,}/);
|
|
224
|
-
if (columns.length < 2 || !/^[a-z0-9][a-z0-9._-]*$/i.test(columns[0]))
|
|
257
|
+
if (columns.length < 2 || !/^[a-z0-9][a-z0-9._-]*$/i.test(columns[0]) || !/^\S+$/.test(columns[1])) {
|
|
258
|
+
diagnostics.push(`Pi --list-models line ${index + 1}: malformed provider/model row`);
|
|
225
259
|
return [];
|
|
260
|
+
}
|
|
226
261
|
return [{ id: `${columns[0]}/${columns[1]}` }];
|
|
227
262
|
});
|
|
228
263
|
return { provider: 'pi', source: 'native', observedAt,
|
|
229
264
|
...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime', models,
|
|
230
|
-
...(models.length === 0 ? { errorKind: 'invalid-output',
|
|
231
|
-
error: 'Pi returned no parseable provider/model rows.' } : {}) };
|
|
265
|
+
...(diagnostics.length || models.length === 0 ? { errorKind: 'invalid-output',
|
|
266
|
+
error: diagnostics.join('; ') || 'Pi returned no parseable provider/model rows.' } : {}) };
|
|
232
267
|
}
|
|
233
268
|
export async function discoverOpenClawModels(command = 'openclaw', runner = runModelDiscoveryCommand) {
|
|
234
269
|
const observedAt = new Date().toISOString();
|
|
@@ -237,7 +272,7 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
|
|
|
237
272
|
runner(command, ['models', 'list', '--json'], { timeoutMs: 15_000 }),
|
|
238
273
|
]);
|
|
239
274
|
if (result.exitCode !== 0) {
|
|
240
|
-
const error = result.stderr.trim() || `OpenClaw models list exited ${result.exitCode}`;
|
|
275
|
+
const error = redactModelDiscoveryText(result.stderr.trim()) || `OpenClaw models list exited ${result.exitCode}`;
|
|
241
276
|
return {
|
|
242
277
|
provider: 'openclaw',
|
|
243
278
|
source: 'native',
|
|
@@ -251,13 +286,25 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
|
|
|
251
286
|
}
|
|
252
287
|
try {
|
|
253
288
|
const payload = JSON.parse(result.stdout);
|
|
254
|
-
|
|
255
|
-
|
|
289
|
+
if (!Array.isArray(payload?.models))
|
|
290
|
+
throw new Error('expected /models array');
|
|
291
|
+
const diagnostics = [];
|
|
292
|
+
const models = payload.models.flatMap((model, index) => {
|
|
293
|
+
if (!model || typeof model !== 'object' || Array.isArray(model)
|
|
294
|
+
|| typeof model.key !== 'string' || !/^[^\s/]+\/\S+$/.test(model.key)
|
|
295
|
+
|| (model.available !== undefined && typeof model.available !== 'boolean')
|
|
296
|
+
|| (model.name !== undefined && typeof model.name !== 'string')
|
|
297
|
+
|| (model.tags !== undefined && (!Array.isArray(model.tags) || model.tags.some((tag) => typeof tag !== 'string')))) {
|
|
298
|
+
diagnostics.push(`OpenClaw /models/${index}: malformed model row`);
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
return model.available !== false ? [{
|
|
256
302
|
id: model.key,
|
|
257
303
|
...(typeof model.name === 'string' ? { displayName: model.name } : {}),
|
|
258
304
|
isDefault: Array.isArray(model.tags) && model.tags.includes('default'),
|
|
259
305
|
}]
|
|
260
|
-
|
|
306
|
+
: [];
|
|
307
|
+
});
|
|
261
308
|
return {
|
|
262
309
|
provider: 'openclaw',
|
|
263
310
|
source: 'native',
|
|
@@ -265,8 +312,8 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
|
|
|
265
312
|
...(version ? { runtimeVersion: version } : {}),
|
|
266
313
|
accountScope: 'local-runtime',
|
|
267
314
|
models,
|
|
268
|
-
...(models.length === 0
|
|
269
|
-
? { errorKind: 'invalid-output', error: 'OpenClaw returned no normalized model keys.' }
|
|
315
|
+
...(diagnostics.length || models.length === 0
|
|
316
|
+
? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'OpenClaw returned no normalized model keys.' }
|
|
270
317
|
: {}),
|
|
271
318
|
};
|
|
272
319
|
}
|
|
@@ -294,6 +341,8 @@ async function readCatalog(file) {
|
|
|
294
341
|
const parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
295
342
|
if (!parsed.version || !parsed.providers || typeof parsed.providers !== 'object')
|
|
296
343
|
return null;
|
|
344
|
+
if (validateWorkerCatalogDiscovery(parsed.discovery).length)
|
|
345
|
+
return null;
|
|
297
346
|
return parsed;
|
|
298
347
|
}
|
|
299
348
|
catch {
|
|
@@ -328,7 +377,27 @@ const STANDARD_FAMILY = /(?:^|[/:._-])(?:sonnet|terra)(?:$|[/:._[-]|\d)/i;
|
|
|
328
377
|
export function selectRoleModels(models) {
|
|
329
378
|
// Flagship families are user-elected only (epic #2642): discovery never
|
|
330
379
|
// assigns them to a role, even when the provider marks one as its default.
|
|
331
|
-
const visible = models.filter(model =>
|
|
380
|
+
const visible = models.filter(model => {
|
|
381
|
+
// Keep the legacy name-based selection only for unenriched providers.
|
|
382
|
+
const worker = model.worker;
|
|
383
|
+
return !retirementGuard(null, model).blocked && !model.hidden && !FLAGSHIP_FAMILY.test(model.id)
|
|
384
|
+
&& (!worker || (worker.qualification.status === 'qualified'
|
|
385
|
+
&& worker.visibility === 'listed' && worker.availability === 'accessible'
|
|
386
|
+
&& (worker.lifecycle === 'active' || worker.lifecycle === 'deprecated')
|
|
387
|
+
&& worker.effortSupport !== 'unknown' && !hasSelectionRelevantConflicts(worker)));
|
|
388
|
+
});
|
|
389
|
+
if (visible.length === 0)
|
|
390
|
+
return {};
|
|
391
|
+
const selected = {};
|
|
392
|
+
for (const role of ['reasoning', 'coding', 'efficiency']) {
|
|
393
|
+
const candidates = visible.filter(model => !model.worker || model.worker.qualification.roles.includes(role));
|
|
394
|
+
const model = rankRoleModels(candidates)[role];
|
|
395
|
+
if (model)
|
|
396
|
+
selected[role] = model;
|
|
397
|
+
}
|
|
398
|
+
return selected;
|
|
399
|
+
}
|
|
400
|
+
function rankRoleModels(visible) {
|
|
332
401
|
if (visible.length === 0)
|
|
333
402
|
return {};
|
|
334
403
|
const coding = visible.find(model => STANDARD_FAMILY.test(model.id))
|
|
@@ -379,6 +448,8 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
379
448
|
const lines = createInterface({ input: child.stdout });
|
|
380
449
|
let settled = false;
|
|
381
450
|
let stderr = '';
|
|
451
|
+
const diagnostics = [];
|
|
452
|
+
let lineNumber = 0;
|
|
382
453
|
let detectedRuntimeVersion = version;
|
|
383
454
|
let timer;
|
|
384
455
|
const finish = (result) => {
|
|
@@ -387,10 +458,10 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
387
458
|
settled = true;
|
|
388
459
|
clearTimeout(timer);
|
|
389
460
|
lines.close();
|
|
390
|
-
child
|
|
461
|
+
signalChild(child);
|
|
391
462
|
resolveDiscovery(result);
|
|
392
463
|
};
|
|
393
|
-
child.stderr.on('data', chunk => { stderr
|
|
464
|
+
child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
|
|
394
465
|
child.on('error', error => finish({
|
|
395
466
|
provider: 'codex',
|
|
396
467
|
source: 'native',
|
|
@@ -398,14 +469,20 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
398
469
|
accountScope: 'local-account',
|
|
399
470
|
models: [],
|
|
400
471
|
errorKind: classifyDiscoveryError(error.message),
|
|
401
|
-
error: error.message,
|
|
472
|
+
error: redactModelDiscoveryText(error.message),
|
|
402
473
|
}));
|
|
403
474
|
lines.on('line', line => {
|
|
475
|
+
lineNumber++;
|
|
404
476
|
let message;
|
|
405
477
|
try {
|
|
406
478
|
message = JSON.parse(line);
|
|
407
479
|
}
|
|
408
480
|
catch {
|
|
481
|
+
diagnostics.push(`Codex app-server line ${lineNumber}: invalid JSON`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!message || typeof message !== 'object') {
|
|
485
|
+
diagnostics.push(`Codex app-server line ${lineNumber}: expected JSON-RPC object`);
|
|
409
486
|
return;
|
|
410
487
|
}
|
|
411
488
|
if (message.id === 1 && message.result) {
|
|
@@ -425,16 +502,34 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
425
502
|
})}\n`);
|
|
426
503
|
}
|
|
427
504
|
if (message.id === 2) {
|
|
505
|
+
if (!Array.isArray(message.result?.data))
|
|
506
|
+
diagnostics.push('Codex model/list /result/data: expected array');
|
|
507
|
+
if (message.result?.nextCursor)
|
|
508
|
+
diagnostics.push('Codex model/list /result/nextCursor: truncated catalog (more pages remain)');
|
|
428
509
|
const models = Array.isArray(message.result?.data)
|
|
429
|
-
? message.result.data.
|
|
430
|
-
id
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
510
|
+
? message.result.data.flatMap((model, index) => {
|
|
511
|
+
const id = model?.model ?? model?.id;
|
|
512
|
+
const efforts = model?.supportedReasoningEfforts;
|
|
513
|
+
if (!model || typeof model !== 'object' || Array.isArray(model) || typeof id !== 'string' || !id.trim()
|
|
514
|
+
|| ['hidden', 'isDefault'].some(key => model[key] !== undefined && typeof model[key] !== 'boolean')
|
|
515
|
+
|| (model.displayName !== undefined && typeof model.displayName !== 'string')
|
|
516
|
+
|| (efforts !== undefined && (!Array.isArray(efforts) || efforts.some((effort) => {
|
|
517
|
+
const value = typeof effort === 'string' ? effort : effort?.reasoningEffort ?? effort?.effort;
|
|
518
|
+
return typeof value !== 'string' || !value.trim();
|
|
519
|
+
})))) {
|
|
520
|
+
diagnostics.push(`Codex model/list /result/data/${index}: malformed model row`);
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
return [{
|
|
524
|
+
id,
|
|
525
|
+
displayName: typeof model.displayName === 'string' ? model.displayName : undefined,
|
|
526
|
+
hidden: Boolean(model.hidden),
|
|
527
|
+
isDefault: Boolean(model.isDefault),
|
|
528
|
+
reasoningEfforts: Array.isArray(model.supportedReasoningEfforts)
|
|
529
|
+
? model.supportedReasoningEfforts.map((effort) => typeof effort === 'string' ? effort : String(effort.reasoningEffort ?? effort.effort ?? effort))
|
|
530
|
+
: undefined,
|
|
531
|
+
}];
|
|
532
|
+
})
|
|
438
533
|
: [];
|
|
439
534
|
finish({
|
|
440
535
|
provider: 'codex',
|
|
@@ -443,7 +538,8 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
443
538
|
...(detectedRuntimeVersion ? { runtimeVersion: detectedRuntimeVersion } : {}),
|
|
444
539
|
accountScope: 'local-account',
|
|
445
540
|
models,
|
|
446
|
-
...(message.error ? { error: JSON.stringify(message.error) }
|
|
541
|
+
...(message.error ? { errorKind: 'command', error: redactModelDiscoveryText(JSON.stringify(message.error)) }
|
|
542
|
+
: diagnostics.length ? { errorKind: 'invalid-output', error: diagnostics.join('; ') } : {}),
|
|
447
543
|
});
|
|
448
544
|
}
|
|
449
545
|
});
|
|
@@ -463,7 +559,7 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
|
|
|
463
559
|
accountScope: 'local-account',
|
|
464
560
|
models: [],
|
|
465
561
|
errorKind: 'timeout',
|
|
466
|
-
error: `Codex app-server model/list timed out${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
562
|
+
error: `Codex app-server model/list timed out${stderr ? `: ${redactModelDiscoveryText(stderr.trim())}` : ''}`,
|
|
467
563
|
}), timeoutMs);
|
|
468
564
|
});
|
|
469
565
|
}
|
|
@@ -472,7 +568,7 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
472
568
|
const homeDir = options.homeDir ?? homedir();
|
|
473
569
|
const cacheFile = options.cacheFile ?? join(homeDir, '.cache/aiwg/model-catalog.v1.json');
|
|
474
570
|
const staticFile = join(options.aiwgRoot, 'agentic/code/providers/model-catalog.v1.json');
|
|
475
|
-
const signature = inventorySignature(options.inventory) + JSON.stringify({ omp: options.omp, profile: process.env.OMP_PROFILE ?? process.env.PI_PROFILE, config: process.env.PI_CONFIG_DIR, data: process.env.XDG_DATA_HOME, cache: process.env.XDG_CACHE_HOME, agent: process.env.PI_CODING_AGENT_DIR });
|
|
571
|
+
const signature = inventorySignature(options.inventory) + JSON.stringify({ codex: options.codex, muse: options.muse, omp: options.omp, profile: process.env.OMP_PROFILE ?? process.env.PI_PROFILE, config: process.env.PI_CONFIG_DIR, data: process.env.XDG_DATA_HOME, cache: process.env.XDG_CACHE_HOME, agent: process.env.PI_CODING_AGENT_DIR });
|
|
476
572
|
const staticCatalog = await readCatalog(staticFile);
|
|
477
573
|
if (!staticCatalog)
|
|
478
574
|
throw new Error(`Invalid or missing static model catalog: ${staticFile}`);
|
|
@@ -480,6 +576,8 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
480
576
|
const cached = useCache ? await readCatalog(cacheFile) : null;
|
|
481
577
|
if (!options.forceRefresh
|
|
482
578
|
&& cached
|
|
579
|
+
&& (!options.codex?.accountBinding || cached.discovery?.providers.codex?.accountBinding === options.codex.accountBinding)
|
|
580
|
+
&& (!options.muse?.accountBinding || cached.discovery?.providers.muse?.accountBinding === options.muse.accountBinding)
|
|
483
581
|
&& isFresh(cached, now, options.ttlMs ?? DEFAULT_TTL_MS, signature)) {
|
|
484
582
|
return {
|
|
485
583
|
...cached,
|
|
@@ -502,6 +600,7 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
502
600
|
fetchedAt: now.toISOString(),
|
|
503
601
|
inventorySignature: signature,
|
|
504
602
|
providers: {},
|
|
603
|
+
...(staticCatalog.discovery?.instances ? { instances: staticCatalog.discovery.instances } : {}),
|
|
505
604
|
},
|
|
506
605
|
};
|
|
507
606
|
}
|
|
@@ -518,7 +617,7 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
518
617
|
if (!response.ok)
|
|
519
618
|
throw new Error(`HTTP ${response.status}`);
|
|
520
619
|
const remote = await response.json();
|
|
521
|
-
if (!remote.version || !remote.providers)
|
|
620
|
+
if (!remote.version || !remote.providers || validateWorkerCatalogDiscovery(remote.discovery).length)
|
|
522
621
|
throw new Error('invalid catalog shape');
|
|
523
622
|
catalog = remote;
|
|
524
623
|
source = 'remote';
|
|
@@ -530,7 +629,12 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
530
629
|
}
|
|
531
630
|
const available = new Set(options.inventory.providers.filter(provider => provider.available).map(provider => provider.id));
|
|
532
631
|
const nativeDiscoverers = options.nativeDiscoverers ?? {
|
|
533
|
-
codex: () =>
|
|
632
|
+
codex: () => discoverCodexDebugModels(options.codex?.command ?? 'codex', options.runner, {
|
|
633
|
+
accountBinding: options.codex?.accountBinding, now: options.now,
|
|
634
|
+
}),
|
|
635
|
+
muse: () => discoverMuseCatalogModels({
|
|
636
|
+
...options.muse, catalogDir: options.muse?.catalogDir ?? join(homeDir, MUSE_CATALOG_DIR), now: options.now,
|
|
637
|
+
}),
|
|
534
638
|
opencode: () => discoverOpenCodeModels(),
|
|
535
639
|
openclaw: () => discoverOpenClawModels(),
|
|
536
640
|
pi: () => discoverPiModels(),
|
|
@@ -546,21 +650,6 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
546
650
|
if (discoverer) {
|
|
547
651
|
try {
|
|
548
652
|
providerDiscovery[provider] = await discoverer();
|
|
549
|
-
const selected = provider === 'omp' ? {} : selectRoleModels(providerDiscovery[provider].models);
|
|
550
|
-
const providerCatalog = catalog.providers[provider];
|
|
551
|
-
if (providerCatalog) {
|
|
552
|
-
for (const role of ['reasoning', 'coding', 'efficiency']) {
|
|
553
|
-
if (!selected[role])
|
|
554
|
-
continue;
|
|
555
|
-
providerCatalog.roles[role] = {
|
|
556
|
-
...(providerCatalog.roles[role] ?? {}),
|
|
557
|
-
id: selected[role].id,
|
|
558
|
-
status: 'active',
|
|
559
|
-
observed: true,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
providerCatalog.verifiedAt = providerDiscovery[provider].observedAt.slice(0, 10);
|
|
563
|
-
}
|
|
564
653
|
}
|
|
565
654
|
catch (error) {
|
|
566
655
|
providerDiscovery[provider] = {
|
|
@@ -571,8 +660,67 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
571
660
|
models: [],
|
|
572
661
|
errorKind: classifyDiscoveryError(error.message),
|
|
573
662
|
error: error.message,
|
|
663
|
+
accountBinding: provider === 'codex' ? options.codex?.accountBinding : provider === 'muse' ? options.muse?.accountBinding : undefined,
|
|
574
664
|
};
|
|
575
665
|
}
|
|
666
|
+
const next = providerDiscovery[provider];
|
|
667
|
+
if (next.error)
|
|
668
|
+
next.error = stripVTControlCharacters(next.error);
|
|
669
|
+
const instanceId = discoveryInstanceId(next);
|
|
670
|
+
const prior = cached?.discovery?.instances?.[instanceId]
|
|
671
|
+
?? cached?.discovery?.providers[provider];
|
|
672
|
+
if (prior)
|
|
673
|
+
providerDiscovery[provider] = mergeProviderDiscovery(prior, next);
|
|
674
|
+
const entry = providerDiscovery[provider];
|
|
675
|
+
if (entry.errorKind && prior && prior.accountBinding === next.accountBinding
|
|
676
|
+
&& cached?.providers[provider])
|
|
677
|
+
catalog.providers[provider] = structuredClone(cached.providers[provider]);
|
|
678
|
+
const providerCatalog = catalog.providers[provider];
|
|
679
|
+
const preservePins = provider === 'omp' || entry.models.some(model => model.worker)
|
|
680
|
+
|| prior?.models.some(model => model.worker);
|
|
681
|
+
if (!next.errorKind && prior && prior.accountBinding === next.accountBinding
|
|
682
|
+
&& prior.accountScope === next.accountScope) {
|
|
683
|
+
for (const role of Object.values(providerCatalog?.roles ?? {})) {
|
|
684
|
+
const alreadyUnavailable = Object.values(cached?.providers[provider]?.roles ?? {}).some(previous => previous.id === role.id && previous.status === 'unverified' && previous.observed === false);
|
|
685
|
+
if (!(alreadyUnavailable || (preservePins && prior.models.some(model => model.id === role.id)))
|
|
686
|
+
|| entry.models.some(model => model.id === role.id))
|
|
687
|
+
continue;
|
|
688
|
+
const guard = retirementGuard(role.id);
|
|
689
|
+
role.status = 'unverified';
|
|
690
|
+
role.observed = false;
|
|
691
|
+
entry.policy ??= { layers: [], selected: {}, constraints: [], diagnostics: [] };
|
|
692
|
+
if (!entry.policy.diagnostics.includes(guard.reason))
|
|
693
|
+
entry.policy.diagnostics.push(guard.reason);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const model of entry.models) {
|
|
697
|
+
if (!model.worker)
|
|
698
|
+
continue;
|
|
699
|
+
const pinnedRoles = Object.values(providerCatalog?.roles ?? {}).filter(role => role.id === model.id);
|
|
700
|
+
const guard = retirementGuard(pinnedRoles.length ? model.id : null, model);
|
|
701
|
+
if (guard.blocked) {
|
|
702
|
+
for (const role of pinnedRoles)
|
|
703
|
+
role.status = 'unverified';
|
|
704
|
+
entry.policy ??= { layers: [], selected: {}, constraints: [], diagnostics: [] };
|
|
705
|
+
if (!entry.policy.diagnostics.includes(guard.reason))
|
|
706
|
+
entry.policy.diagnostics.push(guard.reason);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (providerCatalog) {
|
|
710
|
+
// Observations are facts, not approval to adopt a model. Preserve pins.
|
|
711
|
+
const selected = preservePins || entry.errorKind ? {}
|
|
712
|
+
: selectRoleModels(entry.models);
|
|
713
|
+
for (const role of ['reasoning', 'coding', 'efficiency']) {
|
|
714
|
+
if (!selected[role])
|
|
715
|
+
continue;
|
|
716
|
+
providerCatalog.roles[role] = {
|
|
717
|
+
...(providerCatalog.roles[role] ?? {}), id: selected[role].id,
|
|
718
|
+
status: 'active', observed: true,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (Object.keys(selected).length)
|
|
722
|
+
providerCatalog.verifiedAt = entry.observedAt.slice(0, 10);
|
|
723
|
+
}
|
|
576
724
|
}
|
|
577
725
|
else {
|
|
578
726
|
const decision = PROVIDER_DISCOVERY_DECISIONS[provider];
|
|
@@ -588,7 +736,7 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
588
736
|
};
|
|
589
737
|
}
|
|
590
738
|
}
|
|
591
|
-
|
|
739
|
+
let resolved = {
|
|
592
740
|
...catalog,
|
|
593
741
|
discovery: {
|
|
594
742
|
source,
|
|
@@ -597,8 +745,12 @@ export async function resolveDynamicModelCatalog(options) {
|
|
|
597
745
|
...(remoteError ? { remoteError } : {}),
|
|
598
746
|
inventorySignature: signature,
|
|
599
747
|
providers: providerDiscovery,
|
|
748
|
+
instances: { ...cached?.discovery?.instances, ...catalog.discovery?.instances },
|
|
600
749
|
},
|
|
601
750
|
};
|
|
751
|
+
for (const entry of Object.values(providerDiscovery)) {
|
|
752
|
+
resolved = recordAccountInstance(resolved, discoveryInstanceId(entry), entry);
|
|
753
|
+
}
|
|
602
754
|
await atomicWrite(cacheFile, `${JSON.stringify(resolved, null, 2)}\n`);
|
|
603
755
|
return resolved;
|
|
604
756
|
}
|
|
@@ -616,10 +768,19 @@ export async function discoverOmpModels(command = 'omp', runner = runModelDiscov
|
|
|
616
768
|
if (result.exitCode !== 0)
|
|
617
769
|
return { ...base, errorKind: result.exitCode === 124 ? 'timeout' : /unknown|unsupported|not found/i.test(result.stderr) ? 'unsupported' : classifyDiscoveryError(result.stderr), error: 'OMP model discovery failed; check CLI version, selected profile/config, and credential availability.' };
|
|
618
770
|
const payload = JSON.parse(result.stdout);
|
|
619
|
-
if (!Array.isArray(payload
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
771
|
+
if (!Array.isArray(payload?.models))
|
|
772
|
+
return { ...base, errorKind: 'invalid-output', error: 'OMP /models: expected array' };
|
|
773
|
+
const diagnostics = [];
|
|
774
|
+
base.models = payload.models.flatMap((m, index) => {
|
|
775
|
+
if (!m || typeof m.id !== 'string' || !m.id.trim() || typeof m.provider !== 'string' || !m.provider.trim()
|
|
776
|
+
|| (m.name !== undefined && typeof m.name !== 'string')
|
|
777
|
+
|| (m.thinking !== undefined && (!Array.isArray(m.thinking) || m.thinking.some((effort) => typeof effort !== 'string')))) {
|
|
778
|
+
diagnostics.push(`OMP /models/${index}: malformed model row`);
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
return [{ id: `${m.provider}/${m.id}`, llmProvider: m.provider, displayName: m.name, reasoningEfforts: m.thinking }];
|
|
782
|
+
});
|
|
783
|
+
return { ...base, ...(diagnostics.length ? { errorKind: 'invalid-output', error: diagnostics.join('; ') } : {}) };
|
|
623
784
|
}
|
|
624
785
|
catch {
|
|
625
786
|
return { ...base, errorKind: 'invalid-output', error: 'OMP returned invalid model catalog JSON or discovery could not run.' };
|
|
@@ -634,4 +795,707 @@ export function resolveOmpRoleModel(catalog, role, mappings, explicit) {
|
|
|
634
795
|
throw new Error(`OMP ${role} model unavailable; configure a model from omp models --json or supply an explicit model.`);
|
|
635
796
|
return id;
|
|
636
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* B-OG-03 shared discovery refinements (S1 direct/shared checkpoint).
|
|
800
|
+
*
|
|
801
|
+
* Codex `debug models` JSON and Muse model-catalog rows normalize through one
|
|
802
|
+
* observation contract: per-CLI sources, field provenance, coherent snapshots
|
|
803
|
+
* and freshness. Native and bridged MCP use identical facts. Synthetic fixture
|
|
804
|
+
* copies under test fixtures exercise these paths; synthetic seeds never ship
|
|
805
|
+
* as defaults.
|
|
806
|
+
*/
|
|
807
|
+
/** Evidence dated beyond this skew past now is rejected, never trusted. */
|
|
808
|
+
export const MAX_DISCOVERY_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
809
|
+
const MUSE_CATALOG_DIR = '.local/share/muse/model-catalog';
|
|
810
|
+
const MAX_MUSE_CATALOG_FILES = 64;
|
|
811
|
+
const MAX_MUSE_CATALOG_BYTES = 1024 * 1024;
|
|
812
|
+
/** Sanitized provenance digest for a normalized evidence entry. */
|
|
813
|
+
export function discoveryDigest(value) {
|
|
814
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
815
|
+
}
|
|
816
|
+
/** True when an evidence timestamp is materially in the future. Unknown/unparseable stays explicit, not future. */
|
|
817
|
+
export function isFutureDated(value, now = new Date(), skewMs = MAX_DISCOVERY_CLOCK_SKEW_MS) {
|
|
818
|
+
if (typeof value !== 'string')
|
|
819
|
+
return false;
|
|
820
|
+
const time = Date.parse(value);
|
|
821
|
+
return Number.isFinite(time) && time - now.getTime() > skewMs;
|
|
822
|
+
}
|
|
823
|
+
function normalizeVisibility(value) {
|
|
824
|
+
if (value === true || value === 'listed' || value === 'list' || value === 'visible' || value === 'shown')
|
|
825
|
+
return 'listed';
|
|
826
|
+
if (value === false || value === 'hidden')
|
|
827
|
+
return 'hidden';
|
|
828
|
+
return 'unknown';
|
|
829
|
+
}
|
|
830
|
+
function normalizeLifecycle(entry) {
|
|
831
|
+
const lifecycle = entry.lifecycle ?? entry.status ?? entry.state;
|
|
832
|
+
if (lifecycle === 'retired' || entry.retired === true) {
|
|
833
|
+
return {
|
|
834
|
+
lifecycle: 'retired',
|
|
835
|
+
sunsetAt: typeof entry.sunset_at === 'string' || entry.sunset_at === null ? entry.sunset_at : null,
|
|
836
|
+
...(typeof entry.replacement === 'string' && entry.replacement ? { replacementModelId: entry.replacement } : {}),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (lifecycle === 'deprecated' || entry.deprecated === true) {
|
|
840
|
+
return {
|
|
841
|
+
lifecycle: 'deprecated',
|
|
842
|
+
sunsetAt: typeof entry.sunset_at === 'string' || entry.sunset_at === null ? entry.sunset_at : null,
|
|
843
|
+
...(typeof entry.replacement === 'string' && entry.replacement ? { replacementModelId: entry.replacement } : {}),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
return { lifecycle: 'active', sunsetAt: null };
|
|
847
|
+
}
|
|
848
|
+
function normalizePositiveInt(value) {
|
|
849
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 ? value : null;
|
|
850
|
+
}
|
|
851
|
+
function observationTimestamps(options) {
|
|
852
|
+
const observedAt = options.observedAt ?? (options.now ?? new Date()).toISOString();
|
|
853
|
+
return {
|
|
854
|
+
observedAt,
|
|
855
|
+
firstObservedAt: options.firstObservedAt ?? observedAt,
|
|
856
|
+
fetchedAt: options.fetchedAt ?? observedAt,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/** Build one explicit observation: unknowns stay unknown, every field carries provenance. */
|
|
860
|
+
export function buildWorkerObservation(options, input) {
|
|
861
|
+
const { observedAt, firstObservedAt, fetchedAt } = observationTimestamps(options);
|
|
862
|
+
const releaseDate = input.releaseDate ?? null;
|
|
863
|
+
const efforts = input.efforts ?? [];
|
|
864
|
+
const effortSupport = input.effortSupport
|
|
865
|
+
?? (efforts.length > 0 ? 'explicit' : 'unknown');
|
|
866
|
+
const lifecycle = input.lifecycle;
|
|
867
|
+
const availability = input.availability
|
|
868
|
+
?? (lifecycle === 'retired' ? 'unavailable' : 'accessible');
|
|
869
|
+
const provenanceFields = input.dataUseNote
|
|
870
|
+
? [...input.provenanceFields, '/dataUse:unknown(config-scoped)']
|
|
871
|
+
: input.provenanceFields;
|
|
872
|
+
const observation = {
|
|
873
|
+
version: 1,
|
|
874
|
+
recordId: `${options.cliId}:${input.modelId}:${options.accountBinding ?? options.accountScope}`,
|
|
875
|
+
cliId: options.cliId,
|
|
876
|
+
family: input.family,
|
|
877
|
+
runtimeVersion: options.runtimeVersion,
|
|
878
|
+
accountScope: options.accountBinding ?? options.accountScope,
|
|
879
|
+
endpointFingerprint: `local-cli:${options.cliId}`,
|
|
880
|
+
observedAt,
|
|
881
|
+
verifiedAt: observedAt,
|
|
882
|
+
releaseDate,
|
|
883
|
+
releaseDateKind: releaseDate === null ? 'unknown' : (input.releaseDateKind ?? 'reported'),
|
|
884
|
+
visibility: input.visibility,
|
|
885
|
+
availability,
|
|
886
|
+
lifecycle,
|
|
887
|
+
sunsetAt: input.sunsetAt ?? null,
|
|
888
|
+
...(input.replacementModelId ? { replacementModelId: input.replacementModelId } : {}),
|
|
889
|
+
contextTokens: input.contextTokens ?? null,
|
|
890
|
+
maxOutputTokens: input.maxOutputTokens ?? null,
|
|
891
|
+
modalities: ['text'],
|
|
892
|
+
efforts: effortSupport === 'explicit' ? efforts : [],
|
|
893
|
+
effortSupport,
|
|
894
|
+
pricing: { costClass: 'unknown', billing: 'unknown', currency: 'USD', inputPerMillion: null, outputPerMillion: null },
|
|
895
|
+
dataUse: { processing: 'unknown', productImprovement: null, retentionDays: null },
|
|
896
|
+
qualification: { status: 'unqualified', roles: [] },
|
|
897
|
+
provenance: [],
|
|
898
|
+
firstObservedAt,
|
|
899
|
+
fetchedAt,
|
|
900
|
+
retiredAt: lifecycle === 'retired' ? observedAt : null,
|
|
901
|
+
};
|
|
902
|
+
const { provenance: _provenance, observedAt: _observedAt, verifiedAt: _verifiedAt, firstObservedAt: _firstObservedAt, fetchedAt: _fetchedAt, retiredAt: _retiredAt, ...facts } = observation;
|
|
903
|
+
observation.evidenceDigest = discoveryDigest(JSON.stringify({
|
|
904
|
+
facts, modelId: input.modelId, source: options.source,
|
|
905
|
+
sourceEvidence: options.contentDigest ?? observationTimestamps(options),
|
|
906
|
+
}));
|
|
907
|
+
observation.provenance = [{
|
|
908
|
+
fields: [...new Set([...observationPointers(observation).filter(field => field !== '/provenance'),
|
|
909
|
+
'/id', '/hidden', '/isDefault', '/reasoningEfforts', ...provenanceFields])],
|
|
910
|
+
source: `${options.source}${options.pointer ? `#${options.pointer}` : ''}`,
|
|
911
|
+
kind: 'native', observedAt,
|
|
912
|
+
digest: discoveryDigest(JSON.stringify(observation)), confidence: 'reported',
|
|
913
|
+
}];
|
|
914
|
+
return observation;
|
|
915
|
+
}
|
|
916
|
+
/** Include container and leaf JSON Pointers so explicit unknown/default leaves stay traceable. */
|
|
917
|
+
function observationPointers(value, prefix = '') {
|
|
918
|
+
if (!value || typeof value !== 'object')
|
|
919
|
+
return [];
|
|
920
|
+
return Object.entries(value).flatMap(([key, entry]) => {
|
|
921
|
+
const pointer = `${prefix}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
922
|
+
return [pointer, ...observationPointers(entry, pointer)];
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function stringList(value) {
|
|
926
|
+
return Array.isArray(value) ? value.flatMap(entry => {
|
|
927
|
+
if (typeof entry === 'string')
|
|
928
|
+
return entry ? [entry] : [];
|
|
929
|
+
if (entry && typeof entry === 'object' && typeof entry.effort === 'string' && entry.effort)
|
|
930
|
+
return [entry.effort];
|
|
931
|
+
return [];
|
|
932
|
+
}) : [];
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Normalize `codex debug models` JSON (`.models[]` with slug, visibility,
|
|
936
|
+
* supported_reasoning_levels, default_reasoning_level, context_window).
|
|
937
|
+
* Observed on Codex 0.160.0; hidden CLI defaults are never treated as eligible.
|
|
938
|
+
*/
|
|
939
|
+
export function normalizeCodexDebugModels(payload, options) {
|
|
940
|
+
const raw = payload?.models;
|
|
941
|
+
if (!Array.isArray(raw))
|
|
942
|
+
return { models: [], diagnostics: [`${options.source}: expected .models[] in codex debug models JSON`] };
|
|
943
|
+
return normalizeObservationRows(raw, options, 'codex');
|
|
944
|
+
}
|
|
945
|
+
function museEfforts(row) {
|
|
946
|
+
const variants = row.reasoning_effort_variants;
|
|
947
|
+
if (!Array.isArray(variants))
|
|
948
|
+
return [];
|
|
949
|
+
return [...new Set(variants.flatMap(variant => {
|
|
950
|
+
if (typeof variant === 'string')
|
|
951
|
+
return variant ? [variant] : [];
|
|
952
|
+
if (variant && typeof variant === 'object') {
|
|
953
|
+
const tier = variant.tier;
|
|
954
|
+
return typeof tier === 'string' && tier ? [tier] : [];
|
|
955
|
+
}
|
|
956
|
+
return [];
|
|
957
|
+
}))];
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Normalize Muse model-catalog rows (`.rows[]` with model_id, visibility,
|
|
961
|
+
* is_default, is_current, reasoning_effort_variants[].tier, context_limit,
|
|
962
|
+
* output_limit, release_date). Contributor data-use facts stay config-scoped
|
|
963
|
+
* and are never derived from a row's name or default bit.
|
|
964
|
+
*/
|
|
965
|
+
export function normalizeMuseCatalogRows(payload, options) {
|
|
966
|
+
const rows = Array.isArray(payload) ? payload
|
|
967
|
+
: Array.isArray(payload?.rows) ? payload.rows : [];
|
|
968
|
+
if (!rows.length)
|
|
969
|
+
return { models: [], diagnostics: [`${options.source}: expected .rows[] in Muse model-catalog JSON`] };
|
|
970
|
+
return normalizeObservationRows(rows, options, 'muse');
|
|
971
|
+
}
|
|
972
|
+
function validDate(value) {
|
|
973
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
974
|
+
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
975
|
+
}
|
|
976
|
+
function validTimestamp(value) {
|
|
977
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i.test(value)
|
|
978
|
+
&& validDate(value.slice(0, 10)) && Number.isFinite(Date.parse(value));
|
|
979
|
+
}
|
|
980
|
+
/** Use the active window, capped by the reported maximum, with native headroom. */
|
|
981
|
+
function codexContextTokens(row) {
|
|
982
|
+
const window = normalizePositiveInt(row.context_window);
|
|
983
|
+
const maximum = normalizePositiveInt(row.max_context_window);
|
|
984
|
+
const percent = row.effective_context_window_percent;
|
|
985
|
+
// A malformed supplied limit must not silently enlarge the qualified window.
|
|
986
|
+
if (window === null || row.max_context_window != null && maximum === null
|
|
987
|
+
|| percent != null && (typeof percent !== 'number' || !Number.isFinite(percent) || percent <= 0 || percent > 100))
|
|
988
|
+
return null;
|
|
989
|
+
return normalizePositiveInt(Math.floor(Math.min(window, maximum ?? window) * (typeof percent === 'number' ? percent : 100) / 100));
|
|
990
|
+
}
|
|
991
|
+
function catalogEfforts(ids, raw) {
|
|
992
|
+
return ids.map(id => {
|
|
993
|
+
const entry = Array.isArray(raw) ? raw.find(value => value && typeof value === 'object'
|
|
994
|
+
&& (value.effort ?? value.tier) === id) : undefined;
|
|
995
|
+
// Conductor-verified Codex/Muse catalog rule (#3083): only ultra is
|
|
996
|
+
// "Maximum reasoning with automatic task delegation". Prefer that explicit
|
|
997
|
+
// description when present; other listed levels do not self-delegate.
|
|
998
|
+
// multi_agent_reasoning_effort/version describe child execution, not which
|
|
999
|
+
// selected effort activates delegation (e.g. xhigh is not self-delegating).
|
|
1000
|
+
return { id, selfDelegates: id === 'ultra'
|
|
1001
|
+
|| typeof entry?.description === 'string' && /automatic task delegation/i.test(entry.description) };
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
function normalizeObservationRows(rows, options, cli) {
|
|
1005
|
+
const diagnostics = [];
|
|
1006
|
+
const models = [];
|
|
1007
|
+
const sourcePointers = new Map();
|
|
1008
|
+
for (const [index, entry] of rows.entries()) {
|
|
1009
|
+
const rowOptions = options.rowOptions?.[index] ?? options;
|
|
1010
|
+
const now = rowOptions.now ?? new Date();
|
|
1011
|
+
const sourceOptions = { ...rowOptions, pointer: rowOptions.pointer ?? `/${cli === 'muse' ? 'rows' : 'models'}/${index}` };
|
|
1012
|
+
const source = `${sourceOptions.source}#${sourceOptions.pointer}`;
|
|
1013
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1014
|
+
diagnostics.push(`${source}: skipped non-object model entry`);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const row = entry;
|
|
1018
|
+
const id = cli === 'muse' ? row.model_id : row.slug;
|
|
1019
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
1020
|
+
diagnostics.push(`${source}: skipped entry without a ${cli === 'muse' ? 'model_id' : 'slug'}`);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const times = observationTimestamps(sourceOptions);
|
|
1024
|
+
for (const field of [...(cli === 'muse' ? ['context_limit'] : ['context_window', 'max_context_window']), 'output_limit']) {
|
|
1025
|
+
if (row[field] != null && (typeof row[field] !== 'number' || !Number.isSafeInteger(row[field]) || row[field] < 1)) {
|
|
1026
|
+
diagnostics.push(`${source}: malformed ${field} for ${id}; retained as unknown`);
|
|
1027
|
+
// Do not round or coerce malformed evidence into a valid observation.
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (cli === 'codex' && row.effective_context_window_percent != null
|
|
1031
|
+
&& (typeof row.effective_context_window_percent !== 'number' || !Number.isFinite(row.effective_context_window_percent)
|
|
1032
|
+
|| row.effective_context_window_percent <= 0 || row.effective_context_window_percent > 100)) {
|
|
1033
|
+
diagnostics.push(`${source}: malformed effective_context_window_percent for ${id}; context retained as unknown`);
|
|
1034
|
+
}
|
|
1035
|
+
for (const field of ['is_default', 'is_current', 'retired', 'deprecated']) {
|
|
1036
|
+
if (row[field] != null && typeof row[field] !== 'boolean')
|
|
1037
|
+
diagnostics.push(`${source}: malformed ${field} for ${id}`);
|
|
1038
|
+
}
|
|
1039
|
+
const effortField = cli === 'muse' ? 'reasoning_effort_variants' : 'supported_reasoning_levels';
|
|
1040
|
+
const rawEfforts = row[effortField];
|
|
1041
|
+
if (rawEfforts != null) {
|
|
1042
|
+
if (!Array.isArray(rawEfforts))
|
|
1043
|
+
diagnostics.push(`${source}/${effortField}: expected effort array`);
|
|
1044
|
+
else
|
|
1045
|
+
for (const [effortIndex, effort] of rawEfforts.entries()) {
|
|
1046
|
+
const value = typeof effort === 'string' ? effort : effort?.[cli === 'muse' ? 'tier' : 'effort'];
|
|
1047
|
+
if (typeof value !== 'string' || !value.trim())
|
|
1048
|
+
diagnostics.push(`${source}/${effortField}/${effortIndex}: malformed effort`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (row.default_reasoning_level != null && (typeof row.default_reasoning_level !== 'string' || !row.default_reasoning_level.trim())) {
|
|
1052
|
+
diagnostics.push(`${source}: malformed default_reasoning_level for ${id}`);
|
|
1053
|
+
}
|
|
1054
|
+
let invalidTime = false;
|
|
1055
|
+
for (const [field, value] of Object.entries(times)) {
|
|
1056
|
+
if (!validTimestamp(value) || isFutureDated(value, now)) {
|
|
1057
|
+
diagnostics.push(`${source}: ${field} is unknown (invalid or future-dated evidence); rejected ${id}`);
|
|
1058
|
+
invalidTime = true;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (invalidTime)
|
|
1062
|
+
continue;
|
|
1063
|
+
let releaseDate = null;
|
|
1064
|
+
if (row.release_date !== null && row.release_date !== undefined) {
|
|
1065
|
+
if (!validDate(row.release_date))
|
|
1066
|
+
diagnostics.push(`${source}: malformed release_date for ${id}; retained as unknown`);
|
|
1067
|
+
else if (isFutureDated(row.release_date, now)) {
|
|
1068
|
+
diagnostics.push(`${source}: rejected future-dated release_date for ${id}; date is unknown`);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
else
|
|
1072
|
+
releaseDate = row.release_date;
|
|
1073
|
+
}
|
|
1074
|
+
const visibility = normalizeVisibility(row.visibility);
|
|
1075
|
+
const lifecycle = normalizeLifecycle(row);
|
|
1076
|
+
if (row.sunset_at != null && !validTimestamp(row.sunset_at)) {
|
|
1077
|
+
diagnostics.push(`${source}: malformed sunset_at for ${id}; retained as unknown`);
|
|
1078
|
+
lifecycle.sunsetAt = null;
|
|
1079
|
+
}
|
|
1080
|
+
const efforts = cli === 'muse' ? museEfforts(row) : [...new Set([...stringList(row.supported_reasoning_levels),
|
|
1081
|
+
...(typeof row.default_reasoning_level === 'string' && row.default_reasoning_level ? [row.default_reasoning_level] : [])])];
|
|
1082
|
+
// Codex's registry provider and catalogAdapter contain no inference-provider
|
|
1083
|
+
// identity. Its providerless native catalog is OpenAI; never infer from a slug.
|
|
1084
|
+
// Muse's real local catalogs use provider_id, not provider.
|
|
1085
|
+
const llmProvider = typeof row.provider === 'string' && row.provider ? row.provider
|
|
1086
|
+
: cli === 'muse' && typeof row.provider_id === 'string' && row.provider_id ? row.provider_id
|
|
1087
|
+
: cli === 'codex' ? 'openai' : undefined;
|
|
1088
|
+
const model = {
|
|
1089
|
+
id, apiModelId: typeof row.api_model_id === 'string' ? row.api_model_id : id,
|
|
1090
|
+
...(llmProvider ? { llmProvider } : {}),
|
|
1091
|
+
...(visibility === 'unknown' ? {} : { hidden: visibility === 'hidden' }),
|
|
1092
|
+
...(cli === 'muse' ? { isDefault: row.is_default === true } : {}),
|
|
1093
|
+
...(efforts.length ? { reasoningEfforts: efforts } : {}),
|
|
1094
|
+
worker: buildWorkerObservation(sourceOptions, {
|
|
1095
|
+
modelId: id, family: typeof row.family === 'string' && row.family ? row.family : cli,
|
|
1096
|
+
visibility, ...lifecycle, releaseDate,
|
|
1097
|
+
availability: lifecycle.lifecycle === 'retired' ? 'unavailable'
|
|
1098
|
+
: cli === 'muse' ? (row.is_current === true ? 'accessible' : 'unknown') : 'accessible',
|
|
1099
|
+
contextTokens: cli === 'muse' ? normalizePositiveInt(row.context_limit) : codexContextTokens(row),
|
|
1100
|
+
maxOutputTokens: normalizePositiveInt(row.output_limit),
|
|
1101
|
+
efforts: catalogEfforts(efforts, rawEfforts),
|
|
1102
|
+
// The native renderer passes model_reasoning_effort (Muse: --reasoning-effort).
|
|
1103
|
+
effortSupport: efforts.length ? 'explicit' : 'unknown',
|
|
1104
|
+
...(cli === 'muse' ? { dataUseNote: 'contributor policy is config-scoped' } : {}),
|
|
1105
|
+
provenanceFields: cli === 'muse'
|
|
1106
|
+
? ['/id:model_id', '/isDefault:is_default', '/availability:is_current', '/efforts:reasoning_effort_variants[].tier',
|
|
1107
|
+
'/efforts/selfDelegates:catalog-description+verified-ultra-rule', '/contextTokens:context_limit', '/maxOutputTokens:output_limit', '/releaseDate:release_date']
|
|
1108
|
+
: ['/id:slug', '/efforts:supported_reasoning_levels+default_reasoning_level',
|
|
1109
|
+
'/efforts/selfDelegates:catalog-description+verified-ultra-rule',
|
|
1110
|
+
'/contextTokens:context_window+max_context_window+effective_context_window_percent'],
|
|
1111
|
+
}),
|
|
1112
|
+
};
|
|
1113
|
+
// Bind compatibility-row values without including unrelated credential-bearing row keys.
|
|
1114
|
+
model.worker.provenance[0].fields.push('/apiModelId');
|
|
1115
|
+
if (model.llmProvider)
|
|
1116
|
+
model.worker.provenance[0].fields.push('/llmProvider', `/llmProvider:${typeof row.provider === 'string' && row.provider ? 'provider' : cli === 'muse' ? 'provider_id' : 'codex-openai-default'}`);
|
|
1117
|
+
const compatibility = { id: model.id, apiModelId: model.apiModelId, llmProvider: model.llmProvider, hidden: model.hidden, isDefault: model.isDefault, reasoningEfforts: model.reasoningEfforts };
|
|
1118
|
+
model.worker.evidenceDigest = discoveryDigest(JSON.stringify({
|
|
1119
|
+
workerDigest: model.worker.evidenceDigest, ...compatibility,
|
|
1120
|
+
}));
|
|
1121
|
+
const { provenance: _provenance, ...values } = model.worker;
|
|
1122
|
+
model.worker.provenance[0].digest = discoveryDigest(JSON.stringify({ worker: values, ...compatibility }));
|
|
1123
|
+
sourcePointers.set(model, {
|
|
1124
|
+
visibility: '/visibility', releaseDate: '/release_date', sunsetAt: '/sunset_at', replacementModelId: '/replacement',
|
|
1125
|
+
lifecycle: row.retired === true ? '/retired' : row.deprecated === true ? '/deprecated'
|
|
1126
|
+
: row.lifecycle !== undefined ? '/lifecycle' : row.status !== undefined ? '/status' : '/state',
|
|
1127
|
+
availability: cli === 'muse' ? '/is_current' : '/visibility',
|
|
1128
|
+
efforts: cli === 'muse' ? '/reasoning_effort_variants' : '/supported_reasoning_levels',
|
|
1129
|
+
effortSupport: cli === 'muse' ? '/reasoning_effort_variants' : '/supported_reasoning_levels',
|
|
1130
|
+
contextTokens: cli === 'muse' ? '/context_limit' : '/context_window', maxOutputTokens: '/output_limit', family: '/family',
|
|
1131
|
+
});
|
|
1132
|
+
models.push(model);
|
|
1133
|
+
}
|
|
1134
|
+
return { models: reconcileDuplicateObservations(models, diagnostics, sourcePointers), diagnostics };
|
|
1135
|
+
}
|
|
1136
|
+
/** Retain every conflicting value and source; never let the first row erase retirement. */
|
|
1137
|
+
function reconcileDuplicateObservations(models, diagnostics, sourcePointers) {
|
|
1138
|
+
const groups = new Map();
|
|
1139
|
+
for (const model of models)
|
|
1140
|
+
groups.set(model.id, [...(groups.get(model.id) ?? []), model]);
|
|
1141
|
+
return [...groups.values()].map(group => {
|
|
1142
|
+
if (group.length === 1)
|
|
1143
|
+
return group[0];
|
|
1144
|
+
const model = { ...group[0], worker: { ...group[0].worker } };
|
|
1145
|
+
const worker = model.worker;
|
|
1146
|
+
const fields = ['family', 'visibility', 'lifecycle', 'availability', 'sunsetAt', 'replacementModelId',
|
|
1147
|
+
'efforts', 'effortSupport', 'releaseDate', 'contextTokens', 'maxOutputTokens'];
|
|
1148
|
+
const conflicts = [];
|
|
1149
|
+
for (const field of fields) {
|
|
1150
|
+
const evidence = group.map(m => ({ value: m.worker[field] ?? null,
|
|
1151
|
+
source: m.worker.provenance[0].source, pointer: sourcePointers.get(m)?.[field] ?? `/${field}`,
|
|
1152
|
+
observedAt: m.worker.observedAt, digest: m.worker.provenance[0].digest }));
|
|
1153
|
+
if (new Set(evidence.map(e => JSON.stringify(e.value))).size < 2)
|
|
1154
|
+
continue;
|
|
1155
|
+
conflicts.push({ field: `/${field}`, evidence });
|
|
1156
|
+
diagnostics.push(`${model.id}: conflicting ${field === 'releaseDate' ? 'release_date' : field} values; retained values with individual sources: ${evidence.map(item => item.source).join(', ')}`);
|
|
1157
|
+
switch (field) {
|
|
1158
|
+
case 'lifecycle':
|
|
1159
|
+
worker.lifecycle = group.some(m => m.worker.lifecycle === 'retired') ? 'retired' : 'unverified';
|
|
1160
|
+
break;
|
|
1161
|
+
case 'visibility':
|
|
1162
|
+
worker.visibility = group.some(m => m.worker.visibility === 'hidden') ? 'hidden' : 'unknown';
|
|
1163
|
+
break;
|
|
1164
|
+
case 'availability':
|
|
1165
|
+
worker.availability = 'unknown';
|
|
1166
|
+
break;
|
|
1167
|
+
case 'efforts':
|
|
1168
|
+
case 'effortSupport':
|
|
1169
|
+
worker.efforts = [];
|
|
1170
|
+
worker.effortSupport = 'unknown';
|
|
1171
|
+
break;
|
|
1172
|
+
case 'releaseDate':
|
|
1173
|
+
worker.releaseDate = null;
|
|
1174
|
+
worker.releaseDateKind = 'unknown';
|
|
1175
|
+
break;
|
|
1176
|
+
case 'sunsetAt':
|
|
1177
|
+
case 'contextTokens':
|
|
1178
|
+
case 'maxOutputTokens':
|
|
1179
|
+
worker[field] = null;
|
|
1180
|
+
break;
|
|
1181
|
+
case 'replacementModelId':
|
|
1182
|
+
delete worker.replacementModelId;
|
|
1183
|
+
break;
|
|
1184
|
+
case 'family':
|
|
1185
|
+
worker.family = 'unknown';
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// Provider/API identity is selection evidence too. Conflicting rows must
|
|
1190
|
+
// not acquire a qualified route merely because one row appeared first.
|
|
1191
|
+
for (const field of ['apiModelId', 'llmProvider']) {
|
|
1192
|
+
if (new Set(group.map(row => row[field])).size < 2)
|
|
1193
|
+
continue;
|
|
1194
|
+
conflicts.push({ field: `/${field}`, evidence: group.map(row => ({ value: row[field] ?? null,
|
|
1195
|
+
source: row.worker.provenance[0].source, pointer: field === 'apiModelId' ? '/api_model_id' : '/provider',
|
|
1196
|
+
observedAt: row.worker.observedAt, digest: row.worker.provenance[0].digest })) });
|
|
1197
|
+
diagnostics.push(`${model.id}: conflicting ${field}`);
|
|
1198
|
+
delete model[field];
|
|
1199
|
+
}
|
|
1200
|
+
// Compatibility defaults also remain evidence rather than silently keeping the first.
|
|
1201
|
+
if (new Set(group.map(m => m.isDefault)).size > 1) {
|
|
1202
|
+
diagnostics.push(`${model.id}: conflicting isDefault values from ${group.map(m => m.worker.provenance[0].source).join(', ')}`);
|
|
1203
|
+
conflicts.push({ field: '/isDefault', evidence: group.map(m => ({ value: m.isDefault ?? null,
|
|
1204
|
+
source: m.worker.provenance[0].source, pointer: '/is_default',
|
|
1205
|
+
observedAt: m.worker.observedAt, digest: m.worker.provenance[0].digest })) });
|
|
1206
|
+
model.isDefault = false;
|
|
1207
|
+
}
|
|
1208
|
+
if (worker.lifecycle === 'retired') {
|
|
1209
|
+
worker.availability = 'unavailable';
|
|
1210
|
+
worker.retiredAt = group.find(m => m.worker.lifecycle === 'retired').worker.retiredAt;
|
|
1211
|
+
}
|
|
1212
|
+
model.hidden = worker.visibility === 'hidden';
|
|
1213
|
+
if (worker.visibility === 'unknown')
|
|
1214
|
+
delete model.hidden;
|
|
1215
|
+
model.reasoningEfforts = worker.efforts.map(e => e.id);
|
|
1216
|
+
worker.provenance = group.flatMap(m => m.worker.provenance);
|
|
1217
|
+
worker.evidenceDigest = discoveryDigest(JSON.stringify(group.map(m => m.worker.evidenceDigest)));
|
|
1218
|
+
if (conflicts.length)
|
|
1219
|
+
worker.conflicts = conflicts;
|
|
1220
|
+
const { provenance: _provenance, ...resolved } = worker;
|
|
1221
|
+
worker.provenance.push({ fields: observationPointers(resolved), source: 'src/models/model-discovery.ts#reconcileDuplicateObservations',
|
|
1222
|
+
kind: 'derived', observedAt: worker.observedAt,
|
|
1223
|
+
digest: discoveryDigest(JSON.stringify(resolved)), confidence: 'derived' });
|
|
1224
|
+
return model;
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Merge a refresh into its prior snapshot. A failed refresh never renews age:
|
|
1229
|
+
* prior models and prior observation time are kept while the new error and
|
|
1230
|
+
* fetch time stay visible. A partial success retains prior-only rows with
|
|
1231
|
+
* their original age; per-model firstObservedAt is the earliest evidence.
|
|
1232
|
+
* A complete success contains exactly the returned identities.
|
|
1233
|
+
*/
|
|
1234
|
+
export function mergeProviderDiscovery(prior, next) {
|
|
1235
|
+
if (prior.provider !== next.provider || prior.accountBinding !== next.accountBinding
|
|
1236
|
+
|| prior.accountScope !== next.accountScope)
|
|
1237
|
+
return next;
|
|
1238
|
+
if (next.errorKind && next.models.length === 0) {
|
|
1239
|
+
return {
|
|
1240
|
+
...next,
|
|
1241
|
+
models: prior.models,
|
|
1242
|
+
observedAt: prior.observedAt,
|
|
1243
|
+
...(prior.accountBinding !== undefined ? { accountBinding: prior.accountBinding } : {}),
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const priorById = new Map(prior.models.map(model => [model.id, model]));
|
|
1247
|
+
const seen = new Set();
|
|
1248
|
+
const merged = next.models.map((model) => {
|
|
1249
|
+
seen.add(model.id);
|
|
1250
|
+
const previous = priorById.get(model.id);
|
|
1251
|
+
if (!previous?.worker || !model.worker)
|
|
1252
|
+
return model;
|
|
1253
|
+
if (next.provider === 'muse' && previous.worker.evidenceDigest
|
|
1254
|
+
&& previous.worker.evidenceDigest === model.worker.evidenceDigest)
|
|
1255
|
+
return previous;
|
|
1256
|
+
const firstObservedAt = Date.parse(previous.worker.firstObservedAt) < Date.parse(model.worker.firstObservedAt)
|
|
1257
|
+
? previous.worker.firstObservedAt
|
|
1258
|
+
: model.worker.firstObservedAt;
|
|
1259
|
+
if (firstObservedAt === model.worker.firstObservedAt)
|
|
1260
|
+
return model;
|
|
1261
|
+
return { ...model, worker: {
|
|
1262
|
+
...model.worker, firstObservedAt,
|
|
1263
|
+
provenance: [
|
|
1264
|
+
...model.worker.provenance.map(p => ({ ...p, fields: p.fields.filter(field => field !== '/firstObservedAt') })),
|
|
1265
|
+
{ fields: ['/firstObservedAt'], source: previous.worker.provenance[0].source,
|
|
1266
|
+
kind: 'derived', observedAt: previous.worker.observedAt,
|
|
1267
|
+
digest: discoveryDigest(JSON.stringify({ firstObservedAt, evidence: previous.worker.evidenceDigest })), confidence: 'derived' },
|
|
1268
|
+
],
|
|
1269
|
+
} };
|
|
1270
|
+
});
|
|
1271
|
+
if (next.errorKind) {
|
|
1272
|
+
for (const model of prior.models) {
|
|
1273
|
+
if (!seen.has(model.id))
|
|
1274
|
+
merged.push(model);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const observedAt = next.errorKind ? prior.observedAt
|
|
1278
|
+
: next.provider === 'muse' && merged.some(model => model.worker)
|
|
1279
|
+
? merged.flatMap(model => model.worker ? [model.worker.observedAt] : []).sort((a, b) => Date.parse(a) - Date.parse(b))[0]
|
|
1280
|
+
: next.observedAt;
|
|
1281
|
+
return { ...next, observedAt, models: merged };
|
|
1282
|
+
}
|
|
1283
|
+
/** Retirement blocks dispatch without rewriting pins; replacement is a proposal, never substitution. */
|
|
1284
|
+
export function retirementGuard(pinnedId, model) {
|
|
1285
|
+
if (!model && pinnedId)
|
|
1286
|
+
return {
|
|
1287
|
+
blocked: true, pin: pinnedId,
|
|
1288
|
+
reason: `Unavailable model ${pinnedId} is absent from the complete catalog and blocks dispatch; pin is preserved, never rewritten.`,
|
|
1289
|
+
};
|
|
1290
|
+
if (model?.worker?.lifecycle === 'retired') {
|
|
1291
|
+
const replacement = model.worker.supersededBy ?? model.worker.replacementModelId;
|
|
1292
|
+
return {
|
|
1293
|
+
blocked: true,
|
|
1294
|
+
pin: pinnedId,
|
|
1295
|
+
reason: `Retired model ${model.id} blocks dispatch; pin ${pinnedId ?? '(none)'} is preserved, never rewritten`
|
|
1296
|
+
+ `${replacement ? ` (suggested replacement ${replacement} requires operator approval)` : ''}.`,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
return { blocked: false, pin: pinnedId };
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Record an account-scoped discovery under `discovery.instances` without
|
|
1303
|
+
* merging account entitlements into the provider projection.
|
|
1304
|
+
*/
|
|
1305
|
+
function discoveryInstanceId(entry) {
|
|
1306
|
+
return `${entry.provider}:${entry.accountBinding ?? entry.accountScope}`;
|
|
1307
|
+
}
|
|
1308
|
+
export function recordAccountInstance(catalog, instanceId, entry) {
|
|
1309
|
+
const discovery = catalog.discovery ?? {
|
|
1310
|
+
source: 'static',
|
|
1311
|
+
fetchedAt: entry.observedAt,
|
|
1312
|
+
providers: {},
|
|
1313
|
+
};
|
|
1314
|
+
return {
|
|
1315
|
+
...catalog,
|
|
1316
|
+
discovery: {
|
|
1317
|
+
...discovery,
|
|
1318
|
+
instances: { ...(discovery.instances ?? {}), [instanceId]: entry },
|
|
1319
|
+
},
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
/** Run the version-scoped `codex debug models` strategy and normalize its JSON. */
|
|
1323
|
+
export async function discoverCodexDebugModels(command = 'codex', runner = runModelDiscoveryCommand, options = {}) {
|
|
1324
|
+
const observedAt = (options.now ?? (() => new Date()))().toISOString();
|
|
1325
|
+
const version = options.runtimeVersion ?? await runtimeVersion(command, runner);
|
|
1326
|
+
const base = {
|
|
1327
|
+
provider: 'codex',
|
|
1328
|
+
source: 'native',
|
|
1329
|
+
observedAt,
|
|
1330
|
+
...(version ? { runtimeVersion: version } : {}),
|
|
1331
|
+
accountScope: 'local-account',
|
|
1332
|
+
...(options.accountBinding ? { accountBinding: options.accountBinding } : {}),
|
|
1333
|
+
models: [],
|
|
1334
|
+
};
|
|
1335
|
+
const result = await runner(command, ['debug', 'models'], { cwd: tmpdir(), timeoutMs: 10_000 });
|
|
1336
|
+
if (result.exitCode !== 0) {
|
|
1337
|
+
const error = redactModelDiscoveryText(result.stderr.trim()) || `codex debug models exited ${result.exitCode}`;
|
|
1338
|
+
return { ...base, errorKind: classifyDiscoveryError(error), error };
|
|
1339
|
+
}
|
|
1340
|
+
let payload;
|
|
1341
|
+
try {
|
|
1342
|
+
payload = JSON.parse(result.stdout);
|
|
1343
|
+
}
|
|
1344
|
+
catch (error) {
|
|
1345
|
+
return { ...base, errorKind: 'invalid-output', error: `Invalid codex debug models JSON: ${error.message}` };
|
|
1346
|
+
}
|
|
1347
|
+
const normalized = normalizeCodexDebugModels(payload, {
|
|
1348
|
+
cliId: 'codex',
|
|
1349
|
+
source: `${command} debug models`,
|
|
1350
|
+
runtimeVersion: version ?? 'unknown',
|
|
1351
|
+
accountScope: 'local-account',
|
|
1352
|
+
...(options.accountBinding ? { accountBinding: options.accountBinding } : {}),
|
|
1353
|
+
observedAt,
|
|
1354
|
+
now: (options.now ?? (() => new Date()))(),
|
|
1355
|
+
});
|
|
1356
|
+
return {
|
|
1357
|
+
...base,
|
|
1358
|
+
models: normalized.models,
|
|
1359
|
+
...(normalized.diagnostics.length > 0 || normalized.models.length === 0
|
|
1360
|
+
? {
|
|
1361
|
+
errorKind: 'invalid-output',
|
|
1362
|
+
error: normalized.diagnostics.join('; ')
|
|
1363
|
+
|| 'codex debug models returned no normalized model rows.',
|
|
1364
|
+
}
|
|
1365
|
+
: {}),
|
|
1366
|
+
policy: { layers: [], selected: {}, constraints: [], diagnostics: normalized.diagnostics },
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/** Read bounded Muse model-catalog rows. Reads only declared catalog keys, never credential files. */
|
|
1370
|
+
export async function discoverMuseCatalogModels(options = {}) {
|
|
1371
|
+
const now = (options.now ?? (() => new Date()))();
|
|
1372
|
+
const catalogDir = options.catalogDir ?? join(homedir(), MUSE_CATALOG_DIR);
|
|
1373
|
+
const base = {
|
|
1374
|
+
provider: 'muse', source: 'native', observedAt: now.toISOString(),
|
|
1375
|
+
...(options.runtimeVersion ? { runtimeVersion: options.runtimeVersion } : {}),
|
|
1376
|
+
accountScope: 'local-account', ...(options.accountBinding ? { accountBinding: options.accountBinding } : {}), models: [],
|
|
1377
|
+
};
|
|
1378
|
+
const diagnostics = [];
|
|
1379
|
+
const rows = [];
|
|
1380
|
+
const rowOptions = [];
|
|
1381
|
+
const deadline = Date.now() + (options.timeoutMs ?? 5_000);
|
|
1382
|
+
const bounded = async (operation) => {
|
|
1383
|
+
let timer;
|
|
1384
|
+
try {
|
|
1385
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
1386
|
+
timer = setTimeout(() => reject(new Error('Muse catalog discovery deadline exceeded')), Math.max(0, deadline - Date.now()));
|
|
1387
|
+
})]);
|
|
1388
|
+
}
|
|
1389
|
+
finally {
|
|
1390
|
+
clearTimeout(timer);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
try {
|
|
1394
|
+
const root = await bounded(realpath(catalogDir));
|
|
1395
|
+
const files = (await bounded(readdir(root))).filter(file => file.endsWith('.json')).sort();
|
|
1396
|
+
if (!files.length)
|
|
1397
|
+
diagnostics.push(`${root}: holds no JSON rows`);
|
|
1398
|
+
if (files.length > MAX_MUSE_CATALOG_FILES)
|
|
1399
|
+
diagnostics.push(`${join(root, files[MAX_MUSE_CATALOG_FILES])}: exceeds bounded file count; this and remaining files skipped`);
|
|
1400
|
+
for (const file of files.slice(0, MAX_MUSE_CATALOG_FILES)) {
|
|
1401
|
+
if (Date.now() >= deadline) {
|
|
1402
|
+
diagnostics.push(`${join(root, file)}: discovery deadline exceeded; this and remaining files skipped`);
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
const path = join(root, file);
|
|
1406
|
+
try {
|
|
1407
|
+
const link = await bounded(lstat(path));
|
|
1408
|
+
let target = path;
|
|
1409
|
+
if (link.isSymbolicLink()) {
|
|
1410
|
+
target = await bounded(realpath(path));
|
|
1411
|
+
const subpath = relative(root, target);
|
|
1412
|
+
if (subpath === '..' || subpath.startsWith('../'))
|
|
1413
|
+
throw new Error('symlink escapes catalog directory; regular file required');
|
|
1414
|
+
}
|
|
1415
|
+
else if (!link.isFile())
|
|
1416
|
+
throw new Error('not a regular file; skipped');
|
|
1417
|
+
const stat = link.isSymbolicLink() ? await bounded(lstat(target)) : link;
|
|
1418
|
+
if (!stat.isFile())
|
|
1419
|
+
throw new Error('not a regular file; skipped');
|
|
1420
|
+
if (stat.size > MAX_MUSE_CATALOG_BYTES)
|
|
1421
|
+
throw new Error('exceeds bounded catalog size; skipped before reading');
|
|
1422
|
+
// O_NONBLOCK prevents a stat/open race to a FIFO from blocking. O_NOFOLLOW
|
|
1423
|
+
// rejects a replacement symlink; fstat rechecks the opened descriptor.
|
|
1424
|
+
const pending = open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1425
|
+
// If open itself outlives the deadline, close any late descriptor.
|
|
1426
|
+
let expired = false;
|
|
1427
|
+
let lateHandle;
|
|
1428
|
+
void pending.then(handle => {
|
|
1429
|
+
lateHandle = handle;
|
|
1430
|
+
if (expired)
|
|
1431
|
+
void handle.close().catch(() => { });
|
|
1432
|
+
}, () => { });
|
|
1433
|
+
const handle = await bounded(pending).catch(error => {
|
|
1434
|
+
expired = true;
|
|
1435
|
+
if (lateHandle)
|
|
1436
|
+
void lateHandle.close().catch(() => { });
|
|
1437
|
+
throw error;
|
|
1438
|
+
});
|
|
1439
|
+
try {
|
|
1440
|
+
const opened = await bounded(handle.stat());
|
|
1441
|
+
if (!opened.isFile())
|
|
1442
|
+
throw new Error('not a regular file; skipped');
|
|
1443
|
+
if (opened.size > MAX_MUSE_CATALOG_BYTES)
|
|
1444
|
+
throw new Error('exceeds bounded catalog size; skipped before reading');
|
|
1445
|
+
const buffer = Buffer.alloc(MAX_MUSE_CATALOG_BYTES + 1);
|
|
1446
|
+
let bytes = 0;
|
|
1447
|
+
while (bytes < buffer.length) {
|
|
1448
|
+
const result = await bounded(handle.read(buffer, bytes, buffer.length - bytes, bytes));
|
|
1449
|
+
if (!result.bytesRead)
|
|
1450
|
+
break;
|
|
1451
|
+
bytes += result.bytesRead;
|
|
1452
|
+
}
|
|
1453
|
+
if (bytes > MAX_MUSE_CATALOG_BYTES)
|
|
1454
|
+
throw new Error('exceeds bounded catalog size; skipped');
|
|
1455
|
+
const payload = JSON.parse(buffer.subarray(0, bytes).toString('utf8'));
|
|
1456
|
+
const fileRows = Array.isArray(payload) ? payload : payload?.rows;
|
|
1457
|
+
if (!Array.isArray(fileRows))
|
|
1458
|
+
throw new Error('expected .rows[]; rejected');
|
|
1459
|
+
const metadata = Array.isArray(payload) ? {} : payload;
|
|
1460
|
+
// Re-reading a cache does not create new evidence. Prefer its provider
|
|
1461
|
+
// timestamp, falling back to the file's own modification time.
|
|
1462
|
+
const evidenceTime = metadata.fetchedAt ?? metadata.fetched_at ?? metadata.refreshedAt ?? metadata.refreshed_at ?? opened.mtime.toISOString();
|
|
1463
|
+
const normalizedOptions = {
|
|
1464
|
+
cliId: 'muse', source: path, contentDigest: discoveryDigest(buffer.subarray(0, bytes).toString('utf8')), runtimeVersion: options.runtimeVersion ?? 'unknown',
|
|
1465
|
+
accountScope: 'local-account', accountBinding: options.accountBinding, now,
|
|
1466
|
+
observedAt: metadata.observedAt ?? evidenceTime,
|
|
1467
|
+
fetchedAt: evidenceTime,
|
|
1468
|
+
firstObservedAt: metadata.firstObservedAt ?? evidenceTime,
|
|
1469
|
+
};
|
|
1470
|
+
for (const [index, row] of fileRows.entries()) {
|
|
1471
|
+
rows.push(row);
|
|
1472
|
+
rowOptions.push({ ...normalizedOptions, pointer: `/${Array.isArray(payload) ? '' : 'rows/'}${index}` });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
finally {
|
|
1476
|
+
await bounded(handle.close());
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
diagnostics.push(`${path}: ${error.message}; rejected`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
diagnostics.push(`Muse model catalog is unavailable at ${catalogDir}: ${error.message}`);
|
|
1486
|
+
}
|
|
1487
|
+
const normalized = normalizeMuseCatalogRows(rows, {
|
|
1488
|
+
cliId: 'muse', source: catalogDir, runtimeVersion: options.runtimeVersion ?? 'unknown',
|
|
1489
|
+
accountScope: 'local-account', now, rowOptions,
|
|
1490
|
+
});
|
|
1491
|
+
diagnostics.push(...normalized.diagnostics);
|
|
1492
|
+
const observedAt = normalized.models.length
|
|
1493
|
+
? normalized.models.map(model => model.worker.observedAt).sort((a, b) => Date.parse(a) - Date.parse(b))[0] : base.observedAt;
|
|
1494
|
+
return {
|
|
1495
|
+
...base, observedAt, models: normalized.models,
|
|
1496
|
+
...(diagnostics.length || normalized.models.length === 0
|
|
1497
|
+
? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'Muse model catalog returned no usable rows.' } : {}),
|
|
1498
|
+
policy: { layers: [], selected: {}, constraints: [], diagnostics },
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
637
1501
|
//# sourceMappingURL=model-discovery.js.map
|