@remodex/rmx 1.0.2
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/AGENTS_INSTALL.md +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +348 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "node:fs";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { getCodexHome } from "../codex/paths";
|
|
5
|
+
import type { CodexJsonRpcMessage } from "./codex-app-server";
|
|
6
|
+
import { fileChangesFromToolCall } from "./file-change-parser";
|
|
7
|
+
import { canonicalUserMessageText, publicUserMessageText } from "./user-message-identity";
|
|
8
|
+
import { boundedTurnErrorMessage } from "./turn-activity";
|
|
9
|
+
|
|
10
|
+
type JsonRecord = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
const INITIAL_TAIL_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
// Tool results in real Codex sessions can exceed 2 MiB. The projected Android
|
|
14
|
+
// row is bounded later, so accept a reasonably large source record here rather
|
|
15
|
+
// than silently losing the tool completion (or a long assistant message).
|
|
16
|
+
const MAX_RECORD_BYTES = 16 * 1024 * 1024;
|
|
17
|
+
const MAX_NESTED_TOOL_INPUT_BYTES = 128 * 1024;
|
|
18
|
+
const RECOVERY_POLL_MS = 1_000;
|
|
19
|
+
|
|
20
|
+
type SessionWatch = {
|
|
21
|
+
canonicalPath: string;
|
|
22
|
+
offset: number;
|
|
23
|
+
watcher: FSWatcher;
|
|
24
|
+
poller: ReturnType<typeof setInterval>;
|
|
25
|
+
projector: DesktopSessionRecordProjector;
|
|
26
|
+
draining: boolean;
|
|
27
|
+
pending: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface AndroidDesktopSessionStream {
|
|
31
|
+
watchThread(input: {
|
|
32
|
+
threadId: string;
|
|
33
|
+
sourcePath: string;
|
|
34
|
+
onMessage: (message: CodexJsonRpcMessage) => void;
|
|
35
|
+
}): Promise<boolean>;
|
|
36
|
+
isWatching(threadId: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the projector's bounded live-turn identity without rereading the
|
|
39
|
+
* rollout or asking the Desktop IPC writer. This is intentionally optional
|
|
40
|
+
* so older test/dummy streams can continue to implement the interface.
|
|
41
|
+
*/
|
|
42
|
+
activeTurnId?(threadId: string): string | null;
|
|
43
|
+
unwatchThread(threadId: string): void;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function record(value: unknown): JsonRecord | null {
|
|
48
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stringValue(value: unknown): string {
|
|
52
|
+
return typeof value === "string" ? value : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inside(root: string, candidate: string): boolean {
|
|
56
|
+
const path = relative(root, candidate);
|
|
57
|
+
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function timestampMs(value: unknown): number {
|
|
61
|
+
const parsed = Date.parse(stringValue(value));
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function turnIdFromPayload(payload: JsonRecord, fallback: string | null): string | null {
|
|
66
|
+
const metadata = record(payload.internal_chat_message_metadata_passthrough);
|
|
67
|
+
return stringValue(metadata?.turn_id) || stringValue(payload.turn_id) || fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function responseText(payload: JsonRecord): string {
|
|
71
|
+
if (!Array.isArray(payload.content)) return "";
|
|
72
|
+
return payload.content
|
|
73
|
+
.flatMap((value) => {
|
|
74
|
+
const row = record(value);
|
|
75
|
+
return row && typeof row.text === "string" ? [row.text] : [];
|
|
76
|
+
})
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizedItemType(value: unknown): string {
|
|
81
|
+
return stringValue(value)
|
|
82
|
+
.replace(/[^a-z0-9]/giu, "")
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function userMessageContent(payload: JsonRecord): JsonRecord[] {
|
|
87
|
+
if (!Array.isArray(payload.content)) return [];
|
|
88
|
+
return payload.content.flatMap((value) => {
|
|
89
|
+
const row = record(value);
|
|
90
|
+
const text = publicUserMessageText(row?.text);
|
|
91
|
+
return text ? [{ type: "text", text }] : [];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function reasoningSummary(payload: JsonRecord): string[] {
|
|
96
|
+
if (!Array.isArray(payload.summary)) return [];
|
|
97
|
+
return payload.summary.flatMap((value) => {
|
|
98
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
99
|
+
const row = record(value);
|
|
100
|
+
const text = stringValue(row?.text).trim();
|
|
101
|
+
return text ? [text] : [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodeQuoted(value: string): string {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(`"${value}"`) as string;
|
|
108
|
+
} catch {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function nestedCommand(input: string): string | null {
|
|
114
|
+
const command = /\bcmd\s*:\s*"((?:\\.|[^"\\])*)"/su.exec(input)?.[1];
|
|
115
|
+
if (command) return decodeQuoted(command);
|
|
116
|
+
const sessionId = /\bsession_id\s*:\s*(\d+)/u.exec(input)?.[1];
|
|
117
|
+
if (/\btools\.write_stdin\s*\(/u.test(input)) {
|
|
118
|
+
return sessionId ? `Continue command session ${sessionId}` : "Continue command";
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parsedArguments(payload: JsonRecord): JsonRecord {
|
|
124
|
+
const direct = record(payload.arguments);
|
|
125
|
+
if (direct) return direct;
|
|
126
|
+
const encoded = stringValue(payload.arguments);
|
|
127
|
+
if (!encoded) return {};
|
|
128
|
+
try {
|
|
129
|
+
return record(JSON.parse(encoded)) ?? {};
|
|
130
|
+
} catch {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nestedToolName(input: string): string {
|
|
136
|
+
return /\btools\.([A-Za-z0-9_]+)\s*\(/u.exec(input)?.[1] ?? "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function decodedStringProperty(input: string, property: string): string {
|
|
140
|
+
const boundedInput = input.slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
|
|
141
|
+
const match = new RegExp(
|
|
142
|
+
`\\b${property}\\s*:\\s*(?:"((?:\\\\.|[^"\\\\])*)"|'((?:\\\\.|[^'\\\\])*)'|\`((?:\\\\.|[^\`\\\\])*)\`)`,
|
|
143
|
+
"u",
|
|
144
|
+
).exec(boundedInput);
|
|
145
|
+
const body = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
146
|
+
if (body === undefined) return "";
|
|
147
|
+
if (match?.[1] !== undefined) return decodeQuoted(body);
|
|
148
|
+
const quote = match?.[2] !== undefined ? "'" : "`";
|
|
149
|
+
return body
|
|
150
|
+
.replaceAll(`\\${quote}`, quote)
|
|
151
|
+
.replaceAll("\\n", "\n")
|
|
152
|
+
.replaceAll("\\r", "\r")
|
|
153
|
+
.replaceAll("\\t", "\t")
|
|
154
|
+
.replaceAll("\\\\", "\\");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function planUpdateFromToolCall(
|
|
158
|
+
payload: JsonRecord,
|
|
159
|
+
): { plan: JsonRecord[]; explanation?: string } | null {
|
|
160
|
+
const input = stringValue(payload.input).slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
|
|
161
|
+
const tool = nestedToolName(input) || stringValue(payload.name);
|
|
162
|
+
if (tool.split("__").at(-1) !== "update_plan") return null;
|
|
163
|
+
const direct = parsedArguments(payload);
|
|
164
|
+
const directPlan = Array.isArray(direct.plan) ? direct.plan : [];
|
|
165
|
+
const plan = directPlan.flatMap((value) => {
|
|
166
|
+
const step = record(value);
|
|
167
|
+
const title = stringValue(step?.step).trim();
|
|
168
|
+
const status = stringValue(step?.status).trim();
|
|
169
|
+
return title && status ? [{ step: title, status }] : [];
|
|
170
|
+
});
|
|
171
|
+
if (plan.length === 0) {
|
|
172
|
+
for (const match of input.matchAll(/\{([^{}]{0,8192})\}/gu)) {
|
|
173
|
+
const body = match[1] ?? "";
|
|
174
|
+
const step = decodedStringProperty(body, "step").trim();
|
|
175
|
+
const status = decodedStringProperty(body, "status").trim();
|
|
176
|
+
if (step && status) plan.push({ step, status });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const explanation =
|
|
180
|
+
stringValue(direct.explanation).trim() || decodedStringProperty(input, "explanation").trim();
|
|
181
|
+
return { plan, ...(explanation ? { explanation } : {}) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function nestedToolArguments(input: string, tool: string, direct: JsonRecord): JsonRecord {
|
|
185
|
+
const boundedInput = input.slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
|
|
186
|
+
const augmented: JsonRecord = { ...direct };
|
|
187
|
+
for (const property of [
|
|
188
|
+
"path",
|
|
189
|
+
"query",
|
|
190
|
+
"task_name",
|
|
191
|
+
"taskName",
|
|
192
|
+
"agent_name",
|
|
193
|
+
"agentName",
|
|
194
|
+
"target",
|
|
195
|
+
] as const) {
|
|
196
|
+
if (augmented[property] !== undefined) continue;
|
|
197
|
+
const value = decodedStringProperty(boundedInput, property).trim();
|
|
198
|
+
if (value) augmented[property] = value;
|
|
199
|
+
}
|
|
200
|
+
const reviewTarget =
|
|
201
|
+
tool.split("__").at(-1) === "open_in_codex" &&
|
|
202
|
+
/\btarget\s*:\s*\{[^{}]{0,8192}\btype\s*:\s*["'`]review["'`]/u.test(boundedInput)
|
|
203
|
+
? { type: "review" }
|
|
204
|
+
: null;
|
|
205
|
+
if (reviewTarget) augmented.target = reviewTarget;
|
|
206
|
+
return augmented;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function toolItem(payload: JsonRecord): JsonRecord | null {
|
|
210
|
+
const input = stringValue(payload.input);
|
|
211
|
+
const name = stringValue(payload.name);
|
|
212
|
+
const args = parsedArguments(payload);
|
|
213
|
+
const id = stringValue(payload.id) || stringValue(payload.call_id);
|
|
214
|
+
if (!id) return null;
|
|
215
|
+
if (
|
|
216
|
+
(name === "exec" && /\btools\.(?:exec_command|write_stdin)\s*\(/u.test(input)) ||
|
|
217
|
+
["exec_command", "write_stdin", "shell", "shell_command"].includes(name)
|
|
218
|
+
) {
|
|
219
|
+
const directCommand =
|
|
220
|
+
typeof args.cmd === "string"
|
|
221
|
+
? args.cmd
|
|
222
|
+
: Array.isArray(args.cmd)
|
|
223
|
+
? args.cmd.flatMap((value) => (typeof value === "string" ? [value] : [])).join(" ")
|
|
224
|
+
: "";
|
|
225
|
+
const sessionId =
|
|
226
|
+
typeof args.session_id === "number" || typeof args.session_id === "string"
|
|
227
|
+
? String(args.session_id)
|
|
228
|
+
: "";
|
|
229
|
+
return {
|
|
230
|
+
type: "commandExecution",
|
|
231
|
+
id,
|
|
232
|
+
command:
|
|
233
|
+
directCommand ||
|
|
234
|
+
nestedCommand(input) ||
|
|
235
|
+
(name === "write_stdin" && sessionId
|
|
236
|
+
? `Continue command session ${sessionId}`
|
|
237
|
+
: "Command Execution"),
|
|
238
|
+
status: "inProgress",
|
|
239
|
+
aggregatedOutput: null,
|
|
240
|
+
exitCode: null,
|
|
241
|
+
durationMs: null,
|
|
242
|
+
processId: null,
|
|
243
|
+
commandActions: [],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (name === "apply_patch" || (name === "exec" && /\btools\.apply_patch\s*\(/u.test(input))) {
|
|
247
|
+
return {
|
|
248
|
+
type: "fileChange",
|
|
249
|
+
id,
|
|
250
|
+
status: "inProgress",
|
|
251
|
+
changes: fileChangesFromToolCall(payload),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const nestedTool = nestedToolName(input);
|
|
255
|
+
// A bare custom `exec` record is Codex's internal orchestration wrapper
|
|
256
|
+
// (for example, tool discovery through ALL_TOOLS). It has no stable public
|
|
257
|
+
// activity name, so do not turn it into a misleading "Using Exec" row.
|
|
258
|
+
if (name === "exec" && !nestedTool) return null;
|
|
259
|
+
const tool = nestedTool || name || "tool";
|
|
260
|
+
return {
|
|
261
|
+
type: "dynamicToolCall",
|
|
262
|
+
id,
|
|
263
|
+
callId: stringValue(payload.call_id),
|
|
264
|
+
tool,
|
|
265
|
+
namespace: tool.includes("__") ? tool.split("__")[0] : "",
|
|
266
|
+
status: "inProgress",
|
|
267
|
+
arguments: nestedToolArguments(input, tool, args),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function outputString(payload: JsonRecord): string {
|
|
272
|
+
if (typeof payload.output === "string") return payload.output;
|
|
273
|
+
if (!Array.isArray(payload.output)) return "";
|
|
274
|
+
return payload.output
|
|
275
|
+
.flatMap((value) => {
|
|
276
|
+
const row = record(value);
|
|
277
|
+
return row && typeof row.text === "string" ? [row.text] : [];
|
|
278
|
+
})
|
|
279
|
+
.join("");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function failedOutput(output: string): boolean {
|
|
283
|
+
return /(?:Process|Command|Script) (?:exited|failed) with (?:code|exit code) (?!0\b)-?\d+/u.test(
|
|
284
|
+
output,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Converts append-only Codex session records into the same small notifications used by app-server. */
|
|
289
|
+
export class DesktopSessionRecordProjector {
|
|
290
|
+
private currentTurnId: string | null = null;
|
|
291
|
+
private currentTurnPlanMode = false;
|
|
292
|
+
private nextTurnPlanMode = false;
|
|
293
|
+
private settingsModel = "";
|
|
294
|
+
private settingsModelProviderId = "";
|
|
295
|
+
private settingsReasoningEffort = "";
|
|
296
|
+
private settingsServiceTier = "";
|
|
297
|
+
private settingsServiceTierKnown = false;
|
|
298
|
+
private lastPublishedSettings = "";
|
|
299
|
+
private readonly pendingTools = new Map<string, JsonRecord>();
|
|
300
|
+
private pendingFinalMessage: {
|
|
301
|
+
turnId: string;
|
|
302
|
+
item: JsonRecord;
|
|
303
|
+
completedAtMs: number;
|
|
304
|
+
} | null = null;
|
|
305
|
+
/**
|
|
306
|
+
* A Codex rollout writes one submitted user message twice in adjacent forms:
|
|
307
|
+
* first as the public response item, then as either a `user_message` identity
|
|
308
|
+
* event or a structured `item_completed` event carrying the durable
|
|
309
|
+
* Android/Desktop client id. Hold the first row long enough to join that
|
|
310
|
+
* identity instead of publishing two unrelated Android bubbles.
|
|
311
|
+
*/
|
|
312
|
+
private pendingUserMessage: {
|
|
313
|
+
turnId: string;
|
|
314
|
+
item: JsonRecord;
|
|
315
|
+
completedAtMs: number;
|
|
316
|
+
comparisonText: string;
|
|
317
|
+
} | null = null;
|
|
318
|
+
private pendingCompactionId: string | null = null;
|
|
319
|
+
|
|
320
|
+
constructor(
|
|
321
|
+
private readonly threadId: string,
|
|
322
|
+
private readonly emit: (message: CodexJsonRpcMessage) => void,
|
|
323
|
+
) {}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The canonical turn currently observed in the append-only rollout.
|
|
327
|
+
*
|
|
328
|
+
* Returning only the id keeps the recovery path metadata-only and avoids
|
|
329
|
+
* retaining or cloning any transcript content.
|
|
330
|
+
*/
|
|
331
|
+
activeTurnId(): string | null {
|
|
332
|
+
return this.currentTurnId;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private flushPendingFinalMessage(): void {
|
|
336
|
+
const pending = this.pendingFinalMessage;
|
|
337
|
+
if (!pending) return;
|
|
338
|
+
this.pendingFinalMessage = null;
|
|
339
|
+
this.emit({
|
|
340
|
+
method: "item/completed",
|
|
341
|
+
params: {
|
|
342
|
+
threadId: this.threadId,
|
|
343
|
+
turnId: pending.turnId,
|
|
344
|
+
item: pending.item,
|
|
345
|
+
completedAtMs: pending.completedAtMs,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private flushPendingUserMessage(clientId = ""): void {
|
|
351
|
+
const pending = this.pendingUserMessage;
|
|
352
|
+
if (!pending) return;
|
|
353
|
+
this.pendingUserMessage = null;
|
|
354
|
+
this.emit({
|
|
355
|
+
method: "item/completed",
|
|
356
|
+
params: {
|
|
357
|
+
threadId: this.threadId,
|
|
358
|
+
turnId: pending.turnId,
|
|
359
|
+
item: {
|
|
360
|
+
...pending.item,
|
|
361
|
+
...(clientId ? { clientId } : {}),
|
|
362
|
+
},
|
|
363
|
+
completedAtMs: pending.completedAtMs,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private publishThreadSettings(
|
|
369
|
+
source: JsonRecord,
|
|
370
|
+
turnId: string | null,
|
|
371
|
+
updatedAtMs: number,
|
|
372
|
+
): void {
|
|
373
|
+
const model = stringValue(source.model).trim().slice(0, 256);
|
|
374
|
+
const modelProviderId = stringValue(source.model_provider_id).trim().slice(0, 64);
|
|
375
|
+
const reasoningEffort = stringValue(source.reasoning_effort ?? source.effort)
|
|
376
|
+
.trim()
|
|
377
|
+
.slice(0, 64);
|
|
378
|
+
const serviceTierPresent = Object.hasOwn(source, "service_tier")
|
|
379
|
+
|| Object.hasOwn(source, "serviceTier");
|
|
380
|
+
const rawServiceTier = serviceTierPresent
|
|
381
|
+
? stringValue(source.service_tier ?? source.serviceTier).trim().slice(0, 64)
|
|
382
|
+
: "";
|
|
383
|
+
const serviceTier = rawServiceTier === "fast"
|
|
384
|
+
? "priority"
|
|
385
|
+
: rawServiceTier || "default";
|
|
386
|
+
const modelIdentityChanged = (
|
|
387
|
+
Boolean(model && this.settingsModel && model !== this.settingsModel)
|
|
388
|
+
|| Boolean(
|
|
389
|
+
modelProviderId
|
|
390
|
+
&& this.settingsModelProviderId
|
|
391
|
+
&& modelProviderId !== this.settingsModelProviderId,
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
if (modelIdentityChanged && !serviceTierPresent) {
|
|
395
|
+
// Never carry a prior model's Fast selection onto a new model when the
|
|
396
|
+
// new Desktop context provides no service-tier evidence.
|
|
397
|
+
this.settingsServiceTier = "";
|
|
398
|
+
this.settingsServiceTierKnown = false;
|
|
399
|
+
}
|
|
400
|
+
if (model) this.settingsModel = model;
|
|
401
|
+
if (modelProviderId) this.settingsModelProviderId = modelProviderId;
|
|
402
|
+
if (reasoningEffort) this.settingsReasoningEffort = reasoningEffort;
|
|
403
|
+
if (serviceTierPresent) {
|
|
404
|
+
this.settingsServiceTier = serviceTier;
|
|
405
|
+
this.settingsServiceTierKnown = true;
|
|
406
|
+
}
|
|
407
|
+
if (!this.settingsModel) return;
|
|
408
|
+
const fingerprint = JSON.stringify([
|
|
409
|
+
this.settingsModel,
|
|
410
|
+
this.settingsModelProviderId,
|
|
411
|
+
this.settingsReasoningEffort,
|
|
412
|
+
this.settingsServiceTierKnown ? this.settingsServiceTier : null,
|
|
413
|
+
]);
|
|
414
|
+
if (fingerprint === this.lastPublishedSettings) return;
|
|
415
|
+
this.lastPublishedSettings = fingerprint;
|
|
416
|
+
this.emit({
|
|
417
|
+
method: "thread/settings/updated",
|
|
418
|
+
params: {
|
|
419
|
+
threadId: this.threadId,
|
|
420
|
+
turnId,
|
|
421
|
+
model: this.settingsModel,
|
|
422
|
+
...(this.settingsModelProviderId ? { modelProviderId: this.settingsModelProviderId } : {}),
|
|
423
|
+
...(this.settingsReasoningEffort ? { reasoningEffort: this.settingsReasoningEffort } : {}),
|
|
424
|
+
...(this.settingsServiceTierKnown ? { serviceTier: this.settingsServiceTier } : {}),
|
|
425
|
+
updatedAtMs,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
consume(value: unknown): void {
|
|
431
|
+
const row = record(value);
|
|
432
|
+
const payload = record(row?.payload);
|
|
433
|
+
if (!row || !payload) return;
|
|
434
|
+
const rowType = stringValue(row.type);
|
|
435
|
+
const payloadType = stringValue(payload.type);
|
|
436
|
+
const at = timestampMs(row.timestamp);
|
|
437
|
+
const userMessageEvent = rowType === "event_msg" && payloadType === "user_message";
|
|
438
|
+
const structuredCompletedItem =
|
|
439
|
+
rowType === "event_msg" && payloadType === "item_completed" ? record(payload.item) : null;
|
|
440
|
+
const structuredItemType = normalizedItemType(structuredCompletedItem?.type);
|
|
441
|
+
const structuredUserMessageEvent =
|
|
442
|
+
structuredCompletedItem !== null &&
|
|
443
|
+
(structuredItemType === "usermessage" ||
|
|
444
|
+
(structuredItemType === "message" &&
|
|
445
|
+
stringValue(structuredCompletedItem.role).toLowerCase() === "user"));
|
|
446
|
+
const userMessageIdentityEvent = userMessageEvent || structuredUserMessageEvent;
|
|
447
|
+
|
|
448
|
+
if (this.pendingUserMessage) {
|
|
449
|
+
const clientId = userMessageEvent
|
|
450
|
+
? stringValue(payload.client_id ?? payload.clientId).trim()
|
|
451
|
+
: stringValue(
|
|
452
|
+
structuredCompletedItem?.clientId ??
|
|
453
|
+
structuredCompletedItem?.client_id ??
|
|
454
|
+
structuredCompletedItem?.clientUserMessageId ??
|
|
455
|
+
structuredCompletedItem?.client_user_message_id,
|
|
456
|
+
).trim();
|
|
457
|
+
const comparisonText = userMessageEvent
|
|
458
|
+
? canonicalUserMessageText(payload.message)
|
|
459
|
+
: canonicalUserMessageText(responseText(structuredCompletedItem ?? {}));
|
|
460
|
+
const identityTurnId = structuredUserMessageEvent
|
|
461
|
+
? stringValue(payload.turn_id ?? payload.turnId) || this.currentTurnId
|
|
462
|
+
: this.pendingUserMessage.turnId;
|
|
463
|
+
if (
|
|
464
|
+
userMessageIdentityEvent &&
|
|
465
|
+
clientId &&
|
|
466
|
+
identityTurnId === this.pendingUserMessage.turnId &&
|
|
467
|
+
comparisonText === this.pendingUserMessage.comparisonText
|
|
468
|
+
) {
|
|
469
|
+
this.flushPendingUserMessage(clientId);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Older Codex rollouts may not include the identity event. Preserve
|
|
473
|
+
// ordering by releasing the user row immediately before the next record.
|
|
474
|
+
this.flushPendingUserMessage();
|
|
475
|
+
}
|
|
476
|
+
// These records enrich the immediately preceding response item. They have
|
|
477
|
+
// no independent Android row, even when an older rollout omits an id.
|
|
478
|
+
if (userMessageIdentityEvent) return;
|
|
479
|
+
|
|
480
|
+
if (rowType === "event_msg" && payloadType === "thread_settings_applied") {
|
|
481
|
+
const settings = record(payload.thread_settings);
|
|
482
|
+
this.nextTurnPlanMode = stringValue(record(settings?.collaboration_mode)?.mode) === "plan";
|
|
483
|
+
if (settings) {
|
|
484
|
+
this.publishThreadSettings(
|
|
485
|
+
settings,
|
|
486
|
+
stringValue(payload.turn_id) || this.currentTurnId,
|
|
487
|
+
at,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Codex writes the private compaction answer as an ordinary-looking final
|
|
494
|
+
// assistant item immediately before the top-level `compacted` record. Hold
|
|
495
|
+
// final answers briefly so that private handoff text never reaches Android.
|
|
496
|
+
// Ordinary final answers are released before turn/completed below.
|
|
497
|
+
if (rowType === "compacted") {
|
|
498
|
+
this.pendingFinalMessage = null;
|
|
499
|
+
const turnId = this.currentTurnId;
|
|
500
|
+
if (turnId) {
|
|
501
|
+
const id = `context-compaction-${turnId}-${at}`;
|
|
502
|
+
// `compacted` is the replacement-history record written after Codex
|
|
503
|
+
// has finished compacting. Treating it as a start leaves Android stuck
|
|
504
|
+
// on "Compacting context" because current Codex rollouts do not also
|
|
505
|
+
// emit the older `context_compacted` event. Retain the id only to
|
|
506
|
+
// suppress that optional legacy duplicate when it is present.
|
|
507
|
+
this.pendingCompactionId = id;
|
|
508
|
+
this.emit({
|
|
509
|
+
method: "item/completed",
|
|
510
|
+
params: {
|
|
511
|
+
threadId: this.threadId,
|
|
512
|
+
turnId,
|
|
513
|
+
item: { type: "contextCompaction", id, status: "completed" },
|
|
514
|
+
completedAtMs: at,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
this.pendingFinalMessage &&
|
|
522
|
+
!(
|
|
523
|
+
(rowType === "event_msg" && payloadType === "token_count") ||
|
|
524
|
+
rowType === "world_state" ||
|
|
525
|
+
rowType === "turn_context" ||
|
|
526
|
+
(rowType === "event_msg" && payloadType === "context_compacted")
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
this.flushPendingFinalMessage();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (rowType === "turn_context") {
|
|
533
|
+
const turnId = stringValue(payload.turn_id) || this.currentTurnId;
|
|
534
|
+
// `turn_context` is durable settings metadata, not a lifecycle start.
|
|
535
|
+
// Completed tasks replay their last context so Android can restore the
|
|
536
|
+
// model and reasoning picker; treating that replay as active resurrects
|
|
537
|
+
// the completed turn and converts an ordinary phone send into Queue.
|
|
538
|
+
// A live append has already published `task_started`, so it may still
|
|
539
|
+
// refine that active turn's id without allowing idle metadata to start it.
|
|
540
|
+
if (this.currentTurnId !== null && turnId) this.currentTurnId = turnId;
|
|
541
|
+
const collaborationMode = record(payload.collaboration_mode);
|
|
542
|
+
if (collaborationMode) {
|
|
543
|
+
this.currentTurnPlanMode = stringValue(collaborationMode.mode) === "plan";
|
|
544
|
+
}
|
|
545
|
+
this.publishThreadSettings(payload, turnId, at);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (rowType === "event_msg" && payloadType === "token_count") {
|
|
549
|
+
const tokenUsage = record(payload.info);
|
|
550
|
+
if (!tokenUsage) return;
|
|
551
|
+
this.emit({
|
|
552
|
+
method: "thread/tokenUsage/updated",
|
|
553
|
+
params: {
|
|
554
|
+
threadId: this.threadId,
|
|
555
|
+
turnId: stringValue(payload.turn_id) || this.currentTurnId,
|
|
556
|
+
tokenUsage,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (rowType === "event_msg" && payloadType === "context_compacted") {
|
|
562
|
+
const turnId = stringValue(payload.turn_id) || this.currentTurnId;
|
|
563
|
+
this.pendingFinalMessage = null;
|
|
564
|
+
if (!turnId) return;
|
|
565
|
+
if (this.pendingCompactionId) {
|
|
566
|
+
this.pendingCompactionId = null;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const id = `context-compaction-${turnId}-${at}`;
|
|
570
|
+
this.pendingCompactionId = null;
|
|
571
|
+
this.emit({
|
|
572
|
+
method: "item/completed",
|
|
573
|
+
params: {
|
|
574
|
+
threadId: this.threadId,
|
|
575
|
+
turnId,
|
|
576
|
+
item: {
|
|
577
|
+
type: "contextCompaction",
|
|
578
|
+
id,
|
|
579
|
+
},
|
|
580
|
+
completedAtMs: at,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (rowType === "event_msg" && payloadType === "task_started") {
|
|
586
|
+
const turnId = stringValue(payload.turn_id);
|
|
587
|
+
if (!turnId) return;
|
|
588
|
+
this.currentTurnId = turnId;
|
|
589
|
+
this.currentTurnPlanMode =
|
|
590
|
+
stringValue(payload.collaboration_mode_kind) === "plan" || this.nextTurnPlanMode;
|
|
591
|
+
this.nextTurnPlanMode = false;
|
|
592
|
+
this.emit({
|
|
593
|
+
method: "turn/started",
|
|
594
|
+
params: {
|
|
595
|
+
threadId: this.threadId,
|
|
596
|
+
turnId,
|
|
597
|
+
turn: { id: turnId, status: "inProgress" },
|
|
598
|
+
startedAtMs: at,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (rowType === "event_msg" && ["task_complete", "turn_aborted"].includes(payloadType)) {
|
|
604
|
+
const turnId = stringValue(payload.turn_id) || this.currentTurnId;
|
|
605
|
+
if (!turnId) return;
|
|
606
|
+
this.flushPendingFinalMessage();
|
|
607
|
+
const interrupted = payloadType === "turn_aborted";
|
|
608
|
+
const errorMessage =
|
|
609
|
+
payloadType === "task_complete" ? boundedTurnErrorMessage(payload.error) : "";
|
|
610
|
+
this.emit({
|
|
611
|
+
method: "turn/completed",
|
|
612
|
+
params: {
|
|
613
|
+
threadId: this.threadId,
|
|
614
|
+
turnId,
|
|
615
|
+
turn: {
|
|
616
|
+
id: turnId,
|
|
617
|
+
status: interrupted ? "interrupted" : errorMessage ? "failed" : "completed",
|
|
618
|
+
...(errorMessage ? { error: { message: errorMessage } } : {}),
|
|
619
|
+
},
|
|
620
|
+
completedAtMs: at,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
this.currentTurnId = null;
|
|
624
|
+
this.currentTurnPlanMode = false;
|
|
625
|
+
this.pendingTools.clear();
|
|
626
|
+
this.pendingCompactionId = null;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (rowType !== "response_item") return;
|
|
630
|
+
const turnId = turnIdFromPayload(payload, this.currentTurnId);
|
|
631
|
+
if (!turnId) return;
|
|
632
|
+
this.currentTurnId = turnId;
|
|
633
|
+
const id = stringValue(payload.id);
|
|
634
|
+
|
|
635
|
+
if (payloadType === "message" && payload.role === "user" && id) {
|
|
636
|
+
const text = responseText(payload);
|
|
637
|
+
if (!text) return;
|
|
638
|
+
const content = userMessageContent(payload);
|
|
639
|
+
const metadata = record(payload.internal_chat_message_metadata_passthrough);
|
|
640
|
+
const embeddedClientId = stringValue(metadata?.client_id).trim();
|
|
641
|
+
this.pendingUserMessage = {
|
|
642
|
+
turnId,
|
|
643
|
+
item: {
|
|
644
|
+
type: "userMessage",
|
|
645
|
+
id,
|
|
646
|
+
content,
|
|
647
|
+
},
|
|
648
|
+
completedAtMs: at,
|
|
649
|
+
comparisonText: canonicalUserMessageText(text),
|
|
650
|
+
};
|
|
651
|
+
// Some runtimes already put the durable id on the response item. There
|
|
652
|
+
// is nothing left to join in that case.
|
|
653
|
+
if (embeddedClientId) this.flushPendingUserMessage(embeddedClientId);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (payloadType === "message" && payload.role === "assistant" && id) {
|
|
657
|
+
const text = responseText(payload);
|
|
658
|
+
if (!text) return;
|
|
659
|
+
const item = {
|
|
660
|
+
type: "agentMessage",
|
|
661
|
+
id,
|
|
662
|
+
text,
|
|
663
|
+
phase: stringValue(payload.phase) || null,
|
|
664
|
+
...(this.currentTurnPlanMode ? { androidRemotePlanMode: true } : {}),
|
|
665
|
+
};
|
|
666
|
+
if (payload.phase === "final_answer") {
|
|
667
|
+
this.pendingFinalMessage = { turnId, item, completedAtMs: at };
|
|
668
|
+
} else {
|
|
669
|
+
this.emit({
|
|
670
|
+
method: "item/completed",
|
|
671
|
+
params: { threadId: this.threadId, turnId, item, completedAtMs: at },
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (payloadType === "reasoning" && id) {
|
|
677
|
+
const summary = reasoningSummary(payload);
|
|
678
|
+
this.emit({
|
|
679
|
+
method: "item/completed",
|
|
680
|
+
params: {
|
|
681
|
+
threadId: this.threadId,
|
|
682
|
+
turnId,
|
|
683
|
+
item: { type: "reasoning", id, summary },
|
|
684
|
+
completedAtMs: at,
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (["custom_tool_call", "function_call"].includes(payloadType)) {
|
|
690
|
+
const planUpdate = planUpdateFromToolCall(payload);
|
|
691
|
+
if (planUpdate) {
|
|
692
|
+
this.emit({
|
|
693
|
+
method: "turn/plan/updated",
|
|
694
|
+
params: { threadId: this.threadId, turnId, ...planUpdate },
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const item = toolItem(payload);
|
|
699
|
+
const callId = stringValue(payload.call_id) || id;
|
|
700
|
+
if (!item || !callId) return;
|
|
701
|
+
this.pendingTools.set(callId, item);
|
|
702
|
+
this.emit({
|
|
703
|
+
method: "item/started",
|
|
704
|
+
params: { threadId: this.threadId, turnId, item, startedAtMs: at },
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (["custom_tool_call_output", "function_call_output"].includes(payloadType)) {
|
|
709
|
+
const callId = stringValue(payload.call_id);
|
|
710
|
+
const pending = this.pendingTools.get(callId);
|
|
711
|
+
if (!pending) return;
|
|
712
|
+
this.pendingTools.delete(callId);
|
|
713
|
+
const output = outputString(payload);
|
|
714
|
+
const failed = failedOutput(output);
|
|
715
|
+
this.emit({
|
|
716
|
+
method: "item/completed",
|
|
717
|
+
params: {
|
|
718
|
+
threadId: this.threadId,
|
|
719
|
+
turnId,
|
|
720
|
+
item: {
|
|
721
|
+
...pending,
|
|
722
|
+
status: failed ? "failed" : "completed",
|
|
723
|
+
...(pending.type === "commandExecution"
|
|
724
|
+
? { aggregatedOutput: output.slice(-64 * 1024), exitCode: failed ? 1 : 0 }
|
|
725
|
+
: {}),
|
|
726
|
+
},
|
|
727
|
+
completedAtMs: at,
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function consumeRange(
|
|
735
|
+
path: string,
|
|
736
|
+
startOffset: number,
|
|
737
|
+
projector: DesktopSessionRecordProjector,
|
|
738
|
+
): Promise<number> {
|
|
739
|
+
const decoder = new TextDecoder();
|
|
740
|
+
const reader = Bun.file(path).slice(startOffset).stream().getReader();
|
|
741
|
+
let absoluteOffset = startOffset;
|
|
742
|
+
let lineStart = startOffset;
|
|
743
|
+
let parts: Uint8Array[] = [];
|
|
744
|
+
let lineBytes = 0;
|
|
745
|
+
let oversized = false;
|
|
746
|
+
const append = (bytes: Uint8Array) => {
|
|
747
|
+
lineBytes += bytes.byteLength;
|
|
748
|
+
if (lineBytes > MAX_RECORD_BYTES) {
|
|
749
|
+
oversized = true;
|
|
750
|
+
parts = [];
|
|
751
|
+
} else if (!oversized && bytes.byteLength > 0) {
|
|
752
|
+
parts.push(bytes.slice());
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const emitLine = (nextOffset: number) => {
|
|
756
|
+
if (!oversized && lineBytes > 0) {
|
|
757
|
+
const joined = new Uint8Array(lineBytes);
|
|
758
|
+
let offset = 0;
|
|
759
|
+
for (const part of parts) {
|
|
760
|
+
joined.set(part, offset);
|
|
761
|
+
offset += part.byteLength;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
projector.consume(JSON.parse(decoder.decode(joined)));
|
|
765
|
+
} catch {
|
|
766
|
+
/* incomplete or malformed records are retried from the last newline */
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
lineStart = nextOffset;
|
|
770
|
+
parts = [];
|
|
771
|
+
lineBytes = 0;
|
|
772
|
+
oversized = false;
|
|
773
|
+
};
|
|
774
|
+
try {
|
|
775
|
+
for (;;) {
|
|
776
|
+
const { value, done } = await reader.read();
|
|
777
|
+
if (done) break;
|
|
778
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
779
|
+
let segmentStart = 0;
|
|
780
|
+
for (let index = 0; index < chunk.byteLength; index += 1) {
|
|
781
|
+
if (chunk[index] !== 0x0a) continue;
|
|
782
|
+
append(chunk.subarray(segmentStart, index));
|
|
783
|
+
absoluteOffset += index - segmentStart + 1;
|
|
784
|
+
emitLine(absoluteOffset);
|
|
785
|
+
segmentStart = index + 1;
|
|
786
|
+
}
|
|
787
|
+
append(chunk.subarray(segmentStart));
|
|
788
|
+
absoluteOffset += chunk.byteLength - segmentStart;
|
|
789
|
+
}
|
|
790
|
+
} finally {
|
|
791
|
+
reader.releaseLock();
|
|
792
|
+
}
|
|
793
|
+
return lineStart;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function isThreadSettingsRecord(row: JsonRecord | null, payload: JsonRecord | null): boolean {
|
|
797
|
+
if (!row || !payload) return false;
|
|
798
|
+
if (row.type === "turn_context") return stringValue(payload.model).trim().length > 0;
|
|
799
|
+
if (row.type !== "event_msg" || stringValue(payload.type) !== "thread_settings_applied") {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
return stringValue(record(payload.thread_settings)?.model).trim().length > 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function isTokenUsageRecord(row: JsonRecord | null, payload: JsonRecord | null): boolean {
|
|
806
|
+
return (
|
|
807
|
+
row?.type === "event_msg" &&
|
|
808
|
+
stringValue(payload?.type) === "token_count" &&
|
|
809
|
+
record(payload?.info) !== null
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function recentActiveRows(
|
|
814
|
+
text: string,
|
|
815
|
+
startsInsideRecord: boolean,
|
|
816
|
+
): {
|
|
817
|
+
boundaryFound: boolean;
|
|
818
|
+
settingsFound: boolean;
|
|
819
|
+
tokenUsageFound: boolean;
|
|
820
|
+
rows: unknown[];
|
|
821
|
+
} {
|
|
822
|
+
const lines = text.split("\n");
|
|
823
|
+
if (startsInsideRecord) lines.shift();
|
|
824
|
+
const rows = lines.flatMap((line) => {
|
|
825
|
+
if (!line) return [];
|
|
826
|
+
try {
|
|
827
|
+
return [JSON.parse(line) as unknown];
|
|
828
|
+
} catch {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
let boundary = -1;
|
|
833
|
+
let latestSettings = -1;
|
|
834
|
+
let latestTokenUsage = -1;
|
|
835
|
+
let settingsSinceTerminal = -1;
|
|
836
|
+
let active = false;
|
|
837
|
+
let boundaryFound = false;
|
|
838
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
839
|
+
const row = record(rows[index]);
|
|
840
|
+
const payload = record(row?.payload);
|
|
841
|
+
if (isThreadSettingsRecord(row, payload)) {
|
|
842
|
+
latestSettings = index;
|
|
843
|
+
settingsSinceTerminal = index;
|
|
844
|
+
}
|
|
845
|
+
if (isTokenUsageRecord(row, payload)) latestTokenUsage = index;
|
|
846
|
+
if (row?.type !== "event_msg" || !payload) continue;
|
|
847
|
+
const type = stringValue(payload.type);
|
|
848
|
+
if (type === "task_started") {
|
|
849
|
+
boundary = settingsSinceTerminal >= 0 ? settingsSinceTerminal : index;
|
|
850
|
+
active = true;
|
|
851
|
+
boundaryFound = true;
|
|
852
|
+
}
|
|
853
|
+
if (["task_complete", "turn_aborted"].includes(type)) {
|
|
854
|
+
boundary = index;
|
|
855
|
+
active = false;
|
|
856
|
+
boundaryFound = true;
|
|
857
|
+
settingsSinceTerminal = -1;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const selected = new Set<number>();
|
|
861
|
+
if (active && boundary >= 0) {
|
|
862
|
+
for (let index = boundary; index < rows.length; index += 1) selected.add(index);
|
|
863
|
+
}
|
|
864
|
+
if (latestSettings >= 0) selected.add(latestSettings);
|
|
865
|
+
if (latestTokenUsage >= 0) selected.add(latestTokenUsage);
|
|
866
|
+
return {
|
|
867
|
+
boundaryFound,
|
|
868
|
+
settingsFound: latestSettings >= 0,
|
|
869
|
+
tokenUsageFound: latestTokenUsage >= 0,
|
|
870
|
+
// Completed Desktop tasks still own their last model/reasoning selection
|
|
871
|
+
// and their last context-window reading. Replaying only active turns
|
|
872
|
+
// discarded that durable metadata after every Remodex restart, so
|
|
873
|
+
// Android fell back to defaults and "Waiting for context usage" until a
|
|
874
|
+
// new turn happened to publish another token-count record.
|
|
875
|
+
rows: [...selected].sort((left, right) => left - right).map((index) => rows[index]!),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function scanSessionReplayState(
|
|
880
|
+
path: string,
|
|
881
|
+
size: number,
|
|
882
|
+
): Promise<{
|
|
883
|
+
activeTurnStartOffset: number | null;
|
|
884
|
+
latestSettingsRecord: unknown | null;
|
|
885
|
+
latestTokenUsageRecord: unknown | null;
|
|
886
|
+
latestTokenUsageOffset: number | null;
|
|
887
|
+
}> {
|
|
888
|
+
const reader = Bun.file(path).slice(0, size).stream().getReader();
|
|
889
|
+
let absoluteOffset = 0;
|
|
890
|
+
let lineStart = 0;
|
|
891
|
+
let parts: Uint8Array[] = [];
|
|
892
|
+
let lineBytes = 0;
|
|
893
|
+
let oversized = false;
|
|
894
|
+
let activeTurnStartOffset: number | null = null;
|
|
895
|
+
let latestSettingsRecord: unknown | null = null;
|
|
896
|
+
let latestTokenUsageRecord: unknown | null = null;
|
|
897
|
+
let latestTokenUsageOffset: number | null = null;
|
|
898
|
+
let active = false;
|
|
899
|
+
const decoder = new TextDecoder();
|
|
900
|
+
const append = (bytes: Uint8Array) => {
|
|
901
|
+
lineBytes += bytes.byteLength;
|
|
902
|
+
// A turn_context record can contain a large environment snapshot. Keep the
|
|
903
|
+
// same hard cap as ordinary replay so older sessions without the compact
|
|
904
|
+
// thread_settings_applied record can still recover their picker state.
|
|
905
|
+
if (lineBytes > MAX_RECORD_BYTES) {
|
|
906
|
+
oversized = true;
|
|
907
|
+
parts = [];
|
|
908
|
+
} else if (!oversized && bytes.byteLength > 0) {
|
|
909
|
+
parts.push(bytes.slice());
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
const inspectLine = (nextOffset: number) => {
|
|
913
|
+
if (!oversized && lineBytes > 0) {
|
|
914
|
+
const joined = new Uint8Array(lineBytes);
|
|
915
|
+
let offset = 0;
|
|
916
|
+
for (const part of parts) {
|
|
917
|
+
joined.set(part, offset);
|
|
918
|
+
offset += part.byteLength;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
const row = record(JSON.parse(decoder.decode(joined)));
|
|
922
|
+
const payload = record(row?.payload);
|
|
923
|
+
if (isThreadSettingsRecord(row, payload)) latestSettingsRecord = row;
|
|
924
|
+
if (isTokenUsageRecord(row, payload)) {
|
|
925
|
+
latestTokenUsageRecord = row;
|
|
926
|
+
latestTokenUsageOffset = lineStart;
|
|
927
|
+
}
|
|
928
|
+
if (row?.type === "event_msg" && payload) {
|
|
929
|
+
const type = stringValue(payload.type);
|
|
930
|
+
if (type === "task_started") {
|
|
931
|
+
activeTurnStartOffset = lineStart;
|
|
932
|
+
active = true;
|
|
933
|
+
} else if (["task_complete", "turn_aborted"].includes(type)) {
|
|
934
|
+
activeTurnStartOffset = null;
|
|
935
|
+
active = false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch {
|
|
939
|
+
/* malformed boundary candidates are ignored */
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
lineStart = nextOffset;
|
|
943
|
+
parts = [];
|
|
944
|
+
lineBytes = 0;
|
|
945
|
+
oversized = false;
|
|
946
|
+
};
|
|
947
|
+
try {
|
|
948
|
+
for (;;) {
|
|
949
|
+
const { value, done } = await reader.read();
|
|
950
|
+
if (done) break;
|
|
951
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
952
|
+
let segmentStart = 0;
|
|
953
|
+
for (let index = 0; index < chunk.byteLength; index += 1) {
|
|
954
|
+
if (chunk[index] !== 0x0a) continue;
|
|
955
|
+
append(chunk.subarray(segmentStart, index));
|
|
956
|
+
absoluteOffset += index - segmentStart + 1;
|
|
957
|
+
inspectLine(absoluteOffset);
|
|
958
|
+
segmentStart = index + 1;
|
|
959
|
+
}
|
|
960
|
+
append(chunk.subarray(segmentStart));
|
|
961
|
+
absoluteOffset += chunk.byteLength - segmentStart;
|
|
962
|
+
}
|
|
963
|
+
} finally {
|
|
964
|
+
reader.releaseLock();
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
activeTurnStartOffset: active ? activeTurnStartOffset : null,
|
|
968
|
+
latestSettingsRecord,
|
|
969
|
+
latestTokenUsageRecord,
|
|
970
|
+
latestTokenUsageOffset,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function replayActiveTail(
|
|
975
|
+
path: string,
|
|
976
|
+
size: number,
|
|
977
|
+
projector: DesktopSessionRecordProjector,
|
|
978
|
+
): Promise<void> {
|
|
979
|
+
const start = Math.max(0, size - INITIAL_TAIL_BYTES);
|
|
980
|
+
const text = await Bun.file(path).slice(start, size).text();
|
|
981
|
+
const recent = recentActiveRows(text, start > 0);
|
|
982
|
+
if (start === 0 || (recent.boundaryFound && recent.settingsFound && recent.tokenUsageFound)) {
|
|
983
|
+
for (const row of recent.rows) projector.consume(row);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
// A long live or completed turn can push its model settings outside the fast
|
|
987
|
+
// 16 MiB tail. Find both pieces in one bounded-memory pass: publish only the
|
|
988
|
+
// latest durable settings and context usage, then replay the active turn (if
|
|
989
|
+
// any). Completed message history remains on the authoritative thread/read
|
|
990
|
+
// path and is not duplicated.
|
|
991
|
+
const scan = await scanSessionReplayState(path, size);
|
|
992
|
+
if (scan.latestSettingsRecord) projector.consume(scan.latestSettingsRecord);
|
|
993
|
+
if (
|
|
994
|
+
scan.latestTokenUsageRecord &&
|
|
995
|
+
(scan.activeTurnStartOffset === null ||
|
|
996
|
+
scan.latestTokenUsageOffset === null ||
|
|
997
|
+
scan.latestTokenUsageOffset < scan.activeTurnStartOffset)
|
|
998
|
+
) {
|
|
999
|
+
projector.consume(scan.latestTokenUsageRecord);
|
|
1000
|
+
}
|
|
1001
|
+
if (scan.activeTurnStartOffset !== null) {
|
|
1002
|
+
await consumeRange(path, scan.activeTurnStartOffset, projector);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export class DesktopSessionStream implements AndroidDesktopSessionStream {
|
|
1007
|
+
private readonly watches = new Map<string, SessionWatch>();
|
|
1008
|
+
private readonly codexHome: string | undefined;
|
|
1009
|
+
|
|
1010
|
+
constructor(options: { codexHome?: string } = {}) {
|
|
1011
|
+
// Resolving CODEX_HOME validates that the directory already exists. Keep
|
|
1012
|
+
// construction side-effect free so a disabled Android gateway cannot make
|
|
1013
|
+
// unrelated Remodex management routes fail during server startup.
|
|
1014
|
+
this.codexHome = options.codexHome;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
isWatching(threadId: string): boolean {
|
|
1018
|
+
return this.watches.has(threadId);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
activeTurnId(threadId: string): string | null {
|
|
1022
|
+
return this.watches.get(threadId)?.projector.activeTurnId() ?? null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async watchThread(input: {
|
|
1026
|
+
threadId: string;
|
|
1027
|
+
sourcePath: string;
|
|
1028
|
+
onMessage: (message: CodexJsonRpcMessage) => void;
|
|
1029
|
+
}): Promise<boolean> {
|
|
1030
|
+
const existing = this.watches.get(input.threadId);
|
|
1031
|
+
try {
|
|
1032
|
+
const [home, canonicalPath] = await Promise.all([
|
|
1033
|
+
realpath(resolve(this.codexHome ?? getCodexHome())),
|
|
1034
|
+
realpath(resolve(input.sourcePath)),
|
|
1035
|
+
]);
|
|
1036
|
+
const sessions = resolve(home, "sessions");
|
|
1037
|
+
const archived = resolve(home, "archived_sessions");
|
|
1038
|
+
if (!inside(sessions, canonicalPath) && !inside(archived, canonicalPath)) return false;
|
|
1039
|
+
if (existing?.canonicalPath === canonicalPath) return true;
|
|
1040
|
+
this.unwatchThread(input.threadId);
|
|
1041
|
+
const source = await stat(canonicalPath);
|
|
1042
|
+
if (!source.isFile()) return false;
|
|
1043
|
+
const projector = new DesktopSessionRecordProjector(input.threadId, input.onMessage);
|
|
1044
|
+
await replayActiveTail(canonicalPath, source.size, projector);
|
|
1045
|
+
const watcher = watch(canonicalPath, { persistent: false }, () =>
|
|
1046
|
+
this.queueDrain(input.threadId),
|
|
1047
|
+
);
|
|
1048
|
+
const poller = setInterval(() => this.queueDrain(input.threadId), RECOVERY_POLL_MS);
|
|
1049
|
+
poller.unref?.();
|
|
1050
|
+
this.watches.set(input.threadId, {
|
|
1051
|
+
canonicalPath,
|
|
1052
|
+
offset: source.size,
|
|
1053
|
+
watcher,
|
|
1054
|
+
poller,
|
|
1055
|
+
projector,
|
|
1056
|
+
draining: false,
|
|
1057
|
+
pending: false,
|
|
1058
|
+
});
|
|
1059
|
+
// Close the stat/watch race: anything appended after the stat is read now.
|
|
1060
|
+
this.queueDrain(input.threadId);
|
|
1061
|
+
return true;
|
|
1062
|
+
} catch {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
unwatchThread(threadId: string): void {
|
|
1068
|
+
const current = this.watches.get(threadId);
|
|
1069
|
+
if (!current) return;
|
|
1070
|
+
current.watcher.close();
|
|
1071
|
+
clearInterval(current.poller);
|
|
1072
|
+
this.watches.delete(threadId);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
close(): void {
|
|
1076
|
+
for (const threadId of [...this.watches.keys()]) this.unwatchThread(threadId);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private queueDrain(threadId: string): void {
|
|
1080
|
+
const current = this.watches.get(threadId);
|
|
1081
|
+
if (!current) return;
|
|
1082
|
+
if (current.draining) {
|
|
1083
|
+
current.pending = true;
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
current.draining = true;
|
|
1087
|
+
void this.drain(threadId).finally(() => {
|
|
1088
|
+
const latest = this.watches.get(threadId);
|
|
1089
|
+
if (!latest) return;
|
|
1090
|
+
latest.draining = false;
|
|
1091
|
+
if (latest.pending) {
|
|
1092
|
+
latest.pending = false;
|
|
1093
|
+
this.queueDrain(threadId);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private async drain(threadId: string): Promise<void> {
|
|
1099
|
+
const current = this.watches.get(threadId);
|
|
1100
|
+
if (!current) return;
|
|
1101
|
+
try {
|
|
1102
|
+
const source = await stat(current.canonicalPath);
|
|
1103
|
+
if (source.size < current.offset) current.offset = source.size;
|
|
1104
|
+
if (source.size === current.offset) return;
|
|
1105
|
+
current.offset = await consumeRange(current.canonicalPath, current.offset, current.projector);
|
|
1106
|
+
} catch {
|
|
1107
|
+
// A transient rename or concurrent write is retried by the recovery poll.
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|