create-walle 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The never-prompted OpenCode 1.x lead-model switch (docs/opencode-lead-and-delegate-model-
|
|
4
|
+
// selection-2026-10-28.html §5 R-2/R-4/R-5/R-7/R-8/R-9, §6.3, §8 WP-1).
|
|
5
|
+
//
|
|
6
|
+
// Why this is a SEPARATE controller and not a branch of lib/lead-model-switch.js: the journaled
|
|
7
|
+
// Claude/Codex machine is built around an owner tuple, a provider thread and a history handoff.
|
|
8
|
+
// An OpenCode 1.x tab that has never been prompted has NONE of those -- its session does not exist
|
|
9
|
+
// yet (probe P-9), the owner is null and the resume id is empty -- so it is a distinct request kind,
|
|
10
|
+
// `launch_model`, guarded by the process generation instead of `expected_owner` (R-3). The switch is
|
|
11
|
+
// park -> fresh relaunch with `--model provider/model` (R-2, probe P-8), and it settles only on an
|
|
12
|
+
// `opencode:message` observation carrying the requested model (R-7) -- never on argv, never on
|
|
13
|
+
// `session.model` alone.
|
|
14
|
+
//
|
|
15
|
+
// What it deliberately does NOT do:
|
|
16
|
+
// - a v2 binary keeps its `--model`-free argv (R-8): the 2.x TUI rejects the flag (probe P-12), so
|
|
17
|
+
// its target rides the inline config + the CTM 2.x plugin's `switch_model` instead (WP-2/WP-4).
|
|
18
|
+
// - effort (R-9): OpenCode 1.x has no `--variant` on the TUI argv.
|
|
19
|
+
//
|
|
20
|
+
// Revision 2 (opencode-v2-model-switch-standalone-plugin-2026-10-30.html §5.1 R-9, §6.7): a tab WITH a
|
|
21
|
+
// resume id is NO LONGER refused. TUI 1.x still keeps the model its last user message recorded and
|
|
22
|
+
// still ignores `--model` on a resume (probe P-7, upstream #53806), but a server plugin's
|
|
23
|
+
// `chat.message` hook CAN rewrite `output.message.model` (P-V2/P-V4). So a prompted v1 tab switches by
|
|
24
|
+
// the SAME park + resume relaunch as Claude/Codex, with `lib/opencode-v1-plugin/model-carrier.mjs`
|
|
25
|
+
// added to that one relaunch's inline config and the target riding the spawn env (subagent-pick.js's
|
|
26
|
+
// opencodeV1CarrierEnv). Settlement is unchanged (R-5).
|
|
27
|
+
//
|
|
28
|
+
// Revision 3 (opencode-v2-model-switch-standalone-plugin-2026-10-30.html §5.1 R-4/R-5, §8 WP-4): the
|
|
29
|
+
// SAME park + resume relaunch now serves an OpenCode 2.x tab. Its target rides the launch's inline
|
|
30
|
+
// config `model` and the CTM 2.x plugin's `switch_model` (subagent-pick.js's opencodeV2PluginEnv) --
|
|
31
|
+
// never a flag, since the 2.x TUI rejects `--model`. A v2 tab with no resume id is a fresh relaunch.
|
|
32
|
+
//
|
|
33
|
+
// It reuses the SAME `park` and `restore` primitives the journaled machine uses (the runtime passes
|
|
34
|
+
// the same two lambdas), so a relaunch is one park and one restore, exactly as Claude's is.
|
|
35
|
+
|
|
36
|
+
const crypto = require('node:crypto');
|
|
37
|
+
const { publicSessionModelRequest } = require('./session-model-action-preconditions');
|
|
38
|
+
const { leadWireAgent } = require('./lead-wire-agent');
|
|
39
|
+
const { OPENCODE_LEAD_MODEL_REF } = require('./session-create/opencode-lead-provider');
|
|
40
|
+
|
|
41
|
+
const SESSION_NOT_FOUND = 'session_not_found';
|
|
42
|
+
const FLIGHT_KEY = 'relaunching';
|
|
43
|
+
const MAX_MODEL_ID_LENGTH = 200;
|
|
44
|
+
const MAX_ROUTE_ID_LENGTH = 200;
|
|
45
|
+
const MAX_CLIENT_REQUEST_ID_LENGTH = 128;
|
|
46
|
+
|
|
47
|
+
// The sentence each refusal carries, so every client shows a reason instead of a bare HTTP status
|
|
48
|
+
// (the shared presenter prefers its own sentence for a code it knows, then this `message`).
|
|
49
|
+
const REFUSAL_MESSAGES = Object.freeze({
|
|
50
|
+
invalid_request_kind: 'This OpenCode tab can only switch at launch; reopen the model picker and try again.',
|
|
51
|
+
invalid_route_id: 'The gateway route this pick carried was not usable. Reopen the model picker and try again.',
|
|
52
|
+
lead_route_invalid: 'That gateway route is not available for this lead right now.',
|
|
53
|
+
model_owner_changed: 'This OpenCode tab restarted since the picker opened. Reopen the picker and try again.',
|
|
54
|
+
lead_switch_busy: 'OpenCode is busy right now, so nothing was restarted. Try again in a moment.',
|
|
55
|
+
lead_switch_in_flight: 'A model switch for this tab is already running.',
|
|
56
|
+
opencode_catalog_unavailable: 'OpenCode could not list its models, so nothing was restarted.',
|
|
57
|
+
lead_switch_park_failed: 'CTM could not stop this OpenCode tab to restart it, so it keeps its current model.',
|
|
58
|
+
lead_switch_restore_failed: 'OpenCode did not restart on the new model.',
|
|
59
|
+
lead_switch_not_cancellable: 'There is no pending model switch to cancel.',
|
|
60
|
+
});
|
|
61
|
+
// Park refusals that only mean "not right now" (the journaled machine requeues these, lib/lead-model-switch.js
|
|
62
|
+
// REQUEUE_PARK_CODES): a busy/protected tab or the post-boot restore window. A pinned tab, a draft or a
|
|
63
|
+
// non-resumable tab is a real refusal and is reported as such.
|
|
64
|
+
const TRANSIENT_PARK_CODES = new Set(['session_protected', 'park_race_protected']);
|
|
65
|
+
const NON_TRANSIENT_PROTECTIONS = new Set(['pinned', 'draft', 'not-resumable']);
|
|
66
|
+
|
|
67
|
+
function refusal(status, code, extra = {}) {
|
|
68
|
+
const message = REFUSAL_MESSAGES[code];
|
|
69
|
+
return { ok: false, status, code, ...(message ? { message } : {}), ...extra };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The session's observed model in the SAME `provider/model` spelling a catalog row carries.
|
|
73
|
+
// `session.model_id` may already be qualified (`moonshotai/kimi-k3`) or bare (`kimi-k3`) with the
|
|
74
|
+
// provider kept separately; only the bare form is qualified here.
|
|
75
|
+
function observedModelValue(session) {
|
|
76
|
+
const raw = String((session && session.model_id) || '').trim();
|
|
77
|
+
if (!raw) return '';
|
|
78
|
+
if (raw.includes('/')) return raw;
|
|
79
|
+
const provider = String((session && session.model_provider) || '').trim();
|
|
80
|
+
return provider ? `${provider}/${raw}` : raw;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {object} deps
|
|
85
|
+
* @param {(id: string) => ({ agent: string|null, resumeId?: string, live?: boolean, processGeneration?: number }|null)} deps.getSessionInfo
|
|
86
|
+
* @param {(id: string) => (object|null)} deps.getSession the live session object (model_id/model_provider/model_source/model_observed_at)
|
|
87
|
+
* @param {(opts: { bin: string, env: object }) => Promise<object|null>} deps.opencodeVersion
|
|
88
|
+
* @param {(opts: { bin: string, env: object }) => Promise<object[]>} deps.listModels `opencodeCatalog`
|
|
89
|
+
* @param {(id: string, opts: object) => Promise<object>} deps.park the runtime's parkSession lambda
|
|
90
|
+
* @param {(id: string, opts: object) => Promise<boolean>} deps.restore the runtime's restore lambda
|
|
91
|
+
* @param {(id: string) => Promise<boolean>} deps.isIdle
|
|
92
|
+
* @param {(frame: object) => void} deps.broadcast
|
|
93
|
+
* @param {(agent: string, routeId: string, modelId: string, sessionId: string) => Promise<object>} [deps.resolveLeadRoute]
|
|
94
|
+
* the runtime's shared routed-target resolver (the SAME lambda the journaled machine calls): it logs
|
|
95
|
+
* refusals and returns `{ ok:true, display, contextWindow, deployment, profileFingerprint, … }` or
|
|
96
|
+
* `{ ok:false, reason, message }`.
|
|
97
|
+
* @param {(sessionId: string) => (object|null)} [deps.getLeadRoute] the session's stored lead-route row, or null.
|
|
98
|
+
* @param {(sessionId: string, agent: string, route: object|null) => Promise<void>} [deps.persistLeadRoute]
|
|
99
|
+
* writes (route) or clears (null) the session's lead-route row — the journaled machine's own lambda.
|
|
100
|
+
* @param {(id: string, kind: string, detail: object) => void} [deps.diagnostic]
|
|
101
|
+
* @param {() => number} [deps.now]
|
|
102
|
+
* @param {() => string} [deps.newId]
|
|
103
|
+
*/
|
|
104
|
+
function createOpencodeLaunchModel({
|
|
105
|
+
getSessionInfo, getSession, opencodeVersion, listModels,
|
|
106
|
+
park, restore, isIdle, broadcast, diagnostic = null,
|
|
107
|
+
resolveLeadRoute = null, getLeadRoute = null, persistLeadRoute = null,
|
|
108
|
+
now = Date.now, newId = () => crypto.randomUUID(),
|
|
109
|
+
} = {}) {
|
|
110
|
+
// One entry per session: a later request for the same tab REPLACES the previous one (terminal or
|
|
111
|
+
// not), so the map is bounded by the number of live OpenCode tabs.
|
|
112
|
+
const entries = new Map();
|
|
113
|
+
|
|
114
|
+
// The session's launch env, when the observation pipeline captured one. The version probe must ask
|
|
115
|
+
// the SAME `opencode` the tab launched -- a PATH-only lookup could answer for a different install.
|
|
116
|
+
function launchEnvFor(sessionId) {
|
|
117
|
+
const session = getSession(String(sessionId || ''));
|
|
118
|
+
const overrides = session && session._launchEnvOverrides;
|
|
119
|
+
if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
|
|
120
|
+
return { ...process.env, ...overrides };
|
|
121
|
+
}
|
|
122
|
+
return process.env;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readVersion(sessionId) {
|
|
126
|
+
try { return await opencodeVersion({ bin: 'opencode', env: launchEnvFor(sessionId) }); }
|
|
127
|
+
catch (error) {
|
|
128
|
+
void error; /* REPORT: an unusable probe is "unknown version", which is read-only (R-1) */
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The session's stored lead-route row, or null. Optional dep: a harness without it simply never has a
|
|
134
|
+
// route (the pre-WP-3 behaviour), and a throwing reader is treated as "no route" rather than a crash.
|
|
135
|
+
function readLeadRoute(sessionId) {
|
|
136
|
+
if (typeof getLeadRoute !== 'function') return null;
|
|
137
|
+
try { return getLeadRoute(String(sessionId || '')) || null; }
|
|
138
|
+
catch (error) { void error; return null; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* How this tab may switch, decided per launch from the installed binary (R-1), never guessed.
|
|
143
|
+
* @returns {Promise<{ line: 'v1'|'v2'|null, switch_mode: 'fresh_relaunch'|'resume_relaunch'|'read_only', reason: string, process_generation?: number }>}
|
|
144
|
+
*/
|
|
145
|
+
async function sessionMode(sessionId) {
|
|
146
|
+
const info = getSessionInfo(String(sessionId || ''));
|
|
147
|
+
if (!info || info.agent !== 'opencode') return { line: null, switch_mode: 'read_only', reason: 'opencode_version_unknown' };
|
|
148
|
+
const version = await readVersion(sessionId);
|
|
149
|
+
if (!version || (version.line !== 'v1' && version.line !== 'v2')) {
|
|
150
|
+
return { line: null, switch_mode: 'read_only', reason: 'opencode_version_unknown' };
|
|
151
|
+
}
|
|
152
|
+
// `process_generation` is the CAS token the client echoes as `expected_process_generation` (R-3).
|
|
153
|
+
const processGeneration = Number(info.processGeneration || 0);
|
|
154
|
+
// R-4/R-5: a live 2.x tab switches by the SAME park + resume relaunch Claude/Codex and 1.x use. The
|
|
155
|
+
// target rides the launch's inline config `model` + plugin `switch_model` (WP-2/WP-4), never a flag:
|
|
156
|
+
// the 2.x TUI rejects `--model`, so nothing may reach a v2 argv. Without a resume id there is no
|
|
157
|
+
// conversation to continue, so the relaunch is fresh (the config model seeds it).
|
|
158
|
+
if (version.line === 'v2') {
|
|
159
|
+
return { line: 'v2', switch_mode: info.resumeId ? 'resume_relaunch' : 'fresh_relaunch', reason: '', process_generation: processGeneration };
|
|
160
|
+
}
|
|
161
|
+
// R-9: a prompted v1 tab switches by park + resume relaunch, with the model carrier riding that one
|
|
162
|
+
// relaunch (the TUI keeps sending its recorded model, and the carrier rewrites it). A native pick
|
|
163
|
+
// rides `launchModelOverride`; a routed pick rides `launchLeadRoute` (the fixed ref ctm-lead/lead).
|
|
164
|
+
if (info.resumeId) return { line: 'v1', switch_mode: 'resume_relaunch', reason: '', process_generation: processGeneration };
|
|
165
|
+
return { line: 'v1', switch_mode: 'fresh_relaunch', reason: '', process_generation: processGeneration };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function frame(entry) {
|
|
169
|
+
return { type: 'lead-model-request', request: publicSessionModelRequest(entry.req) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function report(entry) {
|
|
173
|
+
if (typeof diagnostic !== 'function') return;
|
|
174
|
+
try {
|
|
175
|
+
diagnostic(entry.req.ctm_session_id, `opencode-launch-model-${entry.req.state}`, {
|
|
176
|
+
client_request_id: entry.req.client_request_id, model_id: entry.req.model_id,
|
|
177
|
+
reason: entry.req.reason, detail: entry.req.detail,
|
|
178
|
+
});
|
|
179
|
+
} catch { /* expected: a diagnostic sink failure must not change the switch verdict */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A refusal that PRE-DATES the park: nothing was started, so the placeholder entry must not
|
|
183
|
+
// survive as a request the client would later read back as its own.
|
|
184
|
+
function withdraw(entry) {
|
|
185
|
+
if (entry && entry.controller) entry.controller.abort();
|
|
186
|
+
if (entry && entries.get(entry.req.ctm_session_id) === entry) entries.delete(entry.req.ctm_session_id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function failEntry(entry, reason, detail) {
|
|
190
|
+
entry.req.state = 'failed';
|
|
191
|
+
entry.req.phase = 'failed';
|
|
192
|
+
entry.req.reason = reason;
|
|
193
|
+
entry.req.detail = String(detail || '').slice(0, 200);
|
|
194
|
+
entry.req.cancellable = false;
|
|
195
|
+
entry.req.settled_at = now();
|
|
196
|
+
report(entry);
|
|
197
|
+
broadcast(frame(entry));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Settle an `awaiting_turn` request from the session's own observation fields (R-7). Only an
|
|
202
|
+
* `opencode:message` observation NEWER than the relaunch counts; anything else leaves it waiting.
|
|
203
|
+
*/
|
|
204
|
+
function settle(entry) {
|
|
205
|
+
if (!entry || entry.req.state !== 'awaiting_turn') return;
|
|
206
|
+
const session = getSession(entry.req.ctm_session_id);
|
|
207
|
+
if (!session) return;
|
|
208
|
+
if (String(session.model_source || '') !== 'opencode:message') return;
|
|
209
|
+
const at = Number(session.model_observed_at || 0);
|
|
210
|
+
if (!Number.isFinite(at) || at <= Number(entry.req.relaunched_at || 0)) return;
|
|
211
|
+
const observed = observedModelValue(session);
|
|
212
|
+
entry.req.observed_model = observed || null;
|
|
213
|
+
entry.req.observed_source = 'opencode:message';
|
|
214
|
+
entry.req.cancellable = false;
|
|
215
|
+
entry.req.settled_at = now();
|
|
216
|
+
// A routed target launches the fixed ref `ctm-lead/lead` (the gateway provider's alias), NOT the
|
|
217
|
+
// route's model id, so a routed request settles on the ref. Every other target settles on its own id.
|
|
218
|
+
const expected = entry.expectedObserved || entry.req.model_id;
|
|
219
|
+
if (observed && observed === expected) {
|
|
220
|
+
entry.req.state = 'succeeded';
|
|
221
|
+
entry.req.phase = 'succeeded';
|
|
222
|
+
entry.req.reason = null;
|
|
223
|
+
entry.req.detail = '';
|
|
224
|
+
} else {
|
|
225
|
+
entry.req.state = 'failed';
|
|
226
|
+
entry.req.phase = 'failed';
|
|
227
|
+
entry.req.reason = 'observed_model_mismatch';
|
|
228
|
+
entry.req.detail = '';
|
|
229
|
+
}
|
|
230
|
+
report(entry);
|
|
231
|
+
broadcast(frame(entry));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A never-prompted OpenCode 1.x switch request. Validations run in the design's order, and the
|
|
236
|
+
* entry is installed SYNCHRONOUSLY once the cheap refusals are out of the way, so two concurrent
|
|
237
|
+
* requests can never both reach `park` (exactly one park for N concurrent requests).
|
|
238
|
+
* @returns {Promise<{ ok:true, request: object }|{ ok:false, status:number, code:string }>}
|
|
239
|
+
*/
|
|
240
|
+
async function request(sessionId, body) {
|
|
241
|
+
const id = String(sessionId || '');
|
|
242
|
+
const info = getSessionInfo(id);
|
|
243
|
+
// 1. This controller is only ever reached for an OpenCode tab; anything else is the runtime's
|
|
244
|
+
// routing bug, and the honest answer is the same 404 the router would have produced.
|
|
245
|
+
if (!info || info.agent !== 'opencode') return refusal(404, SESSION_NOT_FOUND);
|
|
246
|
+
const input = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
|
|
247
|
+
// 2. A default-scope write is not what this kind offers; a missing/unknown scope is invalid.
|
|
248
|
+
if (input.scope === 'session+default') return refusal(400, 'lead_default_unsupported', { agent: 'opencode' });
|
|
249
|
+
if (input.scope !== 'session') return refusal(400, 'invalid_scope');
|
|
250
|
+
// 3. The distinct request kind (R-3): a launch-time model, never an in-place switch.
|
|
251
|
+
if (input.request_kind !== 'launch_model') return refusal(400, 'invalid_request_kind');
|
|
252
|
+
// 4. Bounded target + idempotency key.
|
|
253
|
+
const modelId = input.model_id;
|
|
254
|
+
if (typeof modelId !== 'string' || !modelId.length || modelId.length > MAX_MODEL_ID_LENGTH) {
|
|
255
|
+
return refusal(400, 'invalid_model_id');
|
|
256
|
+
}
|
|
257
|
+
// 4b. An OPTIONAL gateway route id (WP-3). Present = a ROUTED target: the pick names one of Wall-E's
|
|
258
|
+
// shared routed rows, proven by the same resolver the journaled machine uses, never the
|
|
259
|
+
// `opencode models` catalog (a gateway model is not in the CLI's own list).
|
|
260
|
+
const routeId = input.route_id;
|
|
261
|
+
if (routeId != null && (typeof routeId !== 'string' || !routeId.length || routeId.length > MAX_ROUTE_ID_LENGTH)) {
|
|
262
|
+
return refusal(400, 'invalid_route_id');
|
|
263
|
+
}
|
|
264
|
+
const routed = typeof routeId === 'string' && routeId.length > 0;
|
|
265
|
+
const clientRequestId = input.client_request_id;
|
|
266
|
+
if (typeof clientRequestId !== 'string' || !clientRequestId.length
|
|
267
|
+
|| clientRequestId.length > MAX_CLIENT_REQUEST_ID_LENGTH) {
|
|
268
|
+
return refusal(400, 'invalid_request');
|
|
269
|
+
}
|
|
270
|
+
// 5. Capability of the INSTALLED binary (R-1, R-4, R-5, R-8, R-9). A prompted v1 (R-9) or v2 (R-4)
|
|
271
|
+
// tab switches by the SAME park + resume relaunch: its model rides the launch's carrier — the 1.x
|
|
272
|
+
// plugin env, injected by the spawn path from `launchModelOverride` / `launchLeadRoute` — or the
|
|
273
|
+
// 2.x inline config + plugin `switch_model`, native and routed alike. Only an unreadable binary
|
|
274
|
+
// has nothing to offer.
|
|
275
|
+
const storedRoute = readLeadRoute(id);
|
|
276
|
+
const mode = await sessionMode(id);
|
|
277
|
+
if (mode.switch_mode === 'read_only') return refusal(409, 'lead_switch_unsupported', { reason: mode.reason });
|
|
278
|
+
// 6. A parked/deferred tab has no process to relaunch.
|
|
279
|
+
if (!info.live) return refusal(409, 'lead_switch_unsupported', { reason: 'not_live' });
|
|
280
|
+
// 7. The process-generation CAS (R-3): the client's generation must still be the running one.
|
|
281
|
+
if (String(input.expected_process_generation) !== String(info.processGeneration)) {
|
|
282
|
+
return refusal(409, 'model_owner_changed');
|
|
283
|
+
}
|
|
284
|
+
// 8. Idempotent replay of the SAME request, or a switch already in flight.
|
|
285
|
+
const existing = entries.get(id);
|
|
286
|
+
if (existing && existing.req.client_request_id === clientRequestId) {
|
|
287
|
+
return { ok: true, request: publicSessionModelRequest(existing.req) };
|
|
288
|
+
}
|
|
289
|
+
if (existing && existing.req.state === FLIGHT_KEY) return refusal(409, 'lead_switch_in_flight');
|
|
290
|
+
|
|
291
|
+
const entry = {
|
|
292
|
+
req: {
|
|
293
|
+
id: newId(), ctm_session_id: id, client_request_id: clientRequestId, agent: 'opencode',
|
|
294
|
+
model_id: modelId, effort: null, effort_only: false, scope: 'session',
|
|
295
|
+
...(routed ? { route_id: routeId } : {}),
|
|
296
|
+
state: FLIGHT_KEY, phase: FLIGHT_KEY, reason: null, detail: '', cancellable: false,
|
|
297
|
+
requested_at: now(), relaunched_at: null, settled_at: null,
|
|
298
|
+
observed_model: null, observed_source: null,
|
|
299
|
+
},
|
|
300
|
+
// A routed launch runs the fixed ref `ctm-lead/lead`, so its observation settles on the REF, not
|
|
301
|
+
// the route's model id (R-7). Private to the entry — never in the public request shape.
|
|
302
|
+
expectedObserved: routed ? OPENCODE_LEAD_MODEL_REF : modelId,
|
|
303
|
+
// The shared routed resolver requires an AbortSignal (its profile step throws "AbortSignal
|
|
304
|
+
// required" without one -- observed live, opencode-followups WP-6); a pre-park withdraw aborts it.
|
|
305
|
+
controller: new AbortController(),
|
|
306
|
+
};
|
|
307
|
+
// Installed SYNCHRONOUSLY, BEFORE the catalog/idle awaits below: a second request arriving while
|
|
308
|
+
// this one is between validation and park must see it (and be told `lead_switch_in_flight`).
|
|
309
|
+
// It is NOT broadcast yet: a refusal below withdraws it silently, so no viewer may have seen it.
|
|
310
|
+
entries.set(id, entry);
|
|
311
|
+
|
|
312
|
+
// 9. Target proof. A ROUTED target is proven by the SHARED resolver (existence / enabled / routable /
|
|
313
|
+
// servable-for-this-agent + the model in Wall-E's coding catalog) — never the CLI's own list. A
|
|
314
|
+
// NATIVE target is proven by the `opencode models` catalog (R-5): the CLI stores anything, so the
|
|
315
|
+
// pick is what proves the id is real. An unreadable catalog is its own typed refusal, not an empty 400.
|
|
316
|
+
let resolved = null;
|
|
317
|
+
if (routed) {
|
|
318
|
+
if (typeof resolveLeadRoute !== 'function') {
|
|
319
|
+
withdraw(entry);
|
|
320
|
+
return refusal(400, 'lead_route_invalid', { reason: 'route_invalid', message: '' });
|
|
321
|
+
}
|
|
322
|
+
let v;
|
|
323
|
+
try { v = await resolveLeadRoute('opencode', routeId, modelId, id, { signal: entry.controller.signal }); }
|
|
324
|
+
catch (error) { v = { ok: false, reason: 'route_invalid', message: String(error && error.message || error).slice(0, 200) }; }
|
|
325
|
+
if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
|
|
326
|
+
if (!v || !v.ok) {
|
|
327
|
+
withdraw(entry);
|
|
328
|
+
return refusal(400, 'lead_route_invalid', { reason: (v && v.reason) || 'route_invalid', message: (v && v.message) || '' });
|
|
329
|
+
}
|
|
330
|
+
resolved = v;
|
|
331
|
+
} else {
|
|
332
|
+
let rows;
|
|
333
|
+
try {
|
|
334
|
+
rows = await listModels({ bin: 'opencode', env: launchEnvFor(id) });
|
|
335
|
+
} catch (error) {
|
|
336
|
+
void error; /* REPORT: the refusal below names the code; the cause never reaches the client */
|
|
337
|
+
withdraw(entry);
|
|
338
|
+
return refusal(503, 'opencode_catalog_unavailable');
|
|
339
|
+
}
|
|
340
|
+
if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
|
|
341
|
+
if (!Array.isArray(rows) || !rows.some((row) => String(row && row.value) === modelId)) {
|
|
342
|
+
withdraw(entry);
|
|
343
|
+
return refusal(400, 'lead_model_not_switchable', { model_id: modelId });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 10. Relaunching WOULD interrupt a busy tab, so the same idle verdict Claude/Codex use applies.
|
|
347
|
+
const idle = await isIdle(id);
|
|
348
|
+
if (entries.get(id) !== entry) return refusal(409, 'lead_switch_in_flight');
|
|
349
|
+
if (idle !== true) {
|
|
350
|
+
withdraw(entry);
|
|
351
|
+
return refusal(409, 'lead_switch_busy');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// R-2/R-6: park, then relaunch with the model on the ONE launch's argv. No user config is written.
|
|
355
|
+
// A THROWN park/restore is a refusal too: leaving the entry in `relaunching` would block this tab
|
|
356
|
+
// forever (the in-flight guard), so every exit below is a terminal state.
|
|
357
|
+
let parked;
|
|
358
|
+
try { parked = await park(id, { manual: true, reason: 'model' }); }
|
|
359
|
+
catch (error) {
|
|
360
|
+
const code = String((error && error.message) || error || 'park_failed').slice(0, 200);
|
|
361
|
+
failEntry(entry, 'park_refused', code);
|
|
362
|
+
return refusal(409, 'lead_switch_park_failed', { park_code: 'park_threw' });
|
|
363
|
+
}
|
|
364
|
+
if (!parked || parked.ok === false) {
|
|
365
|
+
const code = String((parked && (parked.code || parked.reason)) || 'park_failed');
|
|
366
|
+
const protections = Array.isArray(parked && parked.protections) ? parked.protections : [];
|
|
367
|
+
if (TRANSIENT_PARK_CODES.has(code) && !protections.some((p) => NON_TRANSIENT_PROTECTIONS.has(p))) {
|
|
368
|
+
// Nothing was stopped: the tab is busy or inside CTM's post-boot restore window. Withdraw the
|
|
369
|
+
// never-broadcast entry and answer "busy", with the time the window ends when park knows it.
|
|
370
|
+
withdraw(entry);
|
|
371
|
+
const retryAt = Number(parked && parked.retryAt);
|
|
372
|
+
return refusal(409, 'lead_switch_busy', { park_code: code, protections,
|
|
373
|
+
...(Number.isFinite(retryAt) && retryAt > 0 ? { retry_at: retryAt } : {}) });
|
|
374
|
+
}
|
|
375
|
+
failEntry(entry, 'park_refused', code);
|
|
376
|
+
// `code` is the refusal's own identity; the park's reason rides `park_code` (the same field
|
|
377
|
+
// name the journaled machine records), so neither can overwrite the other.
|
|
378
|
+
return refusal(409, 'lead_switch_park_failed', { park_code: code });
|
|
379
|
+
}
|
|
380
|
+
if (routed) entry.req.route_display = resolved.display || '';
|
|
381
|
+
let started;
|
|
382
|
+
// A NATIVE pick relaunches with `--model <id>`; a ROUTED pick hands the spawn path a `launchLeadRoute`
|
|
383
|
+
// object (gateway ref + env + configPatch), exactly as a routed Claude/Codex lead does. A native pick
|
|
384
|
+
// on a tab that HAS a stored route passes an EXPLICIT `launchLeadRoute: null`, so the spawn path clears
|
|
385
|
+
// the row instead of re-applying the route the user just left.
|
|
386
|
+
const restoreOpts = routed
|
|
387
|
+
? { launchLeadRoute: {
|
|
388
|
+
routeId, model: modelId,
|
|
389
|
+
display: resolved.display || modelId,
|
|
390
|
+
contextWindow: resolved.contextWindow != null ? resolved.contextWindow : (resolved.deployment?.contextTokens ?? null),
|
|
391
|
+
} }
|
|
392
|
+
: (storedRoute ? { launchModelOverride: modelId, launchLeadRoute: null } : { launchModelOverride: modelId });
|
|
393
|
+
try { started = await restore(id, restoreOpts); }
|
|
394
|
+
catch (error) {
|
|
395
|
+
void error; /* REPORT: the failure path below records the terminal state */
|
|
396
|
+
started = false;
|
|
397
|
+
}
|
|
398
|
+
// The tab is parked: from here on every exit is a broadcast state, so the first frame viewers see
|
|
399
|
+
// is one that will be followed by its outcome (a refusal before this point broadcasts nothing).
|
|
400
|
+
broadcast(frame(entry));
|
|
401
|
+
if (started !== true) {
|
|
402
|
+
failEntry(entry, 'restore_failed', 'restore_refused');
|
|
403
|
+
return refusal(409, 'lead_switch_restore_failed');
|
|
404
|
+
}
|
|
405
|
+
entry.req.state = 'awaiting_turn';
|
|
406
|
+
entry.req.phase = 'awaiting_turn';
|
|
407
|
+
entry.req.relaunched_at = now();
|
|
408
|
+
entry.req.cancellable = true;
|
|
409
|
+
// The session's lead-route row follows the switch (WP-3), so the NEXT resume re-applies it: a routed
|
|
410
|
+
// target writes the row (the same field set the journaled machine persists) and a native target on a
|
|
411
|
+
// routed tab clears it. A write failure is REPORTED via `diagnostic` and never fails the switch — the
|
|
412
|
+
// relaunched process already carries the target.
|
|
413
|
+
if ((routed || storedRoute) && typeof persistLeadRoute === 'function') {
|
|
414
|
+
try {
|
|
415
|
+
if (routed) {
|
|
416
|
+
await persistLeadRoute(id, leadWireAgent('opencode'), {
|
|
417
|
+
routeId, model: modelId, display: resolved.display || modelId,
|
|
418
|
+
contextWindow: resolved.contextWindow != null ? resolved.contextWindow : (resolved.deployment?.contextTokens ?? null),
|
|
419
|
+
profileFingerprint: resolved.profileFingerprint || null,
|
|
420
|
+
factsRevision: resolved.deployment?.factsRevision || null,
|
|
421
|
+
processGeneration: resolved.processGeneration || null,
|
|
422
|
+
transitionId: null,
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
await persistLeadRoute(id, leadWireAgent('opencode'), null);
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (typeof diagnostic === 'function') {
|
|
429
|
+
try { diagnostic(id, 'opencode-launch-model-persist-route-failed', { message: String((error && error.message) || error).slice(0, 200) }); }
|
|
430
|
+
catch { /* expected: a diagnostic sink failure must not change the switch verdict */ }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
broadcast(frame(entry));
|
|
435
|
+
return { ok: true, request: publicSessionModelRequest(entry.req) };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The public request for this tab, settled lazily (the poll path), or null. */
|
|
439
|
+
function get(sessionId) {
|
|
440
|
+
const entry = entries.get(String(sessionId || ''));
|
|
441
|
+
if (!entry) return null;
|
|
442
|
+
if (entry.req.state === 'awaiting_turn') settle(entry);
|
|
443
|
+
return publicSessionModelRequest(entry.req);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Cancelling an unconfirmed switch abandons the REQUEST; the relaunched process keeps running. */
|
|
447
|
+
function cancel(sessionId) {
|
|
448
|
+
const entry = entries.get(String(sessionId || ''));
|
|
449
|
+
if (!entry || entry.req.state !== 'awaiting_turn') {
|
|
450
|
+
return { ok: false, status: 409, code: 'lead_switch_not_cancellable' };
|
|
451
|
+
}
|
|
452
|
+
entry.req.state = 'cancelled';
|
|
453
|
+
entry.req.phase = 'cancelled';
|
|
454
|
+
entry.req.cancellable = false;
|
|
455
|
+
entry.req.settled_at = now();
|
|
456
|
+
broadcast(frame(entry));
|
|
457
|
+
return { ok: true, request: publicSessionModelRequest(entry.req) };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* A model observation for this tab arrived (the runtime calls this on a status edge). Settles an
|
|
462
|
+
* `awaiting_turn` request from the session's own fields, so a switch completes without a poll.
|
|
463
|
+
*/
|
|
464
|
+
function notifyObservation(sessionId) {
|
|
465
|
+
settle(entries.get(String(sessionId || '')));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return { request, get, cancel, sessionMode, notifyObservation };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
module.exports = { createOpencodeLaunchModel, observedModelValue };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// WP-5 (docs/opencode-v2-model-switch-standalone-plugin-2026-10-30.html §3.5 P-V2..P-V5, §5.1 R-9,
|
|
2
|
+
// §5.2 D-9/D-10/D-11, §6.7): the OpenCode 1.x model carrier.
|
|
3
|
+
//
|
|
4
|
+
// A resumed 1.x TUI re-sends the model its conversation recorded on every prompt (the TUI syncs its
|
|
5
|
+
// model from the last user message once per session, prompt/index.tsx:311-331), so `--model` on a
|
|
6
|
+
// resume is ignored (round-1 P-7, upstream #53806). A SERVER plugin's `chat.message` hook can rewrite
|
|
7
|
+
// `output.message.model` before the request is made, so this file is what carries a prompted tab's
|
|
8
|
+
// switch: CTM adds this one file to the launch's inline `plugin` array and sets the target ref and the
|
|
9
|
+
// resumed session id in the spawn env (`CTM_OPENCODE_SWITCH_MODEL`, `CTM_OPENCODE_SWITCH_SESSION`).
|
|
10
|
+
//
|
|
11
|
+
// The rewrite is NOT unconditional (P-V3/P-V4): the first model seen on the target session is `from`,
|
|
12
|
+
// and only a prompt still arriving on `from` is rewritten. Any other model is a pick the user made in
|
|
13
|
+
// the TUI itself, which wins — the carrier disarms for good rather than fighting it.
|
|
14
|
+
//
|
|
15
|
+
// Loaded by OpenCode 1.x under Bun: ESM, no imports. It NEVER throws — a carrier failure leaves the
|
|
16
|
+
// prompt on its own model (the chip then never settles, R-5), which is strictly better than failing a
|
|
17
|
+
// user's turn.
|
|
18
|
+
export const CtmModelCarrier = async () => {
|
|
19
|
+
const target = process.env.CTM_OPENCODE_SWITCH_MODEL || ''; // 'provider/model' (first '/' splits)
|
|
20
|
+
const sessionID = process.env.CTM_OPENCODE_SWITCH_SESSION || '';
|
|
21
|
+
const cut = target.indexOf('/');
|
|
22
|
+
if (!sessionID || cut <= 0 || cut === target.length - 1) return {};
|
|
23
|
+
const providerID = target.slice(0, cut), modelID = target.slice(cut + 1);
|
|
24
|
+
let from = null, off = false;
|
|
25
|
+
return { 'chat.message': async (input, output) => {
|
|
26
|
+
try {
|
|
27
|
+
const m = output && output.message && output.message.model;
|
|
28
|
+
if (off || !m || !input || input.sessionID !== sessionID) return;
|
|
29
|
+
const key = m.providerID + '/' + m.modelID;
|
|
30
|
+
if (from === null) from = key;
|
|
31
|
+
if (key !== from) { off = true; return; } // the user picked in the TUI (P-V4)
|
|
32
|
+
output.message.model = { ...m, providerID, modelID };
|
|
33
|
+
} catch { /* expected: a carrier failure leaves the prompt on its own model; the chip never settles (R-5) */ }
|
|
34
|
+
} };
|
|
35
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// WP-2 (docs/opencode-v2-model-switch-standalone-plugin-2026-10-30.html §3.2 P-S7/P-S8, §5.1 R-3/R-4,
|
|
3
|
+
// §5.2 D-12/D-13, §6.3 revision 3): the CTM OpenCode 2.x plugin. OpenCode 2.x loads a `plugins` entry
|
|
4
|
+
// by DIRECTORY (a single file path is skipped, P-S7) and requires this module's `server.js`; the entry
|
|
5
|
+
// shape that works is CommonJS (`module.exports` + `.default`, P-S3b), so this file is CommonJS even
|
|
6
|
+
// though 1.x loads its plugin as ESM.
|
|
7
|
+
//
|
|
8
|
+
// It runs INSIDE OpenCode's private per-tab server (R-1), so it never requires a CTM module and never
|
|
9
|
+
// throws: no imports, never awaits a post, aborts a request after 1500 ms, and is inert when the spawn
|
|
10
|
+
// path did not pass a port or a session id (the 1.x plugin rules, lib/agent-hooks-opencode-plugin.js:6-8,
|
|
11
|
+
// 30-40). Identity comes from ctx.options, filled by the spawn path from the SAME spawn env values the
|
|
12
|
+
// 1.x plugin reads (CTM_PORT / CTM_SESSION_ID / CTM_MODEL_PROCESS_GENERATION).
|
|
13
|
+
//
|
|
14
|
+
// Three jobs (R-3):
|
|
15
|
+
// 1. switch_model, once, in setup, BEFORE subscribing (D-12): ctx.session.switchModel({ sessionID,
|
|
16
|
+
// model: { providerID, id } }) applies the target to a session that already exists (P-S8c). On a
|
|
17
|
+
// never-prompted tab the call rejects Session.NotFoundError (P-S8a) -- the inline config model
|
|
18
|
+
// applies instead, so this is the expected fresh-tab case and stays silent. Any OTHER rejection is
|
|
19
|
+
// one line to stderr. The outcome is never posted: the next completed turn's model is the truth.
|
|
20
|
+
// 2. turn state (D-13): only events whose sessionID is THIS tab's session id, one post per transition
|
|
21
|
+
// -- session.execution.started -> 'start' (busy once), any other session.execution.* -> 'stop'
|
|
22
|
+
// (the first end after a start). A child's execution events are ignored by construction (the
|
|
23
|
+
// session id check below), so a subagent's turn never posts start/stop for the tab.
|
|
24
|
+
// Routes /hook/opencode/start and /hook/opencode/stop are the 1.x ingress routes, unchanged.
|
|
25
|
+
// 3. permission.asked -> 'waiting', permission.replied -> 'resumed' (OQ-2, cited @opencode/schema 2.2.5;
|
|
26
|
+
// WP-3b, OQ-5): 2.x has no permission.updated (D-6). Waiting is a status report only -- it does not
|
|
27
|
+
// touch the busy flag, so the turn-state transitions above are unchanged. Two pieces of state make
|
|
28
|
+
// the report exact:
|
|
29
|
+
// - children: a subagent (task tool) runs in its OWN session, and P-S11 observed on 2.2.4 that its
|
|
30
|
+
// ask carries the CHILD session id while its session.created carries data.parentID = the parent's
|
|
31
|
+
// id (both at the top level of data). So session.created builds a Set of descendant session ids
|
|
32
|
+
// (parentID is the tab's own id OR an id already in the set -- grandchildren included) BEFORE the
|
|
33
|
+
// own-session filter would drop them, and a permission event is accepted when its sessionID is
|
|
34
|
+
// the tab's own id or in that set. Without this a blocked subagent never shows "Needs you".
|
|
35
|
+
// - pending: the set of open ask ids (permission.asked.data.id). 'waiting' posts only on the
|
|
36
|
+
// empty -> non-empty edge and 'resumed' only when the set is empty again after removing
|
|
37
|
+
// permission.replied.data.requestID, so overlapping asks from the tab and its children yield one
|
|
38
|
+
// waiting/resumed pair; a requestID that was never seen removes nothing and the empty check
|
|
39
|
+
// still holds (the WP-3 reply-without-ask case).
|
|
40
|
+
// Routes /hook/opencode/waiting and /hook/opencode/resumed are the 1.x ingress routes
|
|
41
|
+
// (lib/agent-hooks-opencode-plugin.js:56-57), unchanged.
|
|
42
|
+
|
|
43
|
+
// post(route): best effort, never awaited, never throws (a slow CTM must not stall the agent).
|
|
44
|
+
function makePost(o, ctx) {
|
|
45
|
+
return (route) => {
|
|
46
|
+
try {
|
|
47
|
+
fetch('http://127.0.0.1:' + o.ctm_port + '/hook/opencode/' + route, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'content-type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
ctm_id: o.ctm_session_id,
|
|
52
|
+
process_generation: o.process_generation || undefined,
|
|
53
|
+
cwd: ctx.location && ctx.location.directory,
|
|
54
|
+
session_id: o.session_id,
|
|
55
|
+
}),
|
|
56
|
+
signal: AbortSignal.timeout(1500),
|
|
57
|
+
}).catch(() => { /* expected: a status report is best effort; a slow CTM must not stall the agent */ });
|
|
58
|
+
} catch (e) { /* expected: a status report is best effort; a slow CTM must not stall the agent */ }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function setup(ctx) {
|
|
63
|
+
const o = (ctx && ctx.options) || {};
|
|
64
|
+
// 1. switch_model, once, before subscribing (P-S8c).
|
|
65
|
+
const sw = o.switch_model;
|
|
66
|
+
if (sw && typeof sw === 'object') {
|
|
67
|
+
try {
|
|
68
|
+
await ctx.session.switchModel(sw);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// P-S8a: a never-prompted tab rejects Session.NotFoundError and the config model applies instead.
|
|
71
|
+
if (!e || e._tag !== 'Session.NotFoundError') {
|
|
72
|
+
process.stderr.write('[ctm.status] switchModel failed: ' + String((e && e.message) || e) + '\n');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 2. status: started by setup, never awaited by it; a missing port/id leaves the plugin inert.
|
|
77
|
+
if (!o.ctm_port || !o.ctm_session_id) return;
|
|
78
|
+
const post = makePost(o, ctx);
|
|
79
|
+
void (async () => {
|
|
80
|
+
try {
|
|
81
|
+
let busy = false;
|
|
82
|
+
const children = new Set(); // child (subagent) session ids under this tab, grandchildren included
|
|
83
|
+
const pending = new Set(); // open permission-ask ids (the tab's own and its children's)
|
|
84
|
+
for await (const ev of ctx.event.subscribe()) {
|
|
85
|
+
const data = ev && ev.data;
|
|
86
|
+
if (!data) continue;
|
|
87
|
+
const type = ev.type;
|
|
88
|
+
const sid = data.sessionID;
|
|
89
|
+
if (type === 'session.created') {
|
|
90
|
+
// P-S11 observed on 2.2.4: a child ask carries the child session id, and the child's
|
|
91
|
+
// session.created carries data.parentID = the parent's id. Track descendants BEFORE the
|
|
92
|
+
// own-session filter, which would otherwise drop the event (it is not the tab's own id).
|
|
93
|
+
if (data.parentID && sid && (data.parentID === o.session_id || children.has(data.parentID))) children.add(sid);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (sid !== o.session_id && !children.has(sid)) continue;
|
|
97
|
+
if (type === 'session.execution.started') {
|
|
98
|
+
// Turn state is the tab's OWN session only: a child's execution events never post start/stop.
|
|
99
|
+
if (sid === o.session_id && !busy) { busy = true; post('start'); }
|
|
100
|
+
} else if (type === 'permission.asked') {
|
|
101
|
+
if (pending.size === 0) post('waiting'); // D-6: 2.x has no permission.updated; one pair per overlap
|
|
102
|
+
pending.add(data.id);
|
|
103
|
+
} else if (type === 'permission.replied') {
|
|
104
|
+
pending.delete(data.requestID); // unknown id: nothing to remove, the empty check below still holds
|
|
105
|
+
if (pending.size === 0) post('resumed');
|
|
106
|
+
} else if (typeof type === 'string' && type.startsWith('session.execution.')) {
|
|
107
|
+
// A turn end closes any ask left open (a reject/abort path that emits no permission.replied is
|
|
108
|
+
// unmeasured), so the NEXT turn's first ask posts 'waiting' again.
|
|
109
|
+
if (sid === o.session_id && busy) { busy = false; pending.clear(); post('stop'); }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (e) { /* expected: the status loop is best effort; a broken stream never breaks the agent */ }
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const def = { id: 'ctm.status', setup };
|
|
117
|
+
module.exports = def;
|
|
118
|
+
module.exports.default = def;
|