@iislee/opencodex 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -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 +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +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/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.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/gemini.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-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/kiro.svg +14 -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 +1 -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 +102 -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 +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -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 +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -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 +1214 -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 +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -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 +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -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 +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -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/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -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 +381 -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 +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -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 +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -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 +829 -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/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 +202 -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 +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -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/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -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 +514 -0
- package/src/lib/winsw.ts +375 -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/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -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 +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -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 +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -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 +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -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/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -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 +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -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 +471 -0
- package/src/service.ts +2554 -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 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -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 +164 -0
- package/src/usage/log.ts +658 -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 +125 -0
- package/src/vision/index.ts +467 -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 +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmdirSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import type { GenerationContext } from "./state-store-sweeper";
|
|
16
|
+
|
|
17
|
+
export const CONFIG_OWNER_FILE = ".opencodex-owner.json";
|
|
18
|
+
export const CONFIG_UNINSTALL_MANIFEST = ".opencodex-uninstall.json";
|
|
19
|
+
|
|
20
|
+
export type ConfigRemovalResult = {
|
|
21
|
+
status: "absent" | "removed" | "partial" | "refused";
|
|
22
|
+
reason?: string;
|
|
23
|
+
residualPaths: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ConfigOwner = {
|
|
27
|
+
version: 1;
|
|
28
|
+
ownerId: string;
|
|
29
|
+
root: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ConfigUninstallManifest = ConfigOwner & {
|
|
33
|
+
paths: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const METADATA_MAX_BYTES = 64 * 1024;
|
|
37
|
+
const MANIFEST_MAX_PATHS = 1024;
|
|
38
|
+
const INITIAL_OWNED_PATHS = [
|
|
39
|
+
".star-prompted",
|
|
40
|
+
"artifacts",
|
|
41
|
+
"auth.json",
|
|
42
|
+
"auth.store.lock",
|
|
43
|
+
"catalog-backup.json",
|
|
44
|
+
"claude-env.sh",
|
|
45
|
+
"codex-accounts.json",
|
|
46
|
+
"codex-runtime-clamp.json",
|
|
47
|
+
"codex-runtime.json",
|
|
48
|
+
"codex-shim.autorestore.lock",
|
|
49
|
+
"codex-shim.json",
|
|
50
|
+
"config.json",
|
|
51
|
+
"config-mutation.sqlite",
|
|
52
|
+
"config-mutation.sqlite-journal",
|
|
53
|
+
"config-mutation.sqlite-shm",
|
|
54
|
+
"config-mutation.sqlite-wal",
|
|
55
|
+
"crash.log",
|
|
56
|
+
"kimi-device-id",
|
|
57
|
+
"mimo-client-id",
|
|
58
|
+
"ocx.pid",
|
|
59
|
+
"opencodex-service-launcher.vbs",
|
|
60
|
+
"opencodex-service-task.xml",
|
|
61
|
+
"opencodex-service.cmd",
|
|
62
|
+
"opencodex-tray-offline.ico",
|
|
63
|
+
"opencodex-tray-online.ico",
|
|
64
|
+
"opencodex-tray-warning.ico",
|
|
65
|
+
"opencodex-tray.ps1",
|
|
66
|
+
"responses-state.json",
|
|
67
|
+
"runtime-port.json",
|
|
68
|
+
"service-api-token",
|
|
69
|
+
"service-state.json",
|
|
70
|
+
"service.log",
|
|
71
|
+
"system-env-port",
|
|
72
|
+
"tray-heartbeat.json",
|
|
73
|
+
"tray-state.json",
|
|
74
|
+
"update-job.json",
|
|
75
|
+
"usage-debug.jsonl",
|
|
76
|
+
"usage.jsonl",
|
|
77
|
+
"version.json",
|
|
78
|
+
"winsw",
|
|
79
|
+
] as const;
|
|
80
|
+
const ownershipCache = new Map<string, {
|
|
81
|
+
owner: ConfigOwner;
|
|
82
|
+
manifest: ConfigUninstallManifest;
|
|
83
|
+
} | null>();
|
|
84
|
+
let lastReconciledGeneration = 0;
|
|
85
|
+
|
|
86
|
+
export function listLiveConfigOwnershipRoots(currentConfigDir: string): ReadonlySet<string> {
|
|
87
|
+
const currentRoot = ownershipCacheKey(currentConfigDir);
|
|
88
|
+
const roots = new Set<string>([currentRoot]);
|
|
89
|
+
for (const [root, ownership] of ownershipCache) {
|
|
90
|
+
if (root === currentRoot) continue;
|
|
91
|
+
if (ownership?.manifest.paths.some(rel => existsSync(join(root, ...rel.split("/"))))) {
|
|
92
|
+
roots.add(root);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return roots;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function reconcileConfigOwnershipRoots(context: GenerationContext): number {
|
|
99
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
100
|
+
let removed = 0;
|
|
101
|
+
for (const [root, ownership] of ownershipCache) {
|
|
102
|
+
if (context.configRoots.has(root)) continue;
|
|
103
|
+
const hasLiveOwnedPath = ownership?.manifest.paths.some(rel =>
|
|
104
|
+
existsSync(join(root, ...rel.split("/"))));
|
|
105
|
+
if (hasLiveOwnedPath) continue;
|
|
106
|
+
ownershipCache.delete(root);
|
|
107
|
+
removed += 1;
|
|
108
|
+
}
|
|
109
|
+
lastReconciledGeneration = context.generation;
|
|
110
|
+
return removed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ownershipCacheKey(configDir: string): string {
|
|
114
|
+
const key = resolve(configDir);
|
|
115
|
+
return process.platform === "win32" ? key.toLowerCase() : key;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function samePath(left: string, right: string): boolean {
|
|
119
|
+
return process.platform === "win32"
|
|
120
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
121
|
+
: left === right;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readBoundedJson(path: string): unknown {
|
|
125
|
+
const metadata = lstatSync(path);
|
|
126
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
127
|
+
throw new Error("ownership metadata is not a regular file");
|
|
128
|
+
}
|
|
129
|
+
if (metadata.size > METADATA_MAX_BYTES) throw new Error("ownership metadata is too large");
|
|
130
|
+
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isOwner(value: unknown): value is ConfigOwner {
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
135
|
+
const owner = value as Record<string, unknown>;
|
|
136
|
+
return owner.version === 1
|
|
137
|
+
&& typeof owner.ownerId === "string"
|
|
138
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(owner.ownerId)
|
|
139
|
+
&& typeof owner.root === "string";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isManifest(value: unknown): value is ConfigUninstallManifest {
|
|
143
|
+
if (!isOwner(value)) return false;
|
|
144
|
+
const paths = (value as Record<string, unknown>).paths;
|
|
145
|
+
return Array.isArray(paths)
|
|
146
|
+
&& paths.length <= MANIFEST_MAX_PATHS
|
|
147
|
+
&& paths.every(path => typeof path === "string");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function canonicalRoot(configDir: string): string {
|
|
151
|
+
return realpathSync.native(resolve(configDir));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isWithinRoot(root: string, candidate: string): boolean {
|
|
155
|
+
const rel = relative(root, candidate);
|
|
156
|
+
return rel === "" || (
|
|
157
|
+
rel !== ".."
|
|
158
|
+
&& !rel.startsWith(`..${sep}`)
|
|
159
|
+
&& !isAbsolute(rel)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function manifestRelativePath(configDir: string, candidatePath: string): string | null {
|
|
164
|
+
const root = resolve(configDir);
|
|
165
|
+
const candidate = resolve(candidatePath);
|
|
166
|
+
const rel = relative(root, candidate);
|
|
167
|
+
if (
|
|
168
|
+
!rel
|
|
169
|
+
|| rel === ".."
|
|
170
|
+
|| rel.startsWith(`..${sep}`)
|
|
171
|
+
|| isAbsolute(rel)
|
|
172
|
+
) return null;
|
|
173
|
+
const normalized = rel.split(sep).join("/");
|
|
174
|
+
if (normalized.split("/").some(part => !part || part === "." || part === ".." || part.includes("\\"))) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (normalized === CONFIG_OWNER_FILE || normalized === CONFIG_UNINSTALL_MANIFEST) return null;
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function loadOwnership(configDir: string): { owner: ConfigOwner; manifest: ConfigUninstallManifest } | null {
|
|
182
|
+
const ownerPath = join(configDir, CONFIG_OWNER_FILE);
|
|
183
|
+
const manifestPath = join(configDir, CONFIG_UNINSTALL_MANIFEST);
|
|
184
|
+
if (!existsSync(ownerPath) || !existsSync(manifestPath)) return null;
|
|
185
|
+
try {
|
|
186
|
+
const owner = readBoundedJson(ownerPath);
|
|
187
|
+
const manifest = readBoundedJson(manifestPath);
|
|
188
|
+
const root = canonicalRoot(configDir);
|
|
189
|
+
if (
|
|
190
|
+
!isOwner(owner)
|
|
191
|
+
|| !isManifest(manifest)
|
|
192
|
+
|| owner.ownerId !== manifest.ownerId
|
|
193
|
+
|| !samePath(owner.root, root)
|
|
194
|
+
|| !samePath(manifest.root, root)
|
|
195
|
+
) return null;
|
|
196
|
+
return { owner, manifest };
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createOwnership(configDir: string): { owner: ConfigOwner; manifest: ConfigUninstallManifest } | null {
|
|
203
|
+
const rootStat = lstatSync(configDir);
|
|
204
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || readdirSync(configDir).length !== 0) return null;
|
|
205
|
+
const owner: ConfigOwner = {
|
|
206
|
+
version: 1,
|
|
207
|
+
ownerId: randomUUID(),
|
|
208
|
+
root: canonicalRoot(configDir),
|
|
209
|
+
};
|
|
210
|
+
const manifest: ConfigUninstallManifest = { ...owner, paths: [...INITIAL_OWNED_PATHS] };
|
|
211
|
+
writeFileSync(join(configDir, CONFIG_OWNER_FILE), `${JSON.stringify(owner, null, 2)}\n`, {
|
|
212
|
+
encoding: "utf8",
|
|
213
|
+
flag: "wx",
|
|
214
|
+
mode: 0o600,
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
writeFileSync(join(configDir, CONFIG_UNINSTALL_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
flag: "wx",
|
|
220
|
+
mode: 0o600,
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
try { unlinkSync(join(configDir, CONFIG_OWNER_FILE)); } catch { /* incomplete metadata fails closed */ }
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
return { owner, manifest };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function writeManifest(configDir: string, manifest: ConfigUninstallManifest): void {
|
|
230
|
+
const path = join(configDir, CONFIG_UNINSTALL_MANIFEST);
|
|
231
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
232
|
+
writeFileSync(temp, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
233
|
+
try {
|
|
234
|
+
renameSync(temp, path);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
try { unlinkSync(temp); } catch { /* best effort */ }
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function removeOwnedEntry(root: string, path: string): void {
|
|
242
|
+
const entry = lstatSync(path);
|
|
243
|
+
if (entry.isSymbolicLink()) {
|
|
244
|
+
unlinkSync(path);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!entry.isDirectory()) {
|
|
248
|
+
unlinkSync(path);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const realDirectory = realpathSync.native(path);
|
|
253
|
+
if (!isWithinRoot(root, realDirectory)) {
|
|
254
|
+
throw new Error(`owned directory resolves outside the config root: ${path}`);
|
|
255
|
+
}
|
|
256
|
+
for (const name of readdirSync(path)) {
|
|
257
|
+
removeOwnedEntry(root, join(path, name));
|
|
258
|
+
}
|
|
259
|
+
rmdirSync(path);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function recordOwnedConfigPath(configDir: string, candidatePath: string): boolean {
|
|
263
|
+
const rel = manifestRelativePath(configDir, candidatePath);
|
|
264
|
+
if (!rel) return false;
|
|
265
|
+
const cacheKey = ownershipCacheKey(configDir);
|
|
266
|
+
if (!existsSync(configDir)) {
|
|
267
|
+
ownershipCache.delete(cacheKey);
|
|
268
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
269
|
+
}
|
|
270
|
+
let ownership = ownershipCache.get(cacheKey);
|
|
271
|
+
if (ownership === undefined) {
|
|
272
|
+
ownership = loadOwnership(configDir) ?? createOwnership(configDir);
|
|
273
|
+
ownershipCache.set(cacheKey, ownership);
|
|
274
|
+
}
|
|
275
|
+
if (!ownership) return false;
|
|
276
|
+
if (ownership.manifest.paths.includes(rel)) return true;
|
|
277
|
+
const manifest = {
|
|
278
|
+
...ownership.manifest,
|
|
279
|
+
paths: [...ownership.manifest.paths, rel].sort(),
|
|
280
|
+
};
|
|
281
|
+
writeManifest(configDir, manifest);
|
|
282
|
+
ownershipCache.set(cacheKey, { owner: ownership.owner, manifest });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function removeOwnedConfigState(configDir: string): ConfigRemovalResult {
|
|
287
|
+
ownershipCache.delete(ownershipCacheKey(configDir));
|
|
288
|
+
if (!existsSync(configDir)) return { status: "absent", residualPaths: [] };
|
|
289
|
+
const root = lstatSync(configDir);
|
|
290
|
+
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
291
|
+
return {
|
|
292
|
+
status: "refused",
|
|
293
|
+
reason: "config ownership root is not a real directory",
|
|
294
|
+
residualPaths: [configDir],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const ownership = loadOwnership(configDir);
|
|
298
|
+
if (!ownership) {
|
|
299
|
+
return {
|
|
300
|
+
status: "refused",
|
|
301
|
+
reason: "config ownership metadata is missing or invalid",
|
|
302
|
+
residualPaths: [configDir],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const rel of ownership.manifest.paths) {
|
|
307
|
+
const path = manifestRelativePath(configDir, join(configDir, ...rel.split("/")));
|
|
308
|
+
if (path !== rel) {
|
|
309
|
+
return {
|
|
310
|
+
status: "refused",
|
|
311
|
+
reason: "config ownership manifest contains an unsafe path",
|
|
312
|
+
residualPaths: [configDir],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const rootPath = canonicalRoot(configDir);
|
|
318
|
+
for (const rel of ownership.manifest.paths) {
|
|
319
|
+
const path = join(configDir, ...rel.split("/"));
|
|
320
|
+
if (!existsSync(path)) continue;
|
|
321
|
+
try {
|
|
322
|
+
removeOwnedEntry(rootPath, path);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
status: "partial",
|
|
326
|
+
reason: `could not remove owned path ${rel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
327
|
+
residualPaths: [path],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
unlinkSync(join(configDir, CONFIG_UNINSTALL_MANIFEST));
|
|
334
|
+
unlinkSync(join(configDir, CONFIG_OWNER_FILE));
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return {
|
|
337
|
+
status: "partial",
|
|
338
|
+
reason: `could not remove ownership metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
339
|
+
residualPaths: readdirSync(configDir).map(name => join(configDir, name)),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const residualPaths = readdirSync(configDir).map(name => join(configDir, name));
|
|
343
|
+
if (residualPaths.length > 0) {
|
|
344
|
+
return {
|
|
345
|
+
status: "partial",
|
|
346
|
+
reason: "unowned files remain in the config directory",
|
|
347
|
+
residualPaths,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
rmdirSync(configDir);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return {
|
|
354
|
+
status: "partial",
|
|
355
|
+
reason: `could not remove the empty config directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
356
|
+
residualPaths: [configDir],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { status: "removed", residualPaths: [] };
|
|
360
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { recordOwnedConfigPath } from "./config-ownership";
|
|
5
|
+
import { redactSecretString, redactUrlForLog } from "./redact";
|
|
6
|
+
import { sidecarBreadcrumb, activityBreadcrumb } from "./sidecar-tracker";
|
|
7
|
+
import { retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
|
|
8
|
+
import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Process-level safety net for the long-running proxy daemon.
|
|
12
|
+
*
|
|
13
|
+
* A single request can trigger an async error inside a Bun.serve streaming
|
|
14
|
+
* handler (e.g. a ReadableStream `start(controller)` callback hitting an
|
|
15
|
+
* unexpected upstream response shape). Without a handler, Bun's default
|
|
16
|
+
* behaviour prints the raw error — shown as `(function (controller, error)
|
|
17
|
+
* {"use strict"; ... TypeError: null is not an object` — and can tear down
|
|
18
|
+
* the whole proxy, killing every other in-flight Codex session.
|
|
19
|
+
*
|
|
20
|
+
* We must NOT let one bad stream crash the daemon. These handlers:
|
|
21
|
+
* 1. Append the full error + stack to `<configDir>/crash.log` so the exact
|
|
22
|
+
* fault (with the JSC `(evaluating 'x.y')` clause and file:line) is
|
|
23
|
+
* captured for a precise root-cause fix.
|
|
24
|
+
* 2. Keep the process alive — the failed request is already isolated by
|
|
25
|
+
* Bun.serve; surviving is strictly better than terminating.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
let installed = false;
|
|
29
|
+
|
|
30
|
+
function crashLogPath(): string {
|
|
31
|
+
const dir = getConfigDir();
|
|
32
|
+
try {
|
|
33
|
+
recordOwnedConfigPath(dir, join(dir, "crash.log"));
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
} catch {
|
|
36
|
+
/* best-effort: directory usually already exists */
|
|
37
|
+
}
|
|
38
|
+
return join(dir, "crash.log");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatCrashEntry(kind: string, err: unknown, promise?: unknown): string {
|
|
42
|
+
const ts = new Date().toISOString();
|
|
43
|
+
const detail =
|
|
44
|
+
err instanceof Error
|
|
45
|
+
? `${err.name}: ${redactDiagnosticText(err.message)}\n${redactDiagnosticText(err.stack ?? "(no stack)")}`
|
|
46
|
+
: typeof err === "object"
|
|
47
|
+
? redactDiagnosticText(safeStringify(err))
|
|
48
|
+
: redactDiagnosticText(String(err));
|
|
49
|
+
return `\n[${ts}] ${kind}\n${detail}${diagnose(err)}${diagnosePromise(promise)}${breadcrumb()}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bun surfaces some request-time stream/abort errors with only native frames
|
|
54
|
+
* (`at <anonymous> (native:1:11)`), so `err.stack` alone cannot locate the
|
|
55
|
+
* fault. JSC still records the true throw site on hidden own properties
|
|
56
|
+
* (`sourceURL` / `originalLine` / `originalColumn`) and `Bun.inspect` renders a
|
|
57
|
+
* code snippet from them — capture both so the next occurrence is pinpointable.
|
|
58
|
+
*/
|
|
59
|
+
function diagnose(err: unknown): string {
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
try {
|
|
62
|
+
const ctor = (err as { constructor?: { name?: string } } | null)?.constructor?.name;
|
|
63
|
+
if (ctor && ctor !== "Error" && ctor !== "Object") lines.push(` ctor: ${ctor}`);
|
|
64
|
+
if (err && typeof err === "object") {
|
|
65
|
+
const e = err as Record<string, unknown>;
|
|
66
|
+
const cause = e.cause;
|
|
67
|
+
if (cause !== undefined) {
|
|
68
|
+
lines.push(` cause: ${redactDiagnosticText(cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause))}`);
|
|
69
|
+
}
|
|
70
|
+
if (e.code !== undefined) lines.push(` code: ${redactDiagnosticText(String(e.code))}`);
|
|
71
|
+
// JSC hidden throw-site fields survive even when the stack is native-only.
|
|
72
|
+
const sourceURL = e.sourceURL;
|
|
73
|
+
const line = e.line ?? e.originalLine;
|
|
74
|
+
const column = e.column ?? e.originalColumn;
|
|
75
|
+
if (typeof sourceURL === "string" && sourceURL) {
|
|
76
|
+
lines.push(` origin: ${redactUrlForLog(sourceURL)}${line !== undefined ? `:${String(line)}` : ""}${column !== undefined ? `:${String(column)}` : ""}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
80
|
+
const hasUsableStack = /\((?!native:)[^)]*:\d+:\d+\)/.test(stack);
|
|
81
|
+
if (!hasUsableStack) {
|
|
82
|
+
const snippet = inspectErr(err);
|
|
83
|
+
if (snippet) lines.push(` inspect:\n${snippet.split("\n").map(l => ` ${l}`).join("\n")}`);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* diagnosis must never throw */
|
|
87
|
+
}
|
|
88
|
+
return lines.length ? `\n${lines.join("\n")}` : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Bun.inspect renders the JSC source snippet (with the offending line + caret)
|
|
93
|
+
* for errors whose throw site is otherwise lost to native frames.
|
|
94
|
+
*/
|
|
95
|
+
function inspectErr(err: unknown): string {
|
|
96
|
+
try {
|
|
97
|
+
const bun = (globalThis as { Bun?: { inspect?: (v: unknown, o?: unknown) => string } }).Bun;
|
|
98
|
+
if (!bun?.inspect) return "";
|
|
99
|
+
return redactDiagnosticText(bun.inspect(err, { depth: 2 }).trim());
|
|
100
|
+
} catch {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Inspect the rejected promise itself. Bun sometimes attaches richer context to the promise object
|
|
107
|
+
* than to the reason, and the rendered form helps distinguish a fetch/stream teardown from app code.
|
|
108
|
+
*/
|
|
109
|
+
function diagnosePromise(promise: unknown): string {
|
|
110
|
+
if (promise === undefined) return "";
|
|
111
|
+
try {
|
|
112
|
+
const bun = (globalThis as { Bun?: { inspect?: (v: unknown, o?: unknown) => string } }).Bun;
|
|
113
|
+
const rendered = bun?.inspect ? bun.inspect(promise, { depth: 1 }).trim() : String(promise);
|
|
114
|
+
if (!rendered || rendered === "Promise { <rejected> }") return "";
|
|
115
|
+
return `\n promise: ${redactDiagnosticText(rendered.split("\n").join(" "))}`;
|
|
116
|
+
} catch {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Record whether a sidecar (web-search / vision) was in flight when the fault fired. A native-only
|
|
123
|
+
* rejection coinciding with sidecar work is the prime suspect; this turns the correlation into a
|
|
124
|
+
* logged fact instead of an inference.
|
|
125
|
+
*/
|
|
126
|
+
function breadcrumb(): string {
|
|
127
|
+
try {
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
const b = sidecarBreadcrumb();
|
|
130
|
+
if (b.inFlight > 0 || b.lastLabel) {
|
|
131
|
+
lines.push(` sidecar: inFlight=${b.inFlight} last=${b.lastLabel || "-"} sinceMs=${b.sinceMs}`);
|
|
132
|
+
}
|
|
133
|
+
const a = activityBreadcrumb();
|
|
134
|
+
if (a.note) lines.push(` activity: ${a.note} sinceMs=${a.sinceMs}`);
|
|
135
|
+
const fetches = recentFetches();
|
|
136
|
+
if (fetches) lines.push(fetches);
|
|
137
|
+
return lines.length ? `\n${lines.join("\n")}` : "";
|
|
138
|
+
} catch {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function safeStringify(value: unknown): string {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
146
|
+
} catch {
|
|
147
|
+
return String(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let benignSuppressed = 0;
|
|
152
|
+
let benignLastLoggedAt = 0;
|
|
153
|
+
const BENIGN_LOG_INTERVAL_MS = 5 * 60_000;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Bun raises an off-path `unhandledRejection: TypeError: null is not an object` (native-only stack)
|
|
157
|
+
* whenever a streaming `fetch(..., { signal })` response body is torn down by an abort before/while
|
|
158
|
+
* we read it — turn supersede, client disconnect, upstream RST. The daemon is never at risk (the
|
|
159
|
+
* failed request is already isolated), the throw has no JS source location, and call-site body
|
|
160
|
+
* cancellation cannot fully close the runtime-internal window. Treat this exact shape as benign:
|
|
161
|
+
* keep the process alive, drop the alarmist banner, and fold repeats into a rate-limited summary so
|
|
162
|
+
* crash.log stays readable for genuinely novel faults.
|
|
163
|
+
*
|
|
164
|
+
* Second known shape (260712): `TypeError: Invalid state: ReadableStream is locked`
|
|
165
|
+
* (code ERR_INVALID_STATE, native-only stack ending in onSinkClose*). When a client
|
|
166
|
+
* disconnects mid-SSE on the tee()'d passthrough path (responses.ts Bun#32111
|
|
167
|
+
* workaround), Bun's sink-close teardown tries to cancel the tee-locked source body
|
|
168
|
+
* and rejects off-path. Request lifecycle is already settled at that point; same
|
|
169
|
+
* benign handling applies. The tee path remains the DEFAULT passthrough shape;
|
|
170
|
+
* the gated eager relay (relay-eager.ts, #314) does not tee and may not produce
|
|
171
|
+
* this shape — detection stays unchanged either way.
|
|
172
|
+
*/
|
|
173
|
+
export function isBenignAbortTeardown(err: unknown): boolean {
|
|
174
|
+
if (!(err instanceof TypeError)) return false;
|
|
175
|
+
const bareNullTeardown = err.message === "null is not an object"; // bare form only (no `(evaluating …)`)
|
|
176
|
+
const lockedStreamTeardown = err.message === "Invalid state: ReadableStream is locked"
|
|
177
|
+
&& (err as { code?: unknown }).code === "ERR_INVALID_STATE";
|
|
178
|
+
if (!bareNullTeardown && !lockedStreamTeardown) return false;
|
|
179
|
+
const stack = err.stack ?? "";
|
|
180
|
+
// Native-only: no JS source frame. A real app TypeError would carry a `(file:line:col)` frame.
|
|
181
|
+
return !/\((?!native:)[^)]*:\d+:\d+\)/.test(stack);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function record(kind: string, err: unknown, promise?: unknown): void {
|
|
185
|
+
if (kind === "unhandledRejection" && isBenignAbortTeardown(err)) {
|
|
186
|
+
benignSuppressed++;
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if (now - benignLastLoggedAt < BENIGN_LOG_INTERVAL_MS) return; // fold repeats silently
|
|
189
|
+
benignLastLoggedAt = now;
|
|
190
|
+
const summary = `\n[${new Date(now).toISOString()}] benign-abort-teardown x${benignSuppressed}`
|
|
191
|
+
+ ` (Bun fetch-body abort; proxy unaffected)${diagnose(err)}${diagnosePromise(promise)}${breadcrumb()}\n`;
|
|
192
|
+
benignSuppressed = 0;
|
|
193
|
+
try { appendFileSync(crashLogPath(), summary); } catch { /* logging must never throw */ }
|
|
194
|
+
return; // no stderr banner — this is expected noise, not a crash
|
|
195
|
+
}
|
|
196
|
+
const line = formatCrashEntry(kind, err, promise);
|
|
197
|
+
// Always surface to stderr so foreground `ocx start` users still see it,
|
|
198
|
+
// then persist for later diagnosis.
|
|
199
|
+
console.error(`⚠️ ${kind} (proxy stayed up; logged to crash.log)`);
|
|
200
|
+
console.error(line.trimStart());
|
|
201
|
+
try {
|
|
202
|
+
appendFileSync(crashLogPath(), line);
|
|
203
|
+
} catch {
|
|
204
|
+
/* logging must never throw */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface FetchTrace { url: string; at: number; origin: string; settled: boolean; rejected?: string }
|
|
209
|
+
const FETCH_RING_MAX = 12;
|
|
210
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
211
|
+
const fetchRing: FetchTrace[] = [];
|
|
212
|
+
let fetchInstrumented = false;
|
|
213
|
+
let fetchRingBytes = 0;
|
|
214
|
+
|
|
215
|
+
function fetchTraceBytes(trace: FetchTrace): number {
|
|
216
|
+
return retainedUtf8Bytes(trace.url) + retainedUtf8Bytes(trace.origin) + retainedUtf8Bytes(trace.rejected ?? "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function removeOldestFetchTrace(): number {
|
|
220
|
+
const trace = fetchRing.shift();
|
|
221
|
+
if (!trace) return 0;
|
|
222
|
+
const bytes = fetchTraceBytes(trace);
|
|
223
|
+
fetchRingBytes -= bytes;
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setFetchTraceRejected(trace: FetchTrace, value: string): void {
|
|
228
|
+
const retained = fetchRing.includes(trace);
|
|
229
|
+
if (retained) fetchRingBytes -= retainedUtf8Bytes(trace.rejected ?? "");
|
|
230
|
+
trace.rejected = truncateRetainedUtf8(value, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
231
|
+
if (retained) fetchRingBytes += retainedUtf8Bytes(trace.rejected);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function appendFetchTrace(url: string, origin: string): FetchTrace {
|
|
235
|
+
const trace: FetchTrace = {
|
|
236
|
+
url: truncateRetainedUtf8(redactUrlForLog(url), MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
237
|
+
at: Date.now(),
|
|
238
|
+
origin: truncateRetainedUtf8(origin, MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
239
|
+
settled: false,
|
|
240
|
+
};
|
|
241
|
+
fetchRing.push(trace);
|
|
242
|
+
fetchRingBytes += fetchTraceBytes(trace);
|
|
243
|
+
while (fetchRing.length > FETCH_RING_MAX) removeOldestFetchTrace();
|
|
244
|
+
return trace;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The recurring native-only rejection carries no source location, and every JS `await fetch(...)`
|
|
249
|
+
* is already try/caught — so the offending promise is created INSIDE Bun's fetch and rejects off the
|
|
250
|
+
* awaited path. Wrap global fetch to record each call's origin (a JS stack captured at call time) and
|
|
251
|
+
* whether it later rejected. crash-guard then dumps the still-pending / recently-rejected fetches so
|
|
252
|
+
* the next fault names the exact call site Bun lost.
|
|
253
|
+
*/
|
|
254
|
+
function instrumentFetch(): void {
|
|
255
|
+
if (fetchInstrumented) return;
|
|
256
|
+
const g = globalThis as { fetch?: typeof fetch };
|
|
257
|
+
const original = g.fetch;
|
|
258
|
+
if (typeof original !== "function") return;
|
|
259
|
+
fetchInstrumented = true;
|
|
260
|
+
g.fetch = function instrumentedFetch(this: unknown, ...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
|
|
261
|
+
let url = "";
|
|
262
|
+
try {
|
|
263
|
+
const input = args[0];
|
|
264
|
+
url = typeof input === "string" ? input : input instanceof URL ? input.href : (input as Request)?.url ?? "";
|
|
265
|
+
} catch { /* best-effort */ }
|
|
266
|
+
const origin = (new Error().stack ?? "").split("\n").slice(2, 5).map(l => l.trim()).join(" <- ");
|
|
267
|
+
const trace = appendFetchTrace(url, origin);
|
|
268
|
+
enforceAppOwnedMemoryBudget();
|
|
269
|
+
let p: ReturnType<typeof fetch>;
|
|
270
|
+
try {
|
|
271
|
+
p = original.apply(this, args);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
trace.settled = true;
|
|
274
|
+
setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e)));
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
return p.then(
|
|
278
|
+
r => { trace.settled = true; return r; },
|
|
279
|
+
e => { trace.settled = true; setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e))); throw e; },
|
|
280
|
+
);
|
|
281
|
+
} as typeof fetch;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function crashRingMetrics(): { entries: number; bytes: number; oldestAt: number | null } {
|
|
285
|
+
return { entries: fetchRing.length, bytes: fetchRingBytes, oldestAt: fetchRing[0]?.at ?? null };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function evictOldestCrashTraceForBudget(): number {
|
|
289
|
+
return removeOldestFetchTrace();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function appendCrashTraceForTests(url: string, origin: string, rejected?: string): void {
|
|
293
|
+
const trace = appendFetchTrace(url, origin);
|
|
294
|
+
if (rejected !== undefined) {
|
|
295
|
+
trace.settled = true;
|
|
296
|
+
setFetchTraceRejected(trace, rejected);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function crashRingEntriesForTests(): readonly Readonly<FetchTrace>[] {
|
|
301
|
+
return fetchRing.map(trace => ({ ...trace }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function resetCrashRingForTests(): void {
|
|
305
|
+
fetchRing.length = 0;
|
|
306
|
+
fetchRingBytes = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Render the recent fetch ring (pending first) for the crash breadcrumb. */
|
|
310
|
+
function recentFetches(): string {
|
|
311
|
+
try {
|
|
312
|
+
if (fetchRing.length === 0) return "";
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
const rows = fetchRing.slice(-6).map(f => {
|
|
315
|
+
const state = !f.settled ? "PENDING" : f.rejected ? `REJECTED(${f.rejected})` : "ok";
|
|
316
|
+
return ` [${state}] ${f.url} ageMs=${now - f.at}${!f.settled ? ` origin=${f.origin}` : ""}`;
|
|
317
|
+
});
|
|
318
|
+
return ` fetches:\n${rows.join("\n")}`;
|
|
319
|
+
} catch {
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function redactDiagnosticText(value: string): string {
|
|
325
|
+
return redactSecretString(value);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Register global handlers that keep the proxy alive and capture full stacks.
|
|
330
|
+
* Idempotent: safe to call more than once.
|
|
331
|
+
*/
|
|
332
|
+
export function installCrashGuards(): void {
|
|
333
|
+
if (installed) return;
|
|
334
|
+
installed = true;
|
|
335
|
+
instrumentFetch();
|
|
336
|
+
|
|
337
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
338
|
+
record("unhandledRejection", reason, promise);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
process.on("uncaughtException", err => {
|
|
342
|
+
record("uncaughtException", err);
|
|
343
|
+
});
|
|
344
|
+
}
|