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,1326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Landing-throughput WP-5 shard S1 (design doc §5 D-10 "move-only", §6 "Shard module seam",
|
|
4
|
+
// §8 WP-5, §9 S-21..S-27): four remote-session handlers move out of server.js VERBATIM --
|
|
5
|
+
// handleRemoteApi, _sendRemoteTerminalSubmission, _selectRemoteTerminalChoiceBody and
|
|
6
|
+
// _observeRemoteSubmission. Each body is identical to server.js's declaration except for the
|
|
7
|
+
// `_deps.` prefix on every identifier that used to resolve to a server.js top-level binding
|
|
8
|
+
// (D-10 rule 1), proven by
|
|
9
|
+
// node bin/move-only-check.js --from HEAD:coding-task-manager/server.js \
|
|
10
|
+
// --to coding-task-manager/lib/api/remote-sessions.js \
|
|
11
|
+
// --names handleRemoteApi,_sendRemoteTerminalSubmission,_selectRemoteTerminalChoiceBody,_observeRemoteSubmission
|
|
12
|
+
//
|
|
13
|
+
// This module registers NO routes. The /api/remote/ prefix stays dispatched by
|
|
14
|
+
// lib/api/access.js (`_deps.handleRemoteApi`); server.js now points that key at this module's
|
|
15
|
+
// export instead of its own (deleted) declaration. Callers left in server.js reach the moved
|
|
16
|
+
// functions through this module's exports (D-10 rule 4).
|
|
17
|
+
//
|
|
18
|
+
// Injected deps (D-10 rule 1), per function:
|
|
19
|
+
// handleRemoteApi (52): QRCode, REMOTE_TERMINAL_SUBMIT_DELAY_MS, REMOTE_TERMINAL_SUBMIT_SETTLE_MAX_MS, REMOTE_TERMINAL_SUBMIT_SETTLE_QUIET_MS, _activateDeferredSession, _cancelWalleSessionTurn, _createRemoteSubmissionRecord, _getHeadlessRows, _healRemoteSubmissionRows, _injectSessionInterrupt, _isLiveTerminalSession, _isWalleChatThreadId, _isWalleModelConfigurableSession, _mainDbCensus, _persistWalleSessionModelPreference, _recordRuntimeInputAcceptedForSession, _recordRuntimeInputResultForSession, _remoteCommandRuntime, _remoteHostedPairingReady, _remoteRelayBootInstance, _remoteRelayStatusPayload, _remoteSessionSummary, _remoteSubmissionPublic, _respondToRemoteApproval, _runtimeInputBaseForSession, _selectRemoteTerminalChoice, _selectResumePromptChoice, _sendRemoteJson, _sendRemoteWalleThreadMessage, _snapshotOutputMarker, _storeRemoteSubmissionStatus, _waitForTerminalRenderSettle, _walleModelSelectionForActiveSession, _writeLockBusyApiError, closeWsForDeviceIds, dbModule, deliverAcpSubmission, emitHumanSubmit, handleInput, handleWalleMessage, isAcpSession, mobileAuthStore, readMobileJsonBody, recordSessionDiagnostic, remoteRelayProtocol, remoteRelayStore, remoteSubmissionObserver, requestOrigin, sessions, terminalComposerState, walleClient.
|
|
20
|
+
// _sendRemoteTerminalSubmission (33): REMOTE_TERMINAL_SUBMIT_DELAY_MS, REMOTE_TERMINAL_SUBMIT_SETTLE_MAX_MS, REMOTE_TERMINAL_SUBMIT_SETTLE_QUIET_MS, _createRemoteSubmissionRecord, _getHeadlessViewportRead, _instanceIdentityAccess, _isLiveTerminalSession, _markQueueAutosendDirty, _ownerRecovery, _providerActivelyGenerating, _recordRuntimeInputAcceptedForSession, _recordRuntimeInputResultForSession, _remoteTerminalSubmissionBlocker, _remoteTerminalSubmissionText, _remoteTerminalSubmissionWritePayload, _runtimeInputBaseForSession, _runtimeInputFailureResultForError, _scheduleRemoteSubmissionObservation, _snapshotOutputMarker, _storeRemoteSubmissionStatus, _terminalProcessGeneration, _waitForTerminalRenderSettle, dbModule, handleInput, promptSubmissionContract, recordSessionDiagnostic, remoteRelayStore, remoteTerminalTarget, rootDeliveryRuntime, rootTerminalInputGateway, sessions, stripAnsi, telemetry.
|
|
21
|
+
// _selectRemoteTerminalChoiceBody (12): SELECT_CHOICE_FRAME_GAP_MS, _getHeadlessTextRead, _isLiveTerminalSession, _recordRuntimeInputAcceptedForSession, _recordRuntimeInputResultForSession, _resumeChoiceClickRepair, _runtimeInputBaseForSession, _waitForTerminalRenderSettle, handleInput, recordSessionDiagnostic, sessions, terminalChoice.
|
|
22
|
+
// _observeRemoteSubmission (17): REMOTE_SUBMISSION_FINAL_STATUSES, REMOTE_SUBMISSION_OBSERVE_DELAYS_MS, REMOTE_TERMINAL_ENTER_RETRY_MIN_ELAPSED_MS, _getHeadlessRows, _isLiveTerminalSession, _isWebPromptSubmission, _readPromptSubmissionByMessageId, _remoteSubmissionDurablePromptMatch, _remoteSubmissionObservationCurrent, _scheduleRemoteSubmissionObservation, _snapshotOutputMarker, _storeRemoteSubmissionStatus, handleInput, recordSessionDiagnostic, remoteSubmissionObserver, sessions, submissionAck.
|
|
23
|
+
//
|
|
24
|
+
// Mutability (D-10 rule 2): the only dep that is `let` in server.js and reassigned there is
|
|
25
|
+
// _ownerRecovery (declared server.js:674, assigned server.js:7922), so server.js injects it as a
|
|
26
|
+
// GETTER. Every other dep is an immutable `const`/function declaration and is passed by value.
|
|
27
|
+
// No moved body assigns to any server.js top-level binding (checked mechanically).
|
|
28
|
+
//
|
|
29
|
+
// Two D-10 rule 1 notes:
|
|
30
|
+
// 1. handleRemoteApi's one inline `require('./lib/env-flags')` is the ONE non-verbatim line: it
|
|
31
|
+
// becomes this module's own static `require('../env-flags')` (same file, path relative to
|
|
32
|
+
// lib/api/). Injecting server.js's `require` instead is a loader-value transfer the public
|
|
33
|
+
// runtime closure guard refuses, so move-only-check reports exactly this one difference.
|
|
34
|
+
// 2. A dep in SHORTHAND object-property position cannot carry the prefix without changing
|
|
35
|
+
// Property.shorthand, which move-only-check compares. handleInput is bound at
|
|
36
|
+
// module scope and assigned in configure() instead.
|
|
37
|
+
//
|
|
38
|
+
// WP-15 (docs/deepseek-harness-acp-agent-2026-10-28.html): handleRemoteApi's `sendSessionMessage`
|
|
39
|
+
// is no longer byte-identical to server.js's declaration -- a LIVE ACP (DeepSeek Harness) session
|
|
40
|
+
// has no ptyProcess, so the phone's reply was refused `session_not_live` at this door. The door now
|
|
41
|
+
// admits it and hands the prompt to the SAME lane the desktop uses (`dshPromptLane.wrapDeliver` ->
|
|
42
|
+
// `deliverAcp`), injected as `deliverAcpSubmission({sessionId, text, parentReceiptId, source,
|
|
43
|
+
// attachments})`; `isAcpSession` (lib/session-transport.js) is the predicate. The phone's message id
|
|
44
|
+
// becomes the receipt/submission id, so the lane's own storeStatus (_storeRemoteSubmissionStatus)
|
|
45
|
+
// updates the row the phone polls. A non-live ACP session (no `_acpProcess`, or exited) keeps the
|
|
46
|
+
// existing `session_not_live` / deferred-activate answers, and a PTY session is untouched.
|
|
47
|
+
|
|
48
|
+
let _deps = null;
|
|
49
|
+
let handleInput = null;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
function configure(deps = {}) {
|
|
53
|
+
_deps = deps;
|
|
54
|
+
handleInput = deps.handleInput;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handleRemoteApi(req, res, url) {
|
|
58
|
+
const db = _deps.dbModule.getDb();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (url.pathname === '/api/remote/status' && req.method === 'GET') {
|
|
62
|
+
_deps._sendRemoteJson(res, 200, await _deps._remoteRelayStatusPayload(req));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (url.pathname === '/api/remote/registry' && req.method === 'GET') {
|
|
67
|
+
_deps._sendRemoteJson(res, 200, { ok: true, registry: _deps.remoteRelayProtocol.publicRegistry() });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (url.pathname === '/api/remote/audit' && req.method === 'GET') {
|
|
72
|
+
const limit = Math.max(1, Math.min(500, Number(url.searchParams.get('limit') || 100)));
|
|
73
|
+
_deps._sendRemoteJson(res, 200, { ok: true, audit: _deps.remoteRelayStore.listAudit(db, { limit }) });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (url.pathname === '/api/remote/submissions' && req.method === 'GET') {
|
|
78
|
+
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get('limit') || 50)));
|
|
79
|
+
const sessionId = String(url.searchParams.get('session_id') || url.searchParams.get('sessionId') || '').trim();
|
|
80
|
+
const sinceMs = Math.max(0, Number(url.searchParams.get('since_ms') || url.searchParams.get('sinceMs') || 0) || 0);
|
|
81
|
+
const originalSessionSnapshots = new Map(_deps.sessions);
|
|
82
|
+
await _deps._remoteCommandRuntime.handleSubmissionsGet({
|
|
83
|
+
req, url, sessionId, sinceMs, limit,
|
|
84
|
+
sendJson: (code, payload) => _deps._sendRemoteJson(res, code, payload),
|
|
85
|
+
formatSubmission: _deps._remoteSubmissionPublic,
|
|
86
|
+
isRequestCurrent: () => req.aborted !== true && req.destroyed !== true
|
|
87
|
+
&& res.destroyed !== true && res.writableEnded !== true,
|
|
88
|
+
healBeforeReply: async (submissions, originalScope) => {
|
|
89
|
+
const healingScope = Object.freeze({ ...originalScope, sessionSnapshots: originalSessionSnapshots });
|
|
90
|
+
try {
|
|
91
|
+
return await _deps._mainDbCensus.runOutsideRequestZone(() => _deps._healRemoteSubmissionRows(submissions, healingScope));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn(`[remote-submissions] heal failed: ${error.message}`);
|
|
94
|
+
return submissions;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (url.pathname === '/api/remote/pairing-claims' && req.method === 'POST') {
|
|
102
|
+
const body = await _deps.readMobileJsonBody(req, 64 * 1024);
|
|
103
|
+
const status = await _deps.remoteRelayStore.buildRemoteStatus(db);
|
|
104
|
+
if (!_deps._remoteHostedPairingReady(status)) {
|
|
105
|
+
_deps._sendRemoteJson(res, 400, {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: 'hosted_pairing_unavailable',
|
|
108
|
+
message: 'Hosted Walle Relay pairing is not connected yet.',
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const origin = body.origin || _deps.requestOrigin(req);
|
|
113
|
+
const label = String(body.label || 'Owner iPhone').trim() || 'Owner iPhone';
|
|
114
|
+
const scopes = body.scopes || ['read', 'respond'];
|
|
115
|
+
const claim = await _deps.mobileAuthStore.createDeviceClaim(db, {
|
|
116
|
+
label,
|
|
117
|
+
scopes,
|
|
118
|
+
origin,
|
|
119
|
+
createdBy: req.ctmAuth?.deviceId || 'walle-remote-setup',
|
|
120
|
+
});
|
|
121
|
+
const hostedPairUrl = new URL(`/pair/${encodeURIComponent(claim.id)}`, status.instance.mobile_url);
|
|
122
|
+
hostedPairUrl.searchParams.set('secret', claim.secret);
|
|
123
|
+
hostedPairUrl.searchParams.set('instance', status.instance.id);
|
|
124
|
+
const hostedPairUrlText = hostedPairUrl.toString();
|
|
125
|
+
const qrDataUrl = await _deps.QRCode.toDataURL(hostedPairUrlText, { margin: 1, scale: 5 });
|
|
126
|
+
await _deps.remoteRelayStore.insertAudit(db, {
|
|
127
|
+
accountId: status.instance.relay_account_id,
|
|
128
|
+
instanceId: status.instance.id,
|
|
129
|
+
action: 'pairing_claim_create',
|
|
130
|
+
risk: 'medium',
|
|
131
|
+
result: 'allow',
|
|
132
|
+
metadata: {
|
|
133
|
+
claim_id: claim.id,
|
|
134
|
+
scopes: claim.scopes,
|
|
135
|
+
hosted_pairing_available: false,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
_deps._sendRemoteJson(res, 200, {
|
|
139
|
+
ok: true,
|
|
140
|
+
claim: {
|
|
141
|
+
...claim,
|
|
142
|
+
qr_data_url: qrDataUrl,
|
|
143
|
+
local_pair_url: claim.claimUrl,
|
|
144
|
+
hosted_pair_url: hostedPairUrlText,
|
|
145
|
+
pairing_url: hostedPairUrlText,
|
|
146
|
+
},
|
|
147
|
+
relay: status.relay,
|
|
148
|
+
});
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const revokeMatch = url.pathname.match(/^\/api\/remote\/devices\/([^/]+)\/revoke$/);
|
|
153
|
+
if (revokeMatch && req.method === 'POST') {
|
|
154
|
+
const deviceId = decodeURIComponent(revokeMatch[1]);
|
|
155
|
+
await _deps.mobileAuthStore.revokeDeviceToken(db, deviceId);
|
|
156
|
+
_deps.closeWsForDeviceIds([deviceId]);
|
|
157
|
+
await _deps.remoteRelayStore.insertAudit(db, {
|
|
158
|
+
deviceId,
|
|
159
|
+
action: 'device_revoke',
|
|
160
|
+
risk: 'medium',
|
|
161
|
+
result: 'allow',
|
|
162
|
+
});
|
|
163
|
+
_deps._sendRemoteJson(res, 200, { ok: true });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (url.pathname === '/api/remote/messages' && req.method === 'POST') {
|
|
167
|
+
let body;
|
|
168
|
+
try {
|
|
169
|
+
body = await _deps.readMobileJsonBody(req, 256 * 1024);
|
|
170
|
+
} catch {
|
|
171
|
+
_deps._sendRemoteJson(res, 400, { ok: false, error: 'invalid_remote_command', code: 'invalid_remote_command' });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const rawMessage = body?.message || body;
|
|
175
|
+
const preflight = await _deps._remoteCommandRuntime.freshHttpContext(req, url);
|
|
176
|
+
if (!preflight.ok) {
|
|
177
|
+
_deps._sendRemoteJson(res, preflight.status, { ok: false, error: preflight.code, code: preflight.code });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const status = {
|
|
181
|
+
instance: _deps._remoteRelayBootInstance ? {
|
|
182
|
+
..._deps._remoteRelayBootInstance,
|
|
183
|
+
relay_account_id: String(_deps._remoteRelayBootInstance.relay_account_id || require('../env-flags').strFlag('WALLE_RELAY_ACCOUNT_ID', '') || ''),
|
|
184
|
+
} : null,
|
|
185
|
+
};
|
|
186
|
+
const bound = _deps.remoteRelayProtocol.bindTrustedEnvelope(rawMessage, {
|
|
187
|
+
accountId: status.instance?.relay_account_id || 'local',
|
|
188
|
+
instanceId: status.instance?.id,
|
|
189
|
+
deviceId: preflight.auth?.deviceId || '',
|
|
190
|
+
});
|
|
191
|
+
if (!bound.ok) {
|
|
192
|
+
const unavailable = bound.error === 'trusted_target_unavailable';
|
|
193
|
+
_deps._sendRemoteJson(res, unavailable ? 503 : 400, bound);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const message = _deps.remoteRelayProtocol.normalizeEnvelope(bound.message);
|
|
197
|
+
const guard = _deps._remoteCommandRuntime.inputGuard(message);
|
|
198
|
+
if (guard) { _deps._sendRemoteJson(res, guard.status, guard.payload); return true; }
|
|
199
|
+
const initialEnvelope = _deps.remoteRelayProtocol.authorizeRemoteEnvelope(message, preflight.auth);
|
|
200
|
+
if (!initialEnvelope.ok) {
|
|
201
|
+
const code = String(initialEnvelope.code || initialEnvelope.error || 'unauthorized');
|
|
202
|
+
const statusCode = Number(initialEnvelope.status) || (code === 'unauthorized' ? 401 : 400);
|
|
203
|
+
_deps._sendRemoteJson(res, statusCode, { ok: false, error: code, code });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
const result = await _deps._remoteCommandRuntime.execute({
|
|
207
|
+
namespace: preflight.namespace,
|
|
208
|
+
message,
|
|
209
|
+
auth: preflight.auth,
|
|
210
|
+
trustedTransport: {
|
|
211
|
+
account_id: message.account_id,
|
|
212
|
+
instance_id: message.instance_id,
|
|
213
|
+
device_id: message.device_id,
|
|
214
|
+
created_at: message.created_at || '',
|
|
215
|
+
},
|
|
216
|
+
authorize: async () => {
|
|
217
|
+
const current = await _deps._remoteCommandRuntime.freshHttpContext(req, url, preflight.namespace);
|
|
218
|
+
if (!current.ok) return current;
|
|
219
|
+
const envelope = _deps.remoteRelayProtocol.authorizeRemoteEnvelope(message, current.auth);
|
|
220
|
+
if (!envelope.ok) {
|
|
221
|
+
const code = String(envelope.code || envelope.error || 'unauthorized');
|
|
222
|
+
const statusCode = Number(envelope.status) || (code === 'unauthorized' ? 401 : 400);
|
|
223
|
+
return { ok: false, code, status: statusCode };
|
|
224
|
+
}
|
|
225
|
+
return { ok: true };
|
|
226
|
+
},
|
|
227
|
+
handlers: {
|
|
228
|
+
instanceStatus: () => _deps._remoteRelayStatusPayload(req),
|
|
229
|
+
listSessions: () => ({
|
|
230
|
+
ok: true,
|
|
231
|
+
sessions: Array.from(_deps.sessions.values()).map(_deps._remoteSessionSummary).filter(Boolean),
|
|
232
|
+
}),
|
|
233
|
+
getSession: (sessionId) => {
|
|
234
|
+
const session = _deps.sessions.get(sessionId);
|
|
235
|
+
return session ? { ok: true, session: _deps._remoteSessionSummary(session) } : { ok: false, error: 'session_not_found' };
|
|
236
|
+
},
|
|
237
|
+
wallEStatus: async () => {
|
|
238
|
+
try {
|
|
239
|
+
const upstream = await _deps.walleClient.requestJson('/api/wall-e/status');
|
|
240
|
+
return { ok: upstream.status < 400, status: upstream.json?.data || upstream.json || null };
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return { ok: false, error: err.message || 'wall_e_unavailable' };
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
activateSession: async (sessionId, body = {}, message = {}) => {
|
|
246
|
+
return _deps._activateDeferredSession(req, sessionId, body, message);
|
|
247
|
+
},
|
|
248
|
+
sendSessionMessage: async (sessionId, text, body = {}, message = {}) => {
|
|
249
|
+
const session = _deps.sessions.get(sessionId);
|
|
250
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
251
|
+
if (!text.trim()) return { ok: false, error: 'message_required' };
|
|
252
|
+
// WP-15: a LIVE DeepSeek Harness (ACP) session is a child on pipes with no ptyProcess,
|
|
253
|
+
// so _isLiveTerminalSession below can never admit it -- the phone's reply used to be
|
|
254
|
+
// refused `session_not_live` here. It is driven by the SAME prompt lane the desktop
|
|
255
|
+
// uses (dshPromptLane.wrapDeliver -> deliverAcp), injected as deliverAcpSubmission; the
|
|
256
|
+
// phone's message id is the receipt id, so the lane's own storeStatus
|
|
257
|
+
// (_storeRemoteSubmissionStatus) updates the very row the phone polls for
|
|
258
|
+
// received/delivered/failed. Attachments ride body.attachments exactly as WP-11 defines
|
|
259
|
+
// them. Anything that is not live (no _acpProcess, or exited) falls through to the
|
|
260
|
+
// existing refusal below, which is what a deferred dsh placeholder needs.
|
|
261
|
+
if (_deps.isAcpSession(session) && session._acpProcess && session.status !== 'exited' && session.status !== 'closed') {
|
|
262
|
+
const acpMessageId = String(message.id || '').trim();
|
|
263
|
+
if (acpMessageId) await _deps._createRemoteSubmissionRecord(sessionId, text, {
|
|
264
|
+
messageId: acpMessageId,
|
|
265
|
+
deviceId: message.device_id || req.ctmAuth?.deviceId || '',
|
|
266
|
+
messageType: message.type || 'session.send_message',
|
|
267
|
+
source: 'mobile',
|
|
268
|
+
});
|
|
269
|
+
const acpDelivered = await _deps.deliverAcpSubmission({
|
|
270
|
+
sessionId, text, parentReceiptId: acpMessageId, source: 'mobile', attachments: body.attachments,
|
|
271
|
+
});
|
|
272
|
+
if (acpDelivered && acpDelivered.ok === true) _deps.emitHumanSubmit(sessionId);
|
|
273
|
+
return acpDelivered;
|
|
274
|
+
}
|
|
275
|
+
if (!_deps._isLiveTerminalSession(session)) {
|
|
276
|
+
// A deferred placeholder (post-restart, no PTY yet) can be revived from the
|
|
277
|
+
// phone. Tell the client so it offers Activate instead of retrying forever.
|
|
278
|
+
if (session._restoreDeferred) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: 'session_not_live',
|
|
282
|
+
restore_deferred: true,
|
|
283
|
+
activate_required: true,
|
|
284
|
+
message: 'This session is asleep on your Mac. Activate it to send.',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
error: 'session_not_live',
|
|
290
|
+
message: 'This session is not accepting terminal input.',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const delivered = await _sendRemoteTerminalSubmission(req, sessionId, text, { message, messageId: message.id, deviceId: message.device_id || req.ctmAuth?.deviceId || '', messageType: message.type || 'session.send_message', body });
|
|
294
|
+
if (delivered && delivered.ok === true) _deps.emitHumanSubmit(sessionId); // WP-5 D-13: once per accepted phone message
|
|
295
|
+
return delivered;
|
|
296
|
+
},
|
|
297
|
+
respondApproval: async (sessionId, decision, body = {}, message = {}) => {
|
|
298
|
+
return _deps._respondToRemoteApproval(req, sessionId, decision, body, message);
|
|
299
|
+
},
|
|
300
|
+
selectChoice: async (sessionId, body = {}, message = {}) => {
|
|
301
|
+
return body.generation != null
|
|
302
|
+
? _deps._selectResumePromptChoice(req, sessionId, body, message)
|
|
303
|
+
: _deps._selectRemoteTerminalChoice(req, sessionId, body, message);
|
|
304
|
+
},
|
|
305
|
+
submitSessionInput: async (sessionId, body = {}, message = {}) => {
|
|
306
|
+
const session = _deps.sessions.get(sessionId);
|
|
307
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
308
|
+
const runtimeInput = _deps._runtimeInputBaseForSession(sessionId, session, {
|
|
309
|
+
inputId: message.id ? `remote:${message.id}` : '',
|
|
310
|
+
message,
|
|
311
|
+
messageId: message.id,
|
|
312
|
+
deviceId: message.device_id || req.ctmAuth?.deviceId || '',
|
|
313
|
+
messageType: message.type || 'session.submit_input',
|
|
314
|
+
mode: 'inline',
|
|
315
|
+
source: 'mobile',
|
|
316
|
+
text: '',
|
|
317
|
+
action: 'session.submit_input',
|
|
318
|
+
appendUserInputEvent: false,
|
|
319
|
+
metadata: { key: 'enter' },
|
|
320
|
+
});
|
|
321
|
+
_deps._recordRuntimeInputAcceptedForSession(sessionId, session, runtimeInput);
|
|
322
|
+
if (!_deps._isLiveTerminalSession(session)) {
|
|
323
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'session_not_live');
|
|
324
|
+
return { ok: false, error: 'session_not_live', message: 'This session is not accepting terminal control input.' };
|
|
325
|
+
}
|
|
326
|
+
// A line-editor TUI (Claude Code / Codex) swallows an Enter that
|
|
327
|
+
// arrives before it has committed the composer's current contents,
|
|
328
|
+
// leaving the reply stuck in the composer — the exact failure the
|
|
329
|
+
// full phone-send path avoids by waiting for the terminal to settle
|
|
330
|
+
// before pressing Enter. This Submit-current-input button used to
|
|
331
|
+
// fire a bare \r instantly, so a tap could no-op and the agent kept
|
|
332
|
+
// waiting. Wait for render settle first, then press Enter, and
|
|
333
|
+
// report whether the terminal actually advanced instead of blindly
|
|
334
|
+
// claiming success.
|
|
335
|
+
const beforeEnterMarker = _deps._snapshotOutputMarker(session);
|
|
336
|
+
await _deps._waitForTerminalRenderSettle(session, {
|
|
337
|
+
minMs: _deps.REMOTE_TERMINAL_SUBMIT_DELAY_MS,
|
|
338
|
+
maxMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_MAX_MS,
|
|
339
|
+
quietMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_QUIET_MS,
|
|
340
|
+
});
|
|
341
|
+
const live = _deps.sessions.get(sessionId);
|
|
342
|
+
if (!_deps._isLiveTerminalSession(live)) {
|
|
343
|
+
_deps._recordRuntimeInputResultForSession(sessionId, live || session, runtimeInput, 'rejected', 'session_not_live_before_enter');
|
|
344
|
+
return { ok: false, error: 'session_not_live', message: 'The session stopped before CTM could press Enter.' };
|
|
345
|
+
}
|
|
346
|
+
const composerRows = await _deps._getHeadlessRows(sessionId, 900); // an unreadable screen is not proof the composer is empty
|
|
347
|
+
if (Array.isArray(composerRows) && _deps.terminalComposerState(composerRows).empty) {
|
|
348
|
+
_deps._recordRuntimeInputResultForSession(sessionId, live, runtimeInput, 'rejected', 'composer_empty');
|
|
349
|
+
return { ok: false, error: 'composer_empty', message: 'There is nothing typed in the terminal to submit.' };
|
|
350
|
+
}
|
|
351
|
+
handleInput({ auth: req.ctmAuth }, { id: sessionId, data: '\r' }, { authenticatedRemote: true });
|
|
352
|
+
// Give the agent a beat to accept (clear the composer / start a turn),
|
|
353
|
+
// then check whether the terminal output advanced past the Enter.
|
|
354
|
+
await _deps._waitForTerminalRenderSettle(_deps.sessions.get(sessionId), {
|
|
355
|
+
minMs: _deps.REMOTE_TERMINAL_SUBMIT_DELAY_MS,
|
|
356
|
+
maxMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_MAX_MS,
|
|
357
|
+
quietMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_QUIET_MS,
|
|
358
|
+
});
|
|
359
|
+
const advanced = _deps.remoteSubmissionObserver.terminalSubmissionAcceptedByMarker({
|
|
360
|
+
beforeEnterMarker,
|
|
361
|
+
markerNow: _deps._snapshotOutputMarker(_deps.sessions.get(sessionId)),
|
|
362
|
+
}).accepted;
|
|
363
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-submit-current-input', {
|
|
364
|
+
source: 'remote-api',
|
|
365
|
+
message_id: message.id || '',
|
|
366
|
+
device_id: message.device_id || req.ctmAuth?.deviceId || '',
|
|
367
|
+
advanced,
|
|
368
|
+
});
|
|
369
|
+
_deps._recordRuntimeInputResultForSession(sessionId, _deps.sessions.get(sessionId) || session, runtimeInput, advanced ? 'delivered' : 'deferred', advanced ? 'enter_advanced' : 'enter_no_advance');
|
|
370
|
+
return { ok: true, delivered: true, submitted: advanced, key: 'enter' };
|
|
371
|
+
},
|
|
372
|
+
cancelSessionStream: (sessionId, body = {}, message = {}) => {
|
|
373
|
+
const session = _deps.sessions.get(sessionId);
|
|
374
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
375
|
+
if (session.type === 'walle') {
|
|
376
|
+
return _deps._cancelWalleSessionTurn(sessionId, 'remote-api');
|
|
377
|
+
}
|
|
378
|
+
if (!_deps._isLiveTerminalSession(session)) {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: 'session_not_live',
|
|
382
|
+
message: 'This session is not accepting terminal control input.',
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const interrupt = _deps._injectSessionInterrupt(session, 'remote');
|
|
386
|
+
if (!interrupt.ok) return { ok: false, error: interrupt.reason };
|
|
387
|
+
// Mirror the sibling remote-approval-deny diagnostic (this handler writes
|
|
388
|
+
// the exact same turn-aborting escape byte, just from a different phone
|
|
389
|
+
// action) so "who sent the escape" is attributable from this call site too.
|
|
390
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-cancel-stream', {
|
|
391
|
+
source: 'remote-api',
|
|
392
|
+
messageId: message.id || '',
|
|
393
|
+
key: 'escape',
|
|
394
|
+
});
|
|
395
|
+
return { ok: true, delivered: true, key: 'escape' };
|
|
396
|
+
},
|
|
397
|
+
navigatePromptHistory: (sessionId, direction) => {
|
|
398
|
+
const session = _deps.sessions.get(sessionId);
|
|
399
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
400
|
+
if (!session.ptyProcess || session.status === 'exited' || session.status === 'closed') {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: 'session_not_live',
|
|
404
|
+
message: 'This session is not accepting terminal control input.',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const data = direction === 'previous' ? '\x1b[A' : '\x1b[B';
|
|
408
|
+
handleInput({ auth: req.ctmAuth }, { id: sessionId, data }, { authenticatedRemote: true });
|
|
409
|
+
return { ok: true, delivered: true, direction, key: direction === 'previous' ? 'arrow_up' : 'arrow_down' };
|
|
410
|
+
},
|
|
411
|
+
sendWalleMessage: async (sessionId, text, body = {}, message = {}) => {
|
|
412
|
+
const submission = await _deps._createRemoteSubmissionRecord(sessionId, text, {
|
|
413
|
+
message,
|
|
414
|
+
messageId: message.id,
|
|
415
|
+
deviceId: message.device_id || req.ctmAuth?.deviceId || '',
|
|
416
|
+
messageType: message.type || 'wall_e.send_message',
|
|
417
|
+
});
|
|
418
|
+
const runtimeInput = _deps._runtimeInputBaseForSession(sessionId, _deps.sessions.get(sessionId), {
|
|
419
|
+
inputId: message.id ? `remote:${message.id}` : '',
|
|
420
|
+
message,
|
|
421
|
+
messageId: message.id,
|
|
422
|
+
deviceId: message.device_id || req.ctmAuth?.deviceId || '',
|
|
423
|
+
messageType: message.type || 'wall_e.send_message',
|
|
424
|
+
adapter: 'walle',
|
|
425
|
+
mode: 'inline',
|
|
426
|
+
source: 'mobile',
|
|
427
|
+
text,
|
|
428
|
+
attachments: Array.isArray(body.attachments) ? body.attachments : [],
|
|
429
|
+
action: 'wall_e.send_message',
|
|
430
|
+
metadata: { transport: 'remote-wall-e' },
|
|
431
|
+
});
|
|
432
|
+
_deps._recordRuntimeInputAcceptedForSession(sessionId, _deps.sessions.get(sessionId), runtimeInput);
|
|
433
|
+
const session = _deps.sessions.get(sessionId);
|
|
434
|
+
if (!session && _deps._isWalleChatThreadId(sessionId)) {
|
|
435
|
+
const result = await _deps._sendRemoteWalleThreadMessage(sessionId, text, body);
|
|
436
|
+
_deps._recordRuntimeInputResultForSession(
|
|
437
|
+
sessionId,
|
|
438
|
+
null,
|
|
439
|
+
runtimeInput,
|
|
440
|
+
result.ok ? 'delivered' : 'rejected',
|
|
441
|
+
result.ok ? 'walle_default_chat_delivered' : (result.error || 'walle_chat_failed')
|
|
442
|
+
);
|
|
443
|
+
if (submission) {
|
|
444
|
+
_deps._storeRemoteSubmissionStatus(sessionId, message.id, result.ok ? 'durable_user_message_seen' : 'failed', {
|
|
445
|
+
reason: result.ok ? 'walle_default_chat_delivered' : (result.error || 'walle_chat_failed'),
|
|
446
|
+
message: result.ok ? 'Wall-E accepted this phone reply.' : (result.message || result.error || 'Wall-E could not accept this phone reply.'),
|
|
447
|
+
source: 'wall-e',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return submission ? { ...result, submission_id: message.id, submission_status: result.ok ? 'durable_user_message_seen' : 'failed' } : result;
|
|
451
|
+
}
|
|
452
|
+
if (!session) {
|
|
453
|
+
if (submission) _deps._storeRemoteSubmissionStatus(sessionId, message.id, 'failed', { reason: 'session_not_found', message: 'Session was not found.', source: 'remote-api' });
|
|
454
|
+
_deps._recordRuntimeInputResultForSession(sessionId, null, runtimeInput, 'rejected', 'session_not_found');
|
|
455
|
+
return { ok: false, error: 'session_not_found', submission_id: message.id || undefined, submission_status: submission ? 'failed' : undefined };
|
|
456
|
+
}
|
|
457
|
+
if (session.type !== 'walle') {
|
|
458
|
+
if (submission) _deps._storeRemoteSubmissionStatus(sessionId, message.id, 'failed', { reason: 'not_walle_session', message: 'This is not a Wall-E session.', source: 'remote-api' });
|
|
459
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'not_walle_session');
|
|
460
|
+
return { ok: false, error: 'not_walle_session', submission_id: message.id || undefined, submission_status: submission ? 'failed' : undefined };
|
|
461
|
+
}
|
|
462
|
+
if (!text.trim()) {
|
|
463
|
+
if (submission) _deps._storeRemoteSubmissionStatus(sessionId, message.id, 'failed', { reason: 'message_required', message: 'Message text is required.', source: 'remote-api' });
|
|
464
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'message_required');
|
|
465
|
+
return { ok: false, error: 'message_required', submission_id: message.id || undefined, submission_status: submission ? 'failed' : undefined };
|
|
466
|
+
}
|
|
467
|
+
const attachments = Array.isArray(body.attachments) ? body.attachments : [];
|
|
468
|
+
const explicitModel = body.model_id || body.model || '';
|
|
469
|
+
const explicitProvider = body.model_provider || body.provider || '';
|
|
470
|
+
await _deps.handleWalleMessage({ auth: req.ctmAuth }, {
|
|
471
|
+
id: sessionId,
|
|
472
|
+
text,
|
|
473
|
+
attachments,
|
|
474
|
+
echoUser: true,
|
|
475
|
+
displayText: text,
|
|
476
|
+
model: explicitModel || session.model_id || '',
|
|
477
|
+
provider: explicitProvider || session.model_provider || '',
|
|
478
|
+
model_registry_id: body.model_registry_id || body.modelRegistryId || session.model_registry_id || '',
|
|
479
|
+
model_provider_id: body.model_provider_id || body.modelProviderId || session.model_provider_id || '',
|
|
480
|
+
modelPinned: body.modelPinned === true || body.model_pinned === true || !!explicitModel || !!session.model_pinned,
|
|
481
|
+
allowProviderFallback: explicitModel ? false : !session.model_pinned,
|
|
482
|
+
runtimeInputId: runtimeInput.inputId,
|
|
483
|
+
messageId: message.id || '',
|
|
484
|
+
source: 'remote-api',
|
|
485
|
+
});
|
|
486
|
+
if (submission) {
|
|
487
|
+
_deps._storeRemoteSubmissionStatus(sessionId, message.id, 'durable_user_message_seen', {
|
|
488
|
+
reason: 'walle_session_delivered',
|
|
489
|
+
message: 'Wall-E accepted this phone reply.',
|
|
490
|
+
source: 'wall-e',
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return { ok: true, delivered: true, submission_id: message.id || undefined, submission_status: submission ? 'durable_user_message_seen' : undefined };
|
|
494
|
+
},
|
|
495
|
+
setWalleModel: async (sessionId, body = {}) => {
|
|
496
|
+
const session = _deps.sessions.get(sessionId);
|
|
497
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
498
|
+
if (!_deps._isWalleModelConfigurableSession(session)) return { ok: false, error: 'not_walle_session' };
|
|
499
|
+
const pref = await _deps._persistWalleSessionModelPreference(session, {
|
|
500
|
+
model_id: body.model_id || body.model || '',
|
|
501
|
+
model_provider: body.model_provider || body.provider || '',
|
|
502
|
+
model_registry_id: body.model_registry_id || body.modelRegistryId || '',
|
|
503
|
+
model_provider_id: body.model_provider_id || body.provider_id || '',
|
|
504
|
+
scope: body.scope || 'session',
|
|
505
|
+
source: 'phone',
|
|
506
|
+
pinned: body.pinned !== false,
|
|
507
|
+
});
|
|
508
|
+
const selection = _deps._walleModelSelectionForActiveSession(session);
|
|
509
|
+
return {
|
|
510
|
+
ok: true,
|
|
511
|
+
delivered: true,
|
|
512
|
+
model_id: selection.model_id || null,
|
|
513
|
+
model_provider: selection.model_provider || null,
|
|
514
|
+
model_registry_id: selection.model_registry_id || '',
|
|
515
|
+
model_provider_id: selection.model_provider_id || '',
|
|
516
|
+
model_pinned: !!selection.model_pinned,
|
|
517
|
+
cleared: !pref,
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
_deps._sendRemoteJson(res, _deps._remoteCommandRuntime.httpStatus(result), result);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
// A write-lock-busy error (cold-Dropbox boot storm saturating the db-owner worker)
|
|
527
|
+
// must surface as a retryable 503, never the raw "SQLite write lock busy … <lock path>"
|
|
528
|
+
// string — that leaked to the phone as a red error banner after a restart.
|
|
529
|
+
const busy = _deps._writeLockBusyApiError(err);
|
|
530
|
+
if (busy) {
|
|
531
|
+
_deps._sendRemoteJson(res, busy.status, { ok: false, ...busy.body });
|
|
532
|
+
} else if (url.pathname === '/api/remote/messages' || url.pathname === '/api/remote/submissions') {
|
|
533
|
+
_deps._sendRemoteJson(res, 503, {
|
|
534
|
+
ok: false,
|
|
535
|
+
error: 'remote_command_auth_unavailable',
|
|
536
|
+
code: 'remote_command_auth_unavailable',
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
_deps._sendRemoteJson(res, 400, {
|
|
540
|
+
ok: false,
|
|
541
|
+
error: err && err.message ? err.message : 'remote_api_error',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function _sendRemoteTerminalSubmission(req, sessionId, text, options = {}) {
|
|
551
|
+
_deps._ownerRecovery.assertAdmission(sessionId);
|
|
552
|
+
const captureTarget = session => _deps.remoteTerminalTarget.captureRemoteTerminalTarget(sessionId, session, _deps.sessions, _deps._terminalProcessGeneration);
|
|
553
|
+
const resolved = typeof _deps.rootTerminalInputGateway === 'undefined'
|
|
554
|
+
? (() => {
|
|
555
|
+
const session = _deps.sessions.get(sessionId);
|
|
556
|
+
return { ok: true, canonical: false, session, target: captureTarget(session),
|
|
557
|
+
messageId: String(options.messageId || options.message_id || options.message?.id || '').trim(),
|
|
558
|
+
messageType: options.messageType || options.message_type || options.message?.type || 'session.send_message',
|
|
559
|
+
source: options.source };
|
|
560
|
+
})()
|
|
561
|
+
: _deps.rootTerminalInputGateway.resolveSubmission(sessionId, options, captureTarget);
|
|
562
|
+
if (!resolved.ok) return {
|
|
563
|
+
ok: false, error: 'root_permit_invalid', reason: resolved.reason || 'root_permit_invalid', retryable: false,
|
|
564
|
+
};
|
|
565
|
+
const initialSession = resolved.session;
|
|
566
|
+
const target = resolved.target;
|
|
567
|
+
const canonicalRoot = resolved.canonical === true;
|
|
568
|
+
const messageId = resolved.messageId,
|
|
569
|
+
deviceId = String(options.deviceId || options.device_id || options.message?.device_id || req?.ctmAuth?.deviceId || '').trim(),
|
|
570
|
+
messageType = resolved.messageType;
|
|
571
|
+
const rootJobLifetime = canonicalRoot ? _deps.rootDeliveryRuntime.getRootDeliveryJobLifetime(options.rootDeliveryPermit) : null;
|
|
572
|
+
const rootJobCurrent = () => !canonicalRoot || (rootJobLifetime && _deps.rootDeliveryRuntime.isRootDeliveryJobLifetimeCurrent(rootJobLifetime, sessionId, messageId)
|
|
573
|
+
&& rootJobLifetime.databaseHandle === _deps.dbModule.getDb() && rootJobLifetime.instanceId === _deps._instanceIdentityAccess.getCachedInstanceId()
|
|
574
|
+
&& rootJobLifetime.session === initialSession && _deps.sessions.get(sessionId) === initialSession && target.isCurrent());
|
|
575
|
+
if (canonicalRoot && !rootJobCurrent()) return { ok: false, error: 'root_permit_invalid', reason: 'root_permit_invalid', retryable: false };
|
|
576
|
+
const storeStatus = (id, status, details) => _deps._storeRemoteSubmissionStatus(sessionId, id, status, details, canonicalRoot ? rootJobLifetime : undefined);
|
|
577
|
+
let beforeMarker = '', startedAt = Date.now();
|
|
578
|
+
const submissionLabel = messageType === 'web.submit_prompt'
|
|
579
|
+
? 'prompt'
|
|
580
|
+
: 'phone reply';
|
|
581
|
+
const runtimeInput = _deps._runtimeInputBaseForSession(sessionId, initialSession, {
|
|
582
|
+
inputId: options.runtimeInputId || options.runtime_input_id || (messageId ? `remote:${messageId}` : ''),
|
|
583
|
+
message: options.message,
|
|
584
|
+
messageId,
|
|
585
|
+
deviceId,
|
|
586
|
+
messageType,
|
|
587
|
+
mode: 'inline',
|
|
588
|
+
source: options.source || 'mobile',
|
|
589
|
+
text,
|
|
590
|
+
attachments: Array.isArray(options.body && options.body.attachments) ? options.body.attachments : [],
|
|
591
|
+
action: options.action || 'session.send_message',
|
|
592
|
+
metadata: {
|
|
593
|
+
transport: 'remote-terminal',
|
|
594
|
+
messageType,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
const replacedResult = () => _deps.remoteTerminalTarget.terminalTargetReplacedResult(
|
|
598
|
+
messageId, options.source || 'remote-api', result => storeStatus(messageId, 'needs_attention', result));
|
|
599
|
+
if (messageId && !options.remoteSubmissionPreRecorded) {
|
|
600
|
+
const submission = await _deps._createRemoteSubmissionRecord(sessionId, text, {
|
|
601
|
+
messageId,
|
|
602
|
+
deviceId,
|
|
603
|
+
messageType,
|
|
604
|
+
source: options.source || 'remote-api',
|
|
605
|
+
});
|
|
606
|
+
if (!rootJobCurrent()) return replacedResult();
|
|
607
|
+
if (!_deps.promptSubmissionContract.isConfirmedSubmissionRecord(submission, { messageId, sessionId, messageType, text }, _deps.remoteRelayStore.remoteSubmissionConflict)) return Object.assign(_deps.promptSubmissionContract.unconfirmedRecordResult(), { submission_id: messageId, submission_status: 'blocked' });
|
|
608
|
+
}
|
|
609
|
+
if (!rootJobCurrent() || (target.session && target.ptyProcess && !target.isCurrent())) return replacedResult();
|
|
610
|
+
if (!options.runtimeInputPreRecorded) _deps._recordRuntimeInputAcceptedForSession(sessionId, initialSession, runtimeInput);
|
|
611
|
+
const blocker = _deps._remoteTerminalSubmissionBlocker(sessionId, initialSession, Date.now(), { text });
|
|
612
|
+
if (blocker) {
|
|
613
|
+
if (messageId) {
|
|
614
|
+
storeStatus(messageId, canonicalRoot ? 'needs_attention' : 'blocked', {
|
|
615
|
+
reason: blocker.reason || blocker.error || 'blocked',
|
|
616
|
+
message: blocker.message || `CTM could not deliver this ${submissionLabel}.`,
|
|
617
|
+
retryable: blocker.retryable === true,
|
|
618
|
+
source: options.source || 'remote-api',
|
|
619
|
+
});
|
|
620
|
+
if (!canonicalRoot) _deps._markQueueAutosendDirty(sessionId, blocker.reason || blocker.error);
|
|
621
|
+
}
|
|
622
|
+
_deps._recordRuntimeInputResultForSession(
|
|
623
|
+
sessionId,
|
|
624
|
+
initialSession,
|
|
625
|
+
runtimeInput,
|
|
626
|
+
canonicalRoot ? 'deferred' : _deps._runtimeInputFailureResultForError(blocker.error),
|
|
627
|
+
blocker.reason || blocker.error || 'blocked'
|
|
628
|
+
);
|
|
629
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-blocked', blocker);
|
|
630
|
+
try {
|
|
631
|
+
_deps.telemetry.track('remote_terminal_submit_blocked', {
|
|
632
|
+
session_id: String(sessionId || '').slice(0, 8),
|
|
633
|
+
error: blocker.error || 'blocked',
|
|
634
|
+
reason: blocker.reason || '',
|
|
635
|
+
});
|
|
636
|
+
} catch {}
|
|
637
|
+
return messageId
|
|
638
|
+
? { ...blocker, retryable: canonicalRoot ? false : blocker.retryable === true,
|
|
639
|
+
submission_id: messageId, submission_status: canonicalRoot ? 'needs_attention' : 'blocked' }
|
|
640
|
+
: blocker;
|
|
641
|
+
}
|
|
642
|
+
if (!rootJobCurrent() || !target.isCurrent()) return replacedResult();
|
|
643
|
+
// Acquire synchronously after the blocker so concurrent submits cannot interleave.
|
|
644
|
+
const attempt = _deps.remoteTerminalTarget.acquireRemoteTerminalAttempt(initialSession);
|
|
645
|
+
if (!attempt) return replacedResult();
|
|
646
|
+
try {
|
|
647
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
648
|
+
beforeMarker = _deps._snapshotOutputMarker(initialSession);
|
|
649
|
+
const data = _deps._remoteTerminalSubmissionText(text);
|
|
650
|
+
const writePayload = _deps._remoteTerminalSubmissionWritePayload(initialSession, data);
|
|
651
|
+
const bracketedPaste = writePayload !== data;
|
|
652
|
+
// Read viewport only after admission; failure is weak evidence, not a refusal.
|
|
653
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
654
|
+
const preSubmitRead = await _deps._getHeadlessViewportRead(sessionId, 700);
|
|
655
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
656
|
+
const submittedWhileGenerating = preSubmitRead.ok
|
|
657
|
+
&& _deps._providerActivelyGenerating(initialSession, _deps.stripAnsi(String(preSubmitRead.text || '')));
|
|
658
|
+
let beforeEnterMarker = '';
|
|
659
|
+
const writeResult = await _deps.promptSubmissionContract.executeTerminalWriteSequence({
|
|
660
|
+
payload: writePayload,
|
|
661
|
+
// Normalize every admission refusal for the write-sequence contract.
|
|
662
|
+
write: payload => canonicalRoot
|
|
663
|
+
? _deps.rootTerminalInputGateway.writeRootInput({
|
|
664
|
+
permit: options.rootDeliveryPermit,
|
|
665
|
+
sessionId,
|
|
666
|
+
payload,
|
|
667
|
+
target,
|
|
668
|
+
attempt,
|
|
669
|
+
handleInput,
|
|
670
|
+
})
|
|
671
|
+
: _deps.remoteTerminalTarget.writeRemoteTerminalInput({
|
|
672
|
+
handleInput,
|
|
673
|
+
request: { auth: req.ctmAuth },
|
|
674
|
+
message: { id: sessionId, data: payload },
|
|
675
|
+
isTerminalTargetCurrent: target.isCurrent,
|
|
676
|
+
isTerminalAttemptCurrent: attempt.ownsInitialSession,
|
|
677
|
+
}),
|
|
678
|
+
onTextWritten: () => {
|
|
679
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return;
|
|
680
|
+
if (messageId) {
|
|
681
|
+
storeStatus(messageId, 'typed_to_pty', {
|
|
682
|
+
reason: 'text_written',
|
|
683
|
+
message: 'CTM typed this prompt into the terminal.',
|
|
684
|
+
source: options.source || 'remote-api',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-text', {
|
|
688
|
+
chars: data.length,
|
|
689
|
+
multiline: data.includes('\n'),
|
|
690
|
+
bracketedPaste,
|
|
691
|
+
enterDelayMs: _deps.REMOTE_TERMINAL_SUBMIT_DELAY_MS,
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
settle: async () => {
|
|
695
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return;
|
|
696
|
+
await _deps._waitForTerminalRenderSettle(initialSession, {
|
|
697
|
+
minMs: _deps.REMOTE_TERMINAL_SUBMIT_DELAY_MS,
|
|
698
|
+
maxMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_MAX_MS,
|
|
699
|
+
quietMs: _deps.REMOTE_TERMINAL_SUBMIT_SETTLE_QUIET_MS,
|
|
700
|
+
});
|
|
701
|
+
return rootJobCurrent() && target.isExecutionCurrent(attempt);
|
|
702
|
+
},
|
|
703
|
+
isLive: () => rootJobCurrent() && target.isExecutionCurrent(attempt) && _deps._isLiveTerminalSession(initialSession),
|
|
704
|
+
onBeforeEnter: () => {
|
|
705
|
+
if (rootJobCurrent() && target.isExecutionCurrent(attempt)) beforeEnterMarker = _deps._snapshotOutputMarker(initialSession);
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
const session = initialSession;
|
|
709
|
+
if (!rootJobCurrent() || target.wasReplaced() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
710
|
+
if (!writeResult.ok) {
|
|
711
|
+
// A refused text write is retryable only when the underlying blocker is transient.
|
|
712
|
+
if (writeResult.error === 'terminal_input_refused') {
|
|
713
|
+
const innerReason = String(writeResult.reason || 'pty_unavailable');
|
|
714
|
+
const reason = `terminal_input_${innerReason}`;
|
|
715
|
+
const retryable = !canonicalRoot && innerReason !== 'read_only' && innerReason !== 'owner_mismatch';
|
|
716
|
+
// Text may already be present, so an Enter refusal cannot retry the whole prompt.
|
|
717
|
+
if (writeResult.stage === 'enter') {
|
|
718
|
+
const message = `The text reached the terminal, but CTM could not press Enter (${reason}).`;
|
|
719
|
+
if (messageId) {
|
|
720
|
+
storeStatus(messageId, 'needs_attention', {
|
|
721
|
+
reason: 'enter_refused_after_text',
|
|
722
|
+
retryable: false,
|
|
723
|
+
message,
|
|
724
|
+
source: options.source || 'remote-api',
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-refused', { reason });
|
|
728
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'deferred', 'enter_refused_after_text');
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
error: 'enter_refused_after_text',
|
|
732
|
+
reason,
|
|
733
|
+
retryable: false,
|
|
734
|
+
message,
|
|
735
|
+
submission_id: messageId || undefined,
|
|
736
|
+
submission_status: messageId ? 'needs_attention' : undefined,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const message = `CTM could not type into this terminal (${reason}).`;
|
|
740
|
+
const refusedStatus = canonicalRoot ? 'needs_attention' : 'blocked';
|
|
741
|
+
if (messageId) {
|
|
742
|
+
storeStatus(messageId, refusedStatus, {
|
|
743
|
+
reason,
|
|
744
|
+
retryable,
|
|
745
|
+
message,
|
|
746
|
+
source: options.source || 'remote-api',
|
|
747
|
+
});
|
|
748
|
+
if (!canonicalRoot) _deps._markQueueAutosendDirty(sessionId, reason);
|
|
749
|
+
}
|
|
750
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-write-refused', { reason, retryable });
|
|
751
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput,
|
|
752
|
+
canonicalRoot ? 'deferred' : 'rejected', reason);
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
error: 'terminal_input_refused',
|
|
756
|
+
reason,
|
|
757
|
+
retryable,
|
|
758
|
+
message,
|
|
759
|
+
submission_id: messageId || undefined,
|
|
760
|
+
submission_status: messageId ? refusedStatus : undefined,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (messageId) {
|
|
764
|
+
storeStatus(messageId, 'failed', {
|
|
765
|
+
reason: 'session_not_live_before_enter',
|
|
766
|
+
message: 'The text reached the terminal, but the session stopped before CTM could press Enter.',
|
|
767
|
+
source: options.source || 'remote-api',
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-skipped', { reason: 'session_not_live' });
|
|
771
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'session_not_live_before_enter');
|
|
772
|
+
return {
|
|
773
|
+
ok: false,
|
|
774
|
+
error: 'session_not_live',
|
|
775
|
+
message: 'The text reached the terminal, but the session stopped before CTM could press Enter.',
|
|
776
|
+
submission_id: messageId || undefined,
|
|
777
|
+
submission_status: messageId ? 'failed' : undefined,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
781
|
+
if (messageId) {
|
|
782
|
+
storeStatus(messageId, 'observing', {
|
|
783
|
+
reason: 'enter_sent',
|
|
784
|
+
message: `CTM pressed Enter and is waiting for the agent to accept the ${submissionLabel}.`,
|
|
785
|
+
source: options.source || 'remote-api',
|
|
786
|
+
});
|
|
787
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
788
|
+
_deps._scheduleRemoteSubmissionObservation({
|
|
789
|
+
sessionId,
|
|
790
|
+
messageId,
|
|
791
|
+
text: data,
|
|
792
|
+
beforeMarker,
|
|
793
|
+
beforeEnterMarker,
|
|
794
|
+
startedAt,
|
|
795
|
+
auth: canonicalRoot ? null : req.ctmAuth,
|
|
796
|
+
rootJobLifetime: canonicalRoot ? rootJobLifetime : undefined,
|
|
797
|
+
source: options.source || 'remote-api',
|
|
798
|
+
submittedWhileGenerating,
|
|
799
|
+
preSubmitReadOk: preSubmitRead.ok,
|
|
800
|
+
preSubmitReadReason: preSubmitRead.reason || '',
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (!rootJobCurrent() || !target.isExecutionCurrent(attempt)) return replacedResult();
|
|
804
|
+
if (!options.confirmDelivery) {
|
|
805
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'delivered', 'enter_sent');
|
|
806
|
+
}
|
|
807
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter', { enterDelayMs: _deps.REMOTE_TERMINAL_SUBMIT_DELAY_MS });
|
|
808
|
+
return {
|
|
809
|
+
ok: true,
|
|
810
|
+
delivered: true,
|
|
811
|
+
submitted: true,
|
|
812
|
+
submission_id: messageId || undefined,
|
|
813
|
+
submission_status: messageId ? 'observing' : undefined,
|
|
814
|
+
};
|
|
815
|
+
} finally {
|
|
816
|
+
attempt.release();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function _selectRemoteTerminalChoiceBody(req, sessionId, body = {}, message = {}, options = {}) {
|
|
821
|
+
const session = _deps.sessions.get(sessionId);
|
|
822
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
823
|
+
if (!_deps._isLiveTerminalSession(session)) return { ok: false, error: 'session_not_live', message: 'This session is not accepting terminal input.' };
|
|
824
|
+
const choiceCallerIsRemoteHttp = !!(req && req.ctmAuth);
|
|
825
|
+
const choiceInputWs = choiceCallerIsRemoteHttp ? { auth: req.ctmAuth } : req;
|
|
826
|
+
const choiceInputOptions = { authenticatedRemote: choiceCallerIsRemoteHttp }, targetValue = String(body.value ?? body.index ?? '').trim();
|
|
827
|
+
const targetLabel = String(body.label || '').trim();
|
|
828
|
+
const submitOnly = body.submit === true || String(body.action || '').toLowerCase() === 'submit';
|
|
829
|
+
if (!submitOnly && !targetValue && !targetLabel) {
|
|
830
|
+
return { ok: false, error: 'invalid_choice', message: 'No choice option was specified.' };
|
|
831
|
+
}
|
|
832
|
+
const choiceText = typeof body.text === 'string' ? body.text : ''; // typed after a free-text option is selected
|
|
833
|
+
if (choiceText.length > 4000) return { ok: false, error: 'invalid_choice_text', message: 'The reply is too long.' };
|
|
834
|
+
const expectedFingerprint = String(body.fingerprint || '').trim();
|
|
835
|
+
const runtimeInput = _deps._runtimeInputBaseForSession(sessionId, session, {
|
|
836
|
+
inputId: message.id ? `remote:${message.id}` : '',
|
|
837
|
+
message,
|
|
838
|
+
messageId: message.id,
|
|
839
|
+
deviceId: message.device_id || req.ctmAuth?.deviceId || '',
|
|
840
|
+
messageType: message.type || 'session.select_choice',
|
|
841
|
+
mode: 'inline',
|
|
842
|
+
source: 'mobile',
|
|
843
|
+
text: targetLabel || targetValue || (submitOnly ? 'submit' : ''),
|
|
844
|
+
action: 'session.select_choice',
|
|
845
|
+
appendUserInputEvent: false,
|
|
846
|
+
metadata: {
|
|
847
|
+
value: targetValue,
|
|
848
|
+
label: targetLabel,
|
|
849
|
+
submitOnly,
|
|
850
|
+
expectedFingerprint,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
_deps._recordRuntimeInputAcceptedForSession(sessionId, session, runtimeInput);
|
|
854
|
+
|
|
855
|
+
const inputGuardFailure = () => {
|
|
856
|
+
if (typeof options.validateBeforeInput !== 'function') return null;
|
|
857
|
+
try {
|
|
858
|
+
const result = options.validateBeforeInput();
|
|
859
|
+
if (result == null || result === true || result.ok === true) return null;
|
|
860
|
+
if (result && result.ok === false) return result;
|
|
861
|
+
} catch {}
|
|
862
|
+
return { ok: false, error: 'session_not_live' };
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const read = await _deps._getHeadlessTextRead(sessionId, 700);
|
|
866
|
+
const initialGuardFailure = inputGuardFailure();
|
|
867
|
+
if (initialGuardFailure) return initialGuardFailure;
|
|
868
|
+
if (!read.ok) {
|
|
869
|
+
// WE COULD NOT SEE — this is not proof there is no menu. Return before any repair logic; an
|
|
870
|
+
// unreadable screen is not evidence of anything.
|
|
871
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'choice_read_unavailable');
|
|
872
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select-blocked', {
|
|
873
|
+
reason: 'choice_read_unavailable',
|
|
874
|
+
readReason: read.reason,
|
|
875
|
+
messageId: message.id || '',
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
ok: false,
|
|
879
|
+
error: 'choice_read_unavailable',
|
|
880
|
+
message: 'CTM could not read this terminal — try again.',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const screen = read.text;
|
|
884
|
+
const menu = _deps.terminalChoice.extractTerminalChoice(screen);
|
|
885
|
+
if (!menu) {
|
|
886
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'choice_prompt_not_found');
|
|
887
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select-blocked', {
|
|
888
|
+
reason: 'choice_prompt_not_found',
|
|
889
|
+
hasScreen: !!screen.trim(),
|
|
890
|
+
messageId: message.id || '',
|
|
891
|
+
});
|
|
892
|
+
_deps._resumeChoiceClickRepair(sessionId, session, screen);
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
error: 'choice_prompt_not_found',
|
|
896
|
+
message: 'CTM could not find an active choice menu in this terminal.',
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
if (expectedFingerprint && menu.fingerprint !== expectedFingerprint) {
|
|
900
|
+
// The phone (cleaned tail) and the server (raw headless grid) can legitimately
|
|
901
|
+
// capture a different trailing-option SET for the SAME live menu — Claude's
|
|
902
|
+
// AskUserQuestion widget appends layout-sensitive options ("Type something",
|
|
903
|
+
// "Chat about this") behind a divider, and the phone parses the divider-stripped
|
|
904
|
+
// tail while the server parses the raw grid, so a row-window includes/excludes a
|
|
905
|
+
// trailing option and the set fingerprints differ. A strict fingerprint match
|
|
906
|
+
// rejected every such tap as stale. Reconcile the SPECIFIC tapped option instead:
|
|
907
|
+
// if it is still present in the live menu by value+label, treat it as the same
|
|
908
|
+
// menu and proceed (the value/label keystroke match below + the post-navigation
|
|
909
|
+
// closed-loop verify are the real safety net). Submit-only taps have no single
|
|
910
|
+
// option to reconcile, so they keep the strict check.
|
|
911
|
+
const reconciled = !submitOnly
|
|
912
|
+
&& _deps.terminalChoice.tappedOptionStillPresent(menu, targetValue, targetLabel);
|
|
913
|
+
if (!reconciled) {
|
|
914
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select-blocked', {
|
|
915
|
+
reason: 'choice_stale',
|
|
916
|
+
expected: expectedFingerprint,
|
|
917
|
+
actual: menu.fingerprint,
|
|
918
|
+
messageId: message.id || '',
|
|
919
|
+
});
|
|
920
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'choice_stale');
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
error: 'choice_stale',
|
|
924
|
+
message: 'This menu changed before your tap arrived. CTM refreshed it — try again.',
|
|
925
|
+
fingerprint: menu.fingerprint,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-fingerprint-soft-mismatch', {
|
|
929
|
+
expected: expectedFingerprint,
|
|
930
|
+
actual: menu.fingerprint,
|
|
931
|
+
value: targetValue,
|
|
932
|
+
label: targetLabel.slice(0, 80),
|
|
933
|
+
messageId: message.id || '',
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (submitOnly) {
|
|
938
|
+
const submitPlan = _deps.terminalChoice.planFormSubmitKeystrokes(menu);
|
|
939
|
+
if (!submitPlan) {
|
|
940
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-submit-blocked', {
|
|
941
|
+
reason: menu.requiresSubmit ? 'choice_form_no_checked_option' : 'choice_not_submit_form',
|
|
942
|
+
messageId: message.id || '',
|
|
943
|
+
});
|
|
944
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', menu.requiresSubmit ? 'choice_form_no_checked_option' : 'choice_not_submit_form');
|
|
945
|
+
return {
|
|
946
|
+
ok: false,
|
|
947
|
+
error: menu.requiresSubmit ? 'choice_form_no_checked_option' : 'choice_not_submit_form',
|
|
948
|
+
message: menu.requiresSubmit
|
|
949
|
+
? 'Choose an option before submitting this form.'
|
|
950
|
+
: 'The active choice menu does not have a separate submit step.',
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
for (const frame of submitPlan.frames) {
|
|
954
|
+
const guardFailure = inputGuardFailure();
|
|
955
|
+
if (guardFailure) return guardFailure;
|
|
956
|
+
handleInput(choiceInputWs, { id: sessionId, data: frame }, choiceInputOptions);
|
|
957
|
+
if (frame.startsWith('\x1b')) {
|
|
958
|
+
await new Promise((resolve) => setTimeout(resolve, _deps.SELECT_CHOICE_FRAME_GAP_MS));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-submit', {
|
|
962
|
+
strategy: submitPlan.strategy,
|
|
963
|
+
checkedValues: menu.options.filter((option) => option.checked).map((option) => option.value),
|
|
964
|
+
source: 'remote-api',
|
|
965
|
+
messageId: message.id || '',
|
|
966
|
+
});
|
|
967
|
+
_deps._recordRuntimeInputResultForSession(sessionId, _deps.sessions.get(sessionId) || session, runtimeInput, 'delivered', 'choice_submit');
|
|
968
|
+
return {
|
|
969
|
+
ok: true,
|
|
970
|
+
delivered: true,
|
|
971
|
+
submitted: true,
|
|
972
|
+
strategy: submitPlan.strategy,
|
|
973
|
+
checkedValues: menu.options.filter((option) => option.checked).map((option) => option.value),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const plan = _deps.terminalChoice.planChoiceKeystrokes({
|
|
978
|
+
options: menu.options,
|
|
979
|
+
selectedIndex: menu.selectedIndex,
|
|
980
|
+
targetValue,
|
|
981
|
+
targetLabel,
|
|
982
|
+
});
|
|
983
|
+
if (!plan) {
|
|
984
|
+
_deps._recordRuntimeInputResultForSession(sessionId, session, runtimeInput, 'rejected', 'choice_option_not_found');
|
|
985
|
+
return {
|
|
986
|
+
ok: false,
|
|
987
|
+
error: 'choice_option_not_found',
|
|
988
|
+
message: 'CTM could not match that option against the live menu.',
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Move the highlight (arrows) or pre-type the number, leaving Enter for last.
|
|
993
|
+
for (const frame of plan.navFrames) {
|
|
994
|
+
const guardFailure = inputGuardFailure();
|
|
995
|
+
if (guardFailure) return guardFailure;
|
|
996
|
+
handleInput(choiceInputWs, { id: sessionId, data: frame }, choiceInputOptions);
|
|
997
|
+
if (plan.navFrames.length > 1 || frame.startsWith('\x1b')) {
|
|
998
|
+
await new Promise((resolve) => setTimeout(resolve, _deps.SELECT_CHOICE_FRAME_GAP_MS));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Closed-loop verify: for arrow navigation, confirm the target is now highlighted
|
|
1003
|
+
// before committing, so a miscount never confirms the wrong option.
|
|
1004
|
+
if (plan.strategy === 'arrows' && plan.navFrames.length) {
|
|
1005
|
+
await _deps._waitForTerminalRenderSettle(_deps.sessions.get(sessionId), { minMs: 80, maxMs: 600, quietMs: 120 });
|
|
1006
|
+
const verifyRead = await _deps._getHeadlessTextRead(sessionId, 700);
|
|
1007
|
+
if (!verifyRead.ok) {
|
|
1008
|
+
// An unreadable screen is not proof the option did not land. Stay conservative — never
|
|
1009
|
+
// commit — but report unverified (choice_read_unavailable) rather than choice_verify_failed
|
|
1010
|
+
// so the client retries instead of giving up.
|
|
1011
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select-blocked', {
|
|
1012
|
+
reason: 'choice_read_unavailable',
|
|
1013
|
+
readReason: verifyRead.reason,
|
|
1014
|
+
stage: 'verify',
|
|
1015
|
+
messageId: message.id || '',
|
|
1016
|
+
});
|
|
1017
|
+
_deps._recordRuntimeInputResultForSession(sessionId, _deps.sessions.get(sessionId) || session, runtimeInput, 'rejected', 'choice_read_unavailable');
|
|
1018
|
+
return {
|
|
1019
|
+
ok: false,
|
|
1020
|
+
error: 'choice_read_unavailable',
|
|
1021
|
+
message: 'CTM could not read this terminal — try again.',
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const after = _deps.terminalChoice.extractTerminalChoice(verifyRead.text);
|
|
1025
|
+
const landed = after && after.options[plan.targetOrdinal] && after.options[plan.targetOrdinal].selected;
|
|
1026
|
+
if (!landed) {
|
|
1027
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select-blocked', {
|
|
1028
|
+
reason: 'choice_verify_failed',
|
|
1029
|
+
targetOrdinal: plan.targetOrdinal,
|
|
1030
|
+
delta: plan.delta,
|
|
1031
|
+
messageId: message.id || '',
|
|
1032
|
+
});
|
|
1033
|
+
_deps._recordRuntimeInputResultForSession(sessionId, _deps.sessions.get(sessionId) || session, runtimeInput, 'rejected', 'choice_verify_failed');
|
|
1034
|
+
return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
error: 'choice_verify_failed',
|
|
1037
|
+
message: 'CTM could not confirm the highlighted option, so nothing was selected. Try again.',
|
|
1038
|
+
fingerprint: after?.fingerprint || '',
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const live = _deps.sessions.get(sessionId);
|
|
1044
|
+
if (!_deps._isLiveTerminalSession(live)) {
|
|
1045
|
+
_deps._recordRuntimeInputResultForSession(sessionId, live || session, runtimeInput, 'rejected', 'session_not_live_before_choice_commit');
|
|
1046
|
+
return {
|
|
1047
|
+
ok: false,
|
|
1048
|
+
error: 'session_not_live',
|
|
1049
|
+
message: 'The session stopped before CTM could confirm your choice.',
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const commitGuardFailure = inputGuardFailure();
|
|
1053
|
+
if (commitGuardFailure) return commitGuardFailure;
|
|
1054
|
+
// Enter commits the option; for a free-text option the typed reply follows once the input has opened, then Enter.
|
|
1055
|
+
const typedReply = choiceText.replace(/[\x00-\x1f\x7f]+/g, ' ').trim();
|
|
1056
|
+
const typeReply = typedReply && menu.options[plan.targetOrdinal]?.freeText;
|
|
1057
|
+
const commitFrames = typeReply ? [plan.commitFrame, typedReply, _deps.terminalChoice.ENTER] : [plan.commitFrame];
|
|
1058
|
+
for (const [i, data] of commitFrames.entries()) { if (i === 1) await _deps._waitForTerminalRenderSettle(_deps.sessions.get(sessionId), { minMs: 80, maxMs: 600, quietMs: 120 }); handleInput(choiceInputWs, { id: sessionId, data }, choiceInputOptions); }
|
|
1059
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-choice-select', {
|
|
1060
|
+
value: targetValue,
|
|
1061
|
+
label: targetLabel.slice(0, 80),
|
|
1062
|
+
strategy: plan.strategy,
|
|
1063
|
+
delta: plan.delta ?? null,
|
|
1064
|
+
source: 'remote-api',
|
|
1065
|
+
messageId: message.id || '',
|
|
1066
|
+
});
|
|
1067
|
+
_deps._recordRuntimeInputResultForSession(sessionId, live || session, runtimeInput, 'delivered', 'choice_committed');
|
|
1068
|
+
return {
|
|
1069
|
+
ok: true,
|
|
1070
|
+
delivered: true,
|
|
1071
|
+
requiresSubmit: !!menu.requiresSubmit,
|
|
1072
|
+
submitted: !menu.requiresSubmit,
|
|
1073
|
+
value: targetValue || menu.options[plan.targetOrdinal]?.value || '',
|
|
1074
|
+
label: targetLabel || menu.options[plan.targetOrdinal]?.label || '',
|
|
1075
|
+
strategy: plan.strategy,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function _observeRemoteSubmission(context, attempt = 0) {
|
|
1080
|
+
const { sessionId, messageId, text, beforeMarker, beforeEnterMarker, startedAt } = context;
|
|
1081
|
+
const isCurrent = () => _deps._remoteSubmissionObservationCurrent(context);
|
|
1082
|
+
const storeStatus = (status, details) => _deps._storeRemoteSubmissionStatus(sessionId, messageId, status, details, context.rootJobLifetime);
|
|
1083
|
+
if (!isCurrent()) return;
|
|
1084
|
+
const current = await _deps._readPromptSubmissionByMessageId(messageId);
|
|
1085
|
+
if (!isCurrent() || !current || _deps.REMOTE_SUBMISSION_FINAL_STATUSES.has(current.status)) return;
|
|
1086
|
+
if (context.rootJobLifetime !== undefined && (current.message_id !== messageId
|
|
1087
|
+
|| current.session_id !== sessionId || current.message_type !== 'web.submit_prompt')) return;
|
|
1088
|
+
const canonicalPrivateWeb = _deps._isWebPromptSubmission(current) && String(current.message_id || messageId).startsWith('remote-v2:');
|
|
1089
|
+
if (canonicalPrivateWeb && !context.rootJobLifetime) return;
|
|
1090
|
+
const submissionSource = context.source || current.result?.source || 'remote-api';
|
|
1091
|
+
const submissionLabel = _deps._isWebPromptSubmission(current) ? 'prompt' : 'phone reply';
|
|
1092
|
+
|
|
1093
|
+
const session = context.rootJobLifetime ? context.rootJobLifetime.session : _deps.sessions.get(sessionId);
|
|
1094
|
+
if (!_deps._isLiveTerminalSession(session)) {
|
|
1095
|
+
await storeStatus('failed', {
|
|
1096
|
+
reason: 'session_exited_during_observation',
|
|
1097
|
+
message: `The session exited before CTM could confirm this ${submissionLabel}.`,
|
|
1098
|
+
attempts: attempt,
|
|
1099
|
+
source: submissionSource,
|
|
1100
|
+
evidenceSource: 'session-lifecycle',
|
|
1101
|
+
});
|
|
1102
|
+
if (!isCurrent()) return;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// The status-bus busy stamp is in memory; the durable match below resolves transcript files on
|
|
1107
|
+
// the read pool, which can queue for minutes under load (2026-10-21: a prompt the agent took up
|
|
1108
|
+
// 0.2 s after Enter stayed "Still confirming..." for 90 s behind resolveSessionFiles). An idle
|
|
1109
|
+
// submission is confirmed by the busy stamp the moment it exists. Mid-turn submissions are not:
|
|
1110
|
+
// the session was busy anyway (D-3).
|
|
1111
|
+
const submittedWhileGenerating = !!context.submittedWhileGenerating;
|
|
1112
|
+
if (!submittedWhileGenerating && Number(session?._lastBusyStatusAt || 0) >= Number(startedAt || 0)) {
|
|
1113
|
+
storeStatus('response_started', {
|
|
1114
|
+
reason: 'session_marked_busy_after_submit',
|
|
1115
|
+
message: `The agent started working after the ${submissionLabel} was submitted.`,
|
|
1116
|
+
attempts: attempt,
|
|
1117
|
+
source: submissionSource,
|
|
1118
|
+
evidenceSource: 'status-bus',
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const durableMatch = await _deps._remoteSubmissionDurablePromptMatch(sessionId, text, {
|
|
1124
|
+
minTimestampMs: Number(startedAt || 0) || 0,
|
|
1125
|
+
timestampToleranceMs: 120000,
|
|
1126
|
+
});
|
|
1127
|
+
if (!isCurrent()) return;
|
|
1128
|
+
if (durableMatch.matched) {
|
|
1129
|
+
storeStatus('durable_user_message_seen', {
|
|
1130
|
+
reason: durableMatch.evidenceSource === 'transcript-tail' ? 'transcript_tail_match' : 'prompt_index_match',
|
|
1131
|
+
message: `The agent transcript now contains this ${submissionLabel}.`,
|
|
1132
|
+
attempts: attempt,
|
|
1133
|
+
source: submissionSource,
|
|
1134
|
+
evidenceSource: durableMatch.evidenceSource || 'message-index',
|
|
1135
|
+
});
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const rows = await _deps._getHeadlessRows(sessionId, 900);
|
|
1140
|
+
if (!isCurrent()) return;
|
|
1141
|
+
const rowsKnown = Array.isArray(rows);
|
|
1142
|
+
// D-3: a prompt typed while the agent was already generating is confirmed
|
|
1143
|
+
// ONLY by its on-screen echo or a durable transcript row (checked above) —
|
|
1144
|
+
// "status bus went busy" and "an agent progress line appeared" are both
|
|
1145
|
+
// true anyway mid-turn, so they cannot be used as delivery evidence here.
|
|
1146
|
+
// Fix round 1, finding 3: the tri-state ('composer'/'echoed'/'absent', ❯-aware)
|
|
1147
|
+
// drives ONLY the mid-turn path below. The idle path's `promptStillVisible`
|
|
1148
|
+
// (fed into markerEvidence, enter-retry, and needs_attention) must keep using
|
|
1149
|
+
// the byte-identical-to-base legacyPromptDetail so a prompt stuck at Claude's
|
|
1150
|
+
// idle ❯ composer still resolves the pre-WP-3 way (waiting_for_confirmation →
|
|
1151
|
+
// needs_attention), never a false response_started from the new glyph handling.
|
|
1152
|
+
const promptState = rowsKnown
|
|
1153
|
+
? _deps.remoteSubmissionObserver.terminalSubmittedPromptState(rows, text)
|
|
1154
|
+
: 'absent';
|
|
1155
|
+
const legacyDetail = rowsKnown
|
|
1156
|
+
? _deps.remoteSubmissionObserver.legacyPromptDetail(rows, text)
|
|
1157
|
+
: { activePromptVisible: false, agentProgressAfterPrompt: false };
|
|
1158
|
+
const promptStillVisible = submittedWhileGenerating
|
|
1159
|
+
? promptState === 'composer'
|
|
1160
|
+
: legacyDetail.activePromptVisible;
|
|
1161
|
+
const markerNow = _deps._snapshotOutputMarker(session);
|
|
1162
|
+
const markerAcceptance = _deps.submissionAck.markerEvidence({
|
|
1163
|
+
sessionAlive: true,
|
|
1164
|
+
beforeMarker,
|
|
1165
|
+
beforeEnterMarker,
|
|
1166
|
+
markerNow,
|
|
1167
|
+
promptStillVisible,
|
|
1168
|
+
});
|
|
1169
|
+
const elapsedMs = Date.now() - Number(startedAt || Date.now());
|
|
1170
|
+
|
|
1171
|
+
if (!isCurrent()) return;
|
|
1172
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-submission-observe-state', {
|
|
1173
|
+
messageId,
|
|
1174
|
+
attempt,
|
|
1175
|
+
elapsedMs,
|
|
1176
|
+
beforeMarker,
|
|
1177
|
+
beforeEnterMarker,
|
|
1178
|
+
markerNow,
|
|
1179
|
+
promptState,
|
|
1180
|
+
legacyActivePromptVisible: !!legacyDetail.activePromptVisible,
|
|
1181
|
+
legacyAgentProgressAfterPrompt: !!legacyDetail.agentProgressAfterPrompt,
|
|
1182
|
+
markerAccepted: !!markerAcceptance.accepted,
|
|
1183
|
+
markerReason: markerAcceptance.reason || '',
|
|
1184
|
+
rowsKnown,
|
|
1185
|
+
enterRetried: !!context.enterRetried,
|
|
1186
|
+
submittedWhileGenerating,
|
|
1187
|
+
// Minor 8: surface whether the mid-turn pre-submit read succeeded, so a
|
|
1188
|
+
// job that silently fell back to the idle ladder (a failed viewport read
|
|
1189
|
+
// treated `submittedWhileGenerating` as false) is visible in diagnostics.
|
|
1190
|
+
preSubmitReadOk: context.preSubmitReadOk !== undefined ? !!context.preSubmitReadOk : undefined,
|
|
1191
|
+
preSubmitReadReason: context.preSubmitReadReason || '',
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
if (submittedWhileGenerating) {
|
|
1195
|
+
// Finding 1: mid-turn accepted-evidence is durable (checked above) →
|
|
1196
|
+
// echoed → composer/Enter-retry (below) → unconfirmed. NO busy-stamp
|
|
1197
|
+
// rung, NO agent-progress rung, and NO marker-acceptance rung — the
|
|
1198
|
+
// marker fires whenever `_outputBytesCounter` moved, which the agent's
|
|
1199
|
+
// own streaming does constantly mid-turn, so it is not delivery evidence
|
|
1200
|
+
// for this path at all.
|
|
1201
|
+
if (promptState === 'echoed') {
|
|
1202
|
+
storeStatus('queued_by_agent', {
|
|
1203
|
+
reason: 'agent_queue_echo',
|
|
1204
|
+
message: 'The agent queued this prompt for its next turn.',
|
|
1205
|
+
attempts: attempt,
|
|
1206
|
+
source: submissionSource,
|
|
1207
|
+
evidenceSource: 'terminal-tail',
|
|
1208
|
+
});
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
if (legacyDetail.agentProgressAfterPrompt && elapsedMs >= 500) {
|
|
1213
|
+
storeStatus('response_started', {
|
|
1214
|
+
reason: 'terminal_progress_after_prompt',
|
|
1215
|
+
message: `The terminal shows agent progress after the ${submissionLabel}.`,
|
|
1216
|
+
attempts: attempt,
|
|
1217
|
+
source: submissionSource,
|
|
1218
|
+
evidenceSource: 'terminal-tail',
|
|
1219
|
+
});
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (markerAcceptance.accepted && elapsedMs >= 2000) {
|
|
1224
|
+
storeStatus('accepted_by_terminal', {
|
|
1225
|
+
reason: markerAcceptance.reason,
|
|
1226
|
+
message: `The terminal advanced after CTM pressed Enter for this ${submissionLabel}.`,
|
|
1227
|
+
attempts: attempt,
|
|
1228
|
+
source: submissionSource,
|
|
1229
|
+
evidenceSource: 'terminal-tail',
|
|
1230
|
+
});
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// S-2 (final fix wave): mid-turn, the agent's own streaming moves the
|
|
1236
|
+
// output marker constantly, so gating the retry on `markerNow ===
|
|
1237
|
+
// beforeEnterMarker` (the idle rule -- "nothing happened since Enter")
|
|
1238
|
+
// never holds and the retry rung is unreachable; a mid-turn composer
|
|
1239
|
+
// re-check has no such marker-stillness signal to offer anyway. Gate the
|
|
1240
|
+
// mid-turn case on the tri-state read (`promptState === 'composer'`) and
|
|
1241
|
+
// elapsed time alone. The idle ladder's marker-stillness gate is
|
|
1242
|
+
// byte-identical to base.
|
|
1243
|
+
const enterRetryEligible = submittedWhileGenerating
|
|
1244
|
+
? promptState === 'composer'
|
|
1245
|
+
: (rowsKnown && promptStillVisible && markerNow === beforeEnterMarker);
|
|
1246
|
+
if (
|
|
1247
|
+
beforeEnterMarker
|
|
1248
|
+
&& !canonicalPrivateWeb
|
|
1249
|
+
&& enterRetryEligible
|
|
1250
|
+
&& !context.enterRetried
|
|
1251
|
+
&& elapsedMs >= _deps.REMOTE_TERMINAL_ENTER_RETRY_MIN_ELAPSED_MS
|
|
1252
|
+
) {
|
|
1253
|
+
const retrySession = context.rootJobLifetime ? context.rootJobLifetime.session : _deps.sessions.get(sessionId);
|
|
1254
|
+
if (_deps._isLiveTerminalSession(retrySession)) {
|
|
1255
|
+
const retryBeforeEnterMarker = _deps._snapshotOutputMarker(retrySession);
|
|
1256
|
+
handleInput({ auth: context.auth || {} }, { id: sessionId, data: '\r' }, { authenticatedRemote: true });
|
|
1257
|
+
storeStatus('observing', {
|
|
1258
|
+
reason: 'enter_retry_sent',
|
|
1259
|
+
message: `The ${submissionLabel} was still visible at the prompt, so CTM pressed Enter one more time and is waiting for confirmation.`,
|
|
1260
|
+
attempts: attempt + 1,
|
|
1261
|
+
source: submissionSource,
|
|
1262
|
+
evidenceSource: 'observer',
|
|
1263
|
+
});
|
|
1264
|
+
if (!isCurrent()) return;
|
|
1265
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-retry', {
|
|
1266
|
+
messageId,
|
|
1267
|
+
attempt,
|
|
1268
|
+
elapsedMs,
|
|
1269
|
+
beforeEnterMarker,
|
|
1270
|
+
retryBeforeEnterMarker,
|
|
1271
|
+
});
|
|
1272
|
+
if (!isCurrent()) return;
|
|
1273
|
+
_deps._scheduleRemoteSubmissionObservation({
|
|
1274
|
+
...context,
|
|
1275
|
+
beforeEnterMarker: retryBeforeEnterMarker,
|
|
1276
|
+
enterRetried: true,
|
|
1277
|
+
retryStartedAt: Date.now(),
|
|
1278
|
+
}, attempt + 1);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (!isCurrent()) return;
|
|
1282
|
+
_deps.recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-retry-skipped', {
|
|
1283
|
+
messageId,
|
|
1284
|
+
reason: 'session_not_live',
|
|
1285
|
+
elapsedMs,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (rowsKnown && promptStillVisible && elapsedMs >= 3500) {
|
|
1290
|
+
storeStatus('needs_attention', {
|
|
1291
|
+
reason: 'prompt_still_visible_after_enter',
|
|
1292
|
+
message: `CTM typed this ${submissionLabel}, but the terminal did not show a post-Enter transition and it still appears at the prompt.`,
|
|
1293
|
+
attempts: attempt,
|
|
1294
|
+
source: submissionSource,
|
|
1295
|
+
evidenceSource: 'terminal-tail',
|
|
1296
|
+
});
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (attempt < _deps.REMOTE_SUBMISSION_OBSERVE_DELAYS_MS.length) {
|
|
1301
|
+
storeStatus('observing', {
|
|
1302
|
+
reason: promptStillVisible ? 'prompt_visible_waiting_for_agent' : 'waiting_for_confirmation',
|
|
1303
|
+
message: `CTM delivered the ${submissionLabel} and is waiting for the agent transcript to confirm it.`,
|
|
1304
|
+
attempts: attempt + 1,
|
|
1305
|
+
source: submissionSource,
|
|
1306
|
+
evidenceSource: 'observer',
|
|
1307
|
+
});
|
|
1308
|
+
_deps._scheduleRemoteSubmissionObservation(context, attempt + 1);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const unconfirmed = _deps.remoteSubmissionObserver.unconfirmedSubmissionResult({ promptStillVisible, submissionLabel });
|
|
1313
|
+
storeStatus('needs_attention', {
|
|
1314
|
+
...unconfirmed,
|
|
1315
|
+
attempts: attempt,
|
|
1316
|
+
source: submissionSource,
|
|
1317
|
+
evidenceSource: 'observer',
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
module.exports = {
|
|
1321
|
+
configure,
|
|
1322
|
+
handleRemoteApi,
|
|
1323
|
+
_sendRemoteTerminalSubmission,
|
|
1324
|
+
_selectRemoteTerminalChoiceBody,
|
|
1325
|
+
_observeRemoteSubmission,
|
|
1326
|
+
};
|