@songsid/agend 2.3.5-alpha.4 → 2.3.5-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/README.zh-TW.md +0 -2
- package/THIRD_PARTY_NOTICES.md +12 -28
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +4 -6
- package/dist/agent-endpoint.js +15 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +0 -21
- package/dist/auth-api.js +8 -93
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +30 -4
- package/dist/authority/session-authorizations.js +69 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +2 -12
- package/dist/authority/web-authority.js +4 -25
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +12 -15
- package/dist/backend/claude-code.js +133 -152
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +14 -0
- package/dist/backend/codex-metadata.js +45 -0
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex-package.d.ts +2 -5
- package/dist/backend/codex-package.js +5 -1
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex-quota-history.d.ts +7 -0
- package/dist/backend/codex-quota-history.js +72 -0
- package/dist/backend/codex-quota-history.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +6 -0
- package/dist/backend/codex-session-lookup.js +1 -1
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex-speed.d.ts +14 -0
- package/dist/backend/codex-speed.js +35 -0
- package/dist/backend/codex-speed.js.map +1 -0
- package/dist/backend/codex.d.ts +116 -136
- package/dist/backend/codex.js +912 -942
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok-weekly-reset.d.ts +24 -0
- package/dist/backend/grok-weekly-reset.js +95 -0
- package/dist/backend/grok-weekly-reset.js.map +1 -0
- package/dist/backend/grok.d.ts +30 -3
- package/dist/backend/grok.js +301 -76
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +50 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +15 -8
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-tmux-guard.d.ts +21 -0
- package/dist/bundled-tmux-guard.js +96 -0
- package/dist/bundled-tmux-guard.js.map +1 -0
- package/dist/cache-ledger.d.ts +95 -0
- package/dist/cache-ledger.js +276 -4
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +235 -17
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +0 -2
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/classic-channel-manager.d.ts +2 -0
- package/dist/classic-channel-manager.js +5 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +14 -10
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +30 -71
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -17
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +70 -68
- package/dist/daemon.js +774 -846
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +8 -0
- package/dist/dashboard-permissions.js +16 -0
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +26 -16
- package/dist/delivery-consumed-watch.js +31 -26
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.d.ts +0 -5
- package/dist/delivery-outbox.js +4 -32
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +42 -2
- package/dist/delivery-reconciliation.js +164 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +4 -2
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +0 -2
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +108 -245
- package/dist/fleet-manager.js +656 -2028
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +0 -7
- package/dist/general-instance.js +3 -20
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +9 -3
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-config-impact.js +6 -7
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +19 -23
- package/dist/instance-lifecycle.js +216 -383
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -3
- package/dist/instructions.js +1 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/locale.js +63 -86
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +28 -0
- package/dist/log-service.js +29 -0
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +16 -2
- package/dist/logger.js +346 -66
- package/dist/logger.js.map +1 -1
- package/dist/login-flows.d.ts +7 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.js +25 -4
- package/dist/login-manager.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/muse-usage-relay.d.ts +2 -0
- package/dist/muse-usage-relay.js +5 -3
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/needs-you-hub.d.ts +6 -3
- package/dist/needs-you-hub.js +31 -9
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +20 -18
- package/dist/needs-you.js +18 -14
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share-page.js +1 -1
- package/dist/office-share-page.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -16
- package/dist/outbound-handlers.js +10 -72
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -38
- package/dist/outbound-schemas.js +5 -14
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-quota-history.d.ts +7 -0
- package/dist/pane-quota-history.js +47 -0
- package/dist/pane-quota-history.js.map +1 -0
- package/dist/platform-general.d.ts +46 -0
- package/dist/platform-general.js +91 -0
- package/dist/platform-general.js.map +1 -0
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +0 -11
- package/dist/public-web-link.js +0 -13
- package/dist/public-web-link.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/quickstart-api.js +5 -9
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +8 -1
- package/dist/quickstart.js +42 -4
- package/dist/quickstart.js.map +1 -1
- package/dist/quota-notice-config.d.ts +22 -0
- package/dist/quota-notice-config.js +41 -0
- package/dist/quota-notice-config.js.map +1 -0
- package/dist/quota-notice.d.ts +7 -0
- package/dist/quota-notice.js +40 -0
- package/dist/quota-notice.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -6
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-polls.d.ts +2 -2
- package/dist/reply-polls.js +3 -3
- package/dist/reply-polls.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +9 -0
- package/dist/scheduler/task-board.js +19 -0
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/settings-api.d.ts +13 -5
- package/dist/settings-api.js +26 -96
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +0 -1
- package/dist/settings-baseline.js +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +23 -28
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +1 -16
- package/dist/settings-confirmation.js +9 -47
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.d.ts +0 -4
- package/dist/settings-effect.js +1 -58
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +0 -4
- package/dist/settings-http-confirmation.js +3 -31
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +28 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/settings-request-capability.d.ts +0 -2
- package/dist/settings-request-capability.js +0 -4
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/settings-transaction.d.ts +0 -4
- package/dist/settings-transaction.js +0 -9
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-form.js +3 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +0 -3
- package/dist/setup-i18n.js.map +1 -1
- package/dist/task-board-tool.d.ts +11 -0
- package/dist/task-board-tool.js +44 -0
- package/dist/task-board-tool.js.map +1 -1
- package/dist/task-board-waker.d.ts +6 -0
- package/dist/task-board-waker.js +4 -0
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -2
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +7 -7
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -3
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +1 -1
- package/dist/transcript-delta-reader.js +1 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +20 -3
- package/dist/transcript-monitor.js +92 -24
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +0 -5
- package/dist/transcript-sources.js +79 -8
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +3 -23
- package/dist/ui/app.html +8 -1
- package/dist/ui/backend-signin.js +21 -36
- package/dist/ui/chat-md-strings.js +9 -0
- package/dist/ui/chat-store.js +2 -13
- package/dist/ui/chat-strings.js +2 -50
- package/dist/ui/chat-thread.js +53 -3
- package/dist/ui/fleet-task-card.js +3 -3
- package/dist/ui/fleet-task-common.js +10 -0
- package/dist/ui/fleet-tasks.js +7 -5
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +37 -66
- package/dist/ui/panel-details.js +1 -2
- package/dist/ui/panel-fleet.js +88 -107
- package/dist/ui/panel-needs.js +21 -163
- package/dist/ui/panel-notices.js +4 -3
- package/dist/ui/panel-settings.js +149 -239
- package/dist/ui/preview-panel.js +84 -6
- package/dist/ui/search-dialog.js +3 -2
- package/dist/ui/settings-appearance.js +23 -122
- package/dist/ui/settings-catalog.js +10 -37
- package/dist/ui/settings-codex-copy.js +19 -0
- package/dist/ui/settings-confirm.js +1 -3
- package/dist/ui/settings-dialogs.js +20 -27
- package/dist/ui/settings-guests.js +14 -27
- package/dist/ui/settings-model.js +17 -12
- package/dist/ui/settings-quota-notice.js +100 -0
- package/dist/ui/settings-registry.js +4 -11
- package/dist/ui/settings-strings.js +4 -113
- package/dist/ui/settings-web-authority.js +13 -15
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-route.js +2 -6
- package/dist/ui/shared/app-shell.js +18 -23
- package/dist/ui/shared/app-store.js +2 -3
- package/dist/ui/shared/app-stream.js +1 -3
- package/dist/ui/shared/app.css +40 -1312
- package/dist/ui/shared/app.js +2 -5
- package/dist/ui/shared/header-tools.js +4 -6
- package/dist/ui/shared/instance-nav.js +7 -34
- package/dist/ui/shared/instance-switch.js +3 -4
- package/dist/ui/shared/loading.css +37 -0
- package/dist/ui/shared/panel-view.js +77 -4
- package/dist/ui/shared/signin.js +6 -39
- package/dist/ui/shared/tokens.css +0 -1
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/ui-menu.js +10 -30
- package/dist/ui/shared/ui-states.js +14 -3
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/vendor/vendor.json +2 -19
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +16 -8
- package/dist/ui/signin.html +5 -2
- package/dist/unattended-install.d.ts +11 -2
- package/dist/unattended-install.js +39 -5
- package/dist/unattended-install.js.map +1 -1
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/providers.d.ts +5 -0
- package/dist/usage/providers.js +5 -0
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/quota-notices.d.ts +38 -0
- package/dist/usage/quota-notices.js +160 -0
- package/dist/usage/quota-notices.js.map +1 -0
- package/dist/usage/usage-api.d.ts +32 -0
- package/dist/usage/usage-api.js +96 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +6 -1
- package/dist/usage-rollup.js +16 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +16 -0
- package/dist/view-api.js +100 -16
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +6 -38
- package/dist/web-api.js +20 -255
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +2 -19
- package/dist/web-chat-history.js +3 -72
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-only-fleet.d.ts +0 -20
- package/dist/web-only-fleet.js +0 -21
- package/dist/web-only-fleet.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.d.ts +0 -7
- package/dist/web-session.js +0 -2
- package/dist/web-session.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -4
- package/dist/web-shell-routes.js +2 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/private-home.cjs +1 -1
- package/package.json +3 -9
- package/dist/agent-runtime-api.d.ts +0 -4
- package/dist/agent-runtime-api.js +0 -59
- package/dist/agent-runtime-api.js.map +0 -1
- package/dist/agent-runtime-change.d.ts +0 -74
- package/dist/agent-runtime-change.js +0 -102
- package/dist/agent-runtime-change.js.map +0 -1
- package/dist/backend/grok-model-cache.d.ts +0 -7
- package/dist/backend/grok-model-cache.js +0 -65
- package/dist/backend/grok-model-cache.js.map +0 -1
- package/dist/backend/native-pane.d.ts +0 -9
- package/dist/backend/native-pane.js +0 -59
- package/dist/backend/native-pane.js.map +0 -1
- package/dist/fleet-windows-runtime.d.ts +0 -97
- package/dist/fleet-windows-runtime.js +0 -449
- package/dist/fleet-windows-runtime.js.map +0 -1
- package/dist/general-binding.d.ts +0 -85
- package/dist/general-binding.js +0 -298
- package/dist/general-binding.js.map +0 -1
- package/dist/general-host-owner.d.ts +0 -8
- package/dist/general-host-owner.js +0 -26
- package/dist/general-host-owner.js.map +0 -1
- package/dist/general-role.d.ts +0 -5
- package/dist/general-role.js +0 -26
- package/dist/general-role.js.map +0 -1
- package/dist/local-pane-endpoint.d.ts +0 -46
- package/dist/local-pane-endpoint.js +0 -189
- package/dist/local-pane-endpoint.js.map +0 -1
- package/dist/local-pane-launcher.d.ts +0 -2
- package/dist/local-pane-launcher.js +0 -38
- package/dist/local-pane-launcher.js.map +0 -1
- package/dist/local-pane-stage.ps1 +0 -101
- package/dist/model-catalog-defaults.d.ts +0 -14
- package/dist/model-catalog-defaults.js +0 -20
- package/dist/model-catalog-defaults.js.map +0 -1
- package/dist/native-agent-admission.d.ts +0 -7
- package/dist/native-agent-admission.js +0 -26
- package/dist/native-agent-admission.js.map +0 -1
- package/dist/native-daemon-client.d.ts +0 -13
- package/dist/native-daemon-client.js +0 -28
- package/dist/native-daemon-client.js.map +0 -1
- package/dist/native-instance-api.d.ts +0 -14
- package/dist/native-instance-api.js +0 -205
- package/dist/native-instance-api.js.map +0 -1
- package/dist/native-instance-input.d.ts +0 -84
- package/dist/native-instance-input.js +0 -54
- package/dist/native-instance-input.js.map +0 -1
- package/dist/native-instance-registry.d.ts +0 -32
- package/dist/native-instance-registry.js +0 -105
- package/dist/native-instance-registry.js.map +0 -1
- package/dist/native-mcp.d.ts +0 -40
- package/dist/native-mcp.js +0 -278
- package/dist/native-mcp.js.map +0 -1
- package/dist/native-model-catalog.d.ts +0 -5
- package/dist/native-model-catalog.js +0 -76
- package/dist/native-model-catalog.js.map +0 -1
- package/dist/native-pane-crash-budget.d.ts +0 -20
- package/dist/native-pane-crash-budget.js +0 -60
- package/dist/native-pane-crash-budget.js.map +0 -1
- package/dist/native-pane-create.d.ts +0 -26
- package/dist/native-pane-create.js +0 -53
- package/dist/native-pane-create.js.map +0 -1
- package/dist/native-pane-lifecycle.d.ts +0 -65
- package/dist/native-pane-lifecycle.js +0 -230
- package/dist/native-pane-lifecycle.js.map +0 -1
- package/dist/native-pane-manager.d.ts +0 -59
- package/dist/native-pane-manager.js +0 -584
- package/dist/native-pane-manager.js.map +0 -1
- package/dist/native-pane-output-log.d.ts +0 -15
- package/dist/native-pane-output-log.js +0 -108
- package/dist/native-pane-output-log.js.map +0 -1
- package/dist/native-pane-reconciliation.d.ts +0 -4
- package/dist/native-pane-reconciliation.js +0 -40
- package/dist/native-pane-reconciliation.js.map +0 -1
- package/dist/native-pane-recovery.d.ts +0 -12
- package/dist/native-pane-recovery.js +0 -10
- package/dist/native-pane-recovery.js.map +0 -1
- package/dist/native-pane-tools.d.ts +0 -2
- package/dist/native-pane-tools.js +0 -14
- package/dist/native-pane-tools.js.map +0 -1
- package/dist/pane-port.d.ts +0 -118
- package/dist/pane-port.js +0 -2
- package/dist/pane-port.js.map +0 -1
- package/dist/runtime-api.d.ts +0 -8
- package/dist/runtime-api.js +0 -171
- package/dist/runtime-api.js.map +0 -1
- package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +0 -2
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +0 -19
- package/dist/runtime-assets/@xterm/addon-serialize/package.json +0 -29
- package/dist/runtime-assets/@xterm/headless/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +0 -2
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +0 -46
- package/dist/runtime-assets/@xterm/headless/package.json +0 -29
- package/dist/runtime-assets/node-pty/CONPTY-LICENSE +0 -21
- package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +0 -503
- package/dist/runtime-assets/node-pty/LICENSE +0 -69
- package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +0 -16
- package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +0 -47
- package/dist/runtime-assets/node-pty/lib/index.js +0 -52
- package/dist/runtime-assets/node-pty/lib/interfaces.js +0 -7
- package/dist/runtime-assets/node-pty/lib/shared/conout.js +0 -11
- package/dist/runtime-assets/node-pty/lib/terminal.js +0 -190
- package/dist/runtime-assets/node-pty/lib/types.js +0 -7
- package/dist/runtime-assets/node-pty/lib/unixTerminal.js +0 -346
- package/dist/runtime-assets/node-pty/lib/utils.js +0 -39
- package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +0 -125
- package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +0 -320
- package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +0 -199
- package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +0 -22
- package/dist/runtime-assets/node-pty/package.json +0 -64
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/dist/runtime-build-info.d.ts +0 -9
- package/dist/runtime-build-info.js +0 -57
- package/dist/runtime-build-info.js.map +0 -1
- package/dist/runtime-bundle.d.ts +0 -19
- package/dist/runtime-bundle.js +0 -156
- package/dist/runtime-bundle.js.map +0 -1
- package/dist/runtime-cli.d.ts +0 -7
- package/dist/runtime-cli.js +0 -152
- package/dist/runtime-cli.js.map +0 -1
- package/dist/runtime-client-store.d.ts +0 -68
- package/dist/runtime-client-store.js +0 -344
- package/dist/runtime-client-store.js.map +0 -1
- package/dist/runtime-client.d.ts +0 -112
- package/dist/runtime-client.js +0 -736
- package/dist/runtime-client.js.map +0 -1
- package/dist/runtime-connection.d.ts +0 -22
- package/dist/runtime-connection.js +0 -93
- package/dist/runtime-connection.js.map +0 -1
- package/dist/runtime-coordinator.d.ts +0 -91
- package/dist/runtime-coordinator.js +0 -911
- package/dist/runtime-coordinator.js.map +0 -1
- package/dist/runtime-general-protocol.d.ts +0 -22
- package/dist/runtime-general-protocol.js +0 -57
- package/dist/runtime-general-protocol.js.map +0 -1
- package/dist/runtime-general-workspace.d.ts +0 -17
- package/dist/runtime-general-workspace.js +0 -213
- package/dist/runtime-general-workspace.js.map +0 -1
- package/dist/runtime-pane-assets.d.ts +0 -17
- package/dist/runtime-pane-assets.js +0 -23
- package/dist/runtime-pane-assets.js.map +0 -1
- package/dist/runtime-pane-boot.d.ts +0 -9
- package/dist/runtime-pane-boot.js +0 -47
- package/dist/runtime-pane-boot.js.map +0 -1
- package/dist/runtime-pane-codex-index.d.ts +0 -1
- package/dist/runtime-pane-codex-index.js +0 -47
- package/dist/runtime-pane-codex-index.js.map +0 -1
- package/dist/runtime-pane-config.d.ts +0 -16
- package/dist/runtime-pane-config.js +0 -54
- package/dist/runtime-pane-config.js.map +0 -1
- package/dist/runtime-pane-host.d.ts +0 -44
- package/dist/runtime-pane-host.js +0 -424
- package/dist/runtime-pane-host.js.map +0 -1
- package/dist/runtime-pane-job-owner.d.ts +0 -1
- package/dist/runtime-pane-job-owner.js +0 -29
- package/dist/runtime-pane-job-owner.js.map +0 -1
- package/dist/runtime-pane-job.ps1 +0 -116
- package/dist/runtime-pane-link.d.ts +0 -40
- package/dist/runtime-pane-link.js +0 -246
- package/dist/runtime-pane-link.js.map +0 -1
- package/dist/runtime-pane-mcp-protocol.d.ts +0 -32
- package/dist/runtime-pane-mcp-protocol.js +0 -39
- package/dist/runtime-pane-mcp-protocol.js.map +0 -1
- package/dist/runtime-pane-mcp-relay.d.ts +0 -17
- package/dist/runtime-pane-mcp-relay.js +0 -70
- package/dist/runtime-pane-mcp-relay.js.map +0 -1
- package/dist/runtime-pane-mcp.d.ts +0 -51
- package/dist/runtime-pane-mcp.js +0 -150
- package/dist/runtime-pane-mcp.js.map +0 -1
- package/dist/runtime-pane-native.d.ts +0 -45
- package/dist/runtime-pane-native.js +0 -65
- package/dist/runtime-pane-native.js.map +0 -1
- package/dist/runtime-pane-output.d.ts +0 -19
- package/dist/runtime-pane-output.js +0 -53
- package/dist/runtime-pane-output.js.map +0 -1
- package/dist/runtime-pane-port.d.ts +0 -139
- package/dist/runtime-pane-port.js +0 -558
- package/dist/runtime-pane-port.js.map +0 -1
- package/dist/runtime-pane-private.d.ts +0 -14
- package/dist/runtime-pane-private.js +0 -51
- package/dist/runtime-pane-private.js.map +0 -1
- package/dist/runtime-pane-private.ps1 +0 -14
- package/dist/runtime-pane-process.d.ts +0 -4
- package/dist/runtime-pane-process.js +0 -108
- package/dist/runtime-pane-process.js.map +0 -1
- package/dist/runtime-pane-protocol.d.ts +0 -172
- package/dist/runtime-pane-protocol.js +0 -195
- package/dist/runtime-pane-protocol.js.map +0 -1
- package/dist/runtime-pane-screen.d.ts +0 -19
- package/dist/runtime-pane-screen.js +0 -76
- package/dist/runtime-pane-screen.js.map +0 -1
- package/dist/runtime-pane-service.d.ts +0 -11
- package/dist/runtime-pane-service.js +0 -221
- package/dist/runtime-pane-service.js.map +0 -1
- package/dist/runtime-pane-stdio-protocol.d.ts +0 -54
- package/dist/runtime-pane-stdio-protocol.js +0 -94
- package/dist/runtime-pane-stdio-protocol.js.map +0 -1
- package/dist/runtime-pane-stdio.d.ts +0 -3
- package/dist/runtime-pane-stdio.js +0 -123
- package/dist/runtime-pane-stdio.js.map +0 -1
- package/dist/runtime-pane-store.d.ts +0 -33
- package/dist/runtime-pane-store.js +0 -164
- package/dist/runtime-pane-store.js.map +0 -1
- package/dist/runtime-pane-transcript-protocol.d.ts +0 -43
- package/dist/runtime-pane-transcript-protocol.js +0 -58
- package/dist/runtime-pane-transcript-protocol.js.map +0 -1
- package/dist/runtime-pane-transcript.d.ts +0 -45
- package/dist/runtime-pane-transcript.js +0 -605
- package/dist/runtime-pane-transcript.js.map +0 -1
- package/dist/runtime-pane-worker.d.ts +0 -1
- package/dist/runtime-pane-worker.js +0 -154
- package/dist/runtime-pane-worker.js.map +0 -1
- package/dist/runtime-protocol.d.ts +0 -162
- package/dist/runtime-protocol.js +0 -37
- package/dist/runtime-protocol.js.map +0 -1
- package/dist/runtime-switches.d.ts +0 -79
- package/dist/runtime-switches.js +0 -90
- package/dist/runtime-switches.js.map +0 -1
- package/dist/runtime-transcript-delta-reader.d.ts +0 -23
- package/dist/runtime-transcript-delta-reader.js +0 -74
- package/dist/runtime-transcript-delta-reader.js.map +0 -1
- package/dist/runtime-transcript-source.d.ts +0 -36
- package/dist/runtime-transcript-source.js +0 -189
- package/dist/runtime-transcript-source.js.map +0 -1
- package/dist/runtime-windows-bridge.d.ts +0 -41
- package/dist/runtime-windows-bridge.js +0 -157
- package/dist/runtime-windows-bridge.js.map +0 -1
- package/dist/settings-route-lifetime.d.ts +0 -14
- package/dist/settings-route-lifetime.js +0 -48
- package/dist/settings-route-lifetime.js.map +0 -1
- package/dist/tmux-pane-host.d.ts +0 -26
- package/dist/tmux-pane-host.js +0 -25
- package/dist/tmux-pane-host.js.map +0 -1
- package/dist/transcript-needles.d.ts +0 -35
- package/dist/transcript-needles.js +0 -158
- package/dist/transcript-needles.js.map +0 -1
- package/dist/transcript-records.d.ts +0 -21
- package/dist/transcript-records.js +0 -140
- package/dist/transcript-records.js.map +0 -1
- package/dist/ui/agent-proposals.js +0 -90
- package/dist/ui/agent-runtime-settings.js +0 -67
- package/dist/ui/agent-runtime.js +0 -124
- package/dist/ui/cli-picker.js +0 -42
- package/dist/ui/general-location.js +0 -71
- package/dist/ui/native-instance-settings.js +0 -44
- package/dist/ui/native-pane-recovery.js +0 -45
- package/dist/ui/panel-runtimes.js +0 -334
- package/dist/ui/panel-windows-agent.js +0 -86
- package/dist/ui/panel-windows-cli.js +0 -179
- package/dist/ui/provider-marks.js +0 -83
- package/dist/ui/runtime-catalog.js +0 -55
- package/dist/ui/settings-agent-filters.js +0 -94
- package/dist/ui/settings-layout.js +0 -245
- package/dist/ui/settings-outside.js +0 -176
- package/dist/ui/settings-shortcuts.js +0 -65
- package/dist/ui/shared/machine-name.js +0 -13
- package/dist/ui/shared/qrcode.module.js +0 -2237
- package/dist/ui/shared/settings-width.js +0 -31
- package/dist/ui/shared/vendor/qrcode-generator.LICENSE +0 -27
- package/dist/ui/windows-agents-nav.js +0 -2
- package/dist/ui/windows-cli-catalog.js +0 -108
- package/dist/web-remote-access.d.ts +0 -152
- package/dist/web-remote-access.js +0 -368
- package/dist/web-remote-access.js.map +0 -1
- package/dist/windows-agents.d.ts +0 -126
- package/dist/windows-agents.js +0 -490
- package/dist/windows-agents.js.map +0 -1
- package/dist/windows-cli-api.d.ts +0 -23
- package/dist/windows-cli-api.js +0 -159
- package/dist/windows-cli-api.js.map +0 -1
- package/dist/windows-cli-providers.d.ts +0 -115
- package/dist/windows-cli-providers.js +0 -429
- package/dist/windows-cli-providers.js.map +0 -1
- package/dist/windows-cli-worker.d.ts +0 -59
- package/dist/windows-cli-worker.js +0 -479
- package/dist/windows-cli-worker.js.map +0 -1
- package/dist/windows-cli.d.ts +0 -116
- package/dist/windows-cli.js +0 -407
- package/dist/windows-cli.js.map +0 -1
- package/dist/windows-native-mcp.d.ts +0 -19
- package/dist/windows-native-mcp.js +0 -143
- package/dist/windows-native-mcp.js.map +0 -1
- package/dist/windows-pane-mcp.d.ts +0 -26
- package/dist/windows-pane-mcp.js +0 -187
- package/dist/windows-pane-mcp.js.map +0 -1
- package/templates/runtime-assets/conpty-LICENSE +0 -21
- package/templates/runtime-assets/conpty-NOTICE.md +0 -503
- package/templates/runtime-assets/local-pane-stage.ps1 +0 -101
- package/templates/runtime-assets/runtime-pane-job.ps1 +0 -116
- package/templates/runtime-assets/runtime-pane-private.ps1 +0 -14
- package/templates/runtime-assets/xterm-LICENSE +0 -21
package/dist/backend/codex.js
CHANGED
|
@@ -9,14 +9,14 @@ import { codexPackageWarning } from "./codex-package.js";
|
|
|
9
9
|
import { probeCliVersion, resolveBinary, shellQuote, validateModel, validateProvider, warnIfModelMismatch } from "./types.js";
|
|
10
10
|
import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
|
|
11
11
|
import { getAgendHome } from "../paths.js";
|
|
12
|
-
import {
|
|
13
|
-
import { ResumeBlockedError } from "./types.js";
|
|
14
|
-
import { CODEX_MODELS_CACHE_MAX_BYTES, codexShortHomeFor, readCodexEffortLevels, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./codex-metadata.js";
|
|
12
|
+
import { CODEX_MODELS_CACHE_MAX_BYTES, codexShortHomeFor, readCodexEffortLevels, readCodexModelEffortLevels, readCodexModelServiceTiers, resolveCodexLaunchModel } from "./codex-metadata.js";
|
|
15
13
|
import { EFFORT_CAPABILITIES } from "./effort-metadata.js";
|
|
16
14
|
import { codexEffort } from "./launch-effort.js";
|
|
15
|
+
import { codexSpeed } from "./codex-speed.js";
|
|
17
16
|
import { appendWithMarker, removeMarker } from "./marker-utils.js";
|
|
18
17
|
import { checkResumeId, codexSiblingState, codexStateDbAbsent, codexThreadRowExists, cwdRolloutsProvenAbsent, findExactCwdCodexSession, planCodexResume, newestResumableRollout } from "./codex-session-lookup.js";
|
|
19
18
|
import { t } from "../locale.js";
|
|
19
|
+
import { snapshotCodexQuotaHistory, snapshotCodexSessionQuotaHistory } from "./codex-quota-history.js";
|
|
20
20
|
import { quotaLine, quotaLowLine, quotaPausedLine } from "../quota-notice.js";
|
|
21
21
|
import { parse as parseToml } from "smol-toml";
|
|
22
22
|
import { selectionCard } from "../selection-card.js";
|
|
@@ -306,6 +306,11 @@ function setProjectTrusted(content, root) {
|
|
|
306
306
|
return updated;
|
|
307
307
|
}
|
|
308
308
|
/** Only the bottom, live Codex 0.156 folder-access screen can own stdin. */
|
|
309
|
+
/** A folder-trust prompt's cancel option, and the footer that must come with it. */
|
|
310
|
+
const CODEX_TRUST_CANCEL_FOOTER = {
|
|
311
|
+
"Quit": /^\s*enter continue\s*·\s*esc quit\s*$/i,
|
|
312
|
+
"Back to Agent Command Center": /^\s*enter continue\s*·\s*esc back\s*$/i,
|
|
313
|
+
};
|
|
309
314
|
function codexTrustPromptState(pane) {
|
|
310
315
|
const noPrompt = { active: false, folder: null, root: null, rootNote: "absent", safeChoice: false, trustCursor: false };
|
|
311
316
|
const rows = pane.replace(/\r/g, "").split("\n");
|
|
@@ -348,21 +353,26 @@ function codexTrustPromptState(pane) {
|
|
|
348
353
|
});
|
|
349
354
|
// Unknown option orders, a moved cursor, a third option, or any different
|
|
350
355
|
// footer are held for a human. Never guess where Enter would land.
|
|
356
|
+
// The cancel option and the footer come as a pair: "Quit" / "esc quit" (onboarding, and every codex before 0.167), or
|
|
357
|
+
// "Back to Agent Command Center" / "esc back" (0.167's startup trust screen, where Esc returns to the command center).
|
|
358
|
+
// Exactly one cancel option, and the footer that belongs to it.
|
|
359
|
+
const cancels = choices.filter(choice => Object.prototype.hasOwnProperty.call(CODEX_TRUST_CANCEL_FOOTER, choice.text));
|
|
360
|
+
const footerFits = cancels.length === 1 && CODEX_TRUST_CANCEL_FOOTER[cancels[0].text].test(rows[last]);
|
|
351
361
|
const safeChoice = choices.length === 2
|
|
352
362
|
&& choices[0].row + 1 === choices[1].row
|
|
353
363
|
&& rows.slice(choices[1].row + 1, last).every(row => row.trim() === "")
|
|
354
364
|
&& choices[0].cursor === "›" && choices[0].number === "1" && choices[0].text === "Trust and continue"
|
|
355
|
-
&& choices[1].cursor === "" && choices[1].number === "2" &&
|
|
356
|
-
&&
|
|
357
|
-
const known = new Set(["Trust and continue", "Open restricted",
|
|
358
|
-
// Exactly one selection marker, whichever form (› ❯ >), and it is on Trust; the footer is the one
|
|
359
|
-
//
|
|
365
|
+
&& choices[1].cursor === "" && choices[1].number === "2" && cancels[0] === choices[1]
|
|
366
|
+
&& footerFits;
|
|
367
|
+
const known = new Set(["Trust and continue", "Open restricted", ...Object.keys(CODEX_TRUST_CANCEL_FOOTER)]);
|
|
368
|
+
// Exactly one selection marker, whichever form (› ❯ >), and it is on Trust; the footer is the one its cancel option
|
|
369
|
+
// pairs with. Two markers, another footer mapping or an unknown option is held — never a guess where Enter lands (#2896 review).
|
|
360
370
|
const atCursor = choices.filter(choice => choice.cursor !== "");
|
|
361
371
|
const trustCursor = choices.length >= 2 && choices.every(choice => known.has(choice.text))
|
|
362
372
|
&& choices.every((choice, index) => index === 0 || choice.row === choices[index - 1].row + 1)
|
|
363
373
|
&& rows.slice(choices[choices.length - 1].row + 1, last).every(row => row.trim() === "")
|
|
364
374
|
&& atCursor.length === 1 && atCursor[0].cursor === "›" && atCursor[0].text === "Trust and continue"
|
|
365
|
-
&&
|
|
375
|
+
&& footerFits;
|
|
366
376
|
return { active: true, folder, root, rootNote, safeChoice, trustCursor };
|
|
367
377
|
}
|
|
368
378
|
/** Unknown/older trust layouts are still input-blocking, never auto-answered. */
|
|
@@ -607,20 +617,6 @@ function codexWorkspaceLimitPromptVisible(pane) {
|
|
|
607
617
|
}
|
|
608
618
|
return false;
|
|
609
619
|
}
|
|
610
|
-
/**
|
|
611
|
-
* The composer's own `›` row above the footer, or -1. Walking up from the footer, only blank rows and indented rows (the
|
|
612
|
-
* composer's wrapped text, however many lines a long paste takes) may come before it. The first column-zero row decides:
|
|
613
|
-
* a `›` there is the composer; a numbered `›` row is a selection card; any other column-zero row (`•`/`■`/`⚠` output, a
|
|
614
|
-
* status line) means a `›` above it is transcript history, never the composer.
|
|
615
|
-
*/
|
|
616
|
-
function ownedComposerRow(rows) {
|
|
617
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
618
|
-
if (!rows[i].trim() || /^[ \t]/.test(rows[i]))
|
|
619
|
-
continue;
|
|
620
|
-
return /^[>›][ \t]+\S/.test(rows[i]) && !/^[>›][ \t]+\d+\./.test(rows[i]) ? i : -1;
|
|
621
|
-
}
|
|
622
|
-
return -1;
|
|
623
|
-
}
|
|
624
620
|
/** Unknown pickers own stdin too; never type or press Enter into one. */
|
|
625
621
|
function codexUnknownSelectionVisible(pane) {
|
|
626
622
|
const rows = pane.replace(/\r/g, "").split("\n");
|
|
@@ -1000,15 +996,8 @@ export const CODEX_RESUME_ATTEMPT_FILE = "codex-resume-attempt.json";
|
|
|
1000
996
|
export const CODEX_RESUME_FAILED_FILE = "codex-resume-failed.json";
|
|
1001
997
|
/** Above this, a resumed rollout carries a note that codex may compact it first. */
|
|
1002
998
|
export const CODEX_RESUME_LARGE_BYTES = 20 * 1024 * 1024;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
paneDetectorContext;
|
|
1006
|
-
constructor(paneDetectorContext) {
|
|
1007
|
-
this.paneDetectorContext = paneDetectorContext;
|
|
1008
|
-
}
|
|
1009
|
-
readConfiguredStatusLineItems() { return this.paneDetectorContext?.statusLineItems()?.slice() ?? null; }
|
|
1010
|
-
trustPaneFields(pane) { const state = codexTrustPromptState(pane); return { promptFolder: state.folder, promptRoot: state.root, rootNote: state.rootNote }; }
|
|
1011
|
-
trustHoldReason(pane) { return { ...this.trustPaneFields(pane), reason: "Folder permission needs confirmation on this execution machine" }; }
|
|
999
|
+
export class CodexBackend {
|
|
1000
|
+
instanceDir;
|
|
1012
1001
|
/**
|
|
1013
1002
|
* #1510: the reply-completion guard covers codex, its turn end read from the rollout (task_complete /
|
|
1014
1003
|
* turn_aborted, recorded on 0.162.0 in tests/fixtures/reply-guard-1510): the pane alone stays busy through a tool
|
|
@@ -1017,10 +1006,123 @@ export class CodexPaneDetectors {
|
|
|
1017
1006
|
replyCompletionGuard = true;
|
|
1018
1007
|
turnEndFromTranscript = true;
|
|
1019
1008
|
binaryName = "codex";
|
|
1009
|
+
binaryPath;
|
|
1010
|
+
sharedCodexHome;
|
|
1011
|
+
isolatedCodexHome;
|
|
1012
|
+
/** Which subscription this instance runs on, or null for the shared login. */
|
|
1013
|
+
credentialProfile = null;
|
|
1014
|
+
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
1015
|
+
authorizedTrust = null;
|
|
1020
1016
|
configuredStatusLineItems;
|
|
1021
1017
|
configuredStatusLinePattern;
|
|
1018
|
+
/** Set by buildCommand when the resume plan fell back; read once by the daemon. */
|
|
1019
|
+
launchWarning = null;
|
|
1022
1020
|
/** Whether the last buildCommand launched a resume (`resume <id>` or `--last`). */
|
|
1023
1021
|
lastLaunchResumes = true;
|
|
1022
|
+
quotaReplay = null;
|
|
1023
|
+
quotaReplayHistory() { return this.quotaReplay; }
|
|
1024
|
+
/** Set by writeConfig when the status_line could not be made verifiable (#931). */
|
|
1025
|
+
statusLineWarning = null;
|
|
1026
|
+
/** #2357: set by buildCommand when the codex being launched lacks a package companion (codex-code-mode-host). */
|
|
1027
|
+
packageWarning = null;
|
|
1028
|
+
constructor(instanceDir) {
|
|
1029
|
+
this.instanceDir = instanceDir;
|
|
1030
|
+
this.binaryPath = resolveBinary("codex");
|
|
1031
|
+
this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
1032
|
+
this.isolatedCodexHome = CodexBackend.resolveShortHome(instanceDir);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Compute and (on first call for an existing instance) migrate to a SHORT
|
|
1036
|
+
* persistent CODEX_HOME under `~/.agend/cx/<8-char-hash>/`.
|
|
1037
|
+
*
|
|
1038
|
+
* Background (#953): codex 0.157.0 added `app-server-control.sock` under
|
|
1039
|
+
* CODEX_HOME. For instances with long `-t<topic_id>` suffixes the full path
|
|
1040
|
+
* exceeds the Unix socket SUN_LEN (~107 chars). codex canonicalises the path
|
|
1041
|
+
* before binding the socket, so a symlink workaround does not help — the real
|
|
1042
|
+
* path must be short.
|
|
1043
|
+
*
|
|
1044
|
+
* Migration steps (each is idempotent; half-completed states self-heal):
|
|
1045
|
+
* 1. If shortHome does not exist AND legacyHome is a real directory (not a
|
|
1046
|
+
* symlink) → atomic renameSync. On EXDEV or any other error, fall back
|
|
1047
|
+
* safely: keep using the legacy long path (instance can still start, just
|
|
1048
|
+
* without the short-path fix).
|
|
1049
|
+
* 2. If shortHome exists but legacyHome is missing → recreate the backward-
|
|
1050
|
+
* compat symlink (self-heal for rename-succeeded-but-symlink-failed crash).
|
|
1051
|
+
* 3. If both shortHome and legacyHome (as symlink) exist → nothing to do.
|
|
1052
|
+
* 4. New instance (no legacyHome) → create shortHome directly.
|
|
1053
|
+
*
|
|
1054
|
+
* Fail-safe guarantee: migration failure must never make the instance worse.
|
|
1055
|
+
* The caller always receives a valid path it can use as CODEX_HOME.
|
|
1056
|
+
*/
|
|
1057
|
+
/** Exposed for fleet-manager to delete the short home on instance removal. */
|
|
1058
|
+
static shortHomeFor(instanceDir) {
|
|
1059
|
+
return codexShortHomeFor(instanceDir);
|
|
1060
|
+
}
|
|
1061
|
+
static resolveShortHome(instanceDir) {
|
|
1062
|
+
const shortHome = CodexBackend.shortHomeFor(instanceDir);
|
|
1063
|
+
const shortBase = join(getAgendHome(), "cx");
|
|
1064
|
+
const canonical = resolve(instanceDir);
|
|
1065
|
+
const legacyHome = resolve(instanceDir, "codex-home");
|
|
1066
|
+
// Only migrate/create when the instance directory itself exists.
|
|
1067
|
+
// Constructing CodexBackend for a non-existent dir (cli-env probes, test
|
|
1068
|
+
// backends that never ran) must not litter ~/.agend/cx/ with orphan dirs.
|
|
1069
|
+
if (!existsSync(canonical))
|
|
1070
|
+
return shortHome;
|
|
1071
|
+
const legacyExists = existsSync(legacyHome);
|
|
1072
|
+
const legacyIsRealDir = legacyExists && !lstatSync(legacyHome).isSymbolicLink();
|
|
1073
|
+
const shortExists = existsSync(shortHome);
|
|
1074
|
+
if (!shortExists) {
|
|
1075
|
+
mkdirSync(shortBase, { recursive: true, mode: 0o700 });
|
|
1076
|
+
if (legacyIsRealDir) {
|
|
1077
|
+
// Attempt atomic rename. Requires same device; fails with EXDEV otherwise.
|
|
1078
|
+
try {
|
|
1079
|
+
renameSync(legacyHome, shortHome);
|
|
1080
|
+
// Log the one-time migration for observability.
|
|
1081
|
+
try {
|
|
1082
|
+
const logPath = join(shortBase, `${shortHome.split("/").at(-1)}.migrated`);
|
|
1083
|
+
writeFileSync(logPath, `${new Date().toISOString()} migrated from ${legacyHome}\n`);
|
|
1084
|
+
}
|
|
1085
|
+
catch { /* log failure is non-fatal */ }
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
// EXDEV (cross-device) or any other rename failure → fail-safe:
|
|
1089
|
+
// fall back to the legacy long path so the instance still works.
|
|
1090
|
+
// The socket-length bug persists for this instance but no data is lost.
|
|
1091
|
+
const code = err.code ?? "unknown";
|
|
1092
|
+
try {
|
|
1093
|
+
const warnPath = join(shortBase, `${shortHome.split("/").at(-1)}.migration-failed`);
|
|
1094
|
+
writeFileSync(warnPath, `${new Date().toISOString()} rename failed (${code}): ${err.message}\n`
|
|
1095
|
+
+ `legacy path remains in use: ${legacyHome}\n`);
|
|
1096
|
+
}
|
|
1097
|
+
catch { /* warn log failure is also non-fatal */ }
|
|
1098
|
+
// Return legacy long path so the instance starts (socket may still fail
|
|
1099
|
+
// on 0.157.0 for long names, but no data is lost or corrupted).
|
|
1100
|
+
return legacyHome;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
else if (!legacyExists) {
|
|
1104
|
+
// New instance — no data to migrate.
|
|
1105
|
+
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
1106
|
+
}
|
|
1107
|
+
// If legacyExists but is already a symlink: shortHome was deleted externally
|
|
1108
|
+
// while the symlink still points to it. Re-create shortHome as a fresh dir.
|
|
1109
|
+
if (!existsSync(shortHome)) {
|
|
1110
|
+
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
// Backward-compat symlink: instanceDir/codex-home → shortHome.
|
|
1114
|
+
// Also serves as self-heal: if a previous run renamed successfully but
|
|
1115
|
+
// crashed before creating the symlink, we recreate it here.
|
|
1116
|
+
if (!legacyExists || legacyIsRealDir) {
|
|
1117
|
+
// legacyIsRealDir means rename just happened (now shortHome exists, legacyHome gone).
|
|
1118
|
+
// !legacyExists means new instance or symlink was deleted — create it.
|
|
1119
|
+
try {
|
|
1120
|
+
symlinkSync(shortHome, legacyHome);
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* race or already exists */ }
|
|
1123
|
+
}
|
|
1124
|
+
return shortHome;
|
|
1125
|
+
}
|
|
1024
1126
|
supportsQueuedInput() {
|
|
1025
1127
|
return true;
|
|
1026
1128
|
}
|
|
@@ -1045,10 +1147,25 @@ export class CodexPaneDetectors {
|
|
|
1045
1147
|
}
|
|
1046
1148
|
/** Observed on real Codex 0.156.0: the live input row precedes its footer. */
|
|
1047
1149
|
isDeliveryInputReadyPane(pane) {
|
|
1048
|
-
|
|
1049
|
-
if (
|
|
1150
|
+
// A composer with no app-server behind it accepts text and never submits it (#1099).
|
|
1151
|
+
if (codexAppServerDisconnected(pane))
|
|
1152
|
+
return false;
|
|
1153
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
1154
|
+
while (rows.length && !rows[rows.length - 1].trim())
|
|
1155
|
+
rows.pop();
|
|
1156
|
+
stripCodexPassiveHintRow(rows);
|
|
1157
|
+
const footer = rows.pop() ?? "";
|
|
1158
|
+
// Codex preserves other configured status-line items after the context
|
|
1159
|
+
// meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
|
|
1160
|
+
// are footer chrome, not evidence that the input row is unavailable.
|
|
1161
|
+
const contextFooter = this.isAnyContextFooter(footer);
|
|
1162
|
+
// A deliberately configured status_line may omit Context entirely. In
|
|
1163
|
+
// that case require a recognized value from the actual configured item
|
|
1164
|
+
// list after Codex's exact empty live-composer row. An arbitrary non-empty
|
|
1165
|
+
// footer or a recent transcript echo is not a readiness signal; that was
|
|
1166
|
+
// the false-ready hole in the old Layer 2 deny-list attempt (#931/#947).
|
|
1167
|
+
if (!contextFooter && !this.isConfiguredStatusLineFooter(footer))
|
|
1050
1168
|
return false;
|
|
1051
|
-
const { rows, contextFooter } = frame;
|
|
1052
1169
|
if (!contextFooter) {
|
|
1053
1170
|
if (this.getBusyPattern().test(pane))
|
|
1054
1171
|
return false;
|
|
@@ -1069,41 +1186,6 @@ export class CodexPaneDetectors {
|
|
|
1069
1186
|
}
|
|
1070
1187
|
return false;
|
|
1071
1188
|
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Complete ownership of a long pasted envelope: the `›` composer row and every wrapped row down to a context footer are
|
|
1074
|
-
* proven by ownedComposerRow, however many rows they take. The Enter gate accepts this as well as isDeliveryInputReadyPane,
|
|
1075
|
-
* never instead of the dialog, busy and expected-seq checks.
|
|
1076
|
-
*/
|
|
1077
|
-
ownsComposerRows(pane) {
|
|
1078
|
-
const frame = this.composerFrame(pane);
|
|
1079
|
-
return !!frame?.contextFooter && ownedComposerRow(frame.rows) >= 0;
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* The screen above Codex's footer, shared by both readers: null when the footer is not a Codex footer at all or the
|
|
1083
|
-
* app server behind the composer is gone (#1099). `rows` excludes the footer.
|
|
1084
|
-
*/
|
|
1085
|
-
composerFrame(pane) {
|
|
1086
|
-
// A composer with no app-server behind it accepts text and never submits it (#1099).
|
|
1087
|
-
if (codexAppServerDisconnected(pane))
|
|
1088
|
-
return null;
|
|
1089
|
-
const rows = pane.replace(/\r/g, "").split("\n");
|
|
1090
|
-
while (rows.length && !rows[rows.length - 1].trim())
|
|
1091
|
-
rows.pop();
|
|
1092
|
-
stripCodexPassiveHintRow(rows);
|
|
1093
|
-
const footer = rows.pop() ?? "";
|
|
1094
|
-
// Codex preserves other configured status-line items after the context
|
|
1095
|
-
// meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
|
|
1096
|
-
// are footer chrome, not evidence that the input row is unavailable.
|
|
1097
|
-
const contextFooter = this.isAnyContextFooter(footer);
|
|
1098
|
-
// A deliberately configured status_line may omit Context entirely. In
|
|
1099
|
-
// that case require a recognized value from the actual configured item
|
|
1100
|
-
// list after Codex's exact empty live-composer row. An arbitrary non-empty
|
|
1101
|
-
// footer or a recent transcript echo is not a readiness signal; that was
|
|
1102
|
-
// the false-ready hole in the old Layer 2 deny-list attempt (#931/#947).
|
|
1103
|
-
if (!contextFooter && !this.isConfiguredStatusLineFooter(footer))
|
|
1104
|
-
return null;
|
|
1105
|
-
return { rows, contextFooter };
|
|
1106
|
-
}
|
|
1107
1189
|
/**
|
|
1108
1190
|
* Live status chrome that must veto the broad prompt/context ready match.
|
|
1109
1191
|
*
|
|
@@ -1173,638 +1255,13 @@ export class CodexPaneDetectors {
|
|
|
1173
1255
|
return false;
|
|
1174
1256
|
return true;
|
|
1175
1257
|
}
|
|
1176
|
-
getReadyPattern() {
|
|
1177
|
-
// Header and context text persist in inline scrollback and even while the
|
|
1178
|
-
// CLI is loading or a modal owns stdin. Require the live prompt followed
|
|
1179
|
-
// by the Context footer at the *end* of the capture. A valid custom
|
|
1180
|
-
// status_line without Context gets a separate configured-item pattern;
|
|
1181
|
-
// unknown footer values still fail closed. getBusyPattern vetoes a working
|
|
1182
|
-
// turn whose empty composer remains visible.
|
|
1183
|
-
// U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
|
|
1184
|
-
// prompt, between prompt/footer, and below the footer. A drafted composer
|
|
1185
|
-
// is also idle once this same bottom footer proves it owns the screen.
|
|
1186
|
-
const contextReady = withCodexPassiveHintTail(/(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i);
|
|
1187
|
-
const footerPattern = this.configuredStatusLineFooterPattern();
|
|
1188
|
-
if (!footerPattern)
|
|
1189
|
-
return contextReady;
|
|
1190
|
-
// The no-Context alternative requires the exact empty composer followed by
|
|
1191
|
-
// a recognized configured Codex status_line value. If run-state is
|
|
1192
|
-
// configured, that value must specifically be Ready. This is positive
|
|
1193
|
-
// structural proof, never a deny-list of known dialogs.
|
|
1194
|
-
const footerSource = footerPattern.source.slice(1, -1);
|
|
1195
|
-
const noContextReady = withCodexPassiveHintTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*\r?\n(?:[ \t⋆]*\r?\n){0,3}${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"));
|
|
1196
|
-
// #978: a configured footer that carries a Context item in any position
|
|
1197
|
-
// is the Context footer: same drafted-composer rule as Context-first. The
|
|
1198
|
-
// whole row must match the configured item grammar, so an arbitrary
|
|
1199
|
-
// indented line that merely contains `Context N% left` cannot pass.
|
|
1200
|
-
const configuredContextReady = this.configuredHasContextItem()
|
|
1201
|
-
? withCodexPassiveHintTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}(?=[^\r\n]*${CODEX_CONTEXT_ITEM})${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"))
|
|
1202
|
-
: null;
|
|
1203
|
-
return new RegExp([contextReady.source, noContextReady.source, configuredContextReady?.source]
|
|
1204
|
-
.filter((source) => !!source)
|
|
1205
|
-
.map(source => `(?:${source})`)
|
|
1206
|
-
.join("|"), "iu");
|
|
1207
|
-
}
|
|
1208
|
-
configuredStatusLineFooterPattern() {
|
|
1209
|
-
if (this.configuredStatusLinePattern !== undefined)
|
|
1210
|
-
return this.configuredStatusLinePattern;
|
|
1211
|
-
const items = this.readConfiguredStatusLineItems();
|
|
1212
|
-
if (!items?.length)
|
|
1213
|
-
return this.configuredStatusLinePattern = null;
|
|
1214
|
-
const configuredPatterns = items
|
|
1215
|
-
.map(item => CODEX_STATUS_LINE_VALUE_PATTERNS[item])
|
|
1216
|
-
.filter((pattern) => pattern !== undefined);
|
|
1217
|
-
if (!configuredPatterns.length)
|
|
1218
|
-
return this.configuredStatusLinePattern = null;
|
|
1219
|
-
const segment = `(?:${configuredPatterns.join("|")})`;
|
|
1220
|
-
const separator = String.raw `[ \t]+·[ \t]+`;
|
|
1221
|
-
const hasRunState = items.some(item => CODEX_RUN_STATE_STATUS_ITEMS.has(item));
|
|
1222
|
-
// Ready must occupy its own status-line segment. Other configured items
|
|
1223
|
-
// may precede/follow it, as Codex joins visible values with ` · `.
|
|
1224
|
-
const line = hasRunState
|
|
1225
|
-
? `(?:${segment}${separator})*Ready(?:${separator}${segment})*`
|
|
1226
|
-
: `${segment}(?:${separator}${segment})*`;
|
|
1227
|
-
// The configured items, then (optionally) the CLI's own native fields — the same rule as the Context-first footer, so a
|
|
1228
|
-
// `model · Context 46% left Goal achieved (1h 6m)` row is the footer too, and the readiness regex built from this
|
|
1229
|
-
// source (footerSource) says the same.
|
|
1230
|
-
this.configuredStatusLinePattern = new RegExp(`^[ \t⋆]*${line}(?:${CODEX_NATIVE_FOOTER_FIELDS})?[ \t⋆]*$`, "iu");
|
|
1231
|
-
return this.configuredStatusLinePattern;
|
|
1232
|
-
}
|
|
1233
|
-
isConfiguredStatusLineFooter(row) {
|
|
1234
|
-
const pattern = this.configuredStatusLineFooterPattern();
|
|
1235
|
-
if (!pattern)
|
|
1236
|
-
return false;
|
|
1237
|
-
pattern.lastIndex = 0;
|
|
1238
|
-
return pattern.test(row);
|
|
1239
|
-
}
|
|
1240
|
-
configuredHasContextItem() {
|
|
1241
|
-
return (this.readConfiguredStatusLineItems() ?? []).some(item => CODEX_CONTEXT_STATUS_ITEMS.has(item));
|
|
1242
|
-
}
|
|
1243
1258
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* arbitrary indented transcript line that happens to contain the item does
|
|
1248
|
-
* not match that grammar. A stale config fails closed, and the daemon's
|
|
1249
|
-
* stable-composer escape hatch then keeps the instance from latching.
|
|
1259
|
+
* The row codex paints for input it has taken into its own queue instead of
|
|
1260
|
+
* submitting: `↳ <message>` under "Messages to be submitted after next tool
|
|
1261
|
+
* call (press esc to interrupt and send immediately)".
|
|
1250
1262
|
*/
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
isConfiguredContextFooter(row) {
|
|
1254
|
-
return this.configuredHasContextItem()
|
|
1255
|
-
&& new RegExp(CODEX_CONTEXT_ITEM, "iu").test(row)
|
|
1256
|
-
&& this.isConfiguredStatusLineFooter(row);
|
|
1257
|
-
}
|
|
1258
|
-
/** Any footer that proves the Context meter owns the bottom row. */
|
|
1259
|
-
isAnyContextFooter(row) {
|
|
1260
|
-
return isCodexContextFooter(row) || this.isConfiguredContextFooter(row);
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* #978 escape hatch: the live, EMPTY Codex composer within the last few
|
|
1264
|
-
* non-blank rows, no busy marker, and none of the known selection screens.
|
|
1265
|
-
* Deliberately says nothing about the footer, which is what is unknown.
|
|
1266
|
-
*/
|
|
1267
|
-
isStableUnknownLayoutIdlePane(pane) {
|
|
1268
|
-
if (this.getBusyPattern().test(pane))
|
|
1269
|
-
return false;
|
|
1270
|
-
// The composer is live-looking and the footer is unknown — exactly this
|
|
1271
|
-
// proof's blind spot — but there is no server behind it (#1099).
|
|
1272
|
-
if (codexAppServerDisconnected(pane))
|
|
1273
|
-
return false;
|
|
1274
|
-
// (No second, wider "esc to interrupt" match here: getBusyPattern() above already reads a live status row of ANY
|
|
1275
|
-
// title — #964 — and anchors it to the whole row at column zero, so a reply that quotes the phrase, indented or
|
|
1276
|
-
// in prose, cannot pin the escape hatch shut (#991).)
|
|
1277
|
-
// A retained queued message (`↳ …` under "Messages to be submitted after
|
|
1278
|
-
// next tool call") means work is pending, whatever the composer shows.
|
|
1279
|
-
if (/↳|Messages to be submitted/i.test(pane))
|
|
1280
|
-
return false;
|
|
1281
|
-
if (codexUsageLimitMenuVisible(pane) || codexRateSwitchVisible(pane)
|
|
1282
|
-
|| codexUnknownSelectionVisible(pane) || codexUpdatePickerVisible(pane))
|
|
1283
|
-
return false;
|
|
1284
|
-
const rows = pane.replace(/\r/g, "").split("\n").filter(row => row.trim() !== "");
|
|
1285
|
-
return rows.slice(-6).some(row => /^[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*$/.test(row));
|
|
1286
|
-
}
|
|
1287
|
-
isCodexIdleFooter(row) {
|
|
1288
|
-
return isCodexContextFooter(row) || this.isConfiguredStatusLineFooter(row);
|
|
1289
|
-
}
|
|
1290
|
-
/** A proxy reply filters chrome per line; whole-pane readiness is separate. */
|
|
1291
|
-
isProxyReplyChromeLine(line) {
|
|
1292
|
-
return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
|
|
1293
|
-
|| this.isAnyContextFooter(line);
|
|
1294
|
-
}
|
|
1295
|
-
getErrorPatterns() {
|
|
1296
|
-
return [
|
|
1297
|
-
// Specific quota codes must precede the generic HTTP 429 classifier:
|
|
1298
|
-
// OpenAI returns insufficient_quota with status 429, but switching models
|
|
1299
|
-
// cannot repair an exhausted account and would only start a failover loop.
|
|
1300
|
-
{
|
|
1301
|
-
pattern: new RegExp(String.raw `^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?\b(?:insufficient_quota|billing_hard_limit_reached|exceeded\s+your\s+current\s+quota)\b|^(?:${CODEX_STATUS_GLYPH}) Quota exceeded\. Check your plan and billing details\.\r?$`, "im"),
|
|
1302
|
-
type: "quota",
|
|
1303
|
-
action: "pause",
|
|
1304
|
-
message: "OpenAI quota exceeded",
|
|
1305
|
-
notice: () => quotaPausedLine("OpenAI", "api_quota"),
|
|
1306
|
-
},
|
|
1307
|
-
{
|
|
1308
|
-
// Codex already exhausted HTTP retries and returned to its composer.
|
|
1309
|
-
// The existing conditional continuation preserves context without
|
|
1310
|
-
// replaying earlier tool effects. Quota remains a preceding pause.
|
|
1311
|
-
pattern: new RegExp(`^${CODEX_EXHAUSTED_429_ROW}$`, "im"),
|
|
1312
|
-
type: "rate_limit",
|
|
1313
|
-
action: "nudge_continue",
|
|
1314
|
-
message: t("inst.codex_rate_retry_stopped"),
|
|
1315
|
-
skipCooldown: true,
|
|
1316
|
-
skipRecoveryWait: true,
|
|
1317
|
-
},
|
|
1318
|
-
{
|
|
1319
|
-
// Pane history contains the agent's own prose, source code and search
|
|
1320
|
-
// results. Bare `rate limit` used to fail over an otherwise healthy
|
|
1321
|
-
// Codex merely for discussing this regex. Match machine-readable API
|
|
1322
|
-
// forms or an error-decorated terminal line instead.
|
|
1323
|
-
pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+429\b|^(?![\s\S]{0,240}?${CODEX_EXHAUSTED_429_ROW})[ \t]*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*429\b|\b(?:rate_limit_exceeded|too_many_requests)\b|\brate limit(?:ed| exceeded| reached)?\b|\btoo many requests\b)`, "im"),
|
|
1324
|
-
type: "rate_limit",
|
|
1325
|
-
action: "failover",
|
|
1326
|
-
message: "OpenAI rate limit reached",
|
|
1327
|
-
},
|
|
1328
|
-
{
|
|
1329
|
-
// Same false-positive boundary as rate limits: `authentication` is a
|
|
1330
|
-
// normal English/code word. Require a structured 401/code or a
|
|
1331
|
-
// decorated CLI error line before pausing every instance on the shared
|
|
1332
|
-
// credential.
|
|
1333
|
-
pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+401\b|^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*401\b|\b(?:invalid_api_key|authentication_error)\b|\b401\s+Unauthorized\b|\bauthentication (?:failed|error)\b|\binvalid api key\b)`, "im"),
|
|
1334
|
-
type: "auth_error",
|
|
1335
|
-
action: "pause",
|
|
1336
|
-
message: "OpenAI authentication error",
|
|
1337
|
-
},
|
|
1338
|
-
// Codex writes the apostrophe as U+2019 ("You’ve hit your usage limit.") since
|
|
1339
|
-
// 0.156 and as ASCII before (0.153.4): both must match, or no usage limit
|
|
1340
|
-
// ever pauses the instance (#1098).
|
|
1341
|
-
{ pattern: /you['’]ve hit your usage limit/i, type: "quota", action: "pause", message: "Codex usage limit reached — upgrade plan required",
|
|
1342
|
-
notice: () => quotaPausedLine("Codex", "usage") },
|
|
1343
|
-
{
|
|
1344
|
-
// The terminal stage of a lost app-server (#1099): Codex has given up
|
|
1345
|
-
// reconnecting and says so, with a counter that never stops. Nothing
|
|
1346
|
-
// recovers from here without a relaunch, and every message sent in the
|
|
1347
|
-
// meantime waits for an idle that cannot come. Only the LIVE row counts:
|
|
1348
|
-
// directly above the LAST composer with no later transcript item or composer
|
|
1349
|
-
// (the same test as codexAppServerDisconnected, as a lookahead), so text that
|
|
1350
|
-
// merely quotes it does not raise this.
|
|
1351
|
-
pattern: new RegExp(String.raw `^[ \t]*•[ \t]+Reconnect failed[ \t]+—[ \t]+check the endpoint, then relaunch[ \t]+\(\d+[hms](?:[ \t]+\d+[hms])*\)[ \t]*$`
|
|
1352
|
-
+ String.raw `(?=\n(?:[ \t]*\n)*[›>][^\n]*(?:\n(?![•■⚠›>])[^\n]*)*(?![\s\S]))`, "mu"),
|
|
1353
|
-
type: "network",
|
|
1354
|
-
action: "notify",
|
|
1355
|
-
message: "Codex lost its app-server connection and could not reconnect — restart the instance (messages cannot be delivered until then)",
|
|
1356
|
-
},
|
|
1357
|
-
{
|
|
1358
|
-
// Codex reports an unknown model either as a TUI metadata fallback or
|
|
1359
|
-
// as a ChatGPT-account API rejection. Use whitespace-aware phrases so
|
|
1360
|
-
// capture-pane hard wraps do not hide either form.
|
|
1361
|
-
// Codex quotes the slug with BACKTICKS ("Model metadata for `x` not found.",
|
|
1362
|
-
// verified live on 0.160.0 and in the 0.153.4–0.160.0 binaries). The old
|
|
1363
|
-
// pattern only took ASCII quotes, so it could never match a real line.
|
|
1364
|
-
pattern: /model\s+metadata\s+for\s+['"`‘’“”][^'"`‘’“”]+['"`‘’“”]\s+not\s+found\.\s+defaulting\s+to\s+fallback\s+metadata|model\s+is\s+not\s+supported\s+when\s+using\s+codex\s+with\s+a\s+chatgpt\s+account/i,
|
|
1365
|
-
type: "model_error",
|
|
1366
|
-
action: "notify",
|
|
1367
|
-
message: "Codex model unavailable — use /model to switch",
|
|
1368
|
-
},
|
|
1369
|
-
{
|
|
1370
|
-
// A capacity rejection is a completed failed turn: Codex returns to its
|
|
1371
|
-
// prompt without an answer, with its context intact. Keep this anchored to
|
|
1372
|
-
// the exact decorated TUI line so ordinary prose about model capacity
|
|
1373
|
-
// cannot trigger anything on an otherwise healthy instance. The status
|
|
1374
|
-
// glyph is decoration that changes between Codex versions and error classes
|
|
1375
|
-
// (`■` U+25A0 on 0.159.2 and 0.160.0, `⚠` U+26A0 before), so any one
|
|
1376
|
-
// "Other Symbol" glyph (optionally with its emoji selector) opens the line —
|
|
1377
|
-
// never a bullet, dash, quote marker or number, which prose and the model's
|
|
1378
|
-
// own transcript items use (`•`, `-`, `>`). The sentence must fill the line,
|
|
1379
|
-
// from column 0. Box drawing and block elements (U+2500–U+259F) are
|
|
1380
|
-
// excluded up front: they are `So` too, but a left-border popup or
|
|
1381
|
-
// table row (`│ …`) is chrome, not a status line. The CLI is
|
|
1382
|
-
// already back at the prompt, so skipRecoveryWait avoids an extra wait.
|
|
1383
|
-
// action "nudge_continue": tell the user, then about a minute later tell
|
|
1384
|
-
// the agent to keep going (the lifecycle bounds this to 3 per 30 minutes,
|
|
1385
|
-
// then pauses; see #905). No restart — nothing would re-send the turn — and
|
|
1386
|
-
// no model switch. skipCooldown: each NEW capacity line is a new episode,
|
|
1387
|
-
// including a second one right after the nudge.
|
|
1388
|
-
pattern: /^(?![─-▟])\p{So}\uFE0F? Selected model is at capacity\. Please try a different model\.\r?$/mu,
|
|
1389
|
-
type: "model_error",
|
|
1390
|
-
action: "nudge_continue",
|
|
1391
|
-
message: t("inst.codex_model_capacity"),
|
|
1392
|
-
skipCooldown: true,
|
|
1393
|
-
skipRecoveryWait: true,
|
|
1394
|
-
},
|
|
1395
|
-
// Workspace (business) accounts report exhaustion differently from personal
|
|
1396
|
-
// ones, with codex's own rows (codex-rs/protocol error.rs and tui
|
|
1397
|
-
// turn_runtime.rs, unchanged 0.160.0–0.165.0):
|
|
1398
|
-
// member: "■ Your workspace is out of credits. Ask your workspace owner to refill in order to continue."
|
|
1399
|
-
// "■ You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue."
|
|
1400
|
-
// owner: "■ You're out of credits. Your workspace is out of credits. Add credits to continue using Codex."
|
|
1401
|
-
// "■ Your workspace is out of credits. Add credits to continue."
|
|
1402
|
-
// "■ You hit your spend cap set in your workspace. Increase your spend cap to continue."
|
|
1403
|
-
// "■ Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex."
|
|
1404
|
-
// Neither `insufficient_quota|billing` nor `you've hit your usage limit`
|
|
1405
|
-
// matches them.
|
|
1406
|
-
//
|
|
1407
|
-
// pause, not notify: nothing works until someone refills or raises the cap.
|
|
1408
|
-
// Anchored to the status glyph in COLUMN 0, because these skip the
|
|
1409
|
-
// usage-window check (a workspace limit is not one of those windows, so the
|
|
1410
|
-
// check would call it "available"): codex draws its own error cells there,
|
|
1411
|
-
// while an agent's message, quote or code block is indented under its `•`
|
|
1412
|
-
// (captured live, tests/fixtures/codex-0165-workspace-quoted.pane.txt).
|
|
1413
|
-
// `\s+` at each word gap: capture-pane runs without -J, so tmux's hard wrap
|
|
1414
|
-
// can land a newline anywhere in the sentence. A resumed transcript reprints
|
|
1415
|
-
// these rows, and the first live composer of a spawn takes them as history.
|
|
1416
|
-
{
|
|
1417
|
-
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+(?:You['’]re\s+out\s+of\s+credits\.\s+)?Your\s+workspace\s+is\s+out\s+of\s+credits\.`, "m"),
|
|
1418
|
-
type: "quota",
|
|
1419
|
-
action: "pause",
|
|
1420
|
-
message: "Codex workspace credits exhausted — workspace owner must refill",
|
|
1421
|
-
notice: () => quotaPausedLine("Codex", "workspace_credits"),
|
|
1422
|
-
skipUsageVerify: true,
|
|
1423
|
-
historyUntilLiveComposer: true,
|
|
1424
|
-
},
|
|
1425
|
-
{
|
|
1426
|
-
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+You\s+hit\s+your\s+spend\s+cap\s+set\s+(?:by\s+the\s+owner\s+of|in)\s+your\s+workspace\.`, "m"),
|
|
1427
|
-
type: "quota",
|
|
1428
|
-
action: "pause",
|
|
1429
|
-
message: "Codex workspace spend cap reached — workspace owner must raise it",
|
|
1430
|
-
notice: () => t("inst.quota_spend_cap"),
|
|
1431
|
-
skipUsageVerify: true,
|
|
1432
|
-
historyUntilLiveComposer: true,
|
|
1433
|
-
},
|
|
1434
|
-
{
|
|
1435
|
-
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+Usage\s+limit\s+reached\.\s+You['’]ve\s+reached\s+your\s+usage\s+limit\.\s+Increase\s+your\s+limits`, "m"),
|
|
1436
|
-
type: "quota",
|
|
1437
|
-
action: "pause",
|
|
1438
|
-
message: "Codex workspace usage limit reached — increase the workspace limits to continue",
|
|
1439
|
-
notice: () => quotaPausedLine("Codex", "workspace_usage"),
|
|
1440
|
-
skipUsageVerify: true,
|
|
1441
|
-
historyUntilLiveComposer: true,
|
|
1442
|
-
},
|
|
1443
|
-
// Codex warns at 10% and 5% remaining, and scopes the limit by period —
|
|
1444
|
-
// "weekly" alone missed every `monthly limit` warning.
|
|
1445
|
-
//
|
|
1446
|
-
// `\s+` at EVERY word gap (not literal spaces) because capture-pane runs without -J,
|
|
1447
|
-
// so tmux's hard wrap can land a newline (plus continuation-line padding)
|
|
1448
|
-
// at any of these word gaps. The trailing "Run /status for a breakdown."
|
|
1449
|
-
// is deliberately NOT part of the pattern — that tail is what actually
|
|
1450
|
-
// wrapped in the reported case.
|
|
1451
|
-
//
|
|
1452
|
-
// `of your <period> limit` is load-bearing: it's what keeps the pattern
|
|
1453
|
-
// off the agent's own prose about percentages.
|
|
1454
|
-
{
|
|
1455
|
-
pattern: /less\s+than\s+(\d+)\s*%\s+of\s+your\s+(hourly|daily|weekly|monthly)\s+limit/i,
|
|
1456
|
-
type: "quota",
|
|
1457
|
-
action: "notify",
|
|
1458
|
-
message: "Codex usage limit running low",
|
|
1459
|
-
formatMessage: (m) => `Codex ${m[2].toLowerCase()} limit: less than ${m[1]}% left`,
|
|
1460
|
-
notice: (m) => (m ? quotaLowLine(m[2], m[1]) : quotaLine("Codex", "usage")),
|
|
1461
|
-
},
|
|
1462
|
-
];
|
|
1463
|
-
}
|
|
1464
|
-
getStartupDialogs() {
|
|
1465
|
-
const trustHold = this.trustHoldDialog();
|
|
1466
|
-
return [
|
|
1467
|
-
this.autoTrustDialog(),
|
|
1468
|
-
trustHold,
|
|
1469
|
-
this.updatePickerDialog(),
|
|
1470
|
-
this.rateSwitchPickerDialog(),
|
|
1471
|
-
...this.sessionHoldDialogs(),
|
|
1472
|
-
this.unknownSelectionHoldDialog(),
|
|
1473
|
-
];
|
|
1474
|
-
}
|
|
1475
|
-
/**
|
|
1476
|
-
* #984: session-selection screens only a human may answer. Held (never
|
|
1477
|
-
* keyed) so the delivery gate keeps messages queued and dialog_parked tells
|
|
1478
|
-
* the operator; listed before the generic selection hold for a clear notice.
|
|
1479
|
-
*/
|
|
1480
|
-
sessionHoldDialogs() {
|
|
1481
|
-
return [
|
|
1482
|
-
{
|
|
1483
|
-
pattern: /This conversation is open in another app/,
|
|
1484
|
-
keys: [],
|
|
1485
|
-
description: "Codex session is open in another process (thread lock) — close the other one and press r, or fork manually",
|
|
1486
|
-
holdOnly: true,
|
|
1487
|
-
blocksDelivery: true,
|
|
1488
|
-
inputBlocked: true,
|
|
1489
|
-
remoteCard: codexSelectionCard,
|
|
1490
|
-
isActive: codexSessionLockVisible,
|
|
1491
|
-
},
|
|
1492
|
-
{
|
|
1493
|
-
pattern: /Working directory · resume/,
|
|
1494
|
-
keys: [],
|
|
1495
|
-
description: "Codex resume selected a session from another directory (sibling git worktree) — needs a human choice",
|
|
1496
|
-
holdOnly: true,
|
|
1497
|
-
blocksDelivery: true,
|
|
1498
|
-
inputBlocked: true,
|
|
1499
|
-
remoteCard: codexSelectionCard,
|
|
1500
|
-
isActive: codexResumeCwdPickerVisible,
|
|
1501
|
-
},
|
|
1502
|
-
];
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* The user's decision (hmsg-53600a2c): codex's folder-trust prompt in an AgEnD session is always answered Trust — the
|
|
1506
|
-
* workspace is the agent's own, whatever preTrust covered (preTrust stays, so the prompt rarely shows at all). Enter,
|
|
1507
|
-
* only with the cursor on "Trust and continue" among the known options (trustCursor); any other layout or row is
|
|
1508
|
-
* held (trustHoldDialog). It trusts the folder only: per-action approvals are separate dialogs and are not touched.
|
|
1509
|
-
*/
|
|
1510
|
-
autoTrustDialog() {
|
|
1511
|
-
return {
|
|
1512
|
-
pattern: /^\s*Trust this folder\?/m,
|
|
1513
|
-
keys: ["Enter"],
|
|
1514
|
-
description: "Codex folder trust — trusting the agent's own folder",
|
|
1515
|
-
blocksDelivery: true,
|
|
1516
|
-
inputBlocked: true,
|
|
1517
|
-
autoResolutionKey: "codex-folder-auto-trust",
|
|
1518
|
-
// One Enter per prompt (claimed, then checked): a prompt still up after it is held, never pressed again (#2896 review).
|
|
1519
|
-
verifyAfterKeys: true,
|
|
1520
|
-
isActive: pane => codexTrustPromptState(pane).trustCursor,
|
|
1521
|
-
answerDetail: pane => ({ autoTrusted: true, ...this.trustPaneFields(pane) }),
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
trustHoldDialog() {
|
|
1525
|
-
return {
|
|
1526
|
-
pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
|
|
1527
|
-
keys: [],
|
|
1528
|
-
description: "Codex folder trust needs human confirmation",
|
|
1529
|
-
holdOnly: true,
|
|
1530
|
-
blocksDelivery: true,
|
|
1531
|
-
inputBlocked: true,
|
|
1532
|
-
isActive: codexTrustVariantActive,
|
|
1533
|
-
remoteCard: codexSelectionCard,
|
|
1534
|
-
heldReason: pane => this.trustHoldReason(pane),
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* #1008: the rate-limit model-switch picker parked unattended instances (it
|
|
1539
|
-
* waits forever and its default option changes the model). Prevention is
|
|
1540
|
-
* `notice.hide_rate_limit_model_nudge` (hideRateLimitModelNudge — real and
|
|
1541
|
-
* effective on 0.157.1 and 0.159.2, verified live); this is the answer for a
|
|
1542
|
-
* picker that appears anyway (config not applied, a CLI that moved the key).
|
|
1543
|
-
*
|
|
1544
|
-
* Escape, not a navigated Enter: it selects nothing, so it can never take
|
|
1545
|
-
* option 1 (switch model) or option 3 (writes "never show again" into the
|
|
1546
|
-
* config). Deliberately not one-shot: if the picker swallowed the key it is
|
|
1547
|
-
* simply sent again on the next poll, and a repeated Escape is harmless.
|
|
1548
|
-
*/
|
|
1549
|
-
rateSwitchPickerDialog() {
|
|
1550
|
-
return {
|
|
1551
|
-
// Documentation only: Daemon.dialogMatches uses isActive INSTEAD of this
|
|
1552
|
-
// pattern, so it filters nothing here — the structural check below is the
|
|
1553
|
-
// whole decision, and ordinary prose about the picker fails it. The words
|
|
1554
|
-
// are the picker's own, kept so a reader can find the screen from the log.
|
|
1555
|
-
pattern: /Keep current model \(never show again\)/,
|
|
1556
|
-
keys: ["Escape"],
|
|
1557
|
-
description: "Codex rate-limit model-switch picker — Escape keeps the current model",
|
|
1558
|
-
blocksDelivery: true,
|
|
1559
|
-
inputBlocked: true,
|
|
1560
|
-
isActive: codexRateSwitchPickerVisible,
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
updatePickerDialog() {
|
|
1564
|
-
return {
|
|
1565
|
-
// Defense in depth for config written by older AgEnD versions or a Codex
|
|
1566
|
-
// regression. Match the release URL as well as the banner so ordinary
|
|
1567
|
-
// agent prose cannot trigger a keypress.
|
|
1568
|
-
pattern: /Update available![\s\S]{0,200}Release notes: https:\/\/github\.com\/openai\/codex\/releases/m,
|
|
1569
|
-
keys: ["Escape"],
|
|
1570
|
-
description: "Codex startup update-available picker",
|
|
1571
|
-
// A delivery must never land on this screen. Its selected option is
|
|
1572
|
-
// `› 1. Update now (runs … curl … | sh)` — the same `›` the input row
|
|
1573
|
-
// uses — so an Enter here runs an installer instead of sending a message.
|
|
1574
|
-
// The runtime dismisser presses Escape, but it only polls every few
|
|
1575
|
-
// seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
|
|
1576
|
-
// which parks on this picker for as long as nobody answers it).
|
|
1577
|
-
blocksDelivery: true,
|
|
1578
|
-
// Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
|
|
1579
|
-
// Check the complete current picker here; a different Enter-only menu
|
|
1580
|
-
// must never receive this automatic Escape key.
|
|
1581
|
-
isActive: codexUpdatePickerVisible,
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
usageLimitLunaReserveDialog() {
|
|
1585
|
-
// The real codex usage-limit dialog states codex has ALREADY switched to
|
|
1586
|
-
// Luna Reserve and labels Escape as "continue working". Pressing Escape via
|
|
1587
|
-
// sendSpecialKey is the ONLY safe key:
|
|
1588
|
-
//
|
|
1589
|
-
// • Escape is sent via sendSpecialKey — never via pasteText (which adds
|
|
1590
|
-
// an implicit bracketed-paste Enter that could confirm the highlighted
|
|
1591
|
-
// server action: Upgrade, Reset usage, Add Credits, …).
|
|
1592
|
-
// • A digit selects its row at once, and pasted its implicit Enter would
|
|
1593
|
-
// confirm whatever it landed on. This path is explicitly NOT used.
|
|
1594
|
-
//
|
|
1595
|
-
// verifyAfterKeys: true — confirm the menu is gone after Escape.
|
|
1596
|
-
// No confirmBeforeEnter / keysAfterConfirm: Escape needs no confirm gate.
|
|
1597
|
-
return {
|
|
1598
|
-
pattern: /Press enter to confirm or esc to continue working/i,
|
|
1599
|
-
keys: ["Escape"],
|
|
1600
|
-
description: "Codex usage limit — pressing Escape to continue with Luna Reserve",
|
|
1601
|
-
blocksDelivery: true,
|
|
1602
|
-
inputBlocked: true,
|
|
1603
|
-
isActive: codexUsageLimitMenuVisible,
|
|
1604
|
-
verifyAfterKeys: true,
|
|
1605
|
-
autoResolutionKey: "codex-usage-limit-luna-reserve",
|
|
1606
|
-
postDismissNotice: {
|
|
1607
|
-
text: t("inst.codex_usage_limit_luna_reserve", ""), // instance name injected by daemon
|
|
1608
|
-
label: "codex-usage-limit-selected",
|
|
1609
|
-
},
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
workspaceLimitPromptDialog() {
|
|
1613
|
-
// Escape via sendSpecialKey, the same as the Luna Reserve menu: never a paste
|
|
1614
|
-
// (its implicit Enter would confirm the cursor's row) and never "y". The error
|
|
1615
|
-
// row codex printed above the prompt stays, and the quota pause takes it from
|
|
1616
|
-
// there: one notice, no parked dialog.
|
|
1617
|
-
return {
|
|
1618
|
-
pattern: /^\s*enter select\s*[·•]\s*esc back\s*$/im,
|
|
1619
|
-
keys: ["Escape"],
|
|
1620
|
-
description: "Codex workspace limit prompt — pressing Escape (never asking the owner)",
|
|
1621
|
-
blocksDelivery: true,
|
|
1622
|
-
inputBlocked: true,
|
|
1623
|
-
isActive: codexWorkspaceLimitPromptVisible,
|
|
1624
|
-
verifyAfterKeys: true,
|
|
1625
|
-
autoResolutionKey: "codex-workspace-limit-prompt",
|
|
1626
|
-
answeredByAgend: true,
|
|
1627
|
-
marksLiveIncident: true,
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
unknownSelectionHoldDialog() {
|
|
1631
|
-
return {
|
|
1632
|
-
pattern: /^\s*[›❯>]\s+\S/m,
|
|
1633
|
-
keys: [],
|
|
1634
|
-
description: "Codex interactive selection needs human input",
|
|
1635
|
-
holdOnly: true,
|
|
1636
|
-
blocksDelivery: true,
|
|
1637
|
-
inputBlocked: true,
|
|
1638
|
-
remoteCard: codexSelectionCard,
|
|
1639
|
-
isActive: codexUnknownSelectionVisible,
|
|
1640
|
-
};
|
|
1641
|
-
}
|
|
1642
|
-
getRuntimeDialogs() {
|
|
1643
|
-
return [
|
|
1644
|
-
this.autoTrustDialog(),
|
|
1645
|
-
this.trustHoldDialog(),
|
|
1646
|
-
this.rateSwitchPickerDialog(),
|
|
1647
|
-
{
|
|
1648
|
-
// Codex 0.156 may change the wording/order of this credit-cost choice.
|
|
1649
|
-
// Never navigate it by position: a moved option could switch to a
|
|
1650
|
-
// more expensive model. A live picker holds delivery and notifies a
|
|
1651
|
-
// human; old transcript mentions are excluded by the tail matcher.
|
|
1652
|
-
pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
|
|
1653
|
-
keys: [],
|
|
1654
|
-
description: "Codex rate limit model switch dialog needs human choice",
|
|
1655
|
-
holdOnly: true,
|
|
1656
|
-
blocksDelivery: true,
|
|
1657
|
-
inputBlocked: true,
|
|
1658
|
-
remoteCard: codexSelectionCard,
|
|
1659
|
-
isActive: codexRateSwitchVisible,
|
|
1660
|
-
},
|
|
1661
|
-
this.updatePickerDialog(),
|
|
1662
|
-
this.usageLimitLunaReserveDialog(),
|
|
1663
|
-
this.workspaceLimitPromptDialog(),
|
|
1664
|
-
...this.sessionHoldDialogs(),
|
|
1665
|
-
this.unknownSelectionHoldDialog(),
|
|
1666
|
-
];
|
|
1667
|
-
}
|
|
1668
|
-
getInputUnavailableTransients() {
|
|
1669
|
-
return [{
|
|
1670
|
-
pattern: /(?:^|\n)\s*Resuming session…\s*(?:\n|$)/,
|
|
1671
|
-
description: "Codex session resume in progress",
|
|
1672
|
-
isActive: (pane) => {
|
|
1673
|
-
const rows = pane.split(/\r?\n/);
|
|
1674
|
-
return codexBoxedResumeLoading(rows) || codexUnboxedResumeLoading(rows);
|
|
1675
|
-
},
|
|
1676
|
-
}];
|
|
1677
|
-
}
|
|
1678
|
-
/** Only a resume paints "Resuming session…"; a fresh launch never does. */
|
|
1679
|
-
launchMayShowInputTransient() {
|
|
1680
|
-
return this.paneDetectorContext?.launchMayResume() ?? this.lastLaunchResumes;
|
|
1681
|
-
}
|
|
1682
|
-
getQuitCommand() { return "/quit"; }
|
|
1683
|
-
getCompactCommand() { return "/compact"; }
|
|
1684
|
-
getClearCommand() { return "/clear"; }
|
|
1685
|
-
getCancelKey() { return "Escape"; }
|
|
1686
|
-
}
|
|
1687
|
-
export class CodexBackend extends CodexPaneDetectors {
|
|
1688
|
-
instanceDir;
|
|
1689
|
-
binaryPath;
|
|
1690
|
-
sharedCodexHome;
|
|
1691
|
-
isolatedCodexHome;
|
|
1692
|
-
/** Which subscription this instance runs on, or null for the shared login. */
|
|
1693
|
-
credentialProfile = null;
|
|
1694
|
-
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
1695
|
-
authorizedTrust = null;
|
|
1696
|
-
/** Set by buildCommand when the resume plan fell back; read once by the daemon. */
|
|
1697
|
-
launchWarning = null;
|
|
1698
|
-
/** Set by writeConfig when the status_line could not be made verifiable (#931). */
|
|
1699
|
-
statusLineWarning = null;
|
|
1700
|
-
/** #2357: set by buildCommand when the codex being launched lacks a package companion (codex-code-mode-host). */
|
|
1701
|
-
packageWarning = null;
|
|
1702
|
-
constructor(instanceDir) {
|
|
1703
|
-
super();
|
|
1704
|
-
this.instanceDir = instanceDir;
|
|
1705
|
-
this.binaryPath = resolveBinary("codex");
|
|
1706
|
-
this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
1707
|
-
this.isolatedCodexHome = CodexBackend.resolveShortHome(instanceDir);
|
|
1708
|
-
}
|
|
1709
|
-
/**
|
|
1710
|
-
* Compute and (on first call for an existing instance) migrate to a SHORT
|
|
1711
|
-
* persistent CODEX_HOME under `~/.agend/cx/<8-char-hash>/`.
|
|
1712
|
-
*
|
|
1713
|
-
* Background (#953): codex 0.157.0 added `app-server-control.sock` under
|
|
1714
|
-
* CODEX_HOME. For instances with long `-t<topic_id>` suffixes the full path
|
|
1715
|
-
* exceeds the Unix socket SUN_LEN (~107 chars). codex canonicalises the path
|
|
1716
|
-
* before binding the socket, so a symlink workaround does not help — the real
|
|
1717
|
-
* path must be short.
|
|
1718
|
-
*
|
|
1719
|
-
* Migration steps (each is idempotent; half-completed states self-heal):
|
|
1720
|
-
* 1. If shortHome does not exist AND legacyHome is a real directory (not a
|
|
1721
|
-
* symlink) → atomic renameSync. On EXDEV or any other error, fall back
|
|
1722
|
-
* safely: keep using the legacy long path (instance can still start, just
|
|
1723
|
-
* without the short-path fix).
|
|
1724
|
-
* 2. If shortHome exists but legacyHome is missing → recreate the backward-
|
|
1725
|
-
* compat symlink (self-heal for rename-succeeded-but-symlink-failed crash).
|
|
1726
|
-
* 3. If both shortHome and legacyHome (as symlink) exist → nothing to do.
|
|
1727
|
-
* 4. New instance (no legacyHome) → create shortHome directly.
|
|
1728
|
-
*
|
|
1729
|
-
* Fail-safe guarantee: migration failure must never make the instance worse.
|
|
1730
|
-
* The caller always receives a valid path it can use as CODEX_HOME.
|
|
1731
|
-
*/
|
|
1732
|
-
/** Exposed for fleet-manager to delete the short home on instance removal. */
|
|
1733
|
-
static shortHomeFor(instanceDir) {
|
|
1734
|
-
return codexShortHomeFor(instanceDir);
|
|
1735
|
-
}
|
|
1736
|
-
static resolveShortHome(instanceDir) {
|
|
1737
|
-
const shortHome = CodexBackend.shortHomeFor(instanceDir);
|
|
1738
|
-
const shortBase = join(getAgendHome(), "cx");
|
|
1739
|
-
const canonical = resolve(instanceDir);
|
|
1740
|
-
const legacyHome = resolve(instanceDir, "codex-home");
|
|
1741
|
-
// Only migrate/create when the instance directory itself exists.
|
|
1742
|
-
// Constructing CodexBackend for a non-existent dir (cli-env probes, test
|
|
1743
|
-
// backends that never ran) must not litter ~/.agend/cx/ with orphan dirs.
|
|
1744
|
-
if (!existsSync(canonical))
|
|
1745
|
-
return shortHome;
|
|
1746
|
-
const legacyExists = existsSync(legacyHome);
|
|
1747
|
-
const legacyIsRealDir = legacyExists && !lstatSync(legacyHome).isSymbolicLink();
|
|
1748
|
-
const shortExists = existsSync(shortHome);
|
|
1749
|
-
if (!shortExists) {
|
|
1750
|
-
mkdirSync(shortBase, { recursive: true, mode: 0o700 });
|
|
1751
|
-
if (legacyIsRealDir) {
|
|
1752
|
-
// Attempt atomic rename. Requires same device; fails with EXDEV otherwise.
|
|
1753
|
-
try {
|
|
1754
|
-
renameSync(legacyHome, shortHome);
|
|
1755
|
-
// Log the one-time migration for observability.
|
|
1756
|
-
try {
|
|
1757
|
-
const logPath = join(shortBase, `${shortHome.split("/").at(-1)}.migrated`);
|
|
1758
|
-
writeFileSync(logPath, `${new Date().toISOString()} migrated from ${legacyHome}\n`);
|
|
1759
|
-
}
|
|
1760
|
-
catch { /* log failure is non-fatal */ }
|
|
1761
|
-
}
|
|
1762
|
-
catch (err) {
|
|
1763
|
-
// EXDEV (cross-device) or any other rename failure → fail-safe:
|
|
1764
|
-
// fall back to the legacy long path so the instance still works.
|
|
1765
|
-
// The socket-length bug persists for this instance but no data is lost.
|
|
1766
|
-
const code = err.code ?? "unknown";
|
|
1767
|
-
try {
|
|
1768
|
-
const warnPath = join(shortBase, `${shortHome.split("/").at(-1)}.migration-failed`);
|
|
1769
|
-
writeFileSync(warnPath, `${new Date().toISOString()} rename failed (${code}): ${err.message}\n`
|
|
1770
|
-
+ `legacy path remains in use: ${legacyHome}\n`);
|
|
1771
|
-
}
|
|
1772
|
-
catch { /* warn log failure is also non-fatal */ }
|
|
1773
|
-
// Return legacy long path so the instance starts (socket may still fail
|
|
1774
|
-
// on 0.157.0 for long names, but no data is lost or corrupted).
|
|
1775
|
-
return legacyHome;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
else if (!legacyExists) {
|
|
1779
|
-
// New instance — no data to migrate.
|
|
1780
|
-
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
1781
|
-
}
|
|
1782
|
-
// If legacyExists but is already a symlink: shortHome was deleted externally
|
|
1783
|
-
// while the symlink still points to it. Re-create shortHome as a fresh dir.
|
|
1784
|
-
if (!existsSync(shortHome)) {
|
|
1785
|
-
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
// Backward-compat symlink: instanceDir/codex-home → shortHome.
|
|
1789
|
-
// Also serves as self-heal: if a previous run renamed successfully but
|
|
1790
|
-
// crashed before creating the symlink, we recreate it here.
|
|
1791
|
-
if (!legacyExists || legacyIsRealDir) {
|
|
1792
|
-
// legacyIsRealDir means rename just happened (now shortHome exists, legacyHome gone).
|
|
1793
|
-
// !legacyExists means new instance or symlink was deleted — create it.
|
|
1794
|
-
try {
|
|
1795
|
-
symlinkSync(shortHome, legacyHome);
|
|
1796
|
-
}
|
|
1797
|
-
catch { /* race or already exists */ }
|
|
1798
|
-
}
|
|
1799
|
-
return shortHome;
|
|
1800
|
-
}
|
|
1801
|
-
/**
|
|
1802
|
-
* The row codex paints for input it has taken into its own queue instead of
|
|
1803
|
-
* submitting: `↳ <message>` under "Messages to be submitted after next tool
|
|
1804
|
-
* call (press esc to interrupt and send immediately)".
|
|
1805
|
-
*/
|
|
1806
|
-
getQueuedInputMarker() {
|
|
1807
|
-
return /↳/;
|
|
1263
|
+
getQueuedInputMarker() {
|
|
1264
|
+
return /↳/;
|
|
1808
1265
|
}
|
|
1809
1266
|
/** #1811: what the last launch left out and why — the Log tab and daemon.log, never a channel. */
|
|
1810
1267
|
quietLaunchNotes = [];
|
|
@@ -1827,22 +1284,16 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
1827
1284
|
let cmd;
|
|
1828
1285
|
this.launchWarning = null;
|
|
1829
1286
|
this.lastLaunchResumes = false;
|
|
1287
|
+
this.quotaReplay = null;
|
|
1830
1288
|
// #2357: read-only (codex is never run here); a launch is not held for it — the operator is told once per launch.
|
|
1831
1289
|
this.packageWarning = codexPackageWarning(this.binaryPath);
|
|
1832
|
-
|
|
1833
|
-
if (retainedGeneral) {
|
|
1834
|
-
const check = checkResumeId(join(this.isolatedCodexHome, "sessions"), config.workingDirectory, retainedGeneral);
|
|
1835
|
-
if (!check.ok)
|
|
1836
|
-
throw new ResumeBlockedError(t("general_location.start_failed"), t("general_location.start_failed"));
|
|
1837
|
-
this.lastLaunchResumes = true;
|
|
1838
|
-
cmd = `${this.binaryPath} resume ${shellQuote(retainedGeneral)} ${approvalFlag}`;
|
|
1839
|
-
}
|
|
1840
|
-
else if (config.skipResume) {
|
|
1290
|
+
if (config.skipResume) {
|
|
1841
1291
|
cmd = `${this.binaryPath} ${approvalFlag}`;
|
|
1842
1292
|
}
|
|
1843
1293
|
else {
|
|
1844
|
-
const resumeBy = (id, why, bytes) => {
|
|
1294
|
+
const resumeBy = (id, why, bytes, path) => {
|
|
1845
1295
|
this.lastLaunchResumes = true;
|
|
1296
|
+
this.quotaReplay = snapshotCodexQuotaHistory(path);
|
|
1846
1297
|
this.writeResumeAttempt(id);
|
|
1847
1298
|
const large = bytes !== null && bytes > CODEX_RESUME_LARGE_BYTES ? ` ${t("codex.resume_large", String(Math.round(bytes / (1024 * 1024))))}` : "";
|
|
1848
1299
|
this.launchWarning = why + large;
|
|
@@ -1866,7 +1317,7 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
1866
1317
|
// M-2266: validated when it was asked for (restart_instance); checked again now, as the rollout may have moved.
|
|
1867
1318
|
const check = checkResumeId(sessionsDir, config.workingDirectory, oneShot);
|
|
1868
1319
|
if (check.ok)
|
|
1869
|
-
cmd = resumeBy(check.rollout.id, t("codex.resume_oneshot", config.workingDirectory, check.rollout.id), check.rollout.bytes);
|
|
1320
|
+
cmd = resumeBy(check.rollout.id, t("codex.resume_oneshot", config.workingDirectory, check.rollout.id), check.rollout.bytes, check.rollout.path);
|
|
1870
1321
|
else {
|
|
1871
1322
|
cmd = this.plannedCommand(config, approvalFlag, sessionsDir, resumeBy);
|
|
1872
1323
|
this.launchWarning = [t("codex.resume_oneshot_invalid", config.workingDirectory, oneShot, check.reason), this.launchWarning].filter(Boolean).join("\n");
|
|
@@ -1896,7 +1347,13 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
1896
1347
|
: { model: runs.model, levels: null, unproven: true });
|
|
1897
1348
|
if (effort.arg)
|
|
1898
1349
|
cmd += effort.arg;
|
|
1899
|
-
|
|
1350
|
+
// M-2446: the per-agent speed (codex-speed.ts) — Fast only where the running model's catalog entry offers it.
|
|
1351
|
+
const speed = codexSpeed(config.backendOptions?.speed, runs.proven
|
|
1352
|
+
? readCodexModelServiceTiers({ isolatedHome: this.isolatedCodexHome, sharedHome: this.sharedCodexHome, model: runs.model })
|
|
1353
|
+
: { model: runs.model, tiers: null, unproven: true });
|
|
1354
|
+
if (speed.arg)
|
|
1355
|
+
cmd += speed.arg;
|
|
1356
|
+
this.quietLaunchNotes = [effort.note, speed.note].filter((n) => !!n);
|
|
1900
1357
|
// AgEnD instances are unattended processes: an interactive self-update
|
|
1901
1358
|
// picker blocks delivery and must never be enabled by a copied global or
|
|
1902
1359
|
// managed config layer. Keep this last so the CLI override is authoritative.
|
|
@@ -2182,253 +1639,615 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2182
1639
|
unlinkSync(target);
|
|
2183
1640
|
}
|
|
2184
1641
|
}
|
|
2185
|
-
catch { /* absent, or another startup already detached the link */ }
|
|
2186
|
-
}
|
|
2187
|
-
// The session dirs must exist in the SHARED home before the symlink pass:
|
|
2188
|
-
// on a fresh install they don't yet, so no link was created, and the first
|
|
2189
|
-
// archive made Codex create a REAL dir in the instance-private home — a
|
|
2190
|
-
// permanent fork the terminal CLI could never see (#506). mkdir -p is
|
|
2191
|
-
// EEXIST-safe under concurrent instance startup.
|
|
2192
|
-
for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
|
|
2193
|
-
try {
|
|
2194
|
-
mkdirSync(join(this.sharedCodexHome, dir), { recursive: true, mode: 0o700 });
|
|
1642
|
+
catch { /* absent, or another startup already detached the link */ }
|
|
1643
|
+
}
|
|
1644
|
+
// The session dirs must exist in the SHARED home before the symlink pass:
|
|
1645
|
+
// on a fresh install they don't yet, so no link was created, and the first
|
|
1646
|
+
// archive made Codex create a REAL dir in the instance-private home — a
|
|
1647
|
+
// permanent fork the terminal CLI could never see (#506). mkdir -p is
|
|
1648
|
+
// EEXIST-safe under concurrent instance startup.
|
|
1649
|
+
for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
|
|
1650
|
+
try {
|
|
1651
|
+
mkdirSync(join(this.sharedCodexHome, dir), { recursive: true, mode: 0o700 });
|
|
1652
|
+
}
|
|
1653
|
+
catch { /* best effort */ }
|
|
1654
|
+
}
|
|
1655
|
+
// Heal homes that already forked: merge the private real dir back into the
|
|
1656
|
+
// shared one (never overwriting), then replace it with the symlink.
|
|
1657
|
+
for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
|
|
1658
|
+
this.migrateDivergedSessionDir(dir);
|
|
1659
|
+
}
|
|
1660
|
+
// Older versions linked SQLite's WAL/SHM files independently of their
|
|
1661
|
+
// base database. If the base file had already been created privately,
|
|
1662
|
+
// that split one SQLite database across two homes and caused CANTOPEN (or
|
|
1663
|
+
// worse, cross-database journal recovery). Remove only links AgEnD made to
|
|
1664
|
+
// the matching shared-home path; real private sidecars remain untouched.
|
|
1665
|
+
const healedSidecars = [];
|
|
1666
|
+
for (const name of readdirSync(this.isolatedCodexHome)) {
|
|
1667
|
+
if (!SQLITE_SIDECAR_RE.test(name))
|
|
1668
|
+
continue;
|
|
1669
|
+
const target = join(this.isolatedCodexHome, name);
|
|
1670
|
+
try {
|
|
1671
|
+
if (!lstatSync(target).isSymbolicLink())
|
|
1672
|
+
continue;
|
|
1673
|
+
const linkTarget = resolve(dirname(target), readlinkSync(target));
|
|
1674
|
+
if (linkTarget !== join(this.sharedCodexHome, name))
|
|
1675
|
+
continue;
|
|
1676
|
+
unlinkSync(target);
|
|
1677
|
+
healedSidecars.push(name);
|
|
1678
|
+
}
|
|
1679
|
+
catch {
|
|
1680
|
+
// A concurrent process may remove an ephemeral sidecar/link first.
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (healedSidecars.length > 0) {
|
|
1684
|
+
console.warn(`[agend] removed unsafe Codex SQLite sidecar links: ${healedSidecars.join(", ")}`);
|
|
1685
|
+
}
|
|
1686
|
+
// One file, possibly from somewhere else. Everything below is unchanged:
|
|
1687
|
+
// sessions, the thread/state/memory databases and every cache still come
|
|
1688
|
+
// from the shared home, which is why switching subscription does not throw
|
|
1689
|
+
// the conversation away.
|
|
1690
|
+
this.linkAuthFile();
|
|
1691
|
+
for (const name of readdirSync(this.sharedCodexHome)) {
|
|
1692
|
+
if (CodexBackend.PRIVATE_RUNTIME_DIRS.has(name))
|
|
1693
|
+
continue;
|
|
1694
|
+
if (name === "config.toml" || name === AGEND_MCP_CLEANUP_LOCK || name.startsWith(".config.toml."))
|
|
1695
|
+
continue;
|
|
1696
|
+
if (name === CODEX_AUTH_FILE)
|
|
1697
|
+
continue; // handled above, possibly from a profile
|
|
1698
|
+
// SQLite resolves a symlinked base DB to the shared path and creates its
|
|
1699
|
+
// own adjacent sidecars there. Linking sidecars separately is redundant
|
|
1700
|
+
// for shared bases and corrupts the file set for private bases.
|
|
1701
|
+
if (SQLITE_SIDECAR_RE.test(name))
|
|
1702
|
+
continue;
|
|
1703
|
+
const source = join(this.sharedCodexHome, name);
|
|
1704
|
+
const target = join(this.isolatedCodexHome, name);
|
|
1705
|
+
if (existsSync(target))
|
|
1706
|
+
continue;
|
|
1707
|
+
try {
|
|
1708
|
+
const type = lstatSync(source).isDirectory() ? "dir" : "file";
|
|
1709
|
+
symlinkSync(source, target, type);
|
|
1710
|
+
}
|
|
1711
|
+
catch {
|
|
1712
|
+
// State/cache links are compatibility aids; config isolation must not
|
|
1713
|
+
// fail merely because a concurrently-created cache entry disappeared.
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Point this home's `auth.json` at whichever login it should use.
|
|
1719
|
+
*
|
|
1720
|
+
* Codex refreshes the token by writing through the link rather than replacing
|
|
1721
|
+
* it — observed across every instance home on a machine that has been running
|
|
1722
|
+
* this arrangement for months, with the shared file's mtime moving. That is
|
|
1723
|
+
* behaviour, though, not a contract: a release that starts renaming over the
|
|
1724
|
+
* file would leave a real file here and quietly fork the login, with refreshes
|
|
1725
|
+
* landing somewhere the profile never sees. So a real file where a link
|
|
1726
|
+
* belongs is treated as exactly that — reported, and put back.
|
|
1727
|
+
*/
|
|
1728
|
+
linkAuthFile() {
|
|
1729
|
+
const source = join(this.authSourceDir(), CODEX_AUTH_FILE);
|
|
1730
|
+
const target = join(this.isolatedCodexHome, CODEX_AUTH_FILE);
|
|
1731
|
+
let existing = null;
|
|
1732
|
+
try {
|
|
1733
|
+
existing = lstatSync(target);
|
|
1734
|
+
}
|
|
1735
|
+
catch { /* absent */ }
|
|
1736
|
+
if (existing?.isSymbolicLink()) {
|
|
1737
|
+
let current = "";
|
|
1738
|
+
try {
|
|
1739
|
+
current = resolve(dirname(target), readlinkSync(target));
|
|
1740
|
+
}
|
|
1741
|
+
catch { /* unreadable */ }
|
|
1742
|
+
if (current === source)
|
|
1743
|
+
return;
|
|
1744
|
+
try {
|
|
1745
|
+
unlinkSync(target);
|
|
1746
|
+
}
|
|
1747
|
+
catch { /* raced */ }
|
|
1748
|
+
}
|
|
1749
|
+
else if (existing) {
|
|
1750
|
+
// Not a link: codex replaced it, or an older AgEnD copied it. Either way
|
|
1751
|
+
// the refreshes it has been collecting are stranded in this instance's
|
|
1752
|
+
// home, so say so rather than deleting them silently.
|
|
1753
|
+
const stranded = `${target}.replaced-${Date.now()}`;
|
|
1754
|
+
try {
|
|
1755
|
+
renameSync(target, stranded);
|
|
1756
|
+
console.warn(`[agend] codex replaced ${CODEX_AUTH_FILE} in ${this.isolatedCodexHome} with a real file — `
|
|
1757
|
+
+ `its login was no longer shared. Kept a copy at ${stranded} and re-linked to ${source}.`);
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
return; // cannot move it; leave the instance working on what it has
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (!existsSync(source))
|
|
1764
|
+
return; // not logged in yet; codex will create it
|
|
1765
|
+
try {
|
|
1766
|
+
symlinkSync(source, target, "file");
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
// EEXIST from a concurrent start is the ordinary case and means the link
|
|
1770
|
+
// is already there.
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* One-time heal for an instance whose isolated home grew a REAL session dir
|
|
1775
|
+
* (#506): merge it into the shared home without overwriting anything, and
|
|
1776
|
+
* only after both the copy and the removal succeed put the symlink in its
|
|
1777
|
+
* place. Any failure keeps the private dir — sessions are never the thing
|
|
1778
|
+
* sacrificed for tidiness — and logs what happened.
|
|
1779
|
+
*/
|
|
1780
|
+
migrateDivergedSessionDir(name) {
|
|
1781
|
+
const target = join(this.isolatedCodexHome, name);
|
|
1782
|
+
let st;
|
|
1783
|
+
try {
|
|
1784
|
+
st = lstatSync(target);
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
return; // absent — the symlink pass below will create the link
|
|
1788
|
+
}
|
|
1789
|
+
if (st.isSymbolicLink())
|
|
1790
|
+
return; // already correct
|
|
1791
|
+
if (!st.isDirectory()) {
|
|
1792
|
+
console.warn(`[agend] codex-home/${name} is neither a symlink nor a directory — leaving it untouched`);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const shared = join(this.sharedCodexHome, name);
|
|
1796
|
+
try {
|
|
1797
|
+
mkdirSync(shared, { recursive: true, mode: 0o700 });
|
|
1798
|
+
// cp -rn semantics: collisions keep the shared copy. Session rollout
|
|
1799
|
+
// filenames are timestamp+uuid, so a genuine collision means the same
|
|
1800
|
+
// file — skipping is lossless either way.
|
|
1801
|
+
cpSync(target, shared, { recursive: true, force: false, errorOnExist: false });
|
|
1802
|
+
rmSync(target, { recursive: true });
|
|
1803
|
+
symlinkSync(shared, target, "dir");
|
|
1804
|
+
console.warn(`[agend] migrated diverged codex ${name} into shared home: ${shared}`);
|
|
1805
|
+
}
|
|
1806
|
+
catch (err) {
|
|
1807
|
+
// Keep whatever is left of the private dir. The copy never overwrites, so
|
|
1808
|
+
// a retry on next start is safe; worst case is duplicated files in the
|
|
1809
|
+
// shared home, never lost ones.
|
|
1810
|
+
console.warn(`[agend] codex ${name} migration failed — keeping the instance-private dir (${err.message})`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* One-time migration for installations polluted by the old global `codex
|
|
1815
|
+
* mcp add` path. The lock protects separate fleet processes/upgrades, and
|
|
1816
|
+
* atomic rename prevents readers from observing a truncated config.
|
|
1817
|
+
*/
|
|
1818
|
+
cleanSharedConfig() {
|
|
1819
|
+
if (this.sharedCodexHome === this.isolatedCodexHome)
|
|
1820
|
+
return;
|
|
1821
|
+
mkdirSync(this.sharedCodexHome, { recursive: true, mode: 0o700 });
|
|
1822
|
+
const lockPath = join(this.sharedCodexHome, AGEND_MCP_CLEANUP_LOCK);
|
|
1823
|
+
let lockFd;
|
|
1824
|
+
const acquire = () => {
|
|
1825
|
+
try {
|
|
1826
|
+
lockFd = openSync(lockPath, "wx", 0o600);
|
|
1827
|
+
return true;
|
|
1828
|
+
}
|
|
1829
|
+
catch {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
if (!acquire()) {
|
|
1834
|
+
try {
|
|
1835
|
+
if (Date.now() - statSync(lockPath).mtimeMs > AGEND_MCP_CLEANUP_LOCK_STALE_MS) {
|
|
1836
|
+
unlinkSync(lockPath);
|
|
1837
|
+
if (!acquire())
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
return;
|
|
2195
1846
|
}
|
|
2196
|
-
catch { /* best effort */ }
|
|
2197
|
-
}
|
|
2198
|
-
// Heal homes that already forked: merge the private real dir back into the
|
|
2199
|
-
// shared one (never overwriting), then replace it with the symlink.
|
|
2200
|
-
for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
|
|
2201
|
-
this.migrateDivergedSessionDir(dir);
|
|
2202
1847
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
// worse, cross-database journal recovery). Remove only links AgEnD made to
|
|
2207
|
-
// the matching shared-home path; real private sidecars remain untouched.
|
|
2208
|
-
const healedSidecars = [];
|
|
2209
|
-
for (const name of readdirSync(this.isolatedCodexHome)) {
|
|
2210
|
-
if (!SQLITE_SIDECAR_RE.test(name))
|
|
2211
|
-
continue;
|
|
2212
|
-
const target = join(this.isolatedCodexHome, name);
|
|
1848
|
+
try {
|
|
1849
|
+
const configPath = join(this.sharedCodexHome, "config.toml");
|
|
1850
|
+
let content;
|
|
2213
1851
|
try {
|
|
2214
|
-
|
|
2215
|
-
continue;
|
|
2216
|
-
const linkTarget = resolve(dirname(target), readlinkSync(target));
|
|
2217
|
-
if (linkTarget !== join(this.sharedCodexHome, name))
|
|
2218
|
-
continue;
|
|
2219
|
-
unlinkSync(target);
|
|
2220
|
-
healedSidecars.push(name);
|
|
1852
|
+
content = readFileSync(configPath, "utf-8");
|
|
2221
1853
|
}
|
|
2222
1854
|
catch {
|
|
2223
|
-
|
|
1855
|
+
return;
|
|
2224
1856
|
}
|
|
1857
|
+
const cleaned = stripAgendMcpTables(content);
|
|
1858
|
+
if (cleaned !== content)
|
|
1859
|
+
atomicWritePrivate(configPath, cleaned);
|
|
2225
1860
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
// the conversation away.
|
|
2233
|
-
this.linkAuthFile();
|
|
2234
|
-
for (const name of readdirSync(this.sharedCodexHome)) {
|
|
2235
|
-
if (CodexBackend.PRIVATE_RUNTIME_DIRS.has(name))
|
|
2236
|
-
continue;
|
|
2237
|
-
if (name === "config.toml" || name === AGEND_MCP_CLEANUP_LOCK || name.startsWith(".config.toml."))
|
|
2238
|
-
continue;
|
|
2239
|
-
if (name === CODEX_AUTH_FILE)
|
|
2240
|
-
continue; // handled above, possibly from a profile
|
|
2241
|
-
// SQLite resolves a symlinked base DB to the shared path and creates its
|
|
2242
|
-
// own adjacent sidecars there. Linking sidecars separately is redundant
|
|
2243
|
-
// for shared bases and corrupts the file set for private bases.
|
|
2244
|
-
if (SQLITE_SIDECAR_RE.test(name))
|
|
2245
|
-
continue;
|
|
2246
|
-
const source = join(this.sharedCodexHome, name);
|
|
2247
|
-
const target = join(this.isolatedCodexHome, name);
|
|
2248
|
-
if (existsSync(target))
|
|
2249
|
-
continue;
|
|
2250
|
-
try {
|
|
2251
|
-
const type = lstatSync(source).isDirectory() ? "dir" : "file";
|
|
2252
|
-
symlinkSync(source, target, type);
|
|
1861
|
+
finally {
|
|
1862
|
+
if (lockFd !== undefined) {
|
|
1863
|
+
try {
|
|
1864
|
+
closeSync(lockFd);
|
|
1865
|
+
}
|
|
1866
|
+
catch { }
|
|
2253
1867
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
// fail merely because a concurrently-created cache entry disappeared.
|
|
1868
|
+
try {
|
|
1869
|
+
unlinkSync(lockPath);
|
|
2257
1870
|
}
|
|
1871
|
+
catch { }
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
getReadyPattern() {
|
|
1875
|
+
// Header and context text persist in inline scrollback and even while the
|
|
1876
|
+
// CLI is loading or a modal owns stdin. Require the live prompt followed
|
|
1877
|
+
// by the Context footer at the *end* of the capture. A valid custom
|
|
1878
|
+
// status_line without Context gets a separate configured-item pattern;
|
|
1879
|
+
// unknown footer values still fail closed. getBusyPattern vetoes a working
|
|
1880
|
+
// turn whose empty composer remains visible.
|
|
1881
|
+
// U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
|
|
1882
|
+
// prompt, between prompt/footer, and below the footer. A drafted composer
|
|
1883
|
+
// is also idle once this same bottom footer proves it owns the screen.
|
|
1884
|
+
const contextReady = withCodexPassiveHintTail(/(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i);
|
|
1885
|
+
const footerPattern = this.configuredStatusLineFooterPattern();
|
|
1886
|
+
if (!footerPattern)
|
|
1887
|
+
return contextReady;
|
|
1888
|
+
// The no-Context alternative requires the exact empty composer followed by
|
|
1889
|
+
// a recognized configured Codex status_line value. If run-state is
|
|
1890
|
+
// configured, that value must specifically be Ready. This is positive
|
|
1891
|
+
// structural proof, never a deny-list of known dialogs.
|
|
1892
|
+
const footerSource = footerPattern.source.slice(1, -1);
|
|
1893
|
+
const noContextReady = withCodexPassiveHintTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*\r?\n(?:[ \t⋆]*\r?\n){0,3}${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"));
|
|
1894
|
+
// #978: a configured footer that carries a Context item in any position
|
|
1895
|
+
// is the Context footer: same drafted-composer rule as Context-first. The
|
|
1896
|
+
// whole row must match the configured item grammar, so an arbitrary
|
|
1897
|
+
// indented line that merely contains `Context N% left` cannot pass.
|
|
1898
|
+
const configuredContextReady = this.configuredHasContextItem()
|
|
1899
|
+
? withCodexPassiveHintTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}(?=[^\r\n]*${CODEX_CONTEXT_ITEM})${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"))
|
|
1900
|
+
: null;
|
|
1901
|
+
return new RegExp([contextReady.source, noContextReady.source, configuredContextReady?.source]
|
|
1902
|
+
.filter((source) => !!source)
|
|
1903
|
+
.map(source => `(?:${source})`)
|
|
1904
|
+
.join("|"), "iu");
|
|
1905
|
+
}
|
|
1906
|
+
readConfiguredStatusLineItems() {
|
|
1907
|
+
if (this.configuredStatusLineItems !== undefined)
|
|
1908
|
+
return this.configuredStatusLineItems;
|
|
1909
|
+
try {
|
|
1910
|
+
const config = tomlTable(parseToml(readFileSync(join(this.isolatedCodexHome, "config.toml"), "utf-8")));
|
|
1911
|
+
const tui = tomlTable(config?.tui);
|
|
1912
|
+
const items = tui?.status_line;
|
|
1913
|
+
this.configuredStatusLineItems = Array.isArray(items) && items.every(item => typeof item === "string")
|
|
1914
|
+
? items
|
|
1915
|
+
: null;
|
|
1916
|
+
}
|
|
1917
|
+
catch {
|
|
1918
|
+
this.configuredStatusLineItems = null;
|
|
2258
1919
|
}
|
|
1920
|
+
return this.configuredStatusLineItems;
|
|
1921
|
+
}
|
|
1922
|
+
configuredStatusLineFooterPattern() {
|
|
1923
|
+
if (this.configuredStatusLinePattern !== undefined)
|
|
1924
|
+
return this.configuredStatusLinePattern;
|
|
1925
|
+
const items = this.readConfiguredStatusLineItems();
|
|
1926
|
+
if (!items?.length)
|
|
1927
|
+
return this.configuredStatusLinePattern = null;
|
|
1928
|
+
const configuredPatterns = items
|
|
1929
|
+
.map(item => CODEX_STATUS_LINE_VALUE_PATTERNS[item])
|
|
1930
|
+
.filter((pattern) => pattern !== undefined);
|
|
1931
|
+
if (!configuredPatterns.length)
|
|
1932
|
+
return this.configuredStatusLinePattern = null;
|
|
1933
|
+
const segment = `(?:${configuredPatterns.join("|")})`;
|
|
1934
|
+
const separator = String.raw `[ \t]+·[ \t]+`;
|
|
1935
|
+
const hasRunState = items.some(item => CODEX_RUN_STATE_STATUS_ITEMS.has(item));
|
|
1936
|
+
// Ready must occupy its own status-line segment. Other configured items
|
|
1937
|
+
// may precede/follow it, as Codex joins visible values with ` · `.
|
|
1938
|
+
const line = hasRunState
|
|
1939
|
+
? `(?:${segment}${separator})*Ready(?:${separator}${segment})*`
|
|
1940
|
+
: `${segment}(?:${separator}${segment})*`;
|
|
1941
|
+
// The configured items, then (optionally) the CLI's own native fields — the same rule as the Context-first footer, so a
|
|
1942
|
+
// `model · Context 46% left Goal achieved (1h 6m)` row is the footer too, and the readiness regex built from this
|
|
1943
|
+
// source (footerSource) says the same.
|
|
1944
|
+
this.configuredStatusLinePattern = new RegExp(`^[ \t⋆]*${line}(?:${CODEX_NATIVE_FOOTER_FIELDS})?[ \t⋆]*$`, "iu");
|
|
1945
|
+
return this.configuredStatusLinePattern;
|
|
1946
|
+
}
|
|
1947
|
+
isConfiguredStatusLineFooter(row) {
|
|
1948
|
+
const pattern = this.configuredStatusLineFooterPattern();
|
|
1949
|
+
if (!pattern)
|
|
1950
|
+
return false;
|
|
1951
|
+
pattern.lastIndex = 0;
|
|
1952
|
+
return pattern.test(row);
|
|
1953
|
+
}
|
|
1954
|
+
configuredHasContextItem() {
|
|
1955
|
+
return (this.readConfiguredStatusLineItems() ?? []).some(item => CODEX_CONTEXT_STATUS_ITEMS.has(item));
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* #978: a Context item in ANY position, accepted only as part of a footer
|
|
1959
|
+
* whose every segment matches the configured status_line items. Codex keeps
|
|
1960
|
+
* the user's order (`gpt-5.6-sol medium · Context 46% left · ~/x`); an
|
|
1961
|
+
* arbitrary indented transcript line that happens to contain the item does
|
|
1962
|
+
* not match that grammar. A stale config fails closed, and the daemon's
|
|
1963
|
+
* stable-composer escape hatch then keeps the instance from latching.
|
|
1964
|
+
*/
|
|
1965
|
+
// The configured grammar already makes Context a whole segment, so the item
|
|
1966
|
+
// test only has to establish that one is present.
|
|
1967
|
+
isConfiguredContextFooter(row) {
|
|
1968
|
+
return this.configuredHasContextItem()
|
|
1969
|
+
&& new RegExp(CODEX_CONTEXT_ITEM, "iu").test(row)
|
|
1970
|
+
&& this.isConfiguredStatusLineFooter(row);
|
|
1971
|
+
}
|
|
1972
|
+
/** Any footer that proves the Context meter owns the bottom row. */
|
|
1973
|
+
isAnyContextFooter(row) {
|
|
1974
|
+
return isCodexContextFooter(row) || this.isConfiguredContextFooter(row);
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* #978 escape hatch: the live, EMPTY Codex composer within the last few
|
|
1978
|
+
* non-blank rows, no busy marker, and none of the known selection screens.
|
|
1979
|
+
* Deliberately says nothing about the footer, which is what is unknown.
|
|
1980
|
+
*/
|
|
1981
|
+
isStableUnknownLayoutIdlePane(pane) {
|
|
1982
|
+
if (this.getBusyPattern().test(pane))
|
|
1983
|
+
return false;
|
|
1984
|
+
// The composer is live-looking and the footer is unknown — exactly this
|
|
1985
|
+
// proof's blind spot — but there is no server behind it (#1099).
|
|
1986
|
+
if (codexAppServerDisconnected(pane))
|
|
1987
|
+
return false;
|
|
1988
|
+
// (No second, wider "esc to interrupt" match here: getBusyPattern() above already reads a live status row of ANY
|
|
1989
|
+
// title — #964 — and anchors it to the whole row at column zero, so a reply that quotes the phrase, indented or
|
|
1990
|
+
// in prose, cannot pin the escape hatch shut (#991).)
|
|
1991
|
+
// A retained queued message (`↳ …` under "Messages to be submitted after
|
|
1992
|
+
// next tool call") means work is pending, whatever the composer shows.
|
|
1993
|
+
if (/↳|Messages to be submitted/i.test(pane))
|
|
1994
|
+
return false;
|
|
1995
|
+
if (codexUsageLimitMenuVisible(pane) || codexRateSwitchVisible(pane)
|
|
1996
|
+
|| codexUnknownSelectionVisible(pane) || codexUpdatePickerVisible(pane))
|
|
1997
|
+
return false;
|
|
1998
|
+
const rows = pane.replace(/\r/g, "").split("\n").filter(row => row.trim() !== "");
|
|
1999
|
+
return rows.slice(-6).some(row => /^[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*$/.test(row));
|
|
2000
|
+
}
|
|
2001
|
+
isCodexIdleFooter(row) {
|
|
2002
|
+
return isCodexContextFooter(row) || this.isConfiguredStatusLineFooter(row);
|
|
2003
|
+
}
|
|
2004
|
+
/** A proxy reply filters chrome per line; whole-pane readiness is separate. */
|
|
2005
|
+
isProxyReplyChromeLine(line) {
|
|
2006
|
+
return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
|
|
2007
|
+
|| this.isAnyContextFooter(line);
|
|
2008
|
+
}
|
|
2009
|
+
getErrorPatterns() {
|
|
2010
|
+
return [
|
|
2011
|
+
// Specific quota codes must precede the generic HTTP 429 classifier:
|
|
2012
|
+
// OpenAI returns insufficient_quota with status 429, but switching models
|
|
2013
|
+
// cannot repair an exhausted account and would only start a failover loop.
|
|
2014
|
+
{
|
|
2015
|
+
pattern: new RegExp(String.raw `^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?\b(?:insufficient_quota|billing_hard_limit_reached|exceeded\s+your\s+current\s+quota)\b|^(?:${CODEX_STATUS_GLYPH}) Quota exceeded\. Check your plan and billing details\.\r?$`, "im"),
|
|
2016
|
+
type: "quota",
|
|
2017
|
+
action: "pause",
|
|
2018
|
+
message: "OpenAI quota exceeded",
|
|
2019
|
+
notice: () => quotaPausedLine("OpenAI", "api_quota"),
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
// Codex already exhausted HTTP retries and returned to its composer.
|
|
2023
|
+
// The existing conditional continuation preserves context without
|
|
2024
|
+
// replaying earlier tool effects. Quota remains a preceding pause.
|
|
2025
|
+
pattern: new RegExp(`^${CODEX_EXHAUSTED_429_ROW}$`, "im"),
|
|
2026
|
+
type: "rate_limit",
|
|
2027
|
+
action: "nudge_continue",
|
|
2028
|
+
message: t("inst.codex_rate_retry_stopped"),
|
|
2029
|
+
skipCooldown: true,
|
|
2030
|
+
skipRecoveryWait: true,
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
// Pane history contains the agent's own prose, source code and search
|
|
2034
|
+
// results. Bare `rate limit` used to fail over an otherwise healthy
|
|
2035
|
+
// Codex merely for discussing this regex. Match machine-readable API
|
|
2036
|
+
// forms or an error-decorated terminal line instead.
|
|
2037
|
+
pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+429\b|^(?![\s\S]{0,240}?${CODEX_EXHAUSTED_429_ROW})[ \t]*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*429\b|\b(?:rate_limit_exceeded|too_many_requests)\b|\brate limit(?:ed| exceeded| reached)?\b|\btoo many requests\b)`, "im"),
|
|
2038
|
+
type: "rate_limit",
|
|
2039
|
+
action: "failover",
|
|
2040
|
+
message: "OpenAI rate limit reached",
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
// Same false-positive boundary as rate limits: `authentication` is a
|
|
2044
|
+
// normal English/code word. Require a structured 401/code or a
|
|
2045
|
+
// decorated CLI error line before pausing every instance on the shared
|
|
2046
|
+
// credential.
|
|
2047
|
+
pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+401\b|^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*401\b|\b(?:invalid_api_key|authentication_error)\b|\b401\s+Unauthorized\b|\bauthentication (?:failed|error)\b|\binvalid api key\b)`, "im"),
|
|
2048
|
+
type: "auth_error",
|
|
2049
|
+
action: "pause",
|
|
2050
|
+
message: "OpenAI authentication error",
|
|
2051
|
+
},
|
|
2052
|
+
// Codex writes the apostrophe as U+2019 ("You’ve hit your usage limit.") since
|
|
2053
|
+
// 0.156 and as ASCII before (0.153.4): both must match, or no usage limit
|
|
2054
|
+
// ever pauses the instance (#1098).
|
|
2055
|
+
{ pattern: /you['’]ve hit your usage limit/i, type: "quota", action: "pause", message: "Codex usage limit reached — upgrade plan required",
|
|
2056
|
+
notice: () => quotaPausedLine("Codex", "usage") },
|
|
2057
|
+
{
|
|
2058
|
+
// The terminal stage of a lost app-server (#1099): Codex has given up
|
|
2059
|
+
// reconnecting and says so, with a counter that never stops. Nothing
|
|
2060
|
+
// recovers from here without a relaunch, and every message sent in the
|
|
2061
|
+
// meantime waits for an idle that cannot come. Only the LIVE row counts:
|
|
2062
|
+
// directly above the LAST composer with no later transcript item or composer
|
|
2063
|
+
// (the same test as codexAppServerDisconnected, as a lookahead), so text that
|
|
2064
|
+
// merely quotes it does not raise this.
|
|
2065
|
+
pattern: new RegExp(String.raw `^[ \t]*•[ \t]+Reconnect failed[ \t]+—[ \t]+check the endpoint, then relaunch[ \t]+\(\d+[hms](?:[ \t]+\d+[hms])*\)[ \t]*$`
|
|
2066
|
+
+ String.raw `(?=\n(?:[ \t]*\n)*[›>][^\n]*(?:\n(?![•■⚠›>])[^\n]*)*(?![\s\S]))`, "mu"),
|
|
2067
|
+
type: "network",
|
|
2068
|
+
action: "notify",
|
|
2069
|
+
message: "Codex lost its app-server connection and could not reconnect — restart the instance (messages cannot be delivered until then)",
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
// Codex reports an unknown model either as a TUI metadata fallback or
|
|
2073
|
+
// as a ChatGPT-account API rejection. Use whitespace-aware phrases so
|
|
2074
|
+
// capture-pane hard wraps do not hide either form.
|
|
2075
|
+
// Codex quotes the slug with BACKTICKS ("Model metadata for `x` not found.",
|
|
2076
|
+
// verified live on 0.160.0 and in the 0.153.4–0.160.0 binaries). The old
|
|
2077
|
+
// pattern only took ASCII quotes, so it could never match a real line.
|
|
2078
|
+
pattern: /model\s+metadata\s+for\s+['"`‘’“”][^'"`‘’“”]+['"`‘’“”]\s+not\s+found\.\s+defaulting\s+to\s+fallback\s+metadata|model\s+is\s+not\s+supported\s+when\s+using\s+codex\s+with\s+a\s+chatgpt\s+account/i,
|
|
2079
|
+
type: "model_error",
|
|
2080
|
+
action: "notify",
|
|
2081
|
+
message: "Codex model unavailable — use /model to switch",
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
// A capacity rejection is a completed failed turn: Codex returns to its
|
|
2085
|
+
// prompt without an answer, with its context intact. Keep this anchored to
|
|
2086
|
+
// the exact decorated TUI line so ordinary prose about model capacity
|
|
2087
|
+
// cannot trigger anything on an otherwise healthy instance. The status
|
|
2088
|
+
// glyph is decoration that changes between Codex versions and error classes
|
|
2089
|
+
// (`■` U+25A0 on 0.159.2 and 0.160.0, `⚠` U+26A0 before), so any one
|
|
2090
|
+
// "Other Symbol" glyph (optionally with its emoji selector) opens the line —
|
|
2091
|
+
// never a bullet, dash, quote marker or number, which prose and the model's
|
|
2092
|
+
// own transcript items use (`•`, `-`, `>`). The sentence must fill the line,
|
|
2093
|
+
// from column 0. Box drawing and block elements (U+2500–U+259F) are
|
|
2094
|
+
// excluded up front: they are `So` too, but a left-border popup or
|
|
2095
|
+
// table row (`│ …`) is chrome, not a status line. The CLI is
|
|
2096
|
+
// already back at the prompt, so skipRecoveryWait avoids an extra wait.
|
|
2097
|
+
// action "nudge_continue": tell the user, then about a minute later tell
|
|
2098
|
+
// the agent to keep going (the lifecycle bounds this to 3 per 30 minutes,
|
|
2099
|
+
// then pauses; see #905). No restart — nothing would re-send the turn — and
|
|
2100
|
+
// no model switch. skipCooldown: each NEW capacity line is a new episode,
|
|
2101
|
+
// including a second one right after the nudge.
|
|
2102
|
+
pattern: /^(?![─-▟])\p{So}\uFE0F? Selected model is at capacity\. Please try a different model\.\r?$/mu,
|
|
2103
|
+
type: "model_error",
|
|
2104
|
+
action: "nudge_continue",
|
|
2105
|
+
message: t("inst.codex_model_capacity"),
|
|
2106
|
+
skipCooldown: true,
|
|
2107
|
+
skipRecoveryWait: true,
|
|
2108
|
+
},
|
|
2109
|
+
// Workspace (business) accounts report exhaustion differently from personal
|
|
2110
|
+
// ones, with codex's own rows (codex-rs/protocol error.rs and tui
|
|
2111
|
+
// turn_runtime.rs, unchanged 0.160.0–0.165.0):
|
|
2112
|
+
// member: "■ Your workspace is out of credits. Ask your workspace owner to refill in order to continue."
|
|
2113
|
+
// "■ You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue."
|
|
2114
|
+
// owner: "■ You're out of credits. Your workspace is out of credits. Add credits to continue using Codex."
|
|
2115
|
+
// "■ Your workspace is out of credits. Add credits to continue."
|
|
2116
|
+
// "■ You hit your spend cap set in your workspace. Increase your spend cap to continue."
|
|
2117
|
+
// "■ Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex."
|
|
2118
|
+
// Neither `insufficient_quota|billing` nor `you've hit your usage limit`
|
|
2119
|
+
// matches them.
|
|
2120
|
+
//
|
|
2121
|
+
// pause, not notify: nothing works until someone refills or raises the cap.
|
|
2122
|
+
// Anchored to the status glyph in COLUMN 0, because these skip the
|
|
2123
|
+
// usage-window check (a workspace limit is not one of those windows, so the
|
|
2124
|
+
// check would call it "available"): codex draws its own error cells there,
|
|
2125
|
+
// while an agent's message, quote or code block is indented under its `•`
|
|
2126
|
+
// (captured live, tests/fixtures/codex-0165-workspace-quoted.pane.txt).
|
|
2127
|
+
// `\s+` at each word gap: capture-pane runs without -J, so tmux's hard wrap
|
|
2128
|
+
// can land a newline anywhere in the sentence. A resumed transcript reprints
|
|
2129
|
+
// these rows, and the first live composer of a spawn takes them as history.
|
|
2130
|
+
{
|
|
2131
|
+
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+(?:You['’]re\s+out\s+of\s+credits\.\s+)?Your\s+workspace\s+is\s+out\s+of\s+credits\.`, "m"),
|
|
2132
|
+
type: "quota",
|
|
2133
|
+
action: "pause",
|
|
2134
|
+
message: "Codex workspace credits exhausted — workspace owner must refill",
|
|
2135
|
+
notice: () => quotaPausedLine("Codex", "workspace_credits"),
|
|
2136
|
+
skipUsageVerify: true,
|
|
2137
|
+
historyUntilLiveComposer: true,
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+You\s+hit\s+your\s+spend\s+cap\s+set\s+(?:by\s+the\s+owner\s+of|in)\s+your\s+workspace\.`, "m"),
|
|
2141
|
+
type: "quota",
|
|
2142
|
+
action: "pause",
|
|
2143
|
+
message: "Codex workspace spend cap reached — workspace owner must raise it",
|
|
2144
|
+
notice: () => t("inst.quota_spend_cap"),
|
|
2145
|
+
skipUsageVerify: true,
|
|
2146
|
+
historyUntilLiveComposer: true,
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+Usage\s+limit\s+reached\.\s+You['’]ve\s+reached\s+your\s+usage\s+limit\.\s+Increase\s+your\s+limits`, "m"),
|
|
2150
|
+
type: "quota",
|
|
2151
|
+
action: "pause",
|
|
2152
|
+
message: "Codex workspace usage limit reached — increase the workspace limits to continue",
|
|
2153
|
+
notice: () => quotaPausedLine("Codex", "workspace_usage"),
|
|
2154
|
+
skipUsageVerify: true,
|
|
2155
|
+
historyUntilLiveComposer: true,
|
|
2156
|
+
},
|
|
2157
|
+
// Codex warns at 10% and 5% remaining, and scopes the limit by period —
|
|
2158
|
+
// "weekly" alone missed every `monthly limit` warning.
|
|
2159
|
+
//
|
|
2160
|
+
// `\s+` at EVERY word gap (not literal spaces) because capture-pane runs without -J,
|
|
2161
|
+
// so tmux's hard wrap can land a newline (plus continuation-line padding)
|
|
2162
|
+
// at any of these word gaps. The trailing "Run /status for a breakdown."
|
|
2163
|
+
// is deliberately NOT part of the pattern — that tail is what actually
|
|
2164
|
+
// wrapped in the reported case.
|
|
2165
|
+
//
|
|
2166
|
+
// `of your <period> limit` is load-bearing: it's what keeps the pattern
|
|
2167
|
+
// off the agent's own prose about percentages.
|
|
2168
|
+
{
|
|
2169
|
+
pattern: /less\s+than\s+(\d+)\s*%\s+of\s+your\s+(hourly|daily|weekly|monthly)\s+limit/i,
|
|
2170
|
+
type: "quota",
|
|
2171
|
+
action: "notify",
|
|
2172
|
+
message: "Codex usage limit running low",
|
|
2173
|
+
quotaAdvisory: true,
|
|
2174
|
+
formatMessage: (m) => `Codex ${m[2].toLowerCase()} limit: less than ${m[1]}% left`,
|
|
2175
|
+
notice: (m) => (m ? quotaLowLine(m[2], m[1]) : quotaLine("Codex", "usage")),
|
|
2176
|
+
},
|
|
2177
|
+
];
|
|
2259
2178
|
}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
*/
|
|
2271
|
-
linkAuthFile() {
|
|
2272
|
-
const source = join(this.authSourceDir(), CODEX_AUTH_FILE);
|
|
2273
|
-
const target = join(this.isolatedCodexHome, CODEX_AUTH_FILE);
|
|
2274
|
-
let existing = null;
|
|
2275
|
-
try {
|
|
2276
|
-
existing = lstatSync(target);
|
|
2277
|
-
}
|
|
2278
|
-
catch { /* absent */ }
|
|
2279
|
-
if (existing?.isSymbolicLink()) {
|
|
2280
|
-
let current = "";
|
|
2281
|
-
try {
|
|
2282
|
-
current = resolve(dirname(target), readlinkSync(target));
|
|
2283
|
-
}
|
|
2284
|
-
catch { /* unreadable */ }
|
|
2285
|
-
if (current === source)
|
|
2286
|
-
return;
|
|
2287
|
-
try {
|
|
2288
|
-
unlinkSync(target);
|
|
2289
|
-
}
|
|
2290
|
-
catch { /* raced */ }
|
|
2291
|
-
}
|
|
2292
|
-
else if (existing) {
|
|
2293
|
-
// Not a link: codex replaced it, or an older AgEnD copied it. Either way
|
|
2294
|
-
// the refreshes it has been collecting are stranded in this instance's
|
|
2295
|
-
// home, so say so rather than deleting them silently.
|
|
2296
|
-
const stranded = `${target}.replaced-${Date.now()}`;
|
|
2297
|
-
try {
|
|
2298
|
-
renameSync(target, stranded);
|
|
2299
|
-
console.warn(`[agend] codex replaced ${CODEX_AUTH_FILE} in ${this.isolatedCodexHome} with a real file — `
|
|
2300
|
-
+ `its login was no longer shared. Kept a copy at ${stranded} and re-linked to ${source}.`);
|
|
2301
|
-
}
|
|
2302
|
-
catch {
|
|
2303
|
-
return; // cannot move it; leave the instance working on what it has
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
if (!existsSync(source))
|
|
2307
|
-
return; // not logged in yet; codex will create it
|
|
2308
|
-
try {
|
|
2309
|
-
symlinkSync(source, target, "file");
|
|
2310
|
-
}
|
|
2311
|
-
catch {
|
|
2312
|
-
// EEXIST from a concurrent start is the ordinary case and means the link
|
|
2313
|
-
// is already there.
|
|
2314
|
-
}
|
|
2179
|
+
getStartupDialogs() {
|
|
2180
|
+
const trustHold = this.trustHoldDialog();
|
|
2181
|
+
return [
|
|
2182
|
+
this.autoTrustDialog(),
|
|
2183
|
+
trustHold,
|
|
2184
|
+
this.updatePickerDialog(),
|
|
2185
|
+
this.rateSwitchPickerDialog(),
|
|
2186
|
+
...this.sessionHoldDialogs(),
|
|
2187
|
+
this.unknownSelectionHoldDialog(),
|
|
2188
|
+
];
|
|
2315
2189
|
}
|
|
2316
2190
|
/**
|
|
2317
|
-
*
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
* place. Any failure keeps the private dir — sessions are never the thing
|
|
2321
|
-
* sacrificed for tidiness — and logs what happened.
|
|
2191
|
+
* #984: session-selection screens only a human may answer. Held (never
|
|
2192
|
+
* keyed) so the delivery gate keeps messages queued and dialog_parked tells
|
|
2193
|
+
* the operator; listed before the generic selection hold for a clear notice.
|
|
2322
2194
|
*/
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
symlinkSync(shared, target, "dir");
|
|
2347
|
-
console.warn(`[agend] migrated diverged codex ${name} into shared home: ${shared}`);
|
|
2348
|
-
}
|
|
2349
|
-
catch (err) {
|
|
2350
|
-
// Keep whatever is left of the private dir. The copy never overwrites, so
|
|
2351
|
-
// a retry on next start is safe; worst case is duplicated files in the
|
|
2352
|
-
// shared home, never lost ones.
|
|
2353
|
-
console.warn(`[agend] codex ${name} migration failed — keeping the instance-private dir (${err.message})`);
|
|
2354
|
-
}
|
|
2195
|
+
sessionHoldDialogs() {
|
|
2196
|
+
return [
|
|
2197
|
+
{
|
|
2198
|
+
pattern: /This conversation is open in another app/,
|
|
2199
|
+
keys: [],
|
|
2200
|
+
description: "Codex session is open in another process (thread lock) — close the other one and press r, or fork manually",
|
|
2201
|
+
holdOnly: true,
|
|
2202
|
+
blocksDelivery: true,
|
|
2203
|
+
inputBlocked: true,
|
|
2204
|
+
remoteCard: codexSelectionCard,
|
|
2205
|
+
isActive: codexSessionLockVisible,
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
pattern: /Working directory · resume/,
|
|
2209
|
+
keys: [],
|
|
2210
|
+
description: "Codex resume selected a session from another directory (sibling git worktree) — needs a human choice",
|
|
2211
|
+
holdOnly: true,
|
|
2212
|
+
blocksDelivery: true,
|
|
2213
|
+
inputBlocked: true,
|
|
2214
|
+
remoteCard: codexSelectionCard,
|
|
2215
|
+
isActive: codexResumeCwdPickerVisible,
|
|
2216
|
+
},
|
|
2217
|
+
];
|
|
2355
2218
|
}
|
|
2356
2219
|
/**
|
|
2357
|
-
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2220
|
+
* The user's decision (hmsg-53600a2c): codex's folder-trust prompt in an AgEnD session is always answered Trust — the
|
|
2221
|
+
* workspace is the agent's own, whatever preTrust covered (preTrust stays, so the prompt rarely shows at all). Enter,
|
|
2222
|
+
* only with the cursor on "Trust and continue" among the known options (trustCursor); any other layout or row is
|
|
2223
|
+
* held (trustHoldDialog). It trusts the folder only: per-action approvals are separate dialogs and are not touched.
|
|
2360
2224
|
*/
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
return false;
|
|
2374
|
-
}
|
|
2225
|
+
autoTrustDialog() {
|
|
2226
|
+
return {
|
|
2227
|
+
pattern: /^\s*Trust this folder\?/m,
|
|
2228
|
+
keys: ["Enter"],
|
|
2229
|
+
description: "Codex folder trust — trusting the agent's own folder",
|
|
2230
|
+
blocksDelivery: true,
|
|
2231
|
+
inputBlocked: true,
|
|
2232
|
+
autoResolutionKey: "codex-folder-auto-trust",
|
|
2233
|
+
// One Enter per prompt (claimed, then checked): a prompt still up after it is held, never pressed again (#2896 review).
|
|
2234
|
+
verifyAfterKeys: true,
|
|
2235
|
+
isActive: pane => codexTrustPromptState(pane).trustCursor,
|
|
2236
|
+
answerDetail: pane => ({ autoTrusted: true, ...this.trustPaneFields(pane) }),
|
|
2375
2237
|
};
|
|
2376
|
-
if (!acquire()) {
|
|
2377
|
-
try {
|
|
2378
|
-
if (Date.now() - statSync(lockPath).mtimeMs > AGEND_MCP_CLEANUP_LOCK_STALE_MS) {
|
|
2379
|
-
unlinkSync(lockPath);
|
|
2380
|
-
if (!acquire())
|
|
2381
|
-
return;
|
|
2382
|
-
}
|
|
2383
|
-
else {
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
catch {
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
try {
|
|
2392
|
-
const configPath = join(this.sharedCodexHome, "config.toml");
|
|
2393
|
-
let content;
|
|
2394
|
-
try {
|
|
2395
|
-
content = readFileSync(configPath, "utf-8");
|
|
2396
|
-
}
|
|
2397
|
-
catch {
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
const cleaned = stripAgendMcpTables(content);
|
|
2401
|
-
if (cleaned !== content)
|
|
2402
|
-
atomicWritePrivate(configPath, cleaned);
|
|
2403
|
-
}
|
|
2404
|
-
finally {
|
|
2405
|
-
if (lockFd !== undefined) {
|
|
2406
|
-
try {
|
|
2407
|
-
closeSync(lockFd);
|
|
2408
|
-
}
|
|
2409
|
-
catch { }
|
|
2410
|
-
}
|
|
2411
|
-
try {
|
|
2412
|
-
unlinkSync(lockPath);
|
|
2413
|
-
}
|
|
2414
|
-
catch { }
|
|
2415
|
-
}
|
|
2416
2238
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
this.configuredStatusLineItems = null;
|
|
2430
|
-
}
|
|
2431
|
-
return this.configuredStatusLineItems;
|
|
2239
|
+
trustHoldDialog() {
|
|
2240
|
+
return {
|
|
2241
|
+
pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
|
|
2242
|
+
keys: [],
|
|
2243
|
+
description: "Codex folder trust needs human confirmation",
|
|
2244
|
+
holdOnly: true,
|
|
2245
|
+
blocksDelivery: true,
|
|
2246
|
+
inputBlocked: true,
|
|
2247
|
+
isActive: codexTrustVariantActive,
|
|
2248
|
+
remoteCard: codexSelectionCard,
|
|
2249
|
+
heldReason: pane => this.trustHoldReason(pane),
|
|
2250
|
+
};
|
|
2432
2251
|
}
|
|
2433
2252
|
/**
|
|
2434
2253
|
* #2893: why the authorized folder-trust answer did not take this prompt (log only): what the pane shows against what
|
|
@@ -2463,6 +2282,147 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2463
2282
|
authorizedCwd: authorized?.cwd ?? null, authorizedRoot: authorized?.root ?? null,
|
|
2464
2283
|
configPath: join(this.isolatedCodexHome, "config.toml") };
|
|
2465
2284
|
}
|
|
2285
|
+
/**
|
|
2286
|
+
* #1008: the rate-limit model-switch picker parked unattended instances (it
|
|
2287
|
+
* waits forever and its default option changes the model). Prevention is
|
|
2288
|
+
* `notice.hide_rate_limit_model_nudge` (hideRateLimitModelNudge — real and
|
|
2289
|
+
* effective on 0.157.1 and 0.159.2, verified live); this is the answer for a
|
|
2290
|
+
* picker that appears anyway (config not applied, a CLI that moved the key).
|
|
2291
|
+
*
|
|
2292
|
+
* Escape, not a navigated Enter: it selects nothing, so it can never take
|
|
2293
|
+
* option 1 (switch model) or option 3 (writes "never show again" into the
|
|
2294
|
+
* config). Deliberately not one-shot: if the picker swallowed the key it is
|
|
2295
|
+
* simply sent again on the next poll, and a repeated Escape is harmless.
|
|
2296
|
+
*/
|
|
2297
|
+
rateSwitchPickerDialog() {
|
|
2298
|
+
return {
|
|
2299
|
+
// Documentation only: Daemon.dialogMatches uses isActive INSTEAD of this
|
|
2300
|
+
// pattern, so it filters nothing here — the structural check below is the
|
|
2301
|
+
// whole decision, and ordinary prose about the picker fails it. The words
|
|
2302
|
+
// are the picker's own, kept so a reader can find the screen from the log.
|
|
2303
|
+
pattern: /Keep current model \(never show again\)/,
|
|
2304
|
+
keys: ["Escape"],
|
|
2305
|
+
description: "Codex rate-limit model-switch picker — Escape keeps the current model",
|
|
2306
|
+
blocksDelivery: true,
|
|
2307
|
+
inputBlocked: true,
|
|
2308
|
+
isActive: codexRateSwitchPickerVisible,
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
updatePickerDialog() {
|
|
2312
|
+
return {
|
|
2313
|
+
// Defense in depth for config written by older AgEnD versions or a Codex
|
|
2314
|
+
// regression. Match the release URL as well as the banner so ordinary
|
|
2315
|
+
// agent prose cannot trigger a keypress.
|
|
2316
|
+
pattern: /Update available![\s\S]{0,200}Release notes: https:\/\/github\.com\/openai\/codex\/releases/m,
|
|
2317
|
+
keys: ["Escape"],
|
|
2318
|
+
description: "Codex startup update-available picker",
|
|
2319
|
+
// A delivery must never land on this screen. Its selected option is
|
|
2320
|
+
// `› 1. Update now (runs … curl … | sh)` — the same `›` the input row
|
|
2321
|
+
// uses — so an Enter here runs an installer instead of sending a message.
|
|
2322
|
+
// The runtime dismisser presses Escape, but it only polls every few
|
|
2323
|
+
// seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
|
|
2324
|
+
// which parks on this picker for as long as nobody answers it).
|
|
2325
|
+
blocksDelivery: true,
|
|
2326
|
+
// Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
|
|
2327
|
+
// Check the complete current picker here; a different Enter-only menu
|
|
2328
|
+
// must never receive this automatic Escape key.
|
|
2329
|
+
isActive: codexUpdatePickerVisible,
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
usageLimitLunaReserveDialog() {
|
|
2333
|
+
// The real codex usage-limit dialog states codex has ALREADY switched to
|
|
2334
|
+
// Luna Reserve and labels Escape as "continue working". Pressing Escape via
|
|
2335
|
+
// sendSpecialKey is the ONLY safe key:
|
|
2336
|
+
//
|
|
2337
|
+
// • Escape is sent via sendSpecialKey — never via pasteText (which adds
|
|
2338
|
+
// an implicit bracketed-paste Enter that could confirm the highlighted
|
|
2339
|
+
// server action: Upgrade, Reset usage, Add Credits, …).
|
|
2340
|
+
// • A digit selects its row at once, and pasted its implicit Enter would
|
|
2341
|
+
// confirm whatever it landed on. This path is explicitly NOT used.
|
|
2342
|
+
//
|
|
2343
|
+
// verifyAfterKeys: true — confirm the menu is gone after Escape.
|
|
2344
|
+
// No confirmBeforeEnter / keysAfterConfirm: Escape needs no confirm gate.
|
|
2345
|
+
return {
|
|
2346
|
+
pattern: /Press enter to confirm or esc to continue working/i,
|
|
2347
|
+
keys: ["Escape"],
|
|
2348
|
+
description: "Codex usage limit — pressing Escape to continue with Luna Reserve",
|
|
2349
|
+
blocksDelivery: true,
|
|
2350
|
+
inputBlocked: true,
|
|
2351
|
+
isActive: codexUsageLimitMenuVisible,
|
|
2352
|
+
verifyAfterKeys: true,
|
|
2353
|
+
autoResolutionKey: "codex-usage-limit-luna-reserve",
|
|
2354
|
+
postDismissNotice: {
|
|
2355
|
+
text: t("inst.codex_usage_limit_luna_reserve", ""), // instance name injected by daemon
|
|
2356
|
+
label: "codex-usage-limit-selected",
|
|
2357
|
+
},
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
workspaceLimitPromptDialog() {
|
|
2361
|
+
// Escape via sendSpecialKey, the same as the Luna Reserve menu: never a paste
|
|
2362
|
+
// (its implicit Enter would confirm the cursor's row) and never "y". The error
|
|
2363
|
+
// row codex printed above the prompt stays, and the quota pause takes it from
|
|
2364
|
+
// there: one notice, no parked dialog.
|
|
2365
|
+
return {
|
|
2366
|
+
pattern: /^\s*enter select\s*[·•]\s*esc back\s*$/im,
|
|
2367
|
+
keys: ["Escape"],
|
|
2368
|
+
description: "Codex workspace limit prompt — pressing Escape (never asking the owner)",
|
|
2369
|
+
blocksDelivery: true,
|
|
2370
|
+
inputBlocked: true,
|
|
2371
|
+
isActive: codexWorkspaceLimitPromptVisible,
|
|
2372
|
+
verifyAfterKeys: true,
|
|
2373
|
+
autoResolutionKey: "codex-workspace-limit-prompt",
|
|
2374
|
+
answeredByAgend: true,
|
|
2375
|
+
marksLiveIncident: true,
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
unknownSelectionHoldDialog() {
|
|
2379
|
+
return {
|
|
2380
|
+
pattern: /^\s*[›❯>]\s+\S/m,
|
|
2381
|
+
keys: [],
|
|
2382
|
+
description: "Codex interactive selection needs human input",
|
|
2383
|
+
holdOnly: true,
|
|
2384
|
+
blocksDelivery: true,
|
|
2385
|
+
inputBlocked: true,
|
|
2386
|
+
remoteCard: codexSelectionCard,
|
|
2387
|
+
isActive: codexUnknownSelectionVisible,
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
getRuntimeDialogs() {
|
|
2391
|
+
return [
|
|
2392
|
+
this.autoTrustDialog(),
|
|
2393
|
+
this.trustHoldDialog(),
|
|
2394
|
+
this.rateSwitchPickerDialog(),
|
|
2395
|
+
{
|
|
2396
|
+
// Codex 0.156 may change the wording/order of this credit-cost choice.
|
|
2397
|
+
// Never navigate it by position: a moved option could switch to a
|
|
2398
|
+
// more expensive model. A live picker holds delivery and notifies a
|
|
2399
|
+
// human; old transcript mentions are excluded by the tail matcher.
|
|
2400
|
+
pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
|
|
2401
|
+
keys: [],
|
|
2402
|
+
description: "Codex rate limit model switch dialog needs human choice",
|
|
2403
|
+
holdOnly: true,
|
|
2404
|
+
blocksDelivery: true,
|
|
2405
|
+
inputBlocked: true,
|
|
2406
|
+
remoteCard: codexSelectionCard,
|
|
2407
|
+
isActive: codexRateSwitchVisible,
|
|
2408
|
+
},
|
|
2409
|
+
this.updatePickerDialog(),
|
|
2410
|
+
this.usageLimitLunaReserveDialog(),
|
|
2411
|
+
this.workspaceLimitPromptDialog(),
|
|
2412
|
+
...this.sessionHoldDialogs(),
|
|
2413
|
+
this.unknownSelectionHoldDialog(),
|
|
2414
|
+
];
|
|
2415
|
+
}
|
|
2416
|
+
getInputUnavailableTransients() {
|
|
2417
|
+
return [{
|
|
2418
|
+
pattern: /(?:^|\n)\s*Resuming session…\s*(?:\n|$)/,
|
|
2419
|
+
description: "Codex session resume in progress",
|
|
2420
|
+
isActive: (pane) => {
|
|
2421
|
+
const rows = pane.split(/\r?\n/);
|
|
2422
|
+
return codexBoxedResumeLoading(rows) || codexUnboxedResumeLoading(rows);
|
|
2423
|
+
},
|
|
2424
|
+
}];
|
|
2425
|
+
}
|
|
2466
2426
|
getContextUsage() {
|
|
2467
2427
|
return null;
|
|
2468
2428
|
}
|
|
@@ -2475,8 +2435,10 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2475
2435
|
plannedCommand(config, approvalFlag, sessionsDir, resumeBy) {
|
|
2476
2436
|
const plan = this.planResume(config);
|
|
2477
2437
|
this.lastLaunchResumes = plan.mode === "resume" || plan.mode === "last";
|
|
2478
|
-
if (plan.mode === "resume")
|
|
2438
|
+
if (plan.mode === "resume") {
|
|
2439
|
+
this.quotaReplay = snapshotCodexSessionQuotaHistory(this.stateDbPath(), config.workingDirectory, plan.id);
|
|
2479
2440
|
return `${this.binaryPath} resume ${shellQuote(plan.id)} ${approvalFlag}`;
|
|
2441
|
+
}
|
|
2480
2442
|
if (plan.mode === "last") {
|
|
2481
2443
|
this.launchWarning = t("codex.resume_db_unreadable_last", plan.cause);
|
|
2482
2444
|
return `${this.binaryPath} resume --last ${approvalFlag}`;
|
|
@@ -2490,7 +2452,7 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2490
2452
|
// there — `codex resume <id>` reads it.
|
|
2491
2453
|
const found = newestResumableRollout(sessionsDir, config.workingDirectory, undefined, this.failedResumes());
|
|
2492
2454
|
if (found)
|
|
2493
|
-
return resumeBy(found.id, t("codex.resume_auto", config.workingDirectory, found.id), found.bytes);
|
|
2455
|
+
return resumeBy(found.id, t("codex.resume_auto", config.workingDirectory, found.id), found.bytes, found.path);
|
|
2494
2456
|
return `${this.binaryPath} ${approvalFlag}`;
|
|
2495
2457
|
}
|
|
2496
2458
|
/** The database this instance's codex writes (see planResume). */
|
|
@@ -2575,6 +2537,10 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2575
2537
|
return peers ? codexSiblingState(config.workingDirectory, peers) : "siblings";
|
|
2576
2538
|
});
|
|
2577
2539
|
}
|
|
2540
|
+
/** Only a resume paints "Resuming session…"; a fresh launch never does. */
|
|
2541
|
+
launchMayShowInputTransient() {
|
|
2542
|
+
return this.lastLaunchResumes;
|
|
2543
|
+
}
|
|
2578
2544
|
consumeLaunchWarning() {
|
|
2579
2545
|
const warnings = [this.packageWarning, this.statusLineWarning, this.launchWarning].filter((w) => !!w);
|
|
2580
2546
|
this.packageWarning = null;
|
|
@@ -2582,6 +2548,10 @@ export class CodexBackend extends CodexPaneDetectors {
|
|
|
2582
2548
|
this.launchWarning = null;
|
|
2583
2549
|
return warnings.length ? warnings.join("\n") : null;
|
|
2584
2550
|
}
|
|
2551
|
+
getQuitCommand() { return "/quit"; }
|
|
2552
|
+
getCompactCommand() { return "/compact"; }
|
|
2553
|
+
getClearCommand() { return "/clear"; }
|
|
2554
|
+
getCancelKey() { return "Escape"; }
|
|
2585
2555
|
// Codex has no `/effort`; reasoning effort is the `model_reasoning_effort`
|
|
2586
2556
|
// config key (settable per launch with `-c`). The TUI reads it at startup, so
|
|
2587
2557
|
// a change needs a respawn — restart, not runtime.
|