botmux 3.11.0 → 3.12.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/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +18 -0
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +154 -2
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/cli/coco.js +3 -3
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.js +2 -2
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/kimi.d.ts.map +1 -1
- package/dist/adapters/cli/kimi.js +22 -8
- package/dist/adapters/cli/kimi.js.map +1 -1
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +3 -1
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +4 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/dashboard-command.d.ts +23 -0
- package/dist/cli/dashboard-command.d.ts.map +1 -0
- package/dist/cli/dashboard-command.js +61 -0
- package/dist/cli/dashboard-command.js.map +1 -0
- package/dist/cli/dashboard-endpoint.d.ts +3 -3
- package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
- package/dist/cli/dashboard-endpoint.js +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +101 -49
- package/dist/cli.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts +33 -3
- package/dist/codex-rpc-engine.d.ts.map +1 -1
- package/dist/codex-rpc-engine.js +182 -20
- package/dist/codex-rpc-engine.js.map +1 -1
- package/dist/codex-rpc-lifecycle.d.ts +42 -7
- package/dist/codex-rpc-lifecycle.d.ts.map +1 -1
- package/dist/codex-rpc-lifecycle.js +73 -7
- package/dist/codex-rpc-lifecycle.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/core/command-handler.d.ts +3 -2
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +19 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +16 -11
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +17 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +13 -1
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dispatch.d.ts +9 -10
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +17 -10
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +5 -0
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/role-resolver.d.ts +5 -2
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +55 -23
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +20 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
- package/dist/core/skills/cli-admin-command.js +142 -3
- package/dist/core/skills/cli-admin-command.js.map +1 -1
- package/dist/core/skills/frontmatter.d.ts.map +1 -1
- package/dist/core/skills/frontmatter.js +44 -7
- package/dist/core/skills/frontmatter.js.map +1 -1
- package/dist/core/skills/im-command.d.ts +6 -0
- package/dist/core/skills/im-command.d.ts.map +1 -1
- package/dist/core/skills/im-command.js +34 -7
- package/dist/core/skills/im-command.js.map +1 -1
- package/dist/core/skills/install-command.d.ts +4 -0
- package/dist/core/skills/install-command.d.ts.map +1 -0
- package/dist/core/skills/install-command.js +89 -0
- package/dist/core/skills/install-command.js.map +1 -0
- package/dist/core/skills/package.d.ts.map +1 -1
- package/dist/core/skills/package.js +10 -3
- package/dist/core/skills/package.js.map +1 -1
- package/dist/core/skills/policy.d.ts +5 -1
- package/dist/core/skills/policy.d.ts.map +1 -1
- package/dist/core/skills/policy.js +66 -3
- package/dist/core/skills/policy.js.map +1 -1
- package/dist/core/skills/references.d.ts +9 -1
- package/dist/core/skills/references.d.ts.map +1 -1
- package/dist/core/skills/references.js +35 -2
- package/dist/core/skills/references.js.map +1 -1
- package/dist/core/skills/registry-paths.d.ts +1 -0
- package/dist/core/skills/registry-paths.d.ts.map +1 -1
- package/dist/core/skills/registry-paths.js +3 -0
- package/dist/core/skills/registry-paths.js.map +1 -1
- package/dist/core/skills/session-resolver.d.ts +2 -1
- package/dist/core/skills/session-resolver.d.ts.map +1 -1
- package/dist/core/skills/session-resolver.js +1 -0
- package/dist/core/skills/session-resolver.js.map +1 -1
- package/dist/core/skills/session-runtime.d.ts.map +1 -1
- package/dist/core/skills/session-runtime.js +2 -0
- package/dist/core/skills/session-runtime.js.map +1 -1
- package/dist/core/skills/sources.d.ts +23 -0
- package/dist/core/skills/sources.d.ts.map +1 -1
- package/dist/core/skills/sources.js +128 -12
- package/dist/core/skills/sources.js.map +1 -1
- package/dist/core/skills/types.d.ts +22 -1
- package/dist/core/skills/types.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +4 -4
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +18 -6
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +52 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +101 -12
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +21 -11
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +39 -25
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +2 -114
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +0 -1
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +0 -1
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +66 -6
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/dashboard-summary-endpoint.d.ts +39 -0
- package/dist/dashboard/dashboard-summary-endpoint.d.ts.map +1 -0
- package/dist/dashboard/dashboard-summary-endpoint.js +50 -0
- package/dist/dashboard/dashboard-summary-endpoint.js.map +1 -0
- package/dist/dashboard/dashboard-summary.d.ts +62 -0
- package/dist/dashboard/dashboard-summary.d.ts.map +1 -0
- package/dist/dashboard/dashboard-summary.js +106 -0
- package/dist/dashboard/dashboard-summary.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +3 -0
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/skill-install-request.js +10 -11
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/skill-pack-response.d.ts +15 -0
- package/dist/dashboard/skill-pack-response.d.ts.map +1 -0
- package/dist/dashboard/skill-pack-response.js +32 -0
- package/dist/dashboard/skill-pack-response.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts +69 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +47 -7
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/feed-group-picker.d.ts +13 -0
- package/dist/dashboard/web/feed-group-picker.d.ts.map +1 -0
- package/dist/dashboard/web/feed-group-picker.js +45 -0
- package/dist/dashboard/web/feed-group-picker.js.map +1 -0
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +2 -5
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +309 -40
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/preferences.d.ts +4 -2
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +14 -6
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/roles-page.d.ts.map +1 -1
- package/dist/dashboard/web/roles-page.js +36 -4
- package/dist/dashboard/web/roles-page.js.map +1 -1
- package/dist/dashboard/web/roles.d.ts +3 -1
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +10 -2
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +8 -2
- package/dist/dashboard/web/schedules-page.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +101 -5
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +3 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +13 -6
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/skills/bot-assignments-tab.d.ts +27 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.js +257 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.js.map +1 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.d.ts +25 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.js +20 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.js.map +1 -0
- package/dist/dashboard/web/skills/shared.d.ts +86 -0
- package/dist/dashboard/web/skills/shared.d.ts.map +1 -0
- package/dist/dashboard/web/skills/shared.js +178 -0
- package/dist/dashboard/web/skills/shared.js.map +1 -0
- package/dist/dashboard/web/skills/skill-library-tab.d.ts +75 -0
- package/dist/dashboard/web/skills/skill-library-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/skill-library-tab.js +108 -0
- package/dist/dashboard/web/skills/skill-library-tab.js.map +1 -0
- package/dist/dashboard/web/skills/skill-packs-tab.d.ts +21 -0
- package/dist/dashboard/web/skills/skill-packs-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/skill-packs-tab.js +185 -0
- package/dist/dashboard/web/skills/skill-packs-tab.js.map +1 -0
- package/dist/dashboard/web/skills/types.d.ts +95 -0
- package/dist/dashboard/web/skills/types.d.ts.map +1 -0
- package/dist/dashboard/web/skills/types.js +2 -0
- package/dist/dashboard/web/skills/types.js.map +1 -0
- package/dist/dashboard/web/skills/use-skills-data.d.ts +31 -0
- package/dist/dashboard/web/skills/use-skills-data.d.ts.map +1 -0
- package/dist/dashboard/web/skills/use-skills-data.js +86 -0
- package/dist/dashboard/web/skills/use-skills-data.js.map +1 -0
- package/dist/dashboard/web/skills-page.d.ts +34 -31
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +321 -176
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts +18 -0
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +35 -1
- package/dist/dashboard/webhook-routes.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-GED4FEMS.js → bot-defaults-page-HL655CNR.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-HK7NIBJG.js → chunk-5PAYQ3KT.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-KRHWZEXU.js → chunk-5ZL7NLR5.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-6ALR252E.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-VBXJOJFH.js → chunk-BFUTTYVK.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-ELL53D62.js +55 -0
- package/dist/dashboard-web/chunks/connectors-page-3QQACF23.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-SDZMJ7UV.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-54UBPPOC.js → insights-page-ZZ3JEJ6L.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-PORHVKRH.js → monitor-room-ROJ5GZ4N.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-XGA7AXNL.js → monitoring-page-IATV7KJF.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-AO3AFG45.js → office-page-TYO5OIIT.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-F5BUTYR6.js → overview-page-VJUULZF2.js} +1 -1
- package/dist/dashboard-web/chunks/roles-page-7RJJKZGK.js +1 -0
- package/dist/dashboard-web/chunks/schedules-page-JTEJ5IXW.js +1 -0
- package/dist/dashboard-web/chunks/sessions-page-KD7HDT3U.js +8 -0
- package/dist/dashboard-web/chunks/{settings-page-OJAZLZQT.js → settings-page-7CTTFBRS.js} +1 -1
- package/dist/dashboard-web/chunks/skills-page-57NZECJ3.js +4 -0
- package/dist/dashboard-web/chunks/{team-federation-page-ZJVAP7HW.js → team-federation-page-6YYIMJG3.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-GYWTRGO2.js → v3-page-ZGKBSBK3.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-MSDAG6P7.js → whiteboards-page-AACJF6DB.js} +1 -1
- package/dist/dashboard-web/style.css +1952 -377
- package/dist/dashboard.js +316 -47
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/main/ipc.js +1 -1
- package/dist/desktop/main/runtime-service.js +2 -2
- package/dist/desktop/main/runtime-service.js.map +1 -1
- package/dist/desktop/shared/types.d.ts +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +5 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +5 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/index-daemon.js +6 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/services/adopt-input-sequence.d.ts +52 -0
- package/dist/services/adopt-input-sequence.d.ts.map +1 -0
- package/dist/services/adopt-input-sequence.js +50 -0
- package/dist/services/adopt-input-sequence.js.map +1 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +34 -11
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/codex-bridge-queue.d.ts +103 -7
- package/dist/services/codex-bridge-queue.d.ts.map +1 -1
- package/dist/services/codex-bridge-queue.js +291 -17
- package/dist/services/codex-bridge-queue.js.map +1 -1
- package/dist/services/codex-transcript.d.ts +4 -3
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/connector-store.d.ts +14 -1
- package/dist/services/connector-store.d.ts.map +1 -1
- package/dist/services/connector-store.js.map +1 -1
- package/dist/services/connector-topic-template.d.ts +7 -0
- package/dist/services/connector-topic-template.d.ts.map +1 -0
- package/dist/services/connector-topic-template.js +210 -0
- package/dist/services/connector-topic-template.js.map +1 -0
- package/dist/services/project-scanner.d.ts +33 -0
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +40 -1
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/skill-pack-store.d.ts +50 -0
- package/dist/services/skill-pack-store.d.ts.map +1 -0
- package/dist/services/skill-pack-store.js +270 -0
- package/dist/services/skill-pack-store.js.map +1 -0
- package/dist/services/skill-registry-store.d.ts +8 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +100 -68
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +8 -1
- package/dist/services/structured-bridge-clis.d.ts.map +1 -1
- package/dist/services/structured-bridge-clis.js +13 -0
- package/dist/services/structured-bridge-clis.js.map +1 -1
- package/dist/services/submit-confirmation.d.ts +50 -0
- package/dist/services/submit-confirmation.d.ts.map +1 -1
- package/dist/services/submit-confirmation.js +82 -0
- package/dist/services/submit-confirmation.js.map +1 -1
- package/dist/services/todo-state.d.ts +74 -0
- package/dist/services/todo-state.d.ts.map +1 -0
- package/dist/services/todo-state.js +351 -0
- package/dist/services/todo-state.js.map +1 -0
- package/dist/setup/agent-preset.d.ts.map +1 -1
- package/dist/setup/agent-preset.js +2 -0
- package/dist/setup/agent-preset.js.map +1 -1
- package/dist/setup/owner-identity.d.ts +47 -0
- package/dist/setup/owner-identity.d.ts.map +1 -0
- package/dist/setup/owner-identity.js +196 -0
- package/dist/setup/owner-identity.js.map +1 -0
- package/dist/setup/register-app.d.ts +4 -3
- package/dist/setup/register-app.d.ts.map +1 -1
- package/dist/setup/register-app.js.map +1 -1
- package/dist/setup/setup-args.d.ts +1 -1
- package/dist/setup/setup-args.d.ts.map +1 -1
- package/dist/setup/setup-args.js +7 -3
- package/dist/setup/setup-args.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/async-serial-queue.d.ts +9 -0
- package/dist/utils/async-serial-queue.d.ts.map +1 -0
- package/dist/utils/async-serial-queue.js +13 -0
- package/dist/utils/async-serial-queue.js.map +1 -0
- package/dist/utils/child-env.d.ts +25 -0
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +47 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/runtime-screen-status.d.ts +52 -0
- package/dist/utils/runtime-screen-status.d.ts.map +1 -0
- package/dist/utils/runtime-screen-status.js +60 -0
- package/dist/utils/runtime-screen-status.js.map +1 -0
- package/dist/worker.js +1697 -468
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +44 -44
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/chunk-6J5LS5AU.js +0 -51
- package/dist/dashboard-web/chunks/connectors-page-EELM66G3.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-WKUV6E42.js +0 -8
- package/dist/dashboard-web/chunks/roles-page-M5GIASJU.js +0 -1
- package/dist/dashboard-web/chunks/schedules-page-O232WQRR.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-YFRPSTIL.js +0 -6
- package/dist/dashboard-web/chunks/skills-page-EFZQPWV4.js +0 -4
package/dist/worker.js
CHANGED
|
@@ -28,7 +28,7 @@ import { readProcessStartIdentity } from './core/session-marker.js';
|
|
|
28
28
|
import { roleLibraryRoot, roleLibrarySubtree } from './core/role-library.js';
|
|
29
29
|
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff, isTranscriptRateLimitEvent, apiErrorMessageText } from './services/claude-transcript.js';
|
|
30
30
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
31
|
-
import { bridgePostText, shouldEmitEmptyCompletedBridgeFallback, shouldEmitFailedBridgeFallback, shouldSuppressBridgeEmit, stripTrailingBridgeSentinelLine } from './services/bridge-fallback-gate.js';
|
|
31
|
+
import { bridgePostText, isBridgeNothingToSendFinal, shouldEmitEmptyCompletedBridgeFallback, shouldEmitFailedBridgeFallback, shouldSuppressBridgeEmit, stripTrailingBridgeSentinelLine } from './services/bridge-fallback-gate.js';
|
|
32
32
|
import { buildSubmitMessagePreview } from './services/submit-notification.js';
|
|
33
33
|
import { decideHardTimeoutAction, decidePostHookPromptEvidence, decideSettleMarkReady, shouldArmPostHookPromptEvidenceFallback, shouldReleaseFirstPromptTimeout, shouldWaitForPostSessionStartPromptEvidence, shouldWriteNow, POST_HOOK_EVIDENCE_FALLBACK_MS, POST_HOOK_EVIDENCE_RETRY_MS, } from './utils/input-gate.js';
|
|
34
34
|
import { decideCodexRunnerFreshness, CodexRunnerFreshnessInputQueue, shouldHoldCodexRunnerInput, } from './services/codex-runner-freshness.js';
|
|
@@ -36,7 +36,7 @@ import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirs
|
|
|
36
36
|
import { decideRestartFollowup, settleDurableTurnForRestart } from './core/restart-followup-policy.js';
|
|
37
37
|
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
38
38
|
import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
|
|
39
|
-
import { EffortConfirmDialogGuard
|
|
39
|
+
import { EffortConfirmDialogGuard } from './utils/effort-confirm-dialog.js';
|
|
40
40
|
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
41
41
|
import { handoffQueuedDurableInputsOnBackendExit, mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, resolveInitialPromptDelivery, shouldArmSpawnArgvInitialPromptBusy, shouldTrackArgvBakedFirstPrompt, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
|
|
42
42
|
import { riffWorkerShutdownInputBlocker } from './core/riff-worker-shutdown-readiness.js';
|
|
@@ -58,7 +58,7 @@ import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
|
58
58
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
59
59
|
import { InputTurnDeduper } from './core/input-turn-dedupe.js';
|
|
60
60
|
import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, shouldHealAbsentBaseline, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
|
|
61
|
-
import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
|
|
61
|
+
import { CodexBridgeQueue, pruneExpiredPreStartHeadsAndEmit } from './services/codex-bridge-queue.js';
|
|
62
62
|
import { detectCodexComposerState } from './services/codex-composer-state.js';
|
|
63
63
|
import { generateCodexAppThreadTitle, readCodexAppThreadMetadata, setCodexAppThreadName, } from './services/codex-app-threads.js';
|
|
64
64
|
import { buildBotmuxLarkNativeSessionTitle } from './core/session-title.js';
|
|
@@ -74,7 +74,7 @@ import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirecto
|
|
|
74
74
|
import { drainPiTranscript } from './services/pi-transcript.js';
|
|
75
75
|
import { drainGrokUpdates, findGrokSessionByPid, grokSessionIdFromPath, } from './services/grok-transcript.js';
|
|
76
76
|
import { resolveFileBridgePath } from './services/file-bridge-path.js';
|
|
77
|
-
import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, } from './services/structured-bridge-clis.js';
|
|
77
|
+
import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, isStructuredBridgeLifecycleBlockingCli, } from './services/structured-bridge-clis.js';
|
|
78
78
|
import { drainCursorTranscript, findCursorChatIdByPid, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
|
|
79
79
|
import { shouldObserveCursorChatId, shouldPersistObservedCursorChatId } from './services/cursor-resume-policy.js';
|
|
80
80
|
import { extractKiroSessionIdFromOutput } from './services/kiro-session.js';
|
|
@@ -93,7 +93,7 @@ import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
|
|
|
93
93
|
import { buildWrappedLaunch, parseWrapperCli, isTtadkWrapper } from './setup/cli-selection.js';
|
|
94
94
|
import { cliUnavailableMessage } from './setup/cli-availability.js';
|
|
95
95
|
import { findLaunchedCliPid, scheduleWrapperRealCliPid, readComm, isBareShellComm, bareShellLaunchKind, settleLaunchComm, } from './core/session-discovery.js';
|
|
96
|
-
import { codexRpcEligible, paneRunsRemoteTui, orchestrateCodexRpcInit, rolloutUserTurnMatches, decideStartupDialogAction, shouldQueueInitialPrompt, shouldPreMarkFirstTurn, killAndVerifyPersistentPane } from './codex-rpc-lifecycle.js';
|
|
96
|
+
import { CODEX_RPC_TERMINAL_HYDRATION_DELAYS_MS, RpcEngagementFence, codexRpcEligible, paneRunsRemoteTui, orchestrateCodexRpcInit, rolloutUserTurnMatches, decideStartupDialogAction, shouldQueueInitialPrompt, shouldPreMarkFirstTurn, killAndVerifyPersistentPane, rpcTranscriptIngestBlockedByAwaitingActivation } from './codex-rpc-lifecycle.js';
|
|
97
97
|
import { delay } from './utils/timing.js';
|
|
98
98
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, syncClaudeResumeTargetToCwd, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
99
99
|
import { sessionReadyHookCommand } from './adapters/hook-command.js';
|
|
@@ -128,8 +128,9 @@ import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web
|
|
|
128
128
|
import { parseWorkerRequestUrl } from './utils/worker-http.js';
|
|
129
129
|
import { detectCliUsageLimit, usageLimitStateKey, structuredRateLimitState, isStructuredRateLimitAuthoritative } from './utils/cli-usage-limit.js';
|
|
130
130
|
import { uploadImageBuffer } from './utils/lark-upload.js';
|
|
131
|
-
import { redactChildEnv, scrubClaudeSessionMarkerEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
|
|
132
|
-
import { decideSubmitConfirmationAction } from './services/submit-confirmation.js';
|
|
131
|
+
import { applySessionOwnerEnv, redactChildEnv, scrubClaudeSessionMarkerEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
|
|
132
|
+
import { decideSubmitConfirmationAction, settleDeferredSubmitConfirmation, settleStaleWriteContinuation, } from './services/submit-confirmation.js';
|
|
133
|
+
import { runAdoptQueuedWriteSequence, runAdoptRawInputSequence, runAdoptSessionRenameSequence, } from './services/adopt-input-sequence.js';
|
|
133
134
|
import { config, resolveChatBotDiscoveryConfig } from './config.js';
|
|
134
135
|
import * as sessionStore from './services/session-store.js';
|
|
135
136
|
import * as pty from 'node-pty';
|
|
@@ -142,7 +143,9 @@ import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.
|
|
|
142
143
|
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
143
144
|
import { normalizeCodexAppLifecycleEvent, normalizeFinalUsage, } from './services/codex-app-runner-protocol.js';
|
|
144
145
|
import { hasMatchingManagedOriginCapability, ensureManagedOriginAttestationDirectory, ensureManagedOriginCapabilityLeafSafe, ensureManagedOriginDataRootProbe, ensureManagedOriginIsolationSentinel, ensureManagedOriginRootLocator, managedOriginLegacyIsolationProbeAccess, managedOriginDataRootProbeAccess, managedOriginIsolationSentinelAccess, managedOriginAttestationDirectory, managedOriginCapabilityPath, managedOriginRootLocatorPath, readManagedOriginAuthorityFile, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, sweepManagedOriginAttestationProofs, } from './core/managed-origin-capability.js';
|
|
145
|
-
import { CodexRpcEngine } from './codex-rpc-engine.js';
|
|
146
|
+
import { CodexRpcEngine, } from './codex-rpc-engine.js';
|
|
147
|
+
import { beginRuntimeWriteCycle, isCliBackendGenerationCurrent, PtyOutputGeneration, projectRuntimeScreenStatus, } from './utils/runtime-screen-status.js';
|
|
148
|
+
import { AsyncSerialQueue } from './utils/async-serial-queue.js';
|
|
146
149
|
// A worker must never trust an INHERITED session-level CLI home pointer
|
|
147
150
|
// (CLAUDE_CONFIG_DIR / CODEX_HOME): a stale pm2 dump can resurrect the daemon
|
|
148
151
|
// with a sibling bot's home baked in (see the index-daemon.ts boot scrub —
|
|
@@ -216,16 +219,184 @@ function cleanupPiInitialPromptFiles() {
|
|
|
216
219
|
piInitialPromptAdditionalArgs = [];
|
|
217
220
|
piInitialPromptEnv = {};
|
|
218
221
|
}
|
|
222
|
+
const rpcEngagementFence = new RpcEngagementFence();
|
|
223
|
+
const pendingRpcTurnTerminals = new Map();
|
|
224
|
+
const rpcTurnsAwaitingActivation = new Map();
|
|
225
|
+
const rpcTurnsAwaitingActivationIdentities = new Map();
|
|
226
|
+
/** Local send-start anchor for each exact awaiting owner. A predecessor's
|
|
227
|
+
* terminal hydration may ingest this successor's transcript before turn/start
|
|
228
|
+
* ACK returns. Reusing the send-start time when the bridge mark is installed
|
|
229
|
+
* keeps that already-buffered user/final pair inside its original bounded
|
|
230
|
+
* attribution window without widening replay for unrelated turns. */
|
|
231
|
+
const rpcTurnsAwaitingActivationReplayAnchors = new Map();
|
|
232
|
+
/** A bridge mark/activation failure after an accepted RPC submit must never
|
|
233
|
+
* degrade to ready. Keep a separate fail-closed gate until that exact native
|
|
234
|
+
* turn reaches terminal or the engine is torn down. */
|
|
235
|
+
const rpcLifecycleFailClosedOwners = new Map();
|
|
236
|
+
const rpcLifecycleFailClosedIdentities = new Map();
|
|
237
|
+
const rpcActiveOwners = new Map();
|
|
238
|
+
const rpcTerminalHydrationOwners = new Map();
|
|
239
|
+
const rpcTerminalHydrationTimers = new Map();
|
|
240
|
+
const rpcTerminalHydrationTerminals = new Map();
|
|
241
|
+
const settlingRpcTerminalOwners = new Map();
|
|
242
|
+
let deferredFreshRpcTurn;
|
|
243
|
+
function rpcTurnOwnerKey(identity) {
|
|
244
|
+
return `${identity.turnId}\0${identity.dispatchAttempt ?? ''}`;
|
|
245
|
+
}
|
|
246
|
+
function sameRpcGeneration(left, right) {
|
|
247
|
+
return left?.engine === right.engine
|
|
248
|
+
&& left.cliGeneration === right.cliGeneration;
|
|
249
|
+
}
|
|
250
|
+
function installRpcLifecycleFailClosedOwner(identity, generation) {
|
|
251
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
252
|
+
rpcLifecycleFailClosedOwners.set(ownerKey, generation);
|
|
253
|
+
rpcLifecycleFailClosedIdentities.set(ownerKey, identity);
|
|
254
|
+
}
|
|
255
|
+
function installAwaitingRpcActivation(identity, generation) {
|
|
256
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
257
|
+
rpcTurnsAwaitingActivation.set(ownerKey, generation);
|
|
258
|
+
rpcTurnsAwaitingActivationIdentities.set(ownerKey, identity);
|
|
259
|
+
rpcTurnsAwaitingActivationReplayAnchors.set(ownerKey, Date.now());
|
|
260
|
+
}
|
|
261
|
+
function awaitingRpcActivationReplayAnchorMs(identity, generation) {
|
|
262
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
263
|
+
if (!sameRpcGeneration(rpcTurnsAwaitingActivation.get(ownerKey), generation)) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return rpcTurnsAwaitingActivationReplayAnchors.get(ownerKey);
|
|
267
|
+
}
|
|
268
|
+
function clearRpcLifecycleFailClosedOwner(identity, generation) {
|
|
269
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
270
|
+
if (!sameRpcGeneration(rpcLifecycleFailClosedOwners.get(ownerKey), generation)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
rpcLifecycleFailClosedOwners.delete(ownerKey);
|
|
274
|
+
rpcLifecycleFailClosedIdentities.delete(ownerKey);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
function clearAwaitingRpcActivation(identity, generation) {
|
|
278
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
279
|
+
if (sameRpcGeneration(rpcTurnsAwaitingActivation.get(ownerKey), generation)) {
|
|
280
|
+
rpcTurnsAwaitingActivation.delete(ownerKey);
|
|
281
|
+
rpcTurnsAwaitingActivationIdentities.delete(ownerKey);
|
|
282
|
+
rpcTurnsAwaitingActivationReplayAnchors.delete(ownerKey);
|
|
283
|
+
}
|
|
284
|
+
const pending = pendingRpcTurnTerminals.get(ownerKey);
|
|
285
|
+
if (pending && sameRpcGeneration(pending.generation, generation)) {
|
|
286
|
+
pendingRpcTurnTerminals.delete(ownerKey);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function notifyRpcTeardownBeforeActivation(identity, terminalStatus) {
|
|
290
|
+
const completedBeforeActivation = terminalStatus === 'completed';
|
|
291
|
+
send({
|
|
292
|
+
type: 'user_notify',
|
|
293
|
+
message: completedBeforeActivation
|
|
294
|
+
? 'Codex RPC 消息已执行完成,但会话在本地完成生命周期登记前重启,兜底输出可能未被捕获;原消息未自动重发,如未看到回复请先查看终端结果再人工跟进。'
|
|
295
|
+
: 'Codex RPC 消息已写出,但会话在取得 turn/start 归属前重启;为避免重复执行未自动重发,请按需人工确认结果。',
|
|
296
|
+
turnId: identity.turnId,
|
|
297
|
+
...(identity.dispatchAttempt !== undefined
|
|
298
|
+
? { dispatchAttempt: identity.dispatchAttempt }
|
|
299
|
+
: {}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
219
302
|
function stopCodexRpcEngine() {
|
|
303
|
+
// Invalidate even when the engine has not yet been published: engageCodexRpc
|
|
304
|
+
// may be awaiting a local engine/thread/probe while another IPC handler starts
|
|
305
|
+
// a restart. That stale continuation must never republish the stopped engine.
|
|
306
|
+
rpcEngagementFence.invalidate();
|
|
220
307
|
const engine = codexRpcEngine;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
308
|
+
const ownedRpcTurns = new Set([
|
|
309
|
+
...rpcTurnsAwaitingActivation.keys(),
|
|
310
|
+
...rpcLifecycleFailClosedOwners.keys(),
|
|
311
|
+
...rpcActiveOwners.keys(),
|
|
312
|
+
...settlingRpcTerminalOwners.keys(),
|
|
313
|
+
]);
|
|
314
|
+
// stop() emits exact 'stopped' terminals for every engine-owned native turn.
|
|
315
|
+
// Keep the engine identity installed until those synchronous callbacks have
|
|
316
|
+
// retired their matching rpcActive entries.
|
|
225
317
|
try {
|
|
226
318
|
engine?.stop();
|
|
227
319
|
}
|
|
228
320
|
catch { /* best effort */ }
|
|
321
|
+
// A native terminal can arrive synchronously from engine.stop() while its
|
|
322
|
+
// turn/start continuation is still awaiting activation. handleRpcTurnTerminal
|
|
323
|
+
// buffers that exact terminal; settle it now, before teardown clears the
|
|
324
|
+
// awaiting/pending maps, so an accepted RPC delivery never loses its only
|
|
325
|
+
// terminal at an operator or in-worker restart boundary.
|
|
326
|
+
for (const [ownerKey, pending] of [...pendingRpcTurnTerminals]) {
|
|
327
|
+
const awaiting = rpcTurnsAwaitingActivation.get(ownerKey);
|
|
328
|
+
if (!awaiting || !sameRpcGeneration(awaiting, pending.generation))
|
|
329
|
+
continue;
|
|
330
|
+
const identity = rpcTurnsAwaitingActivationIdentities.get(ownerKey);
|
|
331
|
+
const shouldNotify = !rpcLifecycleFailClosedOwners.has(ownerKey);
|
|
332
|
+
settleRpcTurnTerminal(pending.terminal, pending.generation);
|
|
333
|
+
if (identity && shouldNotify) {
|
|
334
|
+
notifyRpcTeardownBeforeActivation(identity, pending.terminal.status);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// A follow-up can be accepted by the socket but lose its turn/start response
|
|
338
|
+
// while an operator or liveness restart tears down the engine. RPC submissions
|
|
339
|
+
// intentionally bypass InflightInputTracker (replay would risk duplicate
|
|
340
|
+
// execution), so close every still-unbound logical owner explicitly instead
|
|
341
|
+
// of letting its stale continuation assume an ordinary carryover exists.
|
|
342
|
+
for (const [ownerKey, identity] of [...rpcTurnsAwaitingActivationIdentities]) {
|
|
343
|
+
const awaiting = rpcTurnsAwaitingActivation.get(ownerKey);
|
|
344
|
+
if (!awaiting)
|
|
345
|
+
continue;
|
|
346
|
+
clearAwaitingRpcActivation(identity, awaiting);
|
|
347
|
+
emitTurnTerminal(identity.turnId, 'ambiguous', 'rpc_engine_teardown_before_turn_start_ack', identity.dispatchAttempt);
|
|
348
|
+
if (!rpcLifecycleFailClosedOwners.has(ownerKey)) {
|
|
349
|
+
notifyRpcTeardownBeforeActivation(identity);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const pending of [...rpcTerminalHydrationTerminals.values()]) {
|
|
353
|
+
finalizeRpcTurnTerminal(pending.terminal, pending.generation, true);
|
|
354
|
+
}
|
|
355
|
+
// A dispatched delivery for which the app-server never yielded a native turn
|
|
356
|
+
// id cannot receive engine.stop()'s native terminal callback. Publish one
|
|
357
|
+
// exact logical terminal before clearing the owner so the daemon's durable
|
|
358
|
+
// ledger never waits forever on a teardown-only attempt.
|
|
359
|
+
for (const [ownerKey, identity] of rpcLifecycleFailClosedIdentities) {
|
|
360
|
+
if (!rpcLifecycleFailClosedOwners.has(ownerKey))
|
|
361
|
+
continue;
|
|
362
|
+
emitTurnTerminal(identity.turnId, 'ambiguous', 'rpc_engine_teardown_without_native_terminal', identity.dispatchAttempt);
|
|
363
|
+
}
|
|
364
|
+
codexRpcEngine = undefined;
|
|
365
|
+
remoteWsUrl = undefined;
|
|
366
|
+
remoteThreadId = undefined;
|
|
367
|
+
clearRpcEnginePidMarker();
|
|
368
|
+
// Defensive cleanup for a protocol/transport failure that never yielded a
|
|
369
|
+
// native id. No RPC turn may survive an engine teardown holding the lifecycle
|
|
370
|
+
// gate open.
|
|
371
|
+
for (const turn of codexBridgeQueue.peek()) {
|
|
372
|
+
if (turn.finalText === undefined
|
|
373
|
+
&& (turn.rpcActive
|
|
374
|
+
|| ownedRpcTurns.has(rpcTurnOwnerKey({
|
|
375
|
+
turnId: turn.turnId,
|
|
376
|
+
...(turn.dispatchAttempt !== undefined
|
|
377
|
+
? { dispatchAttempt: turn.dispatchAttempt }
|
|
378
|
+
: {}),
|
|
379
|
+
})))) {
|
|
380
|
+
codexBridgeQueue.stopRpcActive(turn.turnId, turn.dispatchAttempt);
|
|
381
|
+
codexBridgeQueue.dropPendingTurn(turn.turnId, turn.dispatchAttempt, true);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
pendingRpcTurnTerminals.clear();
|
|
385
|
+
rpcTurnsAwaitingActivation.clear();
|
|
386
|
+
rpcTurnsAwaitingActivationIdentities.clear();
|
|
387
|
+
rpcTurnsAwaitingActivationReplayAnchors.clear();
|
|
388
|
+
rpcLifecycleFailClosedOwners.clear();
|
|
389
|
+
rpcLifecycleFailClosedIdentities.clear();
|
|
390
|
+
rpcActiveOwners.clear();
|
|
391
|
+
for (const timer of rpcTerminalHydrationTimers.values())
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
rpcTerminalHydrationTimers.clear();
|
|
394
|
+
rpcTerminalHydrationTerminals.clear();
|
|
395
|
+
rpcTerminalHydrationOwners.clear();
|
|
396
|
+
settlingRpcTerminalOwners.clear();
|
|
397
|
+
deferredFreshRpcTurn = undefined;
|
|
398
|
+
stopStructuredStartGraceRecheck();
|
|
399
|
+
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
229
400
|
}
|
|
230
401
|
/** Resolve the persistent pane name + liveness for a backend type, mirroring
|
|
231
402
|
* spawnCli's willReattachPersistent probe (worker.ts) but callable from the init
|
|
@@ -566,8 +737,24 @@ async function engageCodexRpc(cfg) {
|
|
|
566
737
|
return 'not-engaged';
|
|
567
738
|
const wantResume = cfg.resume === true && !!cfg.cliSessionId;
|
|
568
739
|
stopCodexRpcEngine();
|
|
740
|
+
const engagementLease = rpcEngagementFence.begin();
|
|
569
741
|
let engine;
|
|
742
|
+
let enginePidMarker = null;
|
|
743
|
+
let freshDeliveryOwned = false;
|
|
744
|
+
const assertRpcEngagementCurrent = () => {
|
|
745
|
+
if (!rpcEngagementFence.isCurrent(engagementLease)) {
|
|
746
|
+
throw new CliSpawnSupersededError();
|
|
747
|
+
}
|
|
748
|
+
if (!rpcEngagementFence.isLive(engagementLease)) {
|
|
749
|
+
throw new CodexRpcEngineDiedDuringEngagementError();
|
|
750
|
+
}
|
|
751
|
+
};
|
|
570
752
|
try {
|
|
753
|
+
// engage runs immediately before spawnCli, which advances the viewer
|
|
754
|
+
// generation once. Bind every callback from this app-server to that target
|
|
755
|
+
// generation so a late terminal from an old server can never retire a
|
|
756
|
+
// same-(turnId,attempt) entry installed by its replacement.
|
|
757
|
+
const engineCliGeneration = cliSpawnGeneration + 1;
|
|
571
758
|
const cliBin = createCliAdapterSync(cfg.cliId, cfg.cliPathOverride).resolvedBin;
|
|
572
759
|
const engineEnv = { ...redactChildEnv(process.env) };
|
|
573
760
|
engineEnv.PATH = prependBotmuxBin(resolveBotmuxWrapperBinDir(process.env), engineEnv.PATH);
|
|
@@ -583,10 +770,6 @@ async function engageCodexRpc(cfg) {
|
|
|
583
770
|
engineEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
|
|
584
771
|
engineEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
|
|
585
772
|
engineEnv.BOTMUX_SESSION_SCOPE = cfg.rootMessageId?.startsWith('om_') ? 'thread' : 'chat';
|
|
586
|
-
if (cfg.ownerOpenId)
|
|
587
|
-
engineEnv.BOTMUX_OWNER_OPEN_ID = cfg.ownerOpenId;
|
|
588
|
-
else
|
|
589
|
-
delete engineEnv.BOTMUX_OWNER_OPEN_ID;
|
|
590
773
|
// The app-server owns model execution in RPC mode. Its MCP gateway child
|
|
591
774
|
// must inherit the trusted host socket just like a native CLI process does.
|
|
592
775
|
if (sessionMcpGatewayHost) {
|
|
@@ -602,6 +785,9 @@ async function engageCodexRpc(cfg) {
|
|
|
602
785
|
// injects into the TUI — else a 3rd-party-provider bot's app-server silently
|
|
603
786
|
// falls back to the default provider. Re-sanitized (crossed IPC).
|
|
604
787
|
Object.assign(engineEnv, sanitizePerBotEnv(cfg.env));
|
|
788
|
+
// Session identity is host-owned. Pin it after the config-controlled merge,
|
|
789
|
+
// matching every other backend and preventing stale owner resurrection.
|
|
790
|
+
applySessionOwnerEnv(engineEnv, cfg.ownerOpenId);
|
|
605
791
|
engine = new CodexRpcEngine({
|
|
606
792
|
cliBin, cwd: cfg.workingDir, env: engineEnv, sessionId: cfg.sessionId,
|
|
607
793
|
model: cfg.model, reasoningEffort: cfg.reasoningEffort, log: (m) => log(m),
|
|
@@ -609,7 +795,21 @@ async function engageCodexRpc(cfg) {
|
|
|
609
795
|
onRequestUserInput: cfg.cliId === 'traex'
|
|
610
796
|
? (params) => bridgeTraexUserInput(cfg, params)
|
|
611
797
|
: undefined,
|
|
798
|
+
onTurnTerminal: (terminal) => {
|
|
799
|
+
if (!engine)
|
|
800
|
+
return;
|
|
801
|
+
handleRpcTurnTerminal(terminal, {
|
|
802
|
+
engine,
|
|
803
|
+
cliGeneration: engineCliGeneration,
|
|
804
|
+
});
|
|
805
|
+
},
|
|
612
806
|
onDead: () => {
|
|
807
|
+
// Death can race after the final awaited response but before the engine
|
|
808
|
+
// is published below. Record it against this exact engagement even when
|
|
809
|
+
// codexRpcEngine is still undefined; otherwise the continuation can
|
|
810
|
+
// publish a permanently dead/deadNotified engine whose later failures
|
|
811
|
+
// can no longer trigger onDead recovery.
|
|
812
|
+
rpcEngagementFence.markDead(engagementLease);
|
|
613
813
|
if (codexRpcEngine === engine) {
|
|
614
814
|
log('Codex RPC app-server died; replacing the tmux session and re-engaging the thread');
|
|
615
815
|
// failAll() rejects the active sendTurn promise immediately after this
|
|
@@ -627,18 +827,37 @@ async function engageCodexRpc(cfg) {
|
|
|
627
827
|
},
|
|
628
828
|
});
|
|
629
829
|
await engine.start();
|
|
830
|
+
assertRpcEngagementCurrent();
|
|
630
831
|
// Shell tools run under the app-server process tree, not the viewer TUI.
|
|
631
832
|
// Mark its pid before the first turn so `botmux send` resolves the current
|
|
632
833
|
// per-turn identity instead of falling back to a stale/session-only env.
|
|
633
|
-
registerRpcEnginePidMarker(engine.appServerPid);
|
|
834
|
+
enginePidMarker = registerRpcEnginePidMarker(engine.appServerPid);
|
|
634
835
|
const threadId = wantResume ? await engine.resumeThread(cfg.cliSessionId) : await engine.startThread();
|
|
836
|
+
assertRpcEngagementCurrent();
|
|
635
837
|
let outcome = wantResume ? 'resumed' : 'accepted';
|
|
636
838
|
if (!wantResume && cfg.prompt) {
|
|
839
|
+
const firstIdentity = {
|
|
840
|
+
turnId: cfg.turnId ?? `codex-rpc-${randomBytes(8).toString('hex')}`,
|
|
841
|
+
...(cfg.dispatchAttempt !== undefined
|
|
842
|
+
? { dispatchAttempt: cfg.dispatchAttempt }
|
|
843
|
+
: {}),
|
|
844
|
+
};
|
|
845
|
+
const firstGeneration = {
|
|
846
|
+
engine,
|
|
847
|
+
cliGeneration: engineCliGeneration,
|
|
848
|
+
};
|
|
849
|
+
installAwaitingRpcActivation(firstIdentity, firstGeneration);
|
|
637
850
|
// Three-state delivery (P1-1, exactly-once priority): 'accepted' (ack or
|
|
638
851
|
// rollout evidence), 'not-sent' (frame never dispatched → safe paste), or
|
|
639
852
|
// 'ambiguous' (dispatched, unconfirmed → engaged but NEVER resend).
|
|
640
|
-
const first = await engine.sendFirstTurn(cfg.prompt,
|
|
641
|
-
|
|
853
|
+
const first = await engine.sendFirstTurn(cfg.prompt, firstIdentity, (tid) => codexRolloutProbe(cfg.cliId, tid, cfg.prompt, 12_000));
|
|
854
|
+
freshDeliveryOwned = first.outcome !== 'not-sent';
|
|
855
|
+
// restart/close may have settled this exact attempt as ambiguous while the
|
|
856
|
+
// rollout probe was pending. Fence before ANY durable/bridge/global engine
|
|
857
|
+
// mutation; a superseded delivery is never pasted or re-queued.
|
|
858
|
+
assertRpcEngagementCurrent();
|
|
859
|
+
if (first.outcome === 'not-sent') {
|
|
860
|
+
clearAwaitingRpcActivation(firstIdentity, firstGeneration);
|
|
642
861
|
// The turn/start frame never left → the turn cannot have run → tear the
|
|
643
862
|
// engine down and fall back to paste. flushPending marks the bridge once
|
|
644
863
|
// on the paste path — we must NOT pre-mark here or that would double-mark
|
|
@@ -651,16 +870,69 @@ async function engageCodexRpc(cfg) {
|
|
|
651
870
|
catch { /* best effort */ }
|
|
652
871
|
return 'not-engaged';
|
|
653
872
|
}
|
|
873
|
+
// Fresh RPC delivery bypasses flushPending(), which is the normal owner
|
|
874
|
+
// of this durable head-of-line gate. Claim it here only after the frame is
|
|
875
|
+
// known dispatched (accepted or ambiguous); the not-sent branch above
|
|
876
|
+
// safely falls back to paste and lets flushPending claim it once.
|
|
877
|
+
if (firstIdentity.dispatchAttempt !== undefined) {
|
|
878
|
+
durableTurnInFlight = true;
|
|
879
|
+
}
|
|
654
880
|
// Bridge mark ONLY for a confirmed-accepted turn — so the structured
|
|
655
881
|
// fallback can attribute the reply even if the model skips `botmux send`.
|
|
656
882
|
// Marked here (after the outcome, before persistCliSessionId/attach — late
|
|
657
|
-
// attach from offset 0 still matches, timing-safe).
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
if (shouldPreMarkFirstTurn(first))
|
|
662
|
-
|
|
663
|
-
|
|
883
|
+
// attach from offset 0 still matches, timing-safe). An ambiguous outcome
|
|
884
|
+
// gets an attribution-only mark plus a separate fail-closed owner below:
|
|
885
|
+
// it cannot publish false idle or head-of-line a successor because no
|
|
886
|
+
// successor is admitted until terminal/teardown retires that owner.
|
|
887
|
+
if (shouldPreMarkFirstTurn(first.outcome)) {
|
|
888
|
+
if (!first.nativeTurnId) {
|
|
889
|
+
// Positive rollout evidence proves delivery, but without a native id
|
|
890
|
+
// no terminal can be associated precisely. Preserve exactly-once
|
|
891
|
+
// (never paste/requeue) and hold the explicit fail-closed lifecycle
|
|
892
|
+
// gate until a structured terminal or engine teardown instead of
|
|
893
|
+
// guessing a latest turn.
|
|
894
|
+
codexBridgeMarkPendingTurn(cfg.prompt, firstIdentity.turnId, firstIdentity.dispatchAttempt, awaitingRpcActivationReplayAnchorMs(firstIdentity, firstGeneration));
|
|
895
|
+
installRpcLifecycleFailClosedOwner(firstIdentity, firstGeneration);
|
|
896
|
+
deferredFreshRpcTurn = {
|
|
897
|
+
identity: firstIdentity,
|
|
898
|
+
generation: firstGeneration,
|
|
899
|
+
};
|
|
900
|
+
log(`Codex RPC fresh accepted turn has no native id; lifecycle failed closed for ${firstIdentity.turnId}`);
|
|
901
|
+
send({
|
|
902
|
+
type: 'user_notify',
|
|
903
|
+
message: 'Codex RPC 首条消息已确认落盘,但无法取得原生 turn id;为避免重复执行未重发,并保持忙碌直到检测到终态或会话重启。',
|
|
904
|
+
turnId: firstIdentity.turnId,
|
|
905
|
+
...(firstIdentity.dispatchAttempt !== undefined
|
|
906
|
+
? { dispatchAttempt: firstIdentity.dispatchAttempt }
|
|
907
|
+
: {}),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
// Keep terminal delivery deferred until spawnCli has attached and
|
|
912
|
+
// baselined the rollout; otherwise an ultra-fast first completion can
|
|
913
|
+
// retire the bridge mark before fallback output is harvested.
|
|
914
|
+
activateRpcTurnLifecycle(firstIdentity, cfg.prompt, false, firstGeneration, true);
|
|
915
|
+
deferredFreshRpcTurn = {
|
|
916
|
+
identity: firstIdentity,
|
|
917
|
+
generation: firstGeneration,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
// A dispatched-but-unconfirmed first turn is never re-sent. Keep an
|
|
923
|
+
// exact attribution mark plus an explicit fail-closed gate even without
|
|
924
|
+
// a native id. A structured transcript terminal can retire it; a later
|
|
925
|
+
// native terminal without an owner is intentionally ignored instead of
|
|
926
|
+
// guessed. If no structured terminal becomes visible, exact engine
|
|
927
|
+
// teardown is the expected fallback and drops this attempt's mark.
|
|
928
|
+
codexBridgeMarkPendingTurn(cfg.prompt, firstIdentity.turnId, firstIdentity.dispatchAttempt, awaitingRpcActivationReplayAnchorMs(firstIdentity, firstGeneration));
|
|
929
|
+
installRpcLifecycleFailClosedOwner(firstIdentity, firstGeneration);
|
|
930
|
+
deferredFreshRpcTurn = {
|
|
931
|
+
identity: firstIdentity,
|
|
932
|
+
generation: firstGeneration,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (first.outcome === 'accepted' && cfg.queuedActivationToken) {
|
|
664
936
|
// Fresh RPC input bypasses pendingMessages/flushPending entirely. The
|
|
665
937
|
// app-server's accepted turn/start (or positive rollout proof) is the
|
|
666
938
|
// durable submission boundary for the daemon's queued-opening journal.
|
|
@@ -670,7 +942,7 @@ async function engageCodexRpc(cfg) {
|
|
|
670
942
|
activationToken: cfg.queuedActivationToken,
|
|
671
943
|
});
|
|
672
944
|
}
|
|
673
|
-
outcome = first; //
|
|
945
|
+
outcome = first.outcome; // accepted | ambiguous — both stay engaged, prompt never re-queued
|
|
674
946
|
}
|
|
675
947
|
codexRpcEngine = engine;
|
|
676
948
|
remoteWsUrl = engine.wsUrl;
|
|
@@ -683,15 +955,43 @@ async function engageCodexRpc(cfg) {
|
|
|
683
955
|
return outcome;
|
|
684
956
|
}
|
|
685
957
|
catch (err) {
|
|
958
|
+
if (err instanceof CliSpawnSupersededError
|
|
959
|
+
|| !rpcEngagementFence.isCurrent(engagementLease)) {
|
|
960
|
+
log('Codex RPC engagement was superseded; stopping only its local app-server and preserving the replacement generation');
|
|
961
|
+
try {
|
|
962
|
+
engine?.stop();
|
|
963
|
+
}
|
|
964
|
+
catch { /* best effort */ }
|
|
965
|
+
if (enginePidMarker)
|
|
966
|
+
clearRpcEnginePidMarker(enginePidMarker);
|
|
967
|
+
throw err instanceof CliSpawnSupersededError
|
|
968
|
+
? err
|
|
969
|
+
: new CliSpawnSupersededError();
|
|
970
|
+
}
|
|
971
|
+
if (err instanceof CodexRpcEngineDiedDuringEngagementError && freshDeliveryOwned) {
|
|
972
|
+
// The first frame may already have executed. Never turn this into
|
|
973
|
+
// `not-engaged`, because the init path would paste the same prompt into a
|
|
974
|
+
// native viewer and duplicate side effects. Tear down exact local state
|
|
975
|
+
// and fail the worker generation so durable recovery owns the next step.
|
|
976
|
+
log('Codex RPC app-server died after the fresh delivery boundary; aborting init without paste fallback');
|
|
977
|
+
clearRpcEnginePidMarker();
|
|
978
|
+
try {
|
|
979
|
+
engine?.stop();
|
|
980
|
+
}
|
|
981
|
+
catch { /* best effort */ }
|
|
982
|
+
stopCodexRpcEngine();
|
|
983
|
+
throw err;
|
|
984
|
+
}
|
|
686
985
|
log(`Codex RPC input failed to start (${err?.message ?? err}); falling back to paste mode`);
|
|
687
986
|
clearRpcEnginePidMarker();
|
|
688
987
|
try {
|
|
689
988
|
engine?.stop();
|
|
690
989
|
}
|
|
691
990
|
catch { /* best effort */ } // P1-3a: stop the LOCAL ref (codexRpcEngine may be unassigned)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
991
|
+
// The local engine may not yet have been published into codexRpcEngine.
|
|
992
|
+
// Still clear any exact lifecycle state its callbacks installed before the
|
|
993
|
+
// failure; otherwise paste fallback could inherit a stale fail-closed mark.
|
|
994
|
+
stopCodexRpcEngine();
|
|
695
995
|
return 'not-engaged';
|
|
696
996
|
}
|
|
697
997
|
}
|
|
@@ -754,6 +1054,9 @@ function armRpcStartupDialogDismiss() {
|
|
|
754
1054
|
};
|
|
755
1055
|
rpcDialogDismissTimer = setTimeout(tick, 2500);
|
|
756
1056
|
}
|
|
1057
|
+
/** Monotonic identity for the owned/adopted CLI backend generation. Deferred
|
|
1058
|
+
* callbacks capture it and must re-check after every await before touching
|
|
1059
|
+
* transcript lifecycle or user-visible state. */
|
|
757
1060
|
let cliSpawnGeneration = 0;
|
|
758
1061
|
class CliSpawnSupersededError extends Error {
|
|
759
1062
|
constructor() {
|
|
@@ -761,6 +1064,12 @@ class CliSpawnSupersededError extends Error {
|
|
|
761
1064
|
this.name = 'CliSpawnSupersededError';
|
|
762
1065
|
}
|
|
763
1066
|
}
|
|
1067
|
+
class CodexRpcEngineDiedDuringEngagementError extends Error {
|
|
1068
|
+
constructor() {
|
|
1069
|
+
super('Codex RPC app-server died before engagement publication');
|
|
1070
|
+
this.name = 'CodexRpcEngineDiedDuringEngagementError';
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
764
1073
|
let cliPidMarker = null; // path to .botmux-cli-pids/<pid>
|
|
765
1074
|
let seatbeltProfilePath = null; // per-session Seatbelt .sb profile to rm at exit (external-wrapper read isolation)
|
|
766
1075
|
let sandboxStopWatcher = null; // stop fn for the sandbox outbox watcher
|
|
@@ -1045,6 +1354,10 @@ let activeRestartAttemptId;
|
|
|
1045
1354
|
/** Distinguishes a replacement's synchronous ready signal (Riff) from a late
|
|
1046
1355
|
* idle callback emitted by the backend being torn down. */
|
|
1047
1356
|
let replacementSpawnInProgress = false;
|
|
1357
|
+
let structuredStartGraceRecheckTimer = null;
|
|
1358
|
+
let structuredRejectedReadyEvidenceGeneration;
|
|
1359
|
+
const ptyOutputGeneration = new PtyOutputGeneration();
|
|
1360
|
+
const adoptWriteQueue = new AsyncSerialQueue();
|
|
1048
1361
|
/** The effectiveResume flag used by the most recent spawnCli call. Written
|
|
1049
1362
|
* immediately after the two-tier fallback check so late-attach timers
|
|
1050
1363
|
* (hermes, cursor, etc.) can read THE SAME semantics the spawn used,
|
|
@@ -1527,6 +1840,25 @@ function shortCorrelationId(value) {
|
|
|
1527
1840
|
* an owned CLI restart was fenced. Normal raw_input commands are still
|
|
1528
1841
|
* delivered immediately (including while busy). */
|
|
1529
1842
|
const pendingRawInputs = freshnessInputQueue.raw;
|
|
1843
|
+
/** Adopt messages accepted while native /rename owns the TUI. They cannot use
|
|
1844
|
+
* pendingMessages because adopt writes need writeAdoptMessage's transcript
|
|
1845
|
+
* baseline + complete adapter lifecycle. flushPending drains them only after
|
|
1846
|
+
* deferred raw commands and the latest native rename have settled. */
|
|
1847
|
+
const pendingAdoptMessages = [];
|
|
1848
|
+
function captureAdoptWriteFence() {
|
|
1849
|
+
if (!backend || cliRestartInProgress || rawInputRestartGate)
|
|
1850
|
+
return undefined;
|
|
1851
|
+
return { generation: cliSpawnGeneration, backend };
|
|
1852
|
+
}
|
|
1853
|
+
/** A queued adopt task may outlive its CLI. Never let process-lifetime queue
|
|
1854
|
+
* work run against backend=null or a replacement backend generation. Tasks
|
|
1855
|
+
* that fail this check have not written anything and are safe to requeue. */
|
|
1856
|
+
function adoptWriteFenceIsCurrent(fence) {
|
|
1857
|
+
return cliSpawnGeneration === fence.generation
|
|
1858
|
+
&& backend === fence.backend
|
|
1859
|
+
&& !cliRestartInProgress
|
|
1860
|
+
&& !rawInputRestartGate;
|
|
1861
|
+
}
|
|
1530
1862
|
/** Latest requested canonical session title. Unlike a normal prompt this is an
|
|
1531
1863
|
* administrative TUI command: never type-ahead while the agent is busy, never
|
|
1532
1864
|
* open a model turn, and latest-wins if several renames arrive before idle. */
|
|
@@ -1546,32 +1878,45 @@ function stopNativeSessionTitleSync() {
|
|
|
1546
1878
|
forceClose();
|
|
1547
1879
|
nativeSessionTitleSyncForceClosers.clear();
|
|
1548
1880
|
}
|
|
1549
|
-
|
|
1550
|
-
* prompt. Blocks type-ahead user messages from racing into the command UI. */
|
|
1551
|
-
let sessionRenameInFlight = false;
|
|
1881
|
+
let sessionRenamePhase = 'idle';
|
|
1552
1882
|
const SESSION_RENAME_IDLE_TIMEOUT_MS = 5_000;
|
|
1553
1883
|
let sessionRenameIdleTimer = null;
|
|
1554
|
-
function
|
|
1884
|
+
function sessionRenameInFlight() {
|
|
1885
|
+
return sessionRenamePhase !== 'idle';
|
|
1886
|
+
}
|
|
1887
|
+
function forceClearSessionRenameInFlight() {
|
|
1555
1888
|
if (sessionRenameIdleTimer) {
|
|
1556
1889
|
clearTimeout(sessionRenameIdleTimer);
|
|
1557
1890
|
sessionRenameIdleTimer = null;
|
|
1558
1891
|
}
|
|
1559
|
-
|
|
1892
|
+
sessionRenamePhase = 'idle';
|
|
1893
|
+
}
|
|
1894
|
+
/** A prompt can release rename only after the command Enter was actually sent.
|
|
1895
|
+
* A fast final from an older adopt write may arrive while rename is merely
|
|
1896
|
+
* reserved behind the queue; keep that reservation and let the queued helper
|
|
1897
|
+
* consume the newly-ready prompt. */
|
|
1898
|
+
function settleSessionRenameOnPrompt() {
|
|
1899
|
+
if (sessionRenamePhase === 'sent')
|
|
1900
|
+
forceClearSessionRenameInFlight();
|
|
1901
|
+
}
|
|
1902
|
+
function settleFailedSessionRenameWrite() {
|
|
1903
|
+
if (sessionRenamePhase === 'writing')
|
|
1904
|
+
sessionRenamePhase = 'sent';
|
|
1560
1905
|
}
|
|
1561
1906
|
/** Fail open if a CLI executes /rename without emitting enough redraw output
|
|
1562
1907
|
* for IdleDetector to rediscover the prompt. The verified Codex/Claude paths
|
|
1563
1908
|
* normally settle immediately; this cap prevents one administrative command
|
|
1564
1909
|
* from wedging all later Lark messages forever. */
|
|
1565
1910
|
function armSessionRenameIdleTimeout() {
|
|
1566
|
-
if (
|
|
1911
|
+
if (sessionRenamePhase !== 'sent')
|
|
1567
1912
|
return;
|
|
1568
1913
|
if (sessionRenameIdleTimer)
|
|
1569
1914
|
clearTimeout(sessionRenameIdleTimer);
|
|
1570
1915
|
sessionRenameIdleTimer = setTimeout(() => {
|
|
1571
1916
|
sessionRenameIdleTimer = null;
|
|
1572
|
-
if (
|
|
1917
|
+
if (sessionRenamePhase !== 'sent')
|
|
1573
1918
|
return;
|
|
1574
|
-
|
|
1919
|
+
sessionRenamePhase = 'idle';
|
|
1575
1920
|
// The timeout specifically means prompt redraw detection failed. Fail open
|
|
1576
1921
|
// by restoring the gate explicitly; otherwise a deferred raw_input is the
|
|
1577
1922
|
// only queued work and flushPending would keep waiting for the very signal
|
|
@@ -1587,96 +1932,147 @@ function armSessionRenameIdleTimeout() {
|
|
|
1587
1932
|
* Commands received while /rename owns the TUI are deferred by the IPC handler
|
|
1588
1933
|
* and come through this same function after the prompt returns. */
|
|
1589
1934
|
async function deliverRawInput(msg) {
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1935
|
+
const writeRawInput = async (targetBackend, fence, onFollowUp) => {
|
|
1936
|
+
let sent = false;
|
|
1937
|
+
let recoveryFailureReason;
|
|
1938
|
+
try {
|
|
1939
|
+
await sendRawCommandLineWithRecoveryFence(targetBackend, msg.content, () => {
|
|
1940
|
+
renderer?.markNewTurn();
|
|
1941
|
+
usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
1942
|
+
if (tmuxScrolledHalfPages > 0)
|
|
1943
|
+
exitTmuxScrollMode();
|
|
1944
|
+
currentBotmuxTurnId = msg.turnId;
|
|
1945
|
+
currentBotmuxDispatchAttempt = undefined;
|
|
1946
|
+
currentVcMeetingImTurnOrigin = undefined;
|
|
1947
|
+
writeCliPidMarker();
|
|
1948
|
+
publishSandboxRelayCapability();
|
|
1949
|
+
});
|
|
1950
|
+
sent = true;
|
|
1951
|
+
if (fence && !adoptWriteFenceIsCurrent(fence)) {
|
|
1952
|
+
send({
|
|
1953
|
+
type: 'user_notify',
|
|
1954
|
+
message: `Passthrough command ${msg.content} became ambiguous because the adopted CLI backend changed while it was being written. Its dependent follow-up was withheld; please verify the terminal and retry the bundle explicitly.`,
|
|
1955
|
+
});
|
|
1956
|
+
sent = false;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
catch (err) {
|
|
1960
|
+
recoveryFailureReason = err instanceof SubmissionWriteError
|
|
1961
|
+
? err.recoveryFailureReason
|
|
1962
|
+
: undefined;
|
|
1963
|
+
// Do not send a bundled follow-up or another queued command against a
|
|
1964
|
+
// backend whose literal command write failed.
|
|
1965
|
+
log(`Passthrough slash command failed (${msg.content}): ${err?.message ?? err}`);
|
|
1966
|
+
const failedTurnId = msg.followUpTurnId ?? msg.turnId;
|
|
1967
|
+
if (failedTurnId && !recoveryFailureReason) {
|
|
1968
|
+
emitTurnTerminal(failedTurnId, 'ambiguous', 'raw_input_write_failed');
|
|
1969
|
+
}
|
|
1970
|
+
send({
|
|
1971
|
+
type: 'user_notify',
|
|
1972
|
+
...(failedTurnId ? { turnId: failedTurnId } : {}),
|
|
1973
|
+
message: t(msg.followUpContent
|
|
1974
|
+
? (recoveryFailureReason ? 'worker.raw_input_failed_recovery' : 'worker.raw_input_failed')
|
|
1975
|
+
: (recoveryFailureReason ? 'worker.raw_input_failed_command_only_recovery' : 'worker.raw_input_failed_command_only'), {
|
|
1976
|
+
cliName: cliName(),
|
|
1977
|
+
reason: recoveryFailureReason ?? '',
|
|
1978
|
+
}),
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
const accepted = finalizeRawCommandDelivery({
|
|
1982
|
+
accepted: sent,
|
|
1983
|
+
durableActivation: !!msg.queuedActivationToken,
|
|
1984
|
+
acknowledgeActivation: !!msg.queuedActivationToken,
|
|
1985
|
+
hasFollowUp: !!onFollowUp,
|
|
1986
|
+
onAccepted: () => {
|
|
1987
|
+
isPromptReady = false;
|
|
1988
|
+
idleDetector?.reset();
|
|
1989
|
+
log(`Passthrough slash command: ${msg.content}`);
|
|
1990
|
+
},
|
|
1991
|
+
// Non-adopt follows the normal busy queue. Adopt keeps the complete
|
|
1992
|
+
// adapter lifecycle in runAdoptRawInputSequence below.
|
|
1993
|
+
onFollowUp: () => onFollowUp?.(),
|
|
1994
|
+
// Pending-repo raw openings are durable too. ACK only after text + Enter.
|
|
1995
|
+
onActivationAck: () => send({
|
|
1996
|
+
type: 'queued_activation_submitted',
|
|
1997
|
+
sessionId,
|
|
1998
|
+
activationToken: msg.queuedActivationToken,
|
|
1644
1999
|
}),
|
|
2000
|
+
onDurableFailure: () => {
|
|
2001
|
+
isPromptReady = false;
|
|
2002
|
+
log('Durable raw activation write rejected; retiring worker generation');
|
|
2003
|
+
void sendFatalWorkerErrorAndExit(new Error('durable raw activation was not accepted by the backend'), msg.turnId);
|
|
2004
|
+
},
|
|
2005
|
+
});
|
|
2006
|
+
if (!accepted && !msg.queuedActivationToken) {
|
|
2007
|
+
log(`Passthrough slash command was not accepted by the backend: ${msg.content}`);
|
|
2008
|
+
}
|
|
2009
|
+
return accepted;
|
|
2010
|
+
};
|
|
2011
|
+
if (lastInitConfig?.adoptMode) {
|
|
2012
|
+
const fence = captureAdoptWriteFence();
|
|
2013
|
+
if (!fence) {
|
|
2014
|
+
pendingRawInputs.push(msg);
|
|
2015
|
+
log(`Deferred adopt passthrough until a stable CLI generation is ready: ${msg.content}`);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const followUpContent = msg.followUpContent;
|
|
2019
|
+
// Adopt raw commands, their bundled follow-up and ordinary adopt messages
|
|
2020
|
+
// all share one queue. Keep the queue until writeAdoptMessage has completed
|
|
2021
|
+
// transcript marking, adapter/history verification and lifecycle settling.
|
|
2022
|
+
await runAdoptRawInputSequence({
|
|
2023
|
+
queue: adoptWriteQueue,
|
|
2024
|
+
isCurrent: () => adoptWriteFenceIsCurrent(fence),
|
|
2025
|
+
onStaleBeforeWrite: () => {
|
|
2026
|
+
pendingRawInputs.push(msg);
|
|
2027
|
+
log(`Re-queued stale adopt passthrough for the replacement CLI generation: ${msg.content}`);
|
|
2028
|
+
},
|
|
2029
|
+
onStaleBeforeFollowUp: () => {
|
|
2030
|
+
if (!followUpContent)
|
|
2031
|
+
return;
|
|
2032
|
+
// The raw command already landed on the previous backend. Replaying
|
|
2033
|
+
// only its dependent prompt into the replacement could run in the
|
|
2034
|
+
// wrong repo/session (for example after `/cd ...`). Hold the bundle
|
|
2035
|
+
// for an explicit user retry instead of splitting its atomic meaning.
|
|
2036
|
+
send({
|
|
2037
|
+
type: 'user_notify',
|
|
2038
|
+
message: `Passthrough command ${msg.content} completed on the previous CLI, but the backend changed before its dependent follow-up. The follow-up was withheld; verify the terminal and retry the bundle explicitly.`,
|
|
2039
|
+
});
|
|
2040
|
+
log(`Withheld bundled adopt follow-up after CLI generation changed (${followUpContent.length} chars)`);
|
|
2041
|
+
},
|
|
2042
|
+
writeRawInput: () => writeRawInput(fence.backend, fence),
|
|
2043
|
+
...(followUpContent
|
|
2044
|
+
? {
|
|
2045
|
+
writeFollowUp: async () => {
|
|
2046
|
+
const result = await writeAdoptMessage(followUpContent, msg.followUpTurnId, undefined, undefined, fence);
|
|
2047
|
+
if (result === 'stale-before-write') {
|
|
2048
|
+
send({
|
|
2049
|
+
type: 'user_notify',
|
|
2050
|
+
message: `The CLI backend changed after passthrough command ${msg.content} but before its dependent follow-up. The follow-up was withheld; verify the terminal and retry the bundle explicitly.`,
|
|
2051
|
+
});
|
|
2052
|
+
log(`Withheld stale bundled adopt follow-up after raw command (${followUpContent.length} chars)`);
|
|
2053
|
+
}
|
|
2054
|
+
else if (result === 'completed') {
|
|
2055
|
+
log(`Completed adopt follow-up after raw input (${followUpContent.length} chars)`);
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
}
|
|
2059
|
+
: {}),
|
|
1645
2060
|
});
|
|
1646
2061
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
sendToPty(msg.followUpContent, msg.followUpTurnId, {
|
|
1662
|
-
codexAppInput: msg.followUpCodexAppInput,
|
|
1663
|
-
});
|
|
1664
|
-
log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
|
|
1665
|
-
},
|
|
1666
|
-
// Pending-repo raw openings are durable too. ACK only after text + Enter.
|
|
1667
|
-
onActivationAck: () => send({
|
|
1668
|
-
type: 'queued_activation_submitted',
|
|
1669
|
-
sessionId,
|
|
1670
|
-
activationToken: msg.queuedActivationToken,
|
|
1671
|
-
}),
|
|
1672
|
-
onDurableFailure: () => {
|
|
1673
|
-
isPromptReady = false;
|
|
1674
|
-
log(`Durable raw activation write rejected; retiring worker generation`);
|
|
1675
|
-
void sendFatalWorkerErrorAndExit(new Error('durable raw activation was not accepted by the backend'), msg.turnId);
|
|
1676
|
-
},
|
|
1677
|
-
});
|
|
1678
|
-
if (!sent && !msg.queuedActivationToken) {
|
|
1679
|
-
log(`Passthrough slash command was not accepted by the backend: ${msg.content}`);
|
|
2062
|
+
else {
|
|
2063
|
+
const targetBackend = backend;
|
|
2064
|
+
if (!targetBackend) {
|
|
2065
|
+
pendingRawInputs.push(msg);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
await writeRawInput(targetBackend, undefined, msg.followUpContent
|
|
2069
|
+
? () => {
|
|
2070
|
+
sendToPty(msg.followUpContent, msg.followUpTurnId, {
|
|
2071
|
+
codexAppInput: msg.followUpCodexAppInput,
|
|
2072
|
+
});
|
|
2073
|
+
log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
|
|
2074
|
+
}
|
|
2075
|
+
: undefined);
|
|
1680
2076
|
}
|
|
1681
2077
|
// A pending /rename may have been held by the command-write mutex. It still
|
|
1682
2078
|
// waits for a genuine prompt because isPromptReady was cleared above.
|
|
@@ -1860,7 +2256,9 @@ function authorizeManagedSend(claim) {
|
|
|
1860
2256
|
}
|
|
1861
2257
|
: { ok: false, error: `${decision.errorCode}: ${decision.error}` };
|
|
1862
2258
|
}
|
|
1863
|
-
function clearRpcEnginePidMarker() {
|
|
2259
|
+
function clearRpcEnginePidMarker(expectedMarker) {
|
|
2260
|
+
if (expectedMarker !== undefined && rpcEnginePidMarker !== expectedMarker)
|
|
2261
|
+
return;
|
|
1864
2262
|
if (!rpcEnginePidMarker)
|
|
1865
2263
|
return;
|
|
1866
2264
|
try {
|
|
@@ -1872,17 +2270,19 @@ function clearRpcEnginePidMarker() {
|
|
|
1872
2270
|
function registerRpcEnginePidMarker(pid) {
|
|
1873
2271
|
clearRpcEnginePidMarker();
|
|
1874
2272
|
if (!pid || !process.env.SESSION_DATA_DIR)
|
|
1875
|
-
return;
|
|
2273
|
+
return null;
|
|
1876
2274
|
try {
|
|
1877
2275
|
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
1878
2276
|
mkdirSync(markersDir, { recursive: true });
|
|
1879
2277
|
rpcEnginePidMarker = join(markersDir, String(pid));
|
|
1880
2278
|
writeCliPidMarker();
|
|
1881
2279
|
log(`Codex RPC app-server PID marker written: ${pid}`);
|
|
2280
|
+
return rpcEnginePidMarker;
|
|
1882
2281
|
}
|
|
1883
2282
|
catch (err) {
|
|
1884
2283
|
rpcEnginePidMarker = null;
|
|
1885
2284
|
log(`Failed to write Codex RPC app-server PID marker: ${err?.message ?? err}`);
|
|
2285
|
+
return null;
|
|
1886
2286
|
}
|
|
1887
2287
|
}
|
|
1888
2288
|
function writeCliPidMarker() {
|
|
@@ -4218,6 +4618,10 @@ function emitReadyTurns(opts = {}) {
|
|
|
4218
4618
|
const remainingPending = bridgeQueue.peek();
|
|
4219
4619
|
const nextPendingMarkTimeMs = remainingPending.length > 0 ? remainingPending[0].markTimeMs : undefined;
|
|
4220
4620
|
const cache = new Map();
|
|
4621
|
+
// Turns suppressed as GENUINE SILENCE — see emitReadyCodexTurns for the full
|
|
4622
|
+
// rationale. Tracked by object identity across this function's two loops so
|
|
4623
|
+
// the terminal can carry positive silence evidence for a durable/async caller.
|
|
4624
|
+
const nothingToSendTurns = new Set();
|
|
4221
4625
|
for (let i = 0; i < ready.length; i++) {
|
|
4222
4626
|
const turn = ready[i];
|
|
4223
4627
|
const nextBoundaryMs = (i + 1 < ready.length ? ready[i + 1].markTimeMs : nextPendingMarkTimeMs);
|
|
@@ -4250,6 +4654,11 @@ function emitReadyTurns(opts = {}) {
|
|
|
4250
4654
|
if (shouldSuppressBridgeEmit(gateInput, nextBoundaryMs, markers, adoptMode)) {
|
|
4251
4655
|
const reason = turn.isLocal ? 'local-typed' : 'model called botmux send within window';
|
|
4252
4656
|
log(`Bridge fallback suppressed for turn ${turn.turnId.substring(0, 8)} (${reason})`);
|
|
4657
|
+
// Positive silence evidence for the terminal — only a bare nothing-to-send
|
|
4658
|
+
// sentinel (no prose, no send), never "already sent" / local-typed.
|
|
4659
|
+
if (!adoptMode && isBridgeNothingToSendFinal(assistantText)) {
|
|
4660
|
+
nothingToSendTurns.add(turn);
|
|
4661
|
+
}
|
|
4253
4662
|
notifyExplicitReplyObserved(turn.turnId, explicitReplyMarkerForTurnWindow(gateInput, nextBoundaryMs, markers, adoptMode));
|
|
4254
4663
|
continue;
|
|
4255
4664
|
}
|
|
@@ -4318,7 +4727,7 @@ function emitReadyTurns(opts = {}) {
|
|
|
4318
4727
|
// only after all corresponding final_output IPC messages have been queued so
|
|
4319
4728
|
// the daemon observes a stable per-turn ordering.
|
|
4320
4729
|
for (const turn of ready)
|
|
4321
|
-
emitTurnTerminal(turn.turnId, 'completed', undefined, turn.dispatchAttempt);
|
|
4730
|
+
emitTurnTerminal(turn.turnId, 'completed', undefined, turn.dispatchAttempt, nothingToSendTurns.has(turn) ? 'nothing_to_send' : undefined);
|
|
4322
4731
|
}
|
|
4323
4732
|
/** Drain `path` from `fromOffset` and feed the events to the bridge queue
|
|
4324
4733
|
* with that path as the source stamp. Pure side-effects on bridgeQueue +
|
|
@@ -4346,6 +4755,23 @@ function codexBridgeFallbackActive() {
|
|
|
4346
4755
|
// services/structured-bridge-clis.ts (single source of the allowlist).
|
|
4347
4756
|
return isStructuredBridgeFallbackActive(lastInitConfig?.cliId, lastInitConfig?.adoptMode === true);
|
|
4348
4757
|
}
|
|
4758
|
+
/** Only drivers with a complete normal-final + interrupted-terminal contract
|
|
4759
|
+
* may let a transcript-started turn override the screen-ready heuristic. */
|
|
4760
|
+
function hasStructuredLifecycleBlock() {
|
|
4761
|
+
if (rpcTurnsAwaitingActivation.size > 0)
|
|
4762
|
+
return true;
|
|
4763
|
+
if (rpcLifecycleFailClosedOwners.size > 0)
|
|
4764
|
+
return true;
|
|
4765
|
+
if (rpcTerminalHydrationOwners.size > 0)
|
|
4766
|
+
return true;
|
|
4767
|
+
// rpcActive is an explicit native app-server ownership proof and is valid for
|
|
4768
|
+
// every RPC-capable Codex-family adapter (currently codex + traex), including
|
|
4769
|
+
// drivers intentionally excluded from transcript-only lifecycle blocking.
|
|
4770
|
+
if (codexBridgeQueue.peek().some(turn => turn.rpcActive === true && turn.finalText === undefined))
|
|
4771
|
+
return true;
|
|
4772
|
+
return isStructuredBridgeLifecycleBlockingCli(lastInitConfig?.cliId)
|
|
4773
|
+
&& codexBridgeQueue.hasBlockingTurn();
|
|
4774
|
+
}
|
|
4349
4775
|
function structuredBridgeIsCodex() {
|
|
4350
4776
|
return lastInitConfig?.cliId === 'codex';
|
|
4351
4777
|
}
|
|
@@ -4400,7 +4826,10 @@ function codexBridgeStartTimer() {
|
|
|
4400
4826
|
// up. Emitting here when isPromptReady=true closes that window.
|
|
4401
4827
|
// Codex's queue only releases turns on `assistant_final` (the model's
|
|
4402
4828
|
// declared end-of-turn), so a tick-driven emit can't accidentally
|
|
4403
|
-
// publish a half-streamed response.
|
|
4829
|
+
// publish a half-streamed response. The finally-path also expires stale
|
|
4830
|
+
// attribution heads after every tick even when no transcript exists yet or
|
|
4831
|
+
// its offset produced no events; otherwise an adapter returning undefined
|
|
4832
|
+
// before late-attach could leave a bare fingerprint at the head forever.
|
|
4404
4833
|
codexBridgeTimer = setInterval(() => {
|
|
4405
4834
|
try {
|
|
4406
4835
|
if (structuredBridgeIsHermes()) {
|
|
@@ -4512,6 +4941,18 @@ function codexBridgeStartTimer() {
|
|
|
4512
4941
|
catch (err) {
|
|
4513
4942
|
log(`Codex bridge tick error: ${err.message}`);
|
|
4514
4943
|
}
|
|
4944
|
+
finally {
|
|
4945
|
+
// All branch returns above still pass through here. Ingest always gets
|
|
4946
|
+
// first chance to claim a boundary-time user event; only afterwards do
|
|
4947
|
+
// we expire an unstarted head, replay buffered successors, and emit any
|
|
4948
|
+
// completion created by that replay in the same call stack.
|
|
4949
|
+
try {
|
|
4950
|
+
pruneExpiredStructuredHeadsAndEmit('structured bridge tick');
|
|
4951
|
+
}
|
|
4952
|
+
catch (err) {
|
|
4953
|
+
log(`Codex bridge tick expiry error: ${err.message}`);
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4515
4956
|
}, 1000);
|
|
4516
4957
|
}
|
|
4517
4958
|
function hermesBridgeAttach(mode) {
|
|
@@ -4547,7 +4988,8 @@ function hermesBridgeIngest() {
|
|
|
4547
4988
|
if (filtered.events.length > 0)
|
|
4548
4989
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
4549
4990
|
codexBridgeQueue.ingest(filtered.events);
|
|
4550
|
-
|
|
4991
|
+
pruneExpiredStructuredHeadsAndEmit('Hermes ingest');
|
|
4992
|
+
if (filtered.events.some(event => event.kind === 'assistant_final')) {
|
|
4551
4993
|
idleDetector?.fireIdle();
|
|
4552
4994
|
}
|
|
4553
4995
|
}
|
|
@@ -4559,6 +5001,7 @@ function mtrBridgeAttach(source, mode) {
|
|
|
4559
5001
|
const { history, live } = splitCodexEventsByCutoff(result.events, cutoff);
|
|
4560
5002
|
codexBridgeQueue.absorb(history);
|
|
4561
5003
|
codexBridgeQueue.ingest(live);
|
|
5004
|
+
pruneExpiredStructuredHeadsAndEmit('MTR split-live attach');
|
|
4562
5005
|
mtrBridgeOffset = result.newOffset;
|
|
4563
5006
|
mtrBridgeBaselineDone = true;
|
|
4564
5007
|
log(`MTR bridge split-live: ${source.dbPath}#${source.sessionId} (history=${history.length}, live=${live.length}, cutoff=${cutoff}, offset=${mtrBridgeOffset})`);
|
|
@@ -4587,7 +5030,8 @@ function mtrBridgeIngest() {
|
|
|
4587
5030
|
if (result.events.length > 0)
|
|
4588
5031
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
4589
5032
|
codexBridgeQueue.ingest(result.events);
|
|
4590
|
-
|
|
5033
|
+
pruneExpiredStructuredHeadsAndEmit('MTR ingest');
|
|
5034
|
+
if (result.events.some(event => event.kind === 'assistant_final')) {
|
|
4591
5035
|
idleDetector?.fireIdle();
|
|
4592
5036
|
}
|
|
4593
5037
|
}
|
|
@@ -4619,6 +5063,14 @@ function codexBridgeAttach(rolloutPath, mode) {
|
|
|
4619
5063
|
const { history, live } = splitCodexEventsByCutoff(result.events, cutoff);
|
|
4620
5064
|
codexBridgeQueue.absorb(history);
|
|
4621
5065
|
codexBridgeQueue.ingest(live);
|
|
5066
|
+
pruneExpiredStructuredHeadsAndEmit('structured split-live attach');
|
|
5067
|
+
// Late attach can discover an already-completed live turn in the same
|
|
5068
|
+
// drain. Re-drive prompt readiness from that terminal event immediately;
|
|
5069
|
+
// otherwise a ready edge rejected by the lifecycle gate waits for the
|
|
5070
|
+
// 20-30s lease timer even though the transcript has already ended.
|
|
5071
|
+
if (live.some(event => event.kind === 'assistant_final')) {
|
|
5072
|
+
idleDetector?.fireIdle();
|
|
5073
|
+
}
|
|
4622
5074
|
codexBridgeOffset = result.newOffset;
|
|
4623
5075
|
codexBridgePendingTail = result.pendingTail;
|
|
4624
5076
|
codexBridgeBaselineDone = true;
|
|
@@ -5107,6 +5559,13 @@ function maybeFollowTraexSessionRotationViaPid() {
|
|
|
5107
5559
|
codexBridgeNotifyCliSessionId(observed.cliSessionId);
|
|
5108
5560
|
}
|
|
5109
5561
|
function codexBridgeIngest(opts = {}) {
|
|
5562
|
+
// Follow-up RPC turns install their exact bridge mark only after the
|
|
5563
|
+
// turn/start ACK passes the generation fence. Ordinary ingest must not
|
|
5564
|
+
// advance the rollout cursor in that window. Terminal hydration for an
|
|
5565
|
+
// older owner may continue: successor events reached by the same drain stay
|
|
5566
|
+
// buffered until the exact successor mark is installed.
|
|
5567
|
+
if (rpcTranscriptIngestBlockedByAwaitingActivation(rpcTurnsAwaitingActivation.keys(), opts.hydrationOwnerKey))
|
|
5568
|
+
return;
|
|
5110
5569
|
if (structuredBridgeIsHermes()) {
|
|
5111
5570
|
hermesBridgeIngest();
|
|
5112
5571
|
return;
|
|
@@ -5139,13 +5598,14 @@ function codexBridgeIngest(opts = {}) {
|
|
|
5139
5598
|
if (result.events.length > 0)
|
|
5140
5599
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
5141
5600
|
codexBridgeQueue.ingest(result.events);
|
|
5142
|
-
|
|
5143
|
-
//
|
|
5601
|
+
pruneExpiredStructuredHeadsAndEmit('structured ingest');
|
|
5602
|
+
// Transcript-driven idle: a normal `assistant_final` or no-output
|
|
5603
|
+
// `turn_aborted` is Codex declaring end-of-turn, far more reliable than the screen-pattern heuristic
|
|
5144
5604
|
// (CoCo's status bar varies by --yolo flag, version, theme; codex has
|
|
5145
5605
|
// its own moving targets). Pushing idle here lets the bridge emit
|
|
5146
5606
|
// immediately instead of waiting for readyPattern + quiescence to
|
|
5147
5607
|
// converge. Idempotent — IdleDetector.fireIdle no-ops while already idle.
|
|
5148
|
-
if (opts.signalIdle !== false && result.events.some(
|
|
5608
|
+
if (opts.signalIdle !== false && result.events.some(event => event.kind === 'assistant_final')) {
|
|
5149
5609
|
idleDetector?.fireIdle();
|
|
5150
5610
|
}
|
|
5151
5611
|
}
|
|
@@ -5172,11 +5632,309 @@ function maybeEmitCodexStructuredRateLimit(events) {
|
|
|
5172
5632
|
/** Mark a pending Lark turn for Codex. Crucially this works even before a
|
|
5173
5633
|
* rollout path is known — the queue is path-agnostic, and ingest after
|
|
5174
5634
|
* late-attach picks up the user_message and matches the fingerprint. */
|
|
5175
|
-
function codexBridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
|
|
5635
|
+
function codexBridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt, markTimeMs = Date.now()) {
|
|
5176
5636
|
if (!codexBridgeFallbackActive())
|
|
5177
|
-
return
|
|
5637
|
+
return undefined;
|
|
5178
5638
|
const turnId = preferredTurnId ?? `codex-${randomBytes(8).toString('hex')}`;
|
|
5179
|
-
codexBridgeQueue.mark(turnId, messageText,
|
|
5639
|
+
codexBridgeQueue.mark(turnId, messageText, markTimeMs, dispatchAttempt);
|
|
5640
|
+
return turnId;
|
|
5641
|
+
}
|
|
5642
|
+
function finalizeRpcTurnTerminal(terminal, generation, dropPending) {
|
|
5643
|
+
const { identity } = terminal;
|
|
5644
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5645
|
+
if (!sameRpcGeneration(settlingRpcTerminalOwners.get(ownerKey), generation))
|
|
5646
|
+
return;
|
|
5647
|
+
const timer = rpcTerminalHydrationTimers.get(ownerKey);
|
|
5648
|
+
if (timer)
|
|
5649
|
+
clearTimeout(timer);
|
|
5650
|
+
rpcTerminalHydrationTimers.delete(ownerKey);
|
|
5651
|
+
if (sameRpcGeneration(rpcTerminalHydrationOwners.get(ownerKey), generation)) {
|
|
5652
|
+
rpcTerminalHydrationOwners.delete(ownerKey);
|
|
5653
|
+
rpcTerminalHydrationTerminals.delete(ownerKey);
|
|
5654
|
+
}
|
|
5655
|
+
settlingRpcTerminalOwners.delete(ownerKey);
|
|
5656
|
+
clearAwaitingRpcActivation(identity, generation);
|
|
5657
|
+
clearRpcLifecycleFailClosedOwner(identity, generation);
|
|
5658
|
+
if (sameRpcGeneration(rpcActiveOwners.get(ownerKey), generation)) {
|
|
5659
|
+
rpcActiveOwners.delete(ownerKey);
|
|
5660
|
+
}
|
|
5661
|
+
codexBridgeQueue.stopRpcActive(identity.turnId, identity.dispatchAttempt);
|
|
5662
|
+
if (dropPending) {
|
|
5663
|
+
codexBridgeQueue.dropPendingTurn(identity.turnId, identity.dispatchAttempt, true);
|
|
5664
|
+
}
|
|
5665
|
+
// RPC `aborted` means the turn had already started executing before it was
|
|
5666
|
+
// interrupted, so its side effects may have already run. Map it to
|
|
5667
|
+
// `ambiguous` (fail-closed, no auto-retry) to match the transcript path
|
|
5668
|
+
// (codex-transcript.ts turn_aborted -> ambiguous). Only a genuine app-server
|
|
5669
|
+
// `failed` is retryable. Otherwise durable delivery would auto-redispatch and
|
|
5670
|
+
// re-run already-executed side effects — exactly what this PR guards against.
|
|
5671
|
+
const status = terminal.status === 'completed' ? 'completed'
|
|
5672
|
+
: terminal.status === 'failed' ? 'failed'
|
|
5673
|
+
: 'ambiguous';
|
|
5674
|
+
const errorCode = terminal.errorCode
|
|
5675
|
+
?? (terminal.status === 'engine-dead' ? 'rpc_engine_dead'
|
|
5676
|
+
: terminal.status === 'stopped' ? 'rpc_engine_stopped'
|
|
5677
|
+
: terminal.status === 'aborted' ? 'rpc_turn_aborted'
|
|
5678
|
+
: terminal.status === 'failed' ? 'rpc_turn_failed'
|
|
5679
|
+
: undefined);
|
|
5680
|
+
emitTurnTerminal(identity.turnId, status, errorCode, identity.dispatchAttempt);
|
|
5681
|
+
log(`Codex RPC terminal ${terminal.status}: native=${terminal.nativeTurnId.slice(0, 12)} `
|
|
5682
|
+
+ `turn=${identity.turnId.slice(0, 12)} attempt=${identity.dispatchAttempt ?? '-'}`);
|
|
5683
|
+
redriveRejectedStructuredReady();
|
|
5684
|
+
idleDetector?.fireIdle();
|
|
5685
|
+
}
|
|
5686
|
+
/** turn/completed can race ahead of rollout fs visibility even though it is an
|
|
5687
|
+
* authoritative execution terminal. Release rpcActive immediately, but retain
|
|
5688
|
+
* a short non-ready hydration gate while polling the structured transcript.
|
|
5689
|
+
* This preserves fallback final output without allowing the stale fingerprint
|
|
5690
|
+
* to head-of-line block a new RPC turn indefinitely. */
|
|
5691
|
+
function hydrateCompletedRpcTurn(terminal, generation, attempt = 0) {
|
|
5692
|
+
const { identity } = terminal;
|
|
5693
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5694
|
+
if (!sameRpcGeneration(settlingRpcTerminalOwners.get(ownerKey), generation))
|
|
5695
|
+
return;
|
|
5696
|
+
// restartCliProcess advances the generation before its asynchronous teardown.
|
|
5697
|
+
// Do not let an old hydration timer touch a bridge queue that may already be
|
|
5698
|
+
// owned by the replacement. stopCodexRpcEngine will synchronously retire the
|
|
5699
|
+
// old generation when teardown reaches the engine.
|
|
5700
|
+
if (cliSpawnGeneration !== generation.cliGeneration
|
|
5701
|
+
|| codexRpcEngine !== generation.engine)
|
|
5702
|
+
return;
|
|
5703
|
+
try {
|
|
5704
|
+
codexBridgeDrainAndMaybeEmit({
|
|
5705
|
+
signalIdle: false,
|
|
5706
|
+
hydrationOwnerKey: ownerKey,
|
|
5707
|
+
});
|
|
5708
|
+
}
|
|
5709
|
+
catch { /* retry */ }
|
|
5710
|
+
if (!codexBridgeQueue.hasPendingTurn(identity.turnId, identity.dispatchAttempt)) {
|
|
5711
|
+
// Structured ingest already emitted final_output + terminal in the canonical
|
|
5712
|
+
// order. The explicit native terminal below is idempotently suppressed.
|
|
5713
|
+
finalizeRpcTurnTerminal(terminal, generation, false);
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
// The native terminal can beat rollout persistence by seconds on a cold or
|
|
5717
|
+
// busy filesystem. Keep this bounded (~11.6s), aligned with the fresh-turn
|
|
5718
|
+
// 12s positive-evidence probe, rather than dropping fallback output after the
|
|
5719
|
+
// previous 1.55s window.
|
|
5720
|
+
const delays = CODEX_RPC_TERMINAL_HYDRATION_DELAYS_MS;
|
|
5721
|
+
if (attempt >= delays.length) {
|
|
5722
|
+
finalizeRpcTurnTerminal(terminal, generation, true);
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
const timer = setTimeout(() => {
|
|
5726
|
+
rpcTerminalHydrationTimers.delete(ownerKey);
|
|
5727
|
+
hydrateCompletedRpcTurn(terminal, generation, attempt + 1);
|
|
5728
|
+
}, delays[attempt]);
|
|
5729
|
+
timer.unref?.();
|
|
5730
|
+
rpcTerminalHydrationTimers.set(ownerKey, timer);
|
|
5731
|
+
}
|
|
5732
|
+
function settleRpcTurnTerminal(terminal, generation) {
|
|
5733
|
+
const ownerKey = rpcTurnOwnerKey(terminal.identity);
|
|
5734
|
+
const existingSettlement = settlingRpcTerminalOwners.get(ownerKey);
|
|
5735
|
+
if (existingSettlement) {
|
|
5736
|
+
if (!sameRpcGeneration(existingSettlement, generation)) {
|
|
5737
|
+
log(`Ignored stale Codex RPC terminal settlement for ${terminal.identity.turnId}`);
|
|
5738
|
+
}
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5741
|
+
settlingRpcTerminalOwners.set(ownerKey, generation);
|
|
5742
|
+
clearAwaitingRpcActivation(terminal.identity, generation);
|
|
5743
|
+
clearRpcLifecycleFailClosedOwner(terminal.identity, generation);
|
|
5744
|
+
if (sameRpcGeneration(rpcActiveOwners.get(ownerKey), generation)) {
|
|
5745
|
+
rpcActiveOwners.delete(ownerKey);
|
|
5746
|
+
}
|
|
5747
|
+
codexBridgeQueue.stopRpcActive(terminal.identity.turnId, terminal.identity.dispatchAttempt);
|
|
5748
|
+
if (terminal.status === 'completed'
|
|
5749
|
+
&& codexBridgeQueue.hasPendingTurn(terminal.identity.turnId, terminal.identity.dispatchAttempt)) {
|
|
5750
|
+
rpcTerminalHydrationOwners.set(ownerKey, generation);
|
|
5751
|
+
rpcTerminalHydrationTerminals.set(ownerKey, { terminal, generation });
|
|
5752
|
+
hydrateCompletedRpcTurn(terminal, generation);
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
finalizeRpcTurnTerminal(terminal, generation, true);
|
|
5756
|
+
}
|
|
5757
|
+
function handleRpcTurnTerminal(terminal, generation) {
|
|
5758
|
+
const ownerKey = rpcTurnOwnerKey(terminal.identity);
|
|
5759
|
+
const awaiting = rpcTurnsAwaitingActivation.get(ownerKey);
|
|
5760
|
+
if (awaiting && sameRpcGeneration(awaiting, generation)) {
|
|
5761
|
+
// Response + turn/completed can be decoded before the await continuation
|
|
5762
|
+
// installs rpcActive. The exact terminal is replayed immediately after that
|
|
5763
|
+
// activation, never against a guessed/latest queue entry.
|
|
5764
|
+
pendingRpcTurnTerminals.set(ownerKey, { terminal, generation });
|
|
5765
|
+
return;
|
|
5766
|
+
}
|
|
5767
|
+
if (awaiting && !sameRpcGeneration(awaiting, generation)) {
|
|
5768
|
+
log(`Ignored stale Codex RPC terminal from replaced engine for ${terminal.identity.turnId}`);
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
const activeGeneration = rpcActiveOwners.get(ownerKey);
|
|
5772
|
+
const failedClosedGeneration = rpcLifecycleFailClosedOwners.get(ownerKey);
|
|
5773
|
+
if (!sameRpcGeneration(activeGeneration, generation)
|
|
5774
|
+
&& !sameRpcGeneration(failedClosedGeneration, generation)) {
|
|
5775
|
+
// The structured bridge may already have consumed the same turn and emitted
|
|
5776
|
+
// its terminal, or a replacement generation may own the same logical
|
|
5777
|
+
// delivery key. In either case this callback has no queue state to mutate.
|
|
5778
|
+
log(`Codex RPC terminal already retired: native=${terminal.nativeTurnId.slice(0, 12)} `
|
|
5779
|
+
+ `turn=${terminal.identity.turnId.slice(0, 12)}`);
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
settleRpcTurnTerminal(terminal, generation);
|
|
5783
|
+
}
|
|
5784
|
+
/** Install the exact bridge mark + rpcActive hand-off after turn/start is known
|
|
5785
|
+
* accepted. Failure is fail-closed: a separate lifecycle gate remains asserted
|
|
5786
|
+
* for this owner until its native terminal/engine teardown, so a bookkeeping
|
|
5787
|
+
* bug cannot publish false idle. */
|
|
5788
|
+
function activateRpcTurnLifecycle(identity, messageText, alreadyMarked, generation, deferTerminal = false) {
|
|
5789
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5790
|
+
if (!sameRpcGeneration(rpcTurnsAwaitingActivation.get(ownerKey), generation)) {
|
|
5791
|
+
log(`Refused stale Codex RPC lifecycle activation for ${identity.turnId}`);
|
|
5792
|
+
return false;
|
|
5793
|
+
}
|
|
5794
|
+
const replayAnchorMs = awaitingRpcActivationReplayAnchorMs(identity, generation);
|
|
5795
|
+
let marked = alreadyMarked
|
|
5796
|
+
&& (codexBridgeQueue.hasPendingTurn(identity.turnId, identity.dispatchAttempt)
|
|
5797
|
+
|| codexBridgeQueue.hasTerminalTurn(identity.turnId, identity.dispatchAttempt));
|
|
5798
|
+
if (!alreadyMarked) {
|
|
5799
|
+
const bridgeTurnId = codexBridgeMarkPendingTurn(messageText, identity.turnId, identity.dispatchAttempt, replayAnchorMs);
|
|
5800
|
+
marked = bridgeTurnId === identity.turnId;
|
|
5801
|
+
}
|
|
5802
|
+
// An older turn's terminal hydration may have drained and buffered this
|
|
5803
|
+
// successor's complete user/final pair while turn/start ACK was still
|
|
5804
|
+
// pending. mark() replays that pair synchronously. Treat the exact transcript
|
|
5805
|
+
// terminal as an already-retired activation instead of failing closed merely
|
|
5806
|
+
// because markRpcActive correctly refuses a completed queue entry.
|
|
5807
|
+
const transcriptTerminalObserved = marked
|
|
5808
|
+
&& codexBridgeQueue.hasTerminalTurn(identity.turnId, identity.dispatchAttempt);
|
|
5809
|
+
const activated = transcriptTerminalObserved
|
|
5810
|
+
|| (marked && codexBridgeQueue.markRpcActive(identity.turnId, identity.dispatchAttempt));
|
|
5811
|
+
if (activated && !transcriptTerminalObserved) {
|
|
5812
|
+
rpcActiveOwners.set(ownerKey, generation);
|
|
5813
|
+
}
|
|
5814
|
+
if (transcriptTerminalObserved)
|
|
5815
|
+
emitReadyCodexTurns();
|
|
5816
|
+
if (!deferTerminal
|
|
5817
|
+
&& sameRpcGeneration(rpcTurnsAwaitingActivation.get(ownerKey), generation)) {
|
|
5818
|
+
rpcTurnsAwaitingActivation.delete(ownerKey);
|
|
5819
|
+
rpcTurnsAwaitingActivationIdentities.delete(ownerKey);
|
|
5820
|
+
rpcTurnsAwaitingActivationReplayAnchors.delete(ownerKey);
|
|
5821
|
+
}
|
|
5822
|
+
if (!activated) {
|
|
5823
|
+
if (marked)
|
|
5824
|
+
codexBridgeQueue.dropPendingTurn(identity.turnId, identity.dispatchAttempt, true);
|
|
5825
|
+
installRpcLifecycleFailClosedOwner(identity, generation);
|
|
5826
|
+
log(`Codex RPC lifecycle failed closed: mark=${marked} active=${activated} `
|
|
5827
|
+
+ `turn=${identity.turnId.slice(0, 12)} attempt=${identity.dispatchAttempt ?? '-'}`);
|
|
5828
|
+
send({
|
|
5829
|
+
type: 'user_notify',
|
|
5830
|
+
message: 'Codex RPC 已接收消息,但本地生命周期登记失败;在原生终态到达前保持忙碌,未自动重发。',
|
|
5831
|
+
turnId: identity.turnId,
|
|
5832
|
+
...(identity.dispatchAttempt !== undefined
|
|
5833
|
+
? { dispatchAttempt: identity.dispatchAttempt }
|
|
5834
|
+
: {}),
|
|
5835
|
+
});
|
|
5836
|
+
}
|
|
5837
|
+
const pendingTerminal = pendingRpcTurnTerminals.get(ownerKey);
|
|
5838
|
+
if (pendingTerminal
|
|
5839
|
+
&& sameRpcGeneration(pendingTerminal.generation, generation)
|
|
5840
|
+
&& !deferTerminal) {
|
|
5841
|
+
settleRpcTurnTerminal(pendingTerminal.terminal, generation);
|
|
5842
|
+
}
|
|
5843
|
+
return activated;
|
|
5844
|
+
}
|
|
5845
|
+
function releaseRpcTurnTerminalDeferral(identity, generation) {
|
|
5846
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5847
|
+
if (sameRpcGeneration(rpcTurnsAwaitingActivation.get(ownerKey), generation)) {
|
|
5848
|
+
rpcTurnsAwaitingActivation.delete(ownerKey);
|
|
5849
|
+
rpcTurnsAwaitingActivationIdentities.delete(ownerKey);
|
|
5850
|
+
rpcTurnsAwaitingActivationReplayAnchors.delete(ownerKey);
|
|
5851
|
+
}
|
|
5852
|
+
const pendingTerminal = pendingRpcTurnTerminals.get(ownerKey);
|
|
5853
|
+
if (pendingTerminal && sameRpcGeneration(pendingTerminal.generation, generation)) {
|
|
5854
|
+
settleRpcTurnTerminal(pendingTerminal.terminal, generation);
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
function retireRpcLifecycleFromStructuredTerminal(identity) {
|
|
5858
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5859
|
+
// Native completion hydration owns its own finalization after this canonical
|
|
5860
|
+
// drain returns. Do not tear down that generation's settlement underneath it.
|
|
5861
|
+
if (settlingRpcTerminalOwners.has(ownerKey))
|
|
5862
|
+
return;
|
|
5863
|
+
const generation = rpcActiveOwners.get(ownerKey)
|
|
5864
|
+
?? rpcLifecycleFailClosedOwners.get(ownerKey)
|
|
5865
|
+
?? rpcTurnsAwaitingActivation.get(ownerKey);
|
|
5866
|
+
if (!generation)
|
|
5867
|
+
return;
|
|
5868
|
+
clearAwaitingRpcActivation(identity, generation);
|
|
5869
|
+
if (sameRpcGeneration(rpcActiveOwners.get(ownerKey), generation)) {
|
|
5870
|
+
rpcActiveOwners.delete(ownerKey);
|
|
5871
|
+
}
|
|
5872
|
+
clearRpcLifecycleFailClosedOwner(identity, generation);
|
|
5873
|
+
codexBridgeQueue.stopRpcActive(identity.turnId, identity.dispatchAttempt);
|
|
5874
|
+
}
|
|
5875
|
+
/** Expire confirmed or attribution-only unstarted queue heads at an explicit worker
|
|
5876
|
+
* lifecycle boundary. Pruning can replay a buffered successor user+final and
|
|
5877
|
+
* thereby produce an immediately emittable completion, so query/projection
|
|
5878
|
+
* methods must never do this mutation invisibly: every removal funnels
|
|
5879
|
+
* through this helper and drains ready output in the same call stack. */
|
|
5880
|
+
function pruneExpiredStructuredHeadsAndEmit(source) {
|
|
5881
|
+
let fenceCurrentRpcEngine = false;
|
|
5882
|
+
const dropped = pruneExpiredPreStartHeadsAndEmit(codexBridgeQueue, emitReadyCodexTurns, undefined, turns => {
|
|
5883
|
+
// A bounded pre-start lease is also the terminal boundary for a durable
|
|
5884
|
+
// delivery attempt that the CLI accepted but never wrote to transcript.
|
|
5885
|
+
// Settle N before the prune replay drains successor N+1; ordinary IM
|
|
5886
|
+
// turns have no dispatchAttempt and remain log-only as before.
|
|
5887
|
+
for (const turn of turns) {
|
|
5888
|
+
const identity = {
|
|
5889
|
+
turnId: turn.turnId,
|
|
5890
|
+
...(turn.dispatchAttempt !== undefined
|
|
5891
|
+
? { dispatchAttempt: turn.dispatchAttempt }
|
|
5892
|
+
: {}),
|
|
5893
|
+
};
|
|
5894
|
+
const ownerKey = rpcTurnOwnerKey(identity);
|
|
5895
|
+
const failedClosedGeneration = rpcLifecycleFailClosedOwners.get(ownerKey);
|
|
5896
|
+
const retiredRpcOwner = failedClosedGeneration
|
|
5897
|
+
? clearRpcLifecycleFailClosedOwner(identity, failedClosedGeneration)
|
|
5898
|
+
: false;
|
|
5899
|
+
let shouldFenceRpcEngine = false;
|
|
5900
|
+
if (retiredRpcOwner && failedClosedGeneration) {
|
|
5901
|
+
clearAwaitingRpcActivation(identity, failedClosedGeneration);
|
|
5902
|
+
if (deferredFreshRpcTurn
|
|
5903
|
+
&& rpcTurnOwnerKey(deferredFreshRpcTurn.identity) === ownerKey
|
|
5904
|
+
&& sameRpcGeneration(deferredFreshRpcTurn.generation, failedClosedGeneration)) {
|
|
5905
|
+
deferredFreshRpcTurn = undefined;
|
|
5906
|
+
}
|
|
5907
|
+
if (codexRpcEngine === failedClosedGeneration.engine
|
|
5908
|
+
&& cliSpawnGeneration === failedClosedGeneration.cliGeneration
|
|
5909
|
+
&& !cliRestartInProgress) {
|
|
5910
|
+
fenceCurrentRpcEngine = true;
|
|
5911
|
+
shouldFenceRpcEngine = true;
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
if (retiredRpcOwner || turn.dispatchAttempt !== undefined) {
|
|
5915
|
+
emitTurnTerminal(turn.turnId, 'ambiguous', retiredRpcOwner
|
|
5916
|
+
? 'rpc_delivery_ambiguous_timeout'
|
|
5917
|
+
: 'structured_start_timeout', turn.dispatchAttempt);
|
|
5918
|
+
}
|
|
5919
|
+
// Publish/retire N before a synchronous immediate restart can clear the
|
|
5920
|
+
// current exact identity in killCli(). The callback still installs the
|
|
5921
|
+
// restart fence before it returns, so the helper cannot emit buffered
|
|
5922
|
+
// successor N+1 against this ambiguous engine generation.
|
|
5923
|
+
if (shouldFenceRpcEngine) {
|
|
5924
|
+
void restartCliProcess('Codex RPC ambiguous delivery exceeded its bounded attribution lease', { immediate: true, preservePending: true });
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
});
|
|
5928
|
+
if (dropped.length === 0)
|
|
5929
|
+
return false;
|
|
5930
|
+
log(`${source}: expired ${dropped.length} structured head(s) without transcript start (${dropped.map(turn => turn.turnId).join(', ')})`);
|
|
5931
|
+
if (fenceCurrentRpcEngine)
|
|
5932
|
+
log('Fenced ambiguous Codex RPC generation before structured successor replay');
|
|
5933
|
+
// A rejected prompt-ready signal may be waiting on this exact pre-start
|
|
5934
|
+
// lease. Re-evaluate it after both the queue mark and its separate RPC
|
|
5935
|
+
// fail-closed owner have retired; otherwise the projector can stay busy
|
|
5936
|
+
// forever even though there is no remaining terminal source.
|
|
5937
|
+
redriveRejectedStructuredReady();
|
|
5180
5938
|
return true;
|
|
5181
5939
|
}
|
|
5182
5940
|
function codexBridgeDrainAndMaybeEmit(opts = {}) {
|
|
@@ -5227,6 +5985,14 @@ function emitReadyCodexTurns() {
|
|
|
5227
5985
|
const ready = codexBridgeQueue.drainEmittable();
|
|
5228
5986
|
if (ready.length === 0)
|
|
5229
5987
|
return;
|
|
5988
|
+
// Turns suppressed as GENUINE SILENCE (model terminated with a bare
|
|
5989
|
+
// nothing-to-send sentinel, no `botmux send`). Tracked by object identity —
|
|
5990
|
+
// both the emit loop and the terminal loop below iterate this same `ready`
|
|
5991
|
+
// array — so the terminal for such a turn can carry positive silence evidence
|
|
5992
|
+
// (outputDisposition: 'nothing_to_send'). A durable/async caller uses ONLY
|
|
5993
|
+
// that flag to settle completed-with-empty; a bare `completed` terminal (e.g.
|
|
5994
|
+
// the RPC-hydration timeout path) must never be read as silence.
|
|
5995
|
+
const nothingToSendTurns = new Set();
|
|
5230
5996
|
const adoptMode = lastInitConfig?.adoptMode === true;
|
|
5231
5997
|
// Adopt mode: model is the user's external Codex, no botmux send to
|
|
5232
5998
|
// gate against — every assistant turn (Lark-driven OR locally typed)
|
|
@@ -5266,6 +6032,16 @@ function emitReadyCodexTurns() {
|
|
|
5266
6032
|
continue;
|
|
5267
6033
|
if (shouldSuppressBridgeEmit(gateInput, nextBoundaryMs, markers, adoptMode)) {
|
|
5268
6034
|
log(`Codex bridge fallback suppressed for turn ${turn.turnId.substring(0, 8)} (gate)`);
|
|
6035
|
+
// Distinguish DELIBERATE SILENCE (bare nothing-to-send sentinel, no prose,
|
|
6036
|
+
// no send) from other suppression reasons (already `botmux send`-ed this
|
|
6037
|
+
// turn, local-typed). Only the former is positive evidence that the model
|
|
6038
|
+
// chose to produce no output — the terminal below carries it so a durable/
|
|
6039
|
+
// async caller can settle completed-with-empty. "Already sent" is NOT
|
|
6040
|
+
// silence: its content went out via final_output/botmux send and the async
|
|
6041
|
+
// result is captured there, so it must not be stamped.
|
|
6042
|
+
if (!adoptMode && isBridgeNothingToSendFinal(turn.finalText)) {
|
|
6043
|
+
nothingToSendTurns.add(turn);
|
|
6044
|
+
}
|
|
5269
6045
|
notifyExplicitReplyObserved(turn.turnId, explicitReplyMarkerForTurnWindow(gateInput, nextBoundaryMs, markers, adoptMode));
|
|
5270
6046
|
continue;
|
|
5271
6047
|
}
|
|
@@ -5309,7 +6085,13 @@ function emitReadyCodexTurns() {
|
|
|
5309
6085
|
});
|
|
5310
6086
|
}
|
|
5311
6087
|
for (const turn of ready) {
|
|
5312
|
-
|
|
6088
|
+
retireRpcLifecycleFromStructuredTerminal({
|
|
6089
|
+
turnId: turn.turnId,
|
|
6090
|
+
...(turn.dispatchAttempt !== undefined
|
|
6091
|
+
? { dispatchAttempt: turn.dispatchAttempt }
|
|
6092
|
+
: {}),
|
|
6093
|
+
});
|
|
6094
|
+
emitTurnTerminal(turn.turnId, turn.terminalStatus ?? 'completed', turn.terminalErrorCode, turn.dispatchAttempt, nothingToSendTurns.has(turn) ? 'nothing_to_send' : undefined);
|
|
5313
6095
|
}
|
|
5314
6096
|
}
|
|
5315
6097
|
function stopCodexBridge() {
|
|
@@ -5674,6 +6456,264 @@ function parkCrashDiagnosticTerminal(code, signal) {
|
|
|
5674
6456
|
* the ask-hook / CoCo picker paths (driveCocoPicker, handleTuiKeys); cleared
|
|
5675
6457
|
* when the prompt resolves or a Lark/terminal input overrides it. */
|
|
5676
6458
|
let tuiPromptBlocking = false;
|
|
6459
|
+
/** One composed status projection shared by the immediate screen path,
|
|
6460
|
+
* periodic text updates, and screenshot uploads. */
|
|
6461
|
+
function projectedRuntimeScreenStatus() {
|
|
6462
|
+
return codexAppLivenessStatus(projectRuntimeScreenStatus({
|
|
6463
|
+
promptReady: isPromptReady,
|
|
6464
|
+
analyzing: false,
|
|
6465
|
+
structuredTurnBlocking: hasStructuredLifecycleBlock(),
|
|
6466
|
+
}));
|
|
6467
|
+
}
|
|
6468
|
+
/** Worker-local widening of the status snapshot helper. Codex App composes a
|
|
6469
|
+
* `stalled` state on top of the base screen/structured projection, while the
|
|
6470
|
+
* shared utility intentionally exposes only the base three-state union. */
|
|
6471
|
+
async function snapshotWithLatestRuntimeStatus(capture, projectStatus) {
|
|
6472
|
+
const snapshot = await capture();
|
|
6473
|
+
return { snapshot, status: projectStatus() };
|
|
6474
|
+
}
|
|
6475
|
+
/** Re-arm readiness before every individual CLI write. A whole flush can span
|
|
6476
|
+
* several type-ahead items, and adopt writeInput can await history polling;
|
|
6477
|
+
* either path may observe an assistant_final before the await returns. Reset
|
|
6478
|
+
* here (never after the await) so that final remains a usable ready edge. */
|
|
6479
|
+
function beginCliWriteCycle() {
|
|
6480
|
+
beginRuntimeWriteCycle({
|
|
6481
|
+
setPromptReady: ready => { isPromptReady = ready; },
|
|
6482
|
+
resetIdleDetector: () => { idleDetector?.reset(); },
|
|
6483
|
+
});
|
|
6484
|
+
}
|
|
6485
|
+
async function writeAdoptMessage(content, turnId, dispatchAttempt, vcMeetingImTurnOrigin, fence) {
|
|
6486
|
+
const executionFence = fence ?? captureAdoptWriteFence();
|
|
6487
|
+
if (!executionFence || !adoptWriteFenceIsCurrent(executionFence)) {
|
|
6488
|
+
return 'stale-before-write';
|
|
6489
|
+
}
|
|
6490
|
+
const adoptBackend = executionFence.backend;
|
|
6491
|
+
renderer?.markNewTurn();
|
|
6492
|
+
const turnSeq = usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
6493
|
+
currentBotmuxTurnId = turnId;
|
|
6494
|
+
currentBotmuxDispatchAttempt = dispatchAttempt;
|
|
6495
|
+
currentVcMeetingImTurnOrigin = vcMeetingImTurnOrigin;
|
|
6496
|
+
if (dispatchAttempt !== undefined)
|
|
6497
|
+
durableTurnInFlight = true;
|
|
6498
|
+
writeCliPidMarker();
|
|
6499
|
+
publishSandboxRelayCapability();
|
|
6500
|
+
let adoptStructuredBridgeTurnId;
|
|
6501
|
+
// Capture the transcript baseline immediately before the literal write, inside
|
|
6502
|
+
// the submission transaction (as its beforeWrite hook), so a ZMX composer-
|
|
6503
|
+
// recovery hold that refuses the write never leaves a bare, unwritten bridge
|
|
6504
|
+
// mark. prepareAdoptWrite is idempotent and also re-arms readiness before the
|
|
6505
|
+
// adapter can yield (an assistant_final may arrive while writeInput polls
|
|
6506
|
+
// history), so the re-arm stays BEFORE the write, never after.
|
|
6507
|
+
let adoptWritePrepared = false;
|
|
6508
|
+
const prepareAdoptWrite = () => {
|
|
6509
|
+
if (adoptWritePrepared)
|
|
6510
|
+
return;
|
|
6511
|
+
adoptWritePrepared = true;
|
|
6512
|
+
beginCliWriteCycle();
|
|
6513
|
+
if (bridgeJsonlPath) {
|
|
6514
|
+
try {
|
|
6515
|
+
bridgeIngest();
|
|
6516
|
+
}
|
|
6517
|
+
catch { /* best effort */ }
|
|
6518
|
+
bridgeMarkPendingTurn(content, turnId, dispatchAttempt);
|
|
6519
|
+
}
|
|
6520
|
+
else if (codexBridgeFallbackActive()) {
|
|
6521
|
+
if (codexBridgeIsCursor()) {
|
|
6522
|
+
// Cursor may append the current line before IPC handling; mark first so
|
|
6523
|
+
// the pre-existing line can fingerprint-match instead of becoming seen.
|
|
6524
|
+
adoptStructuredBridgeTurnId = codexBridgeMarkPendingTurn(content, turnId, dispatchAttempt);
|
|
6525
|
+
try {
|
|
6526
|
+
codexBridgeIngest();
|
|
6527
|
+
}
|
|
6528
|
+
catch { /* best effort */ }
|
|
6529
|
+
}
|
|
6530
|
+
else {
|
|
6531
|
+
try {
|
|
6532
|
+
codexBridgeIngest();
|
|
6533
|
+
}
|
|
6534
|
+
catch { /* best effort */ }
|
|
6535
|
+
adoptStructuredBridgeTurnId = codexBridgeMarkPendingTurn(content, turnId, dispatchAttempt);
|
|
6536
|
+
}
|
|
6537
|
+
if (adoptStructuredBridgeTurnId) {
|
|
6538
|
+
codexBridgeQueue.beginSubmitVerification(adoptStructuredBridgeTurnId, undefined, dispatchAttempt);
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
};
|
|
6542
|
+
const settleStaleAfterWrite = (errorCode) => {
|
|
6543
|
+
if (adoptStructuredBridgeTurnId) {
|
|
6544
|
+
codexBridgeQueue.finishSubmitVerification(adoptStructuredBridgeTurnId, undefined, dispatchAttempt);
|
|
6545
|
+
}
|
|
6546
|
+
dropFailedBridgeMark(adoptStructuredBridgeTurnId, dispatchAttempt);
|
|
6547
|
+
if (turnId && dispatchAttempt !== undefined) {
|
|
6548
|
+
emitTurnTerminal(turnId, 'ambiguous', errorCode, dispatchAttempt);
|
|
6549
|
+
}
|
|
6550
|
+
else {
|
|
6551
|
+
send({
|
|
6552
|
+
type: 'user_notify',
|
|
6553
|
+
turnId,
|
|
6554
|
+
message: 'Adopt input could not be reconciled because the CLI backend changed while it was being written. Please verify the terminal before retrying.',
|
|
6555
|
+
});
|
|
6556
|
+
}
|
|
6557
|
+
return 'stale-after-write';
|
|
6558
|
+
};
|
|
6559
|
+
// Adopt mode write:
|
|
6560
|
+
// - Structured-bridge adopt-input CLIs (codex/traex/pi/grok/mtr) route
|
|
6561
|
+
// through cliAdapter.writeInput. The composer-conflict guard refuses input
|
|
6562
|
+
// when the adopted pane already holds an unsubmitted human draft, and the
|
|
6563
|
+
// shared submission transaction gives ZMX its capture→write→confirm/cancel
|
|
6564
|
+
// journal (a transparent pass-through on every non-ZMX backend).
|
|
6565
|
+
// - raw sendText+Enter runs inside the same transaction for identical
|
|
6566
|
+
// commit-journal atomicity.
|
|
6567
|
+
if (isStructuredBridgeAdoptInputCli(lastInitConfig?.cliId) && cliAdapter) {
|
|
6568
|
+
const submissionBackend = adoptBackend;
|
|
6569
|
+
let recoveryFailureReason;
|
|
6570
|
+
// Refuse adopt input while the local composer still holds an unsubmitted
|
|
6571
|
+
// human draft, BEFORE any bridge attribution or terminal write — otherwise
|
|
6572
|
+
// the Lark message would be appended onto the human's half-typed line.
|
|
6573
|
+
const composerConflict = codexAdoptComposerConflict(submissionBackend);
|
|
6574
|
+
if (composerConflict) {
|
|
6575
|
+
log('Refused Codex adopt input because the local composer contains an unsubmitted draft');
|
|
6576
|
+
scheduleSubmitFailureNotify(content, undefined, 'submit history', undefined, composerConflict, turnSeq, { turnId, dispatchAttempt }, 'failed');
|
|
6577
|
+
return 'completed';
|
|
6578
|
+
}
|
|
6579
|
+
try {
|
|
6580
|
+
const transaction = await runAmbiguousSubmissionTransaction(submissionBackend,
|
|
6581
|
+
// adapterInputHandle wraps a ZmxBackend in strictInputHandle, which turns
|
|
6582
|
+
// a `sendText/sendSpecialKeys === false` (ZMX write refusal) into a throw
|
|
6583
|
+
// so the transaction can cancel/poison the WAL. Passing the bare backend
|
|
6584
|
+
// would let a refused write look like success and clear the journal —
|
|
6585
|
+
// silent input loss (the exact failure the transaction guards against).
|
|
6586
|
+
() => cliAdapter.writeInput(adapterInputHandle(submissionBackend), content), settleVerifiableSubmissionForJournal, prepareAdoptWrite);
|
|
6587
|
+
const result = transaction.result;
|
|
6588
|
+
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
6589
|
+
if (!adoptWriteFenceIsCurrent(executionFence)) {
|
|
6590
|
+
return settleStaleAfterWrite('adopt_generation_changed');
|
|
6591
|
+
}
|
|
6592
|
+
if (result?.cliSessionId) {
|
|
6593
|
+
persistCliSessionId(result.cliSessionId);
|
|
6594
|
+
codexBridgeNotifyCliSessionId(result.cliSessionId);
|
|
6595
|
+
}
|
|
6596
|
+
if (result?.submitted === true && adoptStructuredBridgeTurnId) {
|
|
6597
|
+
codexBridgeQueue.confirmPendingTurn(adoptStructuredBridgeTurnId, undefined, dispatchAttempt);
|
|
6598
|
+
}
|
|
6599
|
+
else if (adoptStructuredBridgeTurnId && !(result?.submitted === false && result.recheck && !result.failureReason)) {
|
|
6600
|
+
codexBridgeQueue.finishSubmitVerification(adoptStructuredBridgeTurnId, undefined, dispatchAttempt);
|
|
6601
|
+
}
|
|
6602
|
+
redriveRejectedStructuredReady();
|
|
6603
|
+
if (result?.submitted === false || recoveryFailureReason) {
|
|
6604
|
+
if (recoveryFailureReason) {
|
|
6605
|
+
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, { turnId, dispatchAttempt });
|
|
6606
|
+
}
|
|
6607
|
+
else {
|
|
6608
|
+
scheduleSubmitFailureNotify(content, result?.recheck, 'submit history', adoptStructuredBridgeTurnId, result?.failureReason, turnSeq, { turnId, dispatchAttempt }, 'failed', true);
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
catch (err) {
|
|
6613
|
+
recoveryFailureReason = err instanceof SubmissionWriteError
|
|
6614
|
+
? err.recoveryFailureReason
|
|
6615
|
+
: recoveryFailureReason;
|
|
6616
|
+
const blockedBeforeWrite = err instanceof SubmissionWriteError
|
|
6617
|
+
&& !!err.recoveryFailureReason
|
|
6618
|
+
&& !err.submissionStarted;
|
|
6619
|
+
log(`Adopt writeInput error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
6620
|
+
if (!adoptWriteFenceIsCurrent(executionFence)) {
|
|
6621
|
+
return settleStaleAfterWrite('adopt_generation_changed');
|
|
6622
|
+
}
|
|
6623
|
+
if (adoptStructuredBridgeTurnId) {
|
|
6624
|
+
codexBridgeQueue.finishSubmitVerification(adoptStructuredBridgeTurnId, undefined, dispatchAttempt);
|
|
6625
|
+
}
|
|
6626
|
+
dropFailedBridgeMark(adoptStructuredBridgeTurnId, dispatchAttempt);
|
|
6627
|
+
if (turnId && dispatchAttempt !== undefined && blockedBeforeWrite) {
|
|
6628
|
+
// The ZMX recovery hold refused the write, so the input definitely did
|
|
6629
|
+
// NOT execute — a genuine retryable failure, not an ambiguous one.
|
|
6630
|
+
emitTurnTerminal(turnId, 'failed', 'zmx_recovery_blocked_before_write', dispatchAttempt);
|
|
6631
|
+
}
|
|
6632
|
+
else if (turnId && dispatchAttempt !== undefined && !recoveryFailureReason) {
|
|
6633
|
+
emitTurnTerminal(turnId, 'ambiguous', 'adopt_write_input_threw', dispatchAttempt);
|
|
6634
|
+
}
|
|
6635
|
+
if (recoveryFailureReason) {
|
|
6636
|
+
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, { turnId, dispatchAttempt });
|
|
6637
|
+
}
|
|
6638
|
+
redriveRejectedStructuredReady();
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
else if ('sendText' in adoptBackend && 'sendSpecialKeys' in adoptBackend) {
|
|
6642
|
+
const submissionBackend = adoptBackend;
|
|
6643
|
+
let recoveryFailureReason;
|
|
6644
|
+
try {
|
|
6645
|
+
const transaction = await runAmbiguousSubmissionTransaction(submissionBackend, async () => {
|
|
6646
|
+
// Use adapterInputHandle so a ZMX write refusal (sendText/sendSpecialKeys
|
|
6647
|
+
// returning false) throws instead of silently succeeding — the raw path
|
|
6648
|
+
// needs the same strict-handle guard as the structured writeInput path.
|
|
6649
|
+
const input = adapterInputHandle(submissionBackend);
|
|
6650
|
+
input.sendText(content);
|
|
6651
|
+
// Beat between text and Enter so Ink-based TUIs register pasted text
|
|
6652
|
+
// before submit. The serial queue holds across this await.
|
|
6653
|
+
await new Promise(r => setTimeout(r, 200));
|
|
6654
|
+
if (!adoptWriteFenceIsCurrent(executionFence)) {
|
|
6655
|
+
throw new Error('backend changed before adopt raw Enter');
|
|
6656
|
+
}
|
|
6657
|
+
input.sendSpecialKeys('Enter');
|
|
6658
|
+
}, undefined, prepareAdoptWrite);
|
|
6659
|
+
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
6660
|
+
if (!adoptWriteFenceIsCurrent(executionFence)) {
|
|
6661
|
+
return settleStaleAfterWrite('adopt_generation_changed_before_enter');
|
|
6662
|
+
}
|
|
6663
|
+
if (recoveryFailureReason) {
|
|
6664
|
+
throw new SubmissionWriteError(`backend could not commit the adopt submission journal; ${recoveryFailureReason}`, recoveryFailureReason);
|
|
6665
|
+
}
|
|
6666
|
+
}
|
|
6667
|
+
catch (err) {
|
|
6668
|
+
recoveryFailureReason = err instanceof SubmissionWriteError
|
|
6669
|
+
? err.recoveryFailureReason
|
|
6670
|
+
: recoveryFailureReason;
|
|
6671
|
+
const blockedBeforeWrite = err instanceof SubmissionWriteError
|
|
6672
|
+
&& !!err.recoveryFailureReason
|
|
6673
|
+
&& !err.submissionStarted;
|
|
6674
|
+
log(`Adopt raw input error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
6675
|
+
if (turnId && dispatchAttempt !== undefined && blockedBeforeWrite) {
|
|
6676
|
+
emitTurnTerminal(turnId, 'failed', 'zmx_recovery_blocked_before_write', dispatchAttempt);
|
|
6677
|
+
}
|
|
6678
|
+
else if (turnId && dispatchAttempt !== undefined && !recoveryFailureReason) {
|
|
6679
|
+
emitTurnTerminal(turnId, 'ambiguous', 'write_input_threw', dispatchAttempt);
|
|
6680
|
+
}
|
|
6681
|
+
if (recoveryFailureReason) {
|
|
6682
|
+
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, { turnId, dispatchAttempt });
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
else {
|
|
6687
|
+
prepareAdoptWrite();
|
|
6688
|
+
adoptBackend.write(content + '\r');
|
|
6689
|
+
}
|
|
6690
|
+
return 'completed';
|
|
6691
|
+
}
|
|
6692
|
+
async function runAdoptMessageForCapturedGeneration(item, requeue) {
|
|
6693
|
+
const fence = captureAdoptWriteFence();
|
|
6694
|
+
if (!fence) {
|
|
6695
|
+
requeue();
|
|
6696
|
+
return 'stale-before-write';
|
|
6697
|
+
}
|
|
6698
|
+
let requeued = false;
|
|
6699
|
+
const requeueOnce = () => {
|
|
6700
|
+
if (requeued)
|
|
6701
|
+
return;
|
|
6702
|
+
requeued = true;
|
|
6703
|
+
requeue();
|
|
6704
|
+
};
|
|
6705
|
+
const queued = await runAdoptQueuedWriteSequence({
|
|
6706
|
+
queue: adoptWriteQueue,
|
|
6707
|
+
isCurrent: () => adoptWriteFenceIsCurrent(fence),
|
|
6708
|
+
onStale: requeueOnce,
|
|
6709
|
+
write: () => writeAdoptMessage(item.content, item.turnId, item.dispatchAttempt, item.vcMeetingImTurnOrigin, fence),
|
|
6710
|
+
});
|
|
6711
|
+
if (queued.status === 'stale-before-write')
|
|
6712
|
+
return 'stale-before-write';
|
|
6713
|
+
if (queued.value === 'stale-before-write')
|
|
6714
|
+
requeueOnce();
|
|
6715
|
+
return queued.value;
|
|
6716
|
+
}
|
|
5677
6717
|
function isWorkflowWorker() {
|
|
5678
6718
|
return process.env.BOTMUX_WORKFLOW === '1';
|
|
5679
6719
|
}
|
|
@@ -5939,10 +6979,7 @@ async function captureAndUpload() {
|
|
|
5939
6979
|
logError(`Screenshot upload failed: ${err?.message ?? err}`);
|
|
5940
6980
|
return;
|
|
5941
6981
|
}
|
|
5942
|
-
|
|
5943
|
-
// master removed the AI ScreenAnalyzer, so the `analyzing` projection is gone;
|
|
5944
|
-
// PR #597's Codex App no-progress liveness projection still layers on top.
|
|
5945
|
-
status = codexAppLivenessStatus(status);
|
|
6982
|
+
const status = projectedRuntimeScreenStatus();
|
|
5946
6983
|
send({
|
|
5947
6984
|
type: 'screenshot_uploaded',
|
|
5948
6985
|
imageKey,
|
|
@@ -6168,7 +7205,7 @@ async function flushPendingInjections() {
|
|
|
6168
7205
|
if (!canStartInjectionFlush({
|
|
6169
7206
|
injectionFlushing,
|
|
6170
7207
|
userFlushing: isFlushing,
|
|
6171
|
-
sessionRenameInFlight,
|
|
7208
|
+
sessionRenameInFlight: sessionRenameInFlight(),
|
|
6172
7209
|
commandLineWritesPending,
|
|
6173
7210
|
bareShellLaunchBlocked,
|
|
6174
7211
|
}))
|
|
@@ -6176,7 +7213,7 @@ async function flushPendingInjections() {
|
|
|
6176
7213
|
injectionFlushing = true;
|
|
6177
7214
|
try {
|
|
6178
7215
|
while (pendingInjections.length > 0 && backend && isPromptReady && !bareShellLaunchBlocked
|
|
6179
|
-
&& !sessionRenameInFlight) {
|
|
7216
|
+
&& !sessionRenameInFlight()) {
|
|
6180
7217
|
// Mirror flushPending's one-shot launch-failure guard: when an injection is
|
|
6181
7218
|
// the FIRST writer after a (re)spawn, flushPending may never have run
|
|
6182
7219
|
// (pendingMessages empty ⇒ early return) so the bare-shell check must also
|
|
@@ -6881,7 +7918,15 @@ async function handleTrustedCodexAppMarker(kind, payload, control) {
|
|
|
6881
7918
|
}
|
|
6882
7919
|
if (deliverableContent && startedAtMs !== undefined) {
|
|
6883
7920
|
const suppressMarkers = readSendMarkers();
|
|
6884
|
-
|
|
7921
|
+
// Pass the RAW finalContent (not the pre-stripped deliverableContent) as
|
|
7922
|
+
// finalText: shouldSuppressBridgeEmit needs to SEE the trailing sentinel to
|
|
7923
|
+
// apply the "already sent this turn + sentinel terminator → suppress
|
|
7924
|
+
// narration" branch. It strips internally for the length comparison, so a
|
|
7925
|
+
// prose+sentinel final is still length-matched on the stripped prose. If we
|
|
7926
|
+
// handed it the already-stripped text, the trailing-sentinel signal would
|
|
7927
|
+
// be invisible and a longer-than-send narration would leak (same class as
|
|
7928
|
+
// the transcript-path bug this fixes).
|
|
7929
|
+
const gateInput = { markTimeMs: startedAtMs, isLocal: false, finalText: finalContent };
|
|
6885
7930
|
suppressDelivery = suppressDelivery || shouldSuppressBridgeEmit(gateInput, completedAtMs + 5_001, suppressMarkers, false);
|
|
6886
7931
|
if (suppressDelivery) {
|
|
6887
7932
|
log(`${cliName()} final_output suppressed (model already called botmux send)`);
|
|
@@ -7339,8 +8384,11 @@ function onPtyData(data) {
|
|
|
7339
8384
|
return;
|
|
7340
8385
|
}
|
|
7341
8386
|
// Track last PTY output time for the ready-gate quiescence settle (see
|
|
7342
|
-
// settleThenFlush) and
|
|
8387
|
+
// settleThenFlush) and a monotonic generation for rejected-ready evidence.
|
|
8388
|
+
// Generation (not timestamp equality) distinguishes two redraw chunks that
|
|
8389
|
+
// happen inside the same millisecond.
|
|
7343
8390
|
lastPtyOutputAtMs = Date.now();
|
|
8391
|
+
ptyOutputGeneration.observe();
|
|
7344
8392
|
idleDetector?.feed(data);
|
|
7345
8393
|
}
|
|
7346
8394
|
/**
|
|
@@ -7449,23 +8497,12 @@ function markPromptReadyFromPty(observedBackend) {
|
|
|
7449
8497
|
postSessionStartPromptEvidenceInFlight = false;
|
|
7450
8498
|
}
|
|
7451
8499
|
}
|
|
7452
|
-
/**
|
|
7453
|
-
* Push a coarse screen status to the daemon without waiting for the 2s sampler.
|
|
7454
|
-
* Used so card-off reactions see working→idle even on short turns.
|
|
7455
|
-
*
|
|
7456
|
-
* @param force - When true, send `status` as-is and skip classifyScreenUsageLimit.
|
|
7457
|
-
* Synthetic "working" seeds for card-off reactions MUST force: if the first
|
|
7458
|
-
* screen already shows a rate-limit banner, classify would rewrite working→
|
|
7459
|
-
* limited and the seed collapses to limited→limited (no working edge →
|
|
7460
|
-
* finishTurnReactions never runs). Review: PR #633 third round.
|
|
7461
|
-
*/
|
|
8500
|
+
/** Push a coarse screen status without waiting for the periodic sampler. */
|
|
7462
8501
|
function publishScreenStatus(status, opts) {
|
|
7463
8502
|
if (!renderer)
|
|
7464
8503
|
return;
|
|
7465
8504
|
const { content } = renderer.snapshot();
|
|
7466
|
-
const statusPayload = opts?.force
|
|
7467
|
-
? { status }
|
|
7468
|
-
: classifyScreenUsageLimit(content, status);
|
|
8505
|
+
const statusPayload = opts?.force ? { status } : classifyScreenUsageLimit(content, status);
|
|
7469
8506
|
send({
|
|
7470
8507
|
type: 'screen_update',
|
|
7471
8508
|
content,
|
|
@@ -7474,17 +8511,73 @@ function publishScreenStatus(status, opts) {
|
|
|
7474
8511
|
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
7475
8512
|
});
|
|
7476
8513
|
}
|
|
8514
|
+
function stopStructuredStartGraceRecheck() {
|
|
8515
|
+
if (!structuredStartGraceRecheckTimer)
|
|
8516
|
+
return;
|
|
8517
|
+
clearTimeout(structuredStartGraceRecheckTimer);
|
|
8518
|
+
structuredStartGraceRecheckTimer = null;
|
|
8519
|
+
}
|
|
8520
|
+
function redriveRejectedStructuredReady() {
|
|
8521
|
+
const readyEvidenceGeneration = structuredRejectedReadyEvidenceGeneration;
|
|
8522
|
+
if (readyEvidenceGeneration === undefined)
|
|
8523
|
+
return;
|
|
8524
|
+
stopStructuredStartGraceRecheck();
|
|
8525
|
+
pruneExpiredStructuredHeadsAndEmit('structured pre-start gate');
|
|
8526
|
+
if (!backend || isPromptReady) {
|
|
8527
|
+
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
8528
|
+
return;
|
|
8529
|
+
}
|
|
8530
|
+
if (hasStructuredLifecycleBlock()) {
|
|
8531
|
+
const nextRemainingMs = codexBridgeQueue.preStartLeaseRemainingMs();
|
|
8532
|
+
if (nextRemainingMs !== undefined)
|
|
8533
|
+
scheduleStructuredStartGraceRecheck(nextRemainingMs);
|
|
8534
|
+
return;
|
|
8535
|
+
}
|
|
8536
|
+
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
8537
|
+
if (ptyOutputGeneration.isCurrent(readyEvidenceGeneration)) {
|
|
8538
|
+
log('Structured pre-start gate settled with no newer PTY output — re-driving prior prompt-ready signal');
|
|
8539
|
+
idleDetector?.fireIdle();
|
|
8540
|
+
return;
|
|
8541
|
+
}
|
|
8542
|
+
if (cliAdapter?.busyPattern && (backend.captureCurrentScreen || backend.captureViewport)) {
|
|
8543
|
+
probeBusyPatternIdle('structured pre-start gate', backend);
|
|
8544
|
+
return;
|
|
8545
|
+
}
|
|
8546
|
+
try {
|
|
8547
|
+
const currentScreen = captureBackendScreen(backend);
|
|
8548
|
+
if (currentScreen)
|
|
8549
|
+
idleDetector?.feed(currentScreen);
|
|
8550
|
+
}
|
|
8551
|
+
catch (err) {
|
|
8552
|
+
log(`Structured pre-start screen recheck failed: ${err.message}`);
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
/** Re-drive a prompt signal rejected only because an explicitly confirmed
|
|
8556
|
+
* submit or its in-flight verification had not reached the structured
|
|
8557
|
+
* transcript yet. The bounded lease prevents permanent false-busy. */
|
|
8558
|
+
function scheduleStructuredStartGraceRecheck(remainingMs) {
|
|
8559
|
+
stopStructuredStartGraceRecheck();
|
|
8560
|
+
const backendAtSchedule = backend;
|
|
8561
|
+
const cliGenerationAtSchedule = cliSpawnGeneration;
|
|
8562
|
+
structuredStartGraceRecheckTimer = setTimeout(() => {
|
|
8563
|
+
structuredStartGraceRecheckTimer = null;
|
|
8564
|
+
if (!isCliBackendGenerationCurrent({ generation: cliGenerationAtSchedule, backend: backendAtSchedule }, { generation: cliSpawnGeneration, backend, restartInProgress: cliRestartInProgress })) {
|
|
8565
|
+
return;
|
|
8566
|
+
}
|
|
8567
|
+
redriveRejectedStructuredReady();
|
|
8568
|
+
}, Math.max(1, remainingMs + 1));
|
|
8569
|
+
structuredStartGraceRecheckTimer.unref?.();
|
|
8570
|
+
}
|
|
7477
8571
|
function markPromptReady() {
|
|
7478
|
-
// Only screen readiness plus a non-shell pane leaf may release a launch that
|
|
7479
|
-
// was previously blocked. Transcript/task callbacks can arrive while the
|
|
7480
|
-
// wrapper is still a shell; accepting one here would set isPromptReady and
|
|
7481
|
-
// make the later real PTY callback return early, stranding queued input.
|
|
7482
8572
|
if (bareShellLaunchBlocked) {
|
|
7483
8573
|
log('Ignoring non-PTY prompt-ready while bare-shell launch block is active');
|
|
7484
8574
|
return;
|
|
7485
8575
|
}
|
|
7486
|
-
if (isPromptReady)
|
|
8576
|
+
if (isPromptReady) {
|
|
8577
|
+
stopStructuredStartGraceRecheck();
|
|
7487
8578
|
return; // guard against duplicate calls
|
|
8579
|
+
}
|
|
8580
|
+
stopStructuredStartGraceRecheck();
|
|
7488
8581
|
if (lastInitConfig?.cliId === 'codex-app' && !codexAppControlProven) {
|
|
7489
8582
|
if (!codexAppUnprovenPromptDeferred) {
|
|
7490
8583
|
log('Ignoring Codex App prompt until this worker verifies a fresh Ed25519 challenge response');
|
|
@@ -7550,8 +8643,27 @@ function markPromptReady() {
|
|
|
7550
8643
|
}
|
|
7551
8644
|
if (freshnessAction === 'ignore')
|
|
7552
8645
|
return;
|
|
8646
|
+
// Screen prompt/quiescence is only a UI heuristic. Structured transcript
|
|
8647
|
+
// bridges have the stronger lifecycle signal: a transcript-started turn
|
|
8648
|
+
// without assistant_final is still running even if the TUI redraw exposes a
|
|
8649
|
+
// prompt. An adapter-confirmed type-ahead submit also blocks during a bounded
|
|
8650
|
+
// hand-off lease while the CLI waits to write its transcript user event. A
|
|
8651
|
+
// bare mark is never authoritative, and the lease is explicitly re-driven,
|
|
8652
|
+
// so a dropped Enter cannot create permanent false-busy.
|
|
8653
|
+
// Reject the heuristic and re-arm IdleDetector so the later transcript-final
|
|
8654
|
+
// fireIdle() can drive the real ready edge.
|
|
8655
|
+
if (hasStructuredLifecycleBlock()) {
|
|
8656
|
+
const remainingMs = codexBridgeQueue.preStartLeaseRemainingMs();
|
|
8657
|
+
structuredRejectedReadyEvidenceGeneration = ptyOutputGeneration.snapshot();
|
|
8658
|
+
log('Ignoring prompt-ready heuristic while a structured turn is unfinished or submit verification/start is pending');
|
|
8659
|
+
idleDetector?.reset();
|
|
8660
|
+
if (remainingMs !== undefined)
|
|
8661
|
+
scheduleStructuredStartGraceRecheck(remainingMs);
|
|
8662
|
+
return;
|
|
8663
|
+
}
|
|
8664
|
+
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
7553
8665
|
isPromptReady = true;
|
|
7554
|
-
|
|
8666
|
+
settleSessionRenameOnPrompt();
|
|
7555
8667
|
// An old backend can still report idle while its async teardown is running.
|
|
7556
8668
|
// Only a prompt observed after the general restart fence drops may release
|
|
7557
8669
|
// slash commands to the replacement generation.
|
|
@@ -7588,6 +8700,21 @@ function markPromptReady() {
|
|
|
7588
8700
|
send({ type: 'runner_build_ready', runnerBuildId: lastInitConfig.runnerBuildId });
|
|
7589
8701
|
persistCodexRunnerBuildOnReady = false;
|
|
7590
8702
|
}
|
|
8703
|
+
// Send immediate idle snapshot so Lark card reflects idle status.
|
|
8704
|
+
// BUT: skip when messages are pending — flushPending() will immediately
|
|
8705
|
+
// make the CLI busy, so the idle state is transient and shouldn't appear
|
|
8706
|
+
// in the card. This avoids a false "就绪" flash on daemon restart
|
|
8707
|
+
// (where the initial prompt is queued before the CLI becomes idle).
|
|
8708
|
+
if (renderer && pendingMessages.length === 0 && pendingAdoptMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null && !isFlushing) {
|
|
8709
|
+
const { content } = renderer.snapshot();
|
|
8710
|
+
send({
|
|
8711
|
+
type: 'screen_update',
|
|
8712
|
+
content,
|
|
8713
|
+
...usageLimitTracker.classify(content, projectedRuntimeScreenStatus()),
|
|
8714
|
+
turnId: currentBotmuxTurnId,
|
|
8715
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
8716
|
+
});
|
|
8717
|
+
}
|
|
7591
8718
|
if (activeRestartAttemptId) {
|
|
7592
8719
|
// Defense in depth: only report a successful restart when a replacement
|
|
7593
8720
|
// backend is actually installed. Every legitimate ready path assigns
|
|
@@ -7746,42 +8873,56 @@ function notifyAmbiguousSubmissionRecovery(recoveryReason, turnIdentity) {
|
|
|
7746
8873
|
message: recoveryReason,
|
|
7747
8874
|
});
|
|
7748
8875
|
}
|
|
8876
|
+
/** Remove a transcript mark after the submit path has conclusively failed.
|
|
8877
|
+
* Both bridge implementations mark before writing so they cannot miss a
|
|
8878
|
+
* fast transcript append; a failed write must undo only an UNSTARTED mark.
|
|
8879
|
+
* Cleanup itself does not synthesize ready: an unverified mark never blocks,
|
|
8880
|
+
* and any verified pre-start lease is bounded and owns its expiry re-drive. */
|
|
8881
|
+
function dropFailedBridgeMark(bridgeTurnId, dispatchAttempt) {
|
|
8882
|
+
if (!bridgeTurnId)
|
|
8883
|
+
return;
|
|
8884
|
+
const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId, dispatchAttempt);
|
|
8885
|
+
const droppedStructured = codexBridgeQueue.dropPendingTurn(bridgeTurnId, dispatchAttempt);
|
|
8886
|
+
if (dropped) {
|
|
8887
|
+
if (dropped.contentFingerprint)
|
|
8888
|
+
bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
|
|
8889
|
+
log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — rotation-fallback scan will stop spinning on this fingerprint.`);
|
|
8890
|
+
}
|
|
8891
|
+
if (droppedStructured) {
|
|
8892
|
+
log(`Structured bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — later buffered turns were rechecked.`);
|
|
8893
|
+
// dropPendingTurn can replay a buffered successor user+final pair. Drain
|
|
8894
|
+
// it in the same explicit mutation path; leaving completion production to
|
|
8895
|
+
// a later transcript event can strand fallback output indefinitely.
|
|
8896
|
+
emitReadyCodexTurns();
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
7749
8899
|
/** Worker-side handler for `submitted: false`. Defers the user-facing
|
|
7750
8900
|
* warning and runs the adapter-supplied `recheck` closure first; if the
|
|
7751
8901
|
* message has shown up in the transcript by then (slow path, hook delay),
|
|
7752
8902
|
* suppresses the warning entirely. Adapters without a recheck still fall
|
|
7753
8903
|
* through to the warning after the same delay so the UX is uniform.
|
|
7754
8904
|
*
|
|
7755
|
-
* `bridgeTurnId` is the
|
|
8905
|
+
* `bridgeTurnId` is the transcript queue mark created right before the
|
|
7756
8906
|
* failing writeInput. When the deferred recheck conclusively fails (= no
|
|
7757
|
-
*
|
|
7758
|
-
* attempt's mark
|
|
7759
|
-
*
|
|
7760
|
-
*
|
|
7761
|
-
function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn(), turnIdentity, durableTerminalStatus = 'failed') {
|
|
8907
|
+
* transcript event will ever match this fingerprint), we drop that exact
|
|
8908
|
+
* dispatch attempt's mark. Leaving a Claude mark would keep rotation-fallback
|
|
8909
|
+
* scans spinning; leaving a structured mark would block later fingerprint
|
|
8910
|
+
* attribution even after its bounded status lease expires. */
|
|
8911
|
+
function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn(), turnIdentity, durableTerminalStatus = 'failed', structuredTarget = false) {
|
|
7762
8912
|
const preview = buildSubmitMessagePreview(msg);
|
|
7763
8913
|
const emitDurableTerminal = (errorCode) => {
|
|
7764
8914
|
if (turnIdentity?.turnId && turnIdentity.dispatchAttempt !== undefined) {
|
|
7765
8915
|
emitTurnTerminal(turnIdentity.turnId, durableTerminalStatus, errorCode, turnIdentity.dispatchAttempt);
|
|
7766
8916
|
}
|
|
7767
8917
|
};
|
|
7768
|
-
const dropBridgeMark = () => {
|
|
7769
|
-
if (!bridgeTurnId)
|
|
7770
|
-
return;
|
|
7771
|
-
const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId, turnIdentity?.dispatchAttempt);
|
|
7772
|
-
if (dropped) {
|
|
7773
|
-
if (dropped.contentFingerprint)
|
|
7774
|
-
bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
|
|
7775
|
-
log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}, attempt=${turnIdentity?.dispatchAttempt ?? '-'}) — rotation-fallback scan will stop spinning on this fingerprint.`);
|
|
7776
|
-
}
|
|
7777
|
-
};
|
|
7778
8918
|
if (failureReason) {
|
|
7779
8919
|
const action = decideSubmitConfirmationAction({
|
|
7780
8920
|
failureReason,
|
|
7781
8921
|
recheckSubmitted: false,
|
|
7782
8922
|
usageLimitDetected: false,
|
|
7783
8923
|
});
|
|
7784
|
-
|
|
8924
|
+
dropFailedBridgeMark(bridgeTurnId, turnIdentity?.dispatchAttempt);
|
|
8925
|
+
redriveRejectedStructuredReady();
|
|
7785
8926
|
const reason = action.kind === 'notify-hard-failure' ? action.reason : failureReason;
|
|
7786
8927
|
log(`writeInput: submit impossible — notifying user immediately. reason="${reason}" preview="${preview}"`);
|
|
7787
8928
|
emitDurableTerminal(`submit_impossible:${reason}`);
|
|
@@ -7795,29 +8936,36 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
7795
8936
|
return;
|
|
7796
8937
|
}
|
|
7797
8938
|
const activityBaselineMs = Date.now();
|
|
8939
|
+
const cliGenerationAtSchedule = cliSpawnGeneration;
|
|
8940
|
+
const backendAtSchedule = backend;
|
|
8941
|
+
const deferredAttemptIsCurrent = () => (cliSpawnGeneration === cliGenerationAtSchedule
|
|
8942
|
+
&& backendAtSchedule !== null
|
|
8943
|
+
&& backend === backendAtSchedule
|
|
8944
|
+
&& !cliRestartInProgress);
|
|
7798
8945
|
log(`writeInput: submit not confirmed after retries — deferred ${SUBMIT_DEFERRED_RECHECK_MS}ms recheck queued. preview="${preview}"`);
|
|
7799
8946
|
setTimeout(async () => {
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
cliSessionId = typeof recheckResult === 'object' && recheckResult && typeof recheckResult.cliSessionId === 'string'
|
|
7809
|
-
? recheckResult.cliSessionId
|
|
7810
|
-
: undefined;
|
|
7811
|
-
}
|
|
7812
|
-
catch (err) {
|
|
7813
|
-
log(`Deferred recheck threw (${err?.message ?? err}); falling through to warning.`);
|
|
7814
|
-
}
|
|
7815
|
-
}
|
|
7816
|
-
const action = decideSubmitConfirmationAction({
|
|
7817
|
-
recheckSubmitted,
|
|
7818
|
-
usageLimitDetected: usageLimitTracker.detectedThisTurn(turnSeq),
|
|
7819
|
-
activityEvidence: submitActivityEvidenceSince(activityBaselineMs),
|
|
8947
|
+
const settlement = await settleDeferredSubmitConfirmation(codexBridgeQueue, {
|
|
8948
|
+
turnId: bridgeTurnId,
|
|
8949
|
+
dispatchAttempt: turnIdentity?.dispatchAttempt,
|
|
8950
|
+
structuredTarget,
|
|
8951
|
+
recheck,
|
|
8952
|
+
usageLimitDetected: () => usageLimitTracker.detectedThisTurn(turnSeq),
|
|
8953
|
+
activityEvidence: () => submitActivityEvidenceSince(activityBaselineMs),
|
|
8954
|
+
isCurrent: deferredAttemptIsCurrent,
|
|
7820
8955
|
});
|
|
8956
|
+
// Restart/exit or exact-attempt expiry can happen during either the 20s
|
|
8957
|
+
// delay or the awaited adapter recheck. A stale callback must perform no
|
|
8958
|
+
// side effects at all: no old cliSessionId persistence, ready redrive,
|
|
8959
|
+
// recursive timer, terminal, warning, or mutation of replay attempt N+1.
|
|
8960
|
+
if (settlement.stale) {
|
|
8961
|
+
log(`Discarded stale deferred submit recheck (${settlement.staleReason}) turn=${bridgeTurnId ?? '-'} attempt=${turnIdentity?.dispatchAttempt ?? '-'}`);
|
|
8962
|
+
return;
|
|
8963
|
+
}
|
|
8964
|
+
if (settlement.recheckError !== undefined) {
|
|
8965
|
+
const err = settlement.recheckError;
|
|
8966
|
+
log(`Deferred recheck threw (${err?.message ?? err}); falling through to warning.`);
|
|
8967
|
+
}
|
|
8968
|
+
const { action, cliSessionId } = settlement;
|
|
7821
8969
|
switch (action.kind) {
|
|
7822
8970
|
case 'suppress-confirmed':
|
|
7823
8971
|
if (cliSessionId) {
|
|
@@ -7827,13 +8975,16 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
7827
8975
|
void syncFreshCodexNativeSessionTitle(cliSessionId, codexRpcEngine);
|
|
7828
8976
|
}
|
|
7829
8977
|
log(`Deferred recheck found submit in ${transcriptLabel} — suppressing warning. preview="${preview}"`);
|
|
8978
|
+
redriveRejectedStructuredReady();
|
|
7830
8979
|
return;
|
|
7831
8980
|
case 'suppress-usage-limit':
|
|
7832
|
-
|
|
8981
|
+
dropFailedBridgeMark(bridgeTurnId, turnIdentity?.dispatchAttempt);
|
|
8982
|
+
redriveRejectedStructuredReady();
|
|
7833
8983
|
log(`Deferred recheck missing but usage limit was detected for this turn — suppressing submit warning. preview="${preview}"`);
|
|
7834
8984
|
emitDurableTerminal('submit_usage_limit');
|
|
7835
8985
|
return;
|
|
7836
8986
|
case 'suppress-active':
|
|
8987
|
+
redriveRejectedStructuredReady();
|
|
7837
8988
|
log(`Deferred recheck missing but later ${action.evidence} shows ${cliName()} is active — suppressing submit warning. preview="${preview}"`);
|
|
7838
8989
|
// For a durable turn, activity is evidence of possible submission, not
|
|
7839
8990
|
// a terminal result. Keep the arbiter closed and re-check until either
|
|
@@ -7848,7 +8999,8 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
7848
8999
|
case 'notify-stuck':
|
|
7849
9000
|
break;
|
|
7850
9001
|
}
|
|
7851
|
-
|
|
9002
|
+
dropFailedBridgeMark(bridgeTurnId, turnIdentity?.dispatchAttempt);
|
|
9003
|
+
redriveRejectedStructuredReady();
|
|
7852
9004
|
log(`Deferred recheck still missing — notifying user. preview="${preview}"`);
|
|
7853
9005
|
emitDurableTerminal('submit_unconfirmed');
|
|
7854
9006
|
if (turnIdentity?.dispatchAttempt === undefined) {
|
|
@@ -8009,9 +9161,9 @@ async function flushPending() {
|
|
|
8009
9161
|
}
|
|
8010
9162
|
if (ambiguousSubmissionRecoveryHold?.backend === backend)
|
|
8011
9163
|
return;
|
|
8012
|
-
if (pendingMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null)
|
|
9164
|
+
if (pendingMessages.length === 0 && pendingAdoptMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null)
|
|
8013
9165
|
return; // nothing to flush — keep isPromptReady
|
|
8014
|
-
if (sessionRenameInFlight)
|
|
9166
|
+
if (sessionRenameInFlight())
|
|
8015
9167
|
return; // wait for /rename to finish before any user input
|
|
8016
9168
|
if (commandLineWritesPending > 0)
|
|
8017
9169
|
return; // do not splice into text -> Enter
|
|
@@ -8034,6 +9186,16 @@ async function flushPending() {
|
|
|
8034
9186
|
// the active attempt.
|
|
8035
9187
|
if (!pendingInputMayFlush(durableTurnInFlight))
|
|
8036
9188
|
return;
|
|
9189
|
+
// A no-native or locally-unregistered RPC delivery is deliberately
|
|
9190
|
+
// fail-closed: its exact execution state is unknown. Codex normally permits
|
|
9191
|
+
// type-ahead/steer, but admitting a successor here could merge it into the
|
|
9192
|
+
// unknown turn or overlap a durable replay. Keep every input class queued
|
|
9193
|
+
// until structured evidence, exact terminal, bounded generation restart, or
|
|
9194
|
+
// teardown retires the owner.
|
|
9195
|
+
if (rpcLifecycleFailClosedOwners.size > 0) {
|
|
9196
|
+
log(`Holding pending input behind ${rpcLifecycleFailClosedOwners.size} unresolved Codex RPC delivery owner(s)`);
|
|
9197
|
+
return;
|
|
9198
|
+
}
|
|
8037
9199
|
// Ready-gate: hold the FIRST prompt until the SessionStart hook fires a true-
|
|
8038
9200
|
// ready signal. A cjadk-style startup selector's ❯ falsely matches readyPattern
|
|
8039
9201
|
// and would otherwise eat this message. releaseReadyGate() re-invokes us once
|
|
@@ -8084,12 +9246,13 @@ async function flushPending() {
|
|
|
8084
9246
|
// pending messages can still drain while busy; the rename stays queued.
|
|
8085
9247
|
const sessionRenameReady = isPromptReady && pendingSessionRename !== null;
|
|
8086
9248
|
const rawInputReady = isPromptReady && pendingRawInputs.length > 0;
|
|
9249
|
+
const adoptInputReady = isPromptReady && lastInitConfig?.adoptMode === true && pendingAdoptMessages.length > 0;
|
|
8087
9250
|
let supportedSessionRenameReady = sessionRenameReady;
|
|
8088
9251
|
if (sessionRenameReady && (!cliAdapter.buildSessionRenameCommand || effectiveBackendType === 'riff')) {
|
|
8089
9252
|
pendingSessionRename = null;
|
|
8090
9253
|
supportedSessionRenameReady = false;
|
|
8091
9254
|
log(`Ignoring native session rename — unsupported by ${cliName()}${effectiveBackendType === 'riff' ? ' on riff backend' : ''}`);
|
|
8092
|
-
if (pendingMessages.length === 0 && pendingRawInputs.length === 0)
|
|
9255
|
+
if (pendingMessages.length === 0 && pendingAdoptMessages.length === 0 && pendingRawInputs.length === 0)
|
|
8093
9256
|
return;
|
|
8094
9257
|
}
|
|
8095
9258
|
if (!isPromptReady && pendingMessages.length === 0)
|
|
@@ -8098,14 +9261,12 @@ async function flushPending() {
|
|
|
8098
9261
|
return;
|
|
8099
9262
|
isFlushing = true;
|
|
8100
9263
|
const codexAppPromptReplay = new CodexAppFlushPromptReplay();
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
// force: same rate-limit rewrite trap as argv seed (review third round).
|
|
8108
|
-
publishScreenStatus('working', { force: true });
|
|
9264
|
+
// Raw input and native rename own their explicit command-line/session gates;
|
|
9265
|
+
// pending adopt writes re-arm inside writeAdoptMessage. Clearing readiness
|
|
9266
|
+
// here would make an adopt rename's in-queue readiness recheck always see a
|
|
9267
|
+
// synthetic false. Normal prompt/startup flushes keep the original re-arm.
|
|
9268
|
+
if (!rawInputReady && !supportedSessionRenameReady && !adoptInputReady) {
|
|
9269
|
+
beginCliWriteCycle();
|
|
8109
9270
|
}
|
|
8110
9271
|
try {
|
|
8111
9272
|
// Launch-failure guard, run ONCE per spawn on the first flush, BEFORE startup
|
|
@@ -8155,10 +9316,49 @@ async function flushPending() {
|
|
|
8155
9316
|
if (supportedSessionRenameReady && pendingSessionRename !== null && backend && cliAdapter) {
|
|
8156
9317
|
const title = pendingSessionRename;
|
|
8157
9318
|
const buildRename = cliAdapter.buildSessionRenameCommand;
|
|
9319
|
+
const renameBackend = backend;
|
|
9320
|
+
const renameGeneration = cliSpawnGeneration;
|
|
8158
9321
|
pendingSessionRename = null;
|
|
8159
|
-
|
|
9322
|
+
sessionRenamePhase = 'reserved';
|
|
8160
9323
|
try {
|
|
8161
|
-
|
|
9324
|
+
const writeRename = async () => {
|
|
9325
|
+
// Transition to busy inside the adopt queue, after the readiness
|
|
9326
|
+
// recheck. Keep `writing` through text→beat→Enter: a terminal from
|
|
9327
|
+
// the preceding turn in that window must not release the rename gate.
|
|
9328
|
+
beginCliWriteCycle();
|
|
9329
|
+
sessionRenamePhase = 'writing';
|
|
9330
|
+
await sendRawCommandLineWithRecoveryFence(renameBackend, buildRename(title));
|
|
9331
|
+
if (cliSpawnGeneration !== renameGeneration || backend !== renameBackend || cliRestartInProgress) {
|
|
9332
|
+
throw new Error('rename backend generation changed before Enter settlement');
|
|
9333
|
+
}
|
|
9334
|
+
sessionRenamePhase = 'sent';
|
|
9335
|
+
// A previous turn's terminal may have raised ready while the rename
|
|
9336
|
+
// was still writing. Re-arm after Enter so only rename's own prompt
|
|
9337
|
+
// can settle `sent` (with the bounded timeout as fail-open fallback).
|
|
9338
|
+
beginCliWriteCycle();
|
|
9339
|
+
};
|
|
9340
|
+
const sent = lastInitConfig?.adoptMode
|
|
9341
|
+
? await runAdoptSessionRenameSequence({
|
|
9342
|
+
queue: adoptWriteQueue,
|
|
9343
|
+
// An adopt write queued immediately before rename can make the
|
|
9344
|
+
// outer readiness decision stale while this task waits.
|
|
9345
|
+
isPromptReady: () => isPromptReady
|
|
9346
|
+
&& backend === renameBackend
|
|
9347
|
+
&& cliSpawnGeneration === renameGeneration
|
|
9348
|
+
&& !cliRestartInProgress
|
|
9349
|
+
&& !rawInputRestartGate,
|
|
9350
|
+
writeRename,
|
|
9351
|
+
})
|
|
9352
|
+
: (await writeRename(), true);
|
|
9353
|
+
if (!sent) {
|
|
9354
|
+
// Keep a newer title if one arrived while this rename waited. Retry
|
|
9355
|
+
// this title only when it is still the latest requested canonical title.
|
|
9356
|
+
if (pendingSessionRename === null)
|
|
9357
|
+
pendingSessionRename = title;
|
|
9358
|
+
forceClearSessionRenameInFlight();
|
|
9359
|
+
log(`Deferred native session rename until the next prompt (${cliName()}): ${title}`);
|
|
9360
|
+
return;
|
|
9361
|
+
}
|
|
8162
9362
|
armSessionRenameIdleTimeout();
|
|
8163
9363
|
idleDetector?.reset();
|
|
8164
9364
|
log(`Native session rename command sent (${cliName()}): ${title}`);
|
|
@@ -8169,17 +9369,43 @@ async function flushPending() {
|
|
|
8169
9369
|
// Keep the rename gate for the same bounded fail-open window: the write
|
|
8170
9370
|
// may have stopped after typing only part of the command, so immediately
|
|
8171
9371
|
// appending a deferred raw_input could corrupt both commands.
|
|
8172
|
-
|
|
9372
|
+
if (cliSpawnGeneration === renameGeneration && backend === renameBackend && !cliRestartInProgress) {
|
|
9373
|
+
settleFailedSessionRenameWrite();
|
|
9374
|
+
armSessionRenameIdleTimeout();
|
|
9375
|
+
}
|
|
9376
|
+
else {
|
|
9377
|
+
forceClearSessionRenameInFlight();
|
|
9378
|
+
}
|
|
8173
9379
|
log(`Native session rename command failed (${cliName()}): ${err?.message ?? err}; waiting for prompt or fail-open timeout`);
|
|
8174
9380
|
}
|
|
8175
9381
|
// Wait for the command to finish and the TUI to become idle again before
|
|
8176
9382
|
// sending queued user prompts; otherwise they can land in its picker.
|
|
8177
9383
|
return;
|
|
8178
9384
|
}
|
|
9385
|
+
if (adoptInputReady && pendingAdoptMessages.length > 0) {
|
|
9386
|
+
const item = pendingAdoptMessages.shift();
|
|
9387
|
+
await runAdoptMessageForCapturedGeneration(item, () => {
|
|
9388
|
+
pendingAdoptMessages.unshift(item);
|
|
9389
|
+
log(`Re-queued adopt message at queue head for the replacement CLI generation (${item.content.length} chars)`);
|
|
9390
|
+
});
|
|
9391
|
+
return;
|
|
9392
|
+
}
|
|
8179
9393
|
while (pendingMessages.length > 0 && backend && cliAdapter) {
|
|
8180
9394
|
const item = freshnessInputQueue.takeNormal();
|
|
8181
9395
|
if (!item)
|
|
8182
9396
|
break;
|
|
9397
|
+
// Type-ahead can drain several items in one flush. Each logical submit
|
|
9398
|
+
// starts its own readiness generation before transcript marking/writing.
|
|
9399
|
+
beginCliWriteCycle();
|
|
9400
|
+
const writeGeneration = cliSpawnGeneration;
|
|
9401
|
+
const writeBackend = backend;
|
|
9402
|
+
const writeAdapter = cliAdapter;
|
|
9403
|
+
const writeRpcEngine = codexRpcEngine;
|
|
9404
|
+
const writeContinuationIsCurrent = () => (cliSpawnGeneration === writeGeneration
|
|
9405
|
+
&& backend === writeBackend
|
|
9406
|
+
&& cliAdapter === writeAdapter
|
|
9407
|
+
&& codexRpcEngine === writeRpcEngine
|
|
9408
|
+
&& !cliRestartInProgress);
|
|
8183
9409
|
const durableWrite = item.dispatchAttempt !== undefined;
|
|
8184
9410
|
const msg = item.content;
|
|
8185
9411
|
const logicalMsg = item.logicalContent ?? msg;
|
|
@@ -8206,7 +9432,7 @@ async function flushPending() {
|
|
|
8206
9432
|
// in-flight and must not participate in restart replay. RPC mode is
|
|
8207
9433
|
// excluded: the app-server owns an accepted turn independently of the
|
|
8208
9434
|
// viewer pane, so replaying it after a pane restart could run it twice.
|
|
8209
|
-
if (!
|
|
9435
|
+
if (!writeRpcEngine) {
|
|
8210
9436
|
inflightInputs.onWrite(item);
|
|
8211
9437
|
stuckDetector?.arm();
|
|
8212
9438
|
}
|
|
@@ -8222,8 +9448,11 @@ async function flushPending() {
|
|
|
8222
9448
|
catch { /* best-effort */ }
|
|
8223
9449
|
bridgeTurnId = bridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
|
|
8224
9450
|
}
|
|
8225
|
-
else if (codexBridgeActive) {
|
|
8226
|
-
codexBridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
|
|
9451
|
+
else if (codexBridgeActive && !writeRpcEngine) {
|
|
9452
|
+
bridgeTurnId = codexBridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
|
|
9453
|
+
if (bridgeTurnId) {
|
|
9454
|
+
codexBridgeQueue.beginSubmitVerification(bridgeTurnId, undefined, item.dispatchAttempt);
|
|
9455
|
+
}
|
|
8227
9456
|
}
|
|
8228
9457
|
if (durableWrite
|
|
8229
9458
|
&& cliAdapter.reliableTurnTerminal === true
|
|
@@ -8290,42 +9519,89 @@ async function flushPending() {
|
|
|
8290
9519
|
// backend regression). flushPending is invoked fire-and-forget, so an
|
|
8291
9520
|
// escaping rejection would become an unhandledRejection and crash the
|
|
8292
9521
|
// worker — exactly the failure mode this change is closing. Contain it.
|
|
8293
|
-
let result;
|
|
8294
9522
|
let submissionBackend = null;
|
|
8295
9523
|
let recoveryFailureReason;
|
|
9524
|
+
const handleStaleWriteContinuation = (errorCode) => {
|
|
9525
|
+
const disposition = settleStaleWriteContinuation(item, errorCode, (turnId, code, dispatchAttempt) => {
|
|
9526
|
+
emitTurnTerminal(turnId, 'ambiguous', code, dispatchAttempt);
|
|
9527
|
+
});
|
|
9528
|
+
// Generation change has already handed ordinary input to
|
|
9529
|
+
// InflightInputTracker carryover. Do not touch global bridge queues:
|
|
9530
|
+
// replacement may already have marked the same turnId/undefined key.
|
|
9531
|
+
log(`Discarded stale writeInput continuation turn=${item.turnId ?? '-'} attempt=${item.dispatchAttempt ?? '-'} generation=${writeGeneration} disposition=${disposition}`);
|
|
9532
|
+
};
|
|
9533
|
+
let result;
|
|
9534
|
+
let rpcTurnIdentity;
|
|
9535
|
+
let rpcTurnGeneration;
|
|
8296
9536
|
try {
|
|
8297
|
-
if (
|
|
9537
|
+
if (writeRpcEngine) {
|
|
9538
|
+
rpcTurnIdentity = {
|
|
9539
|
+
turnId: item.turnId ?? `codex-rpc-${randomBytes(8).toString('hex')}`,
|
|
9540
|
+
...(item.dispatchAttempt !== undefined
|
|
9541
|
+
? { dispatchAttempt: item.dispatchAttempt }
|
|
9542
|
+
: {}),
|
|
9543
|
+
};
|
|
9544
|
+
rpcTurnGeneration = {
|
|
9545
|
+
engine: writeRpcEngine,
|
|
9546
|
+
cliGeneration: writeGeneration,
|
|
9547
|
+
};
|
|
9548
|
+
installAwaitingRpcActivation(rpcTurnIdentity, rpcTurnGeneration);
|
|
8298
9549
|
// RPC input mode: deliver via JSON-RPC turn/start (its ack IS the
|
|
8299
9550
|
// submit confirmation), which the attached `codex --remote` TUI
|
|
8300
9551
|
// renders. No tmux paste → the history.jsonl verify/retry/recover
|
|
8301
9552
|
// machinery is bypassed. A throw here falls into the catch below and
|
|
8302
9553
|
// surfaces as a normal submit-failure notice.
|
|
8303
|
-
|
|
8304
|
-
await runAfterAmbiguousSubmissionWrites(targetBackend, async () => {
|
|
9554
|
+
await runAfterAmbiguousSubmissionWrites(writeBackend, async () => {
|
|
8305
9555
|
prepareNormalWrite();
|
|
8306
|
-
await
|
|
9556
|
+
await writeRpcEngine.sendTurn(msg, rpcTurnIdentity);
|
|
8307
9557
|
});
|
|
9558
|
+
// The await may overlap an engine/pane replacement. Fence the captured
|
|
9559
|
+
// generation BEFORE touching the global bridge queue; a stale ack must
|
|
9560
|
+
// never activate a mark owned by the replacement generation.
|
|
9561
|
+
if (!writeContinuationIsCurrent()) {
|
|
9562
|
+
// RPC writes bypass InflightInputTracker because replay could
|
|
9563
|
+
// duplicate an already-accepted server turn. Keep this exact owner
|
|
9564
|
+
// registered until stopCodexRpcEngine settles a native terminal or
|
|
9565
|
+
// publishes one ambiguous teardown terminal + notice.
|
|
9566
|
+
log(`Deferred stale Codex RPC continuation to engine teardown `
|
|
9567
|
+
+ `turn=${rpcTurnIdentity.turnId} generation=${writeGeneration}`);
|
|
9568
|
+
break;
|
|
9569
|
+
}
|
|
8308
9570
|
result = { submitted: true };
|
|
9571
|
+
// Only the ACKed, still-current generation may create bridge state.
|
|
9572
|
+
// While turn/start was pending, codexBridgeIngest left its cursor
|
|
9573
|
+
// untouched; marking now therefore still precedes replay of any
|
|
9574
|
+
// already-persisted user/final events.
|
|
9575
|
+
bridgeTurnId = rpcTurnIdentity.turnId;
|
|
9576
|
+
// The app-server ack confirms execution has begun, but no local
|
|
9577
|
+
// transcript event will follow to flip started. Mark the turn active
|
|
9578
|
+
// so the lifecycle gate stays asserted for the full server-side run
|
|
9579
|
+
// instead of relying on the bounded 20s confirmation lease (which
|
|
9580
|
+
// would expire mid-turn on a long-running or approval-pending turn).
|
|
9581
|
+
const activated = activateRpcTurnLifecycle(rpcTurnIdentity, msg, false, rpcTurnGeneration);
|
|
9582
|
+
if (activated) {
|
|
9583
|
+
codexBridgeDrainAndMaybeEmit({ signalIdle: false });
|
|
9584
|
+
}
|
|
8309
9585
|
}
|
|
8310
|
-
else if (item.codexAppInput &&
|
|
8311
|
-
|
|
8312
|
-
const
|
|
8313
|
-
submissionBackend = targetBackend;
|
|
8314
|
-
const transaction = await runAmbiguousSubmissionTransaction(targetBackend, () => targetAdapter.writeStructuredInput(adapterInputHandle(targetBackend), msg, item.codexAppInput, { turnId: item.turnId, ...(item.codexAppSteerable ? { codexAppSteerable: true } : {}) }), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
9586
|
+
else if (item.codexAppInput && writeAdapter.writeStructuredInput) {
|
|
9587
|
+
submissionBackend = writeBackend;
|
|
9588
|
+
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeStructuredInput(adapterInputHandle(writeBackend), msg, item.codexAppInput, { turnId: item.turnId, ...(item.codexAppSteerable ? { codexAppSteerable: true } : {}) }), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
8315
9589
|
result = transaction.result;
|
|
8316
9590
|
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
8317
9591
|
}
|
|
8318
9592
|
else {
|
|
8319
|
-
|
|
8320
|
-
const
|
|
8321
|
-
submissionBackend = targetBackend;
|
|
8322
|
-
const transaction = await runAmbiguousSubmissionTransaction(targetBackend, () => targetAdapter.writeInput(adapterInputHandle(targetBackend), msg, { turnId: item.turnId, ...(item.codexAppSteerable ? { codexAppSteerable: true } : {}) }), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
9593
|
+
submissionBackend = writeBackend;
|
|
9594
|
+
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeInput(adapterInputHandle(writeBackend), msg, { turnId: item.turnId, ...(item.codexAppSteerable ? { codexAppSteerable: true } : {}) }), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
8323
9595
|
result = transaction.result;
|
|
8324
9596
|
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
8325
9597
|
}
|
|
8326
9598
|
if (recoveryFailureReason) {
|
|
8327
9599
|
result = { ...result, submitted: false };
|
|
8328
9600
|
}
|
|
9601
|
+
if (!writeContinuationIsCurrent()) {
|
|
9602
|
+
handleStaleWriteContinuation('write_generation_changed');
|
|
9603
|
+
break;
|
|
9604
|
+
}
|
|
8329
9605
|
// Transcript-backed CLIs (Grok/Codex/… reliableTurnTerminal) own idle via
|
|
8330
9606
|
// assistant_final → fireIdle. Their busyPattern is often missing for
|
|
8331
9607
|
// several seconds after submit (or never matches the current TUI chrome),
|
|
@@ -8346,6 +9622,42 @@ async function flushPending() {
|
|
|
8346
9622
|
&& !err.submissionStarted;
|
|
8347
9623
|
// Roll back this turn's Codex App liveness slot regardless of the failure
|
|
8348
9624
|
// shape; the slot was opened before the fenced write.
|
|
9625
|
+
// A replacement generation may already have installed an equal
|
|
9626
|
+
// turn/attempt key. Fence before touching any process-global queue or
|
|
9627
|
+
// liveness state owned by that successor.
|
|
9628
|
+
if (!writeContinuationIsCurrent()) {
|
|
9629
|
+
if (rpcTurnIdentity && rpcTurnGeneration) {
|
|
9630
|
+
// The frame may have crossed the socket before the error. Never hand
|
|
9631
|
+
// RPC input to ordinary carryover/replay; exact teardown owns the
|
|
9632
|
+
// terminal and user notice for this still-awaiting identity.
|
|
9633
|
+
log(`Deferred stale failed Codex RPC continuation to engine teardown `
|
|
9634
|
+
+ `turn=${rpcTurnIdentity.turnId} generation=${writeGeneration}`);
|
|
9635
|
+
}
|
|
9636
|
+
else {
|
|
9637
|
+
handleStaleWriteContinuation('write_generation_changed_after_error');
|
|
9638
|
+
}
|
|
9639
|
+
break;
|
|
9640
|
+
}
|
|
9641
|
+
if (rpcTurnIdentity && rpcTurnGeneration && writeRpcEngine) {
|
|
9642
|
+
const replayAnchorMs = awaitingRpcActivationReplayAnchorMs(rpcTurnIdentity, rpcTurnGeneration);
|
|
9643
|
+
clearAwaitingRpcActivation(rpcTurnIdentity, rpcTurnGeneration);
|
|
9644
|
+
bridgeTurnId = codexBridgeMarkPendingTurn(msg, rpcTurnIdentity.turnId, rpcTurnIdentity.dispatchAttempt, replayAnchorMs);
|
|
9645
|
+
installRpcLifecycleFailClosedOwner(rpcTurnIdentity, rpcTurnGeneration);
|
|
9646
|
+
log(`Codex RPC turn/start became ambiguous (${err?.message ?? err}); `
|
|
9647
|
+
+ `held exact owner ${rpcTurnIdentity.turnId} and blocked successors`);
|
|
9648
|
+
send({
|
|
9649
|
+
type: 'user_notify',
|
|
9650
|
+
message: 'Codex RPC 消息已发出但未取得可验证的 turn/start 响应;为避免重复执行,未自动重发,并暂时阻塞后续消息直到结构化终态、边界重启或会话重启。',
|
|
9651
|
+
turnId: rpcTurnIdentity.turnId,
|
|
9652
|
+
...(rpcTurnIdentity.dispatchAttempt !== undefined
|
|
9653
|
+
? { dispatchAttempt: rpcTurnIdentity.dispatchAttempt }
|
|
9654
|
+
: {}),
|
|
9655
|
+
});
|
|
9656
|
+
if (bridgeTurnId !== rpcTurnIdentity.turnId && !cliRestartInProgress) {
|
|
9657
|
+
void restartCliProcess('Codex RPC ambiguous delivery could not establish an attribution lease', { immediate: true, preservePending: true });
|
|
9658
|
+
}
|
|
9659
|
+
break;
|
|
9660
|
+
}
|
|
8349
9661
|
codexAppPromptReplay.cancelSubmission(codexAppTurnLiveness, codexAppReadyAuthority, codexAppLivenessHandle);
|
|
8350
9662
|
if (submissionPreparationFailed) {
|
|
8351
9663
|
if (item.turnId) {
|
|
@@ -8389,6 +9701,10 @@ async function flushPending() {
|
|
|
8389
9701
|
dispatchStillPending = codexAppTurnDispatchQueue.cancelExact(codexAppDispatchReservation.handle);
|
|
8390
9702
|
}
|
|
8391
9703
|
}
|
|
9704
|
+
if (bridgeTurnId) {
|
|
9705
|
+
codexBridgeQueue.stopRpcActive(bridgeTurnId, item.dispatchAttempt);
|
|
9706
|
+
codexBridgeQueue.finishSubmitVerification(bridgeTurnId, undefined, item.dispatchAttempt);
|
|
9707
|
+
}
|
|
8392
9708
|
log(`writeInput threw: ${err?.message ?? err}`);
|
|
8393
9709
|
if (blockedBeforeWrite && submissionBackend) {
|
|
8394
9710
|
// This exact item is known not to have touched the PTY. Keep its
|
|
@@ -8451,6 +9767,15 @@ async function flushPending() {
|
|
|
8451
9767
|
if (threadId)
|
|
8452
9768
|
void syncFreshCodexNativeSessionTitle(threadId, codexRpcEngine);
|
|
8453
9769
|
}
|
|
9770
|
+
if (result?.submitted === true && bridgeTurnId) {
|
|
9771
|
+
codexBridgeQueue.confirmPendingTurn(bridgeTurnId, undefined, item.dispatchAttempt);
|
|
9772
|
+
}
|
|
9773
|
+
else if (bridgeTurnId && !(result?.submitted === false && result.recheck && !result.failureReason)) {
|
|
9774
|
+
// Keep verification pending only while an adapter-supplied deferred
|
|
9775
|
+
// recheck can still produce authoritative submit evidence.
|
|
9776
|
+
codexBridgeQueue.finishSubmitVerification(bridgeTurnId, undefined, item.dispatchAttempt);
|
|
9777
|
+
}
|
|
9778
|
+
redriveRejectedStructuredReady();
|
|
8454
9779
|
// `&& backend`: if the CLI exited during this write (pane gone → onExit
|
|
8455
9780
|
// nulled backend) the user already got a "CLI exited" notice; don't also
|
|
8456
9781
|
// nag that the submit wasn't confirmed.
|
|
@@ -8525,6 +9850,8 @@ async function flushPending() {
|
|
|
8525
9850
|
// that correctly. Durable receiver attempts are the exception: they and
|
|
8526
9851
|
// adjacent IM turns wait for separate idle edges so neither can be
|
|
8527
9852
|
// HOL-dropped or steered into the other.
|
|
9853
|
+
if (rpcLifecycleFailClosedOwners.size > 0)
|
|
9854
|
+
break;
|
|
8528
9855
|
if (shouldStopPendingBatch(item, pendingMessages[0]))
|
|
8529
9856
|
break;
|
|
8530
9857
|
}
|
|
@@ -8614,7 +9941,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
8614
9941
|
// type-ahead write is dropped (no input box yet) — markPromptReady()'s flush
|
|
8615
9942
|
// delivers queued messages instead. See input-gate.ts; this fixes dispatch's
|
|
8616
9943
|
// brief reaching Codex before its first idle and never landing.
|
|
8617
|
-
if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({
|
|
9944
|
+
if (!sessionRenameInFlight() && commandLineWritesPending === 0 && shouldWriteNow({
|
|
8618
9945
|
isPromptReady,
|
|
8619
9946
|
isFlushing,
|
|
8620
9947
|
supportsTypeAhead,
|
|
@@ -8658,59 +9985,65 @@ function startScreenUpdates() {
|
|
|
8658
9985
|
screenUpdateTimer = setInterval(() => {
|
|
8659
9986
|
if (awaitingFirstPrompt)
|
|
8660
9987
|
return;
|
|
8661
|
-
let status = isPromptReady ? 'idle' : 'working';
|
|
8662
|
-
// master removed the AI ScreenAnalyzer; PR #597's Codex App no-progress
|
|
8663
|
-
// liveness projection still layers on top of the base idle/working verdict.
|
|
8664
|
-
status = codexAppLivenessStatus(status);
|
|
8665
9988
|
void (async () => {
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
9989
|
+
const { snapshot, status } = await snapshotWithLatestRuntimeStatus(async () => {
|
|
9990
|
+
let content = lastContent;
|
|
9991
|
+
let changed = false;
|
|
9992
|
+
// Capture only when the pane has emitted output since our last snapshot.
|
|
9993
|
+
// During idle (the steady state for a parked session) this skips a tmux
|
|
9994
|
+
// capture-pane + a throwaway xterm-headless instantiation every tick —
|
|
9995
|
+
// the dominant per-session background cost — while the status-transition
|
|
9996
|
+
// send below still fires off the cached content. The exception is a
|
|
9997
|
+
// self-driven screen (observe backend with a live web-attach): the
|
|
9998
|
+
// watermark can't be trusted there, so capture every tick.
|
|
9999
|
+
const ptyActivity = lastPtyActivityAtMs;
|
|
10000
|
+
if (shouldCaptureScreen({
|
|
10001
|
+
ptyActivity,
|
|
10002
|
+
lastCapturedPtyActivity: lastSnapshotPtyActivity,
|
|
10003
|
+
screenSelfDriven: isScreenSelfDriven(backend),
|
|
10004
|
+
})) {
|
|
10005
|
+
lastSnapshotPtyActivity = ptyActivity;
|
|
10006
|
+
// Preferred path: pipe-pane backends pull a fresh viewport snapshot
|
|
10007
|
+
// from tmux every tick. This eliminates the accumulated-buffer drift
|
|
10008
|
+
// that produced duplicated/staircase text in 'text' display mode.
|
|
10009
|
+
const pipeText = await snapshotToText(backend, renderCols, renderRows, { filter: true });
|
|
10010
|
+
if (pipeText) {
|
|
10011
|
+
content = pipeText.content;
|
|
10012
|
+
const hash = pipeText.ansi;
|
|
10013
|
+
changed = hash !== lastTextSnapshotHash;
|
|
10014
|
+
lastTextSnapshotHash = hash;
|
|
10015
|
+
// Refresh the unfiltered cache that ScreenAnalyzer reads from. Same
|
|
10016
|
+
// tmux call would otherwise need to fire twice per tick.
|
|
10017
|
+
if (changed) {
|
|
10018
|
+
const rawSnap = await snapshotToText(backend, renderCols, renderRows, { filter: false });
|
|
10019
|
+
if (rawSnap)
|
|
10020
|
+
lastAnalyzerSnapshot = rawSnap.content;
|
|
10021
|
+
}
|
|
8698
10022
|
}
|
|
10023
|
+
else if (renderer) {
|
|
10024
|
+
const snap = renderer.snapshot();
|
|
10025
|
+
content = snap.content;
|
|
10026
|
+
changed = snap.changed;
|
|
10027
|
+
}
|
|
10028
|
+
else {
|
|
10029
|
+
return null;
|
|
10030
|
+
}
|
|
10031
|
+
lastContent = content;
|
|
8699
10032
|
}
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
return;
|
|
8707
|
-
}
|
|
8708
|
-
lastContent = content;
|
|
8709
|
-
}
|
|
8710
|
-
const usageAware = classifyScreenUsageLimit(content, status);
|
|
8711
|
-
if (changed || usageAware.status !== lastSentStatus) {
|
|
10033
|
+
return { content, changed };
|
|
10034
|
+
}, projectedRuntimeScreenStatus);
|
|
10035
|
+
if (!snapshot)
|
|
10036
|
+
return;
|
|
10037
|
+
const usageAware = usageLimitTracker.classify(snapshot.content, status);
|
|
10038
|
+
if (snapshot.changed || usageAware.status !== lastSentStatus) {
|
|
8712
10039
|
lastSentStatus = usageAware.status;
|
|
8713
|
-
send({
|
|
10040
|
+
send({
|
|
10041
|
+
type: 'screen_update',
|
|
10042
|
+
content: snapshot.content,
|
|
10043
|
+
...usageAware,
|
|
10044
|
+
turnId: currentBotmuxTurnId,
|
|
10045
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
10046
|
+
});
|
|
8714
10047
|
}
|
|
8715
10048
|
})();
|
|
8716
10049
|
}, SCREEN_UPDATE_INTERVAL_MS);
|
|
@@ -9082,7 +10415,8 @@ function scheduleBusyPatternIdleProbe(source) {
|
|
|
9082
10415
|
}
|
|
9083
10416
|
async function spawnCli(cfg, opts = {}) {
|
|
9084
10417
|
const spawnGeneration = ++cliSpawnGeneration;
|
|
9085
|
-
|
|
10418
|
+
// Prefer force-clear so a half-finished rename cannot block the new generation.
|
|
10419
|
+
forceClearSessionRenameInFlight();
|
|
9086
10420
|
currentCliCredentialIsolated = false;
|
|
9087
10421
|
// Enrollment writes the fixed marker before any device credential appears.
|
|
9088
10422
|
// From that instant onward every NEW local CLI must carry a credential
|
|
@@ -9392,9 +10726,6 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
9392
10726
|
sessionEnv.BOTMUX_DEFERRED_SCHEDULE_TOPIC_TITLE = cfg.deferredScheduleRun.topicTitle;
|
|
9393
10727
|
}
|
|
9394
10728
|
}
|
|
9395
|
-
// Turn sender so `--mention-back` can resolve an @ target in the sandbox.
|
|
9396
|
-
if (cfg.ownerOpenId)
|
|
9397
|
-
sessionEnv.BOTMUX_OWNER_OPEN_ID = cfg.ownerOpenId;
|
|
9398
10729
|
// Lark credentials so `botmux send` works inside the riff sandbox without a
|
|
9399
10730
|
// local daemon or bots.json. The sandbox has no session data / bot config,
|
|
9400
10731
|
// so cmdSend falls back to these env vars to call the Lark API directly.
|
|
@@ -9424,6 +10755,10 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
9424
10755
|
mergedEnv.BOTMUX_API_ONLY = '1';
|
|
9425
10756
|
mergedEnv.BOTMUX_CHAT_ID = cfg.chatId; // host-owned; never from config
|
|
9426
10757
|
}
|
|
10758
|
+
// Session identity is host-owned. Re-freeze it AFTER per-bot/riff config
|
|
10759
|
+
// merge so a stale or user-supplied backend env cannot impersonate another
|
|
10760
|
+
// owner (or resurrect an owner on an ownerless session).
|
|
10761
|
+
applySessionOwnerEnv(mergedEnv, cfg.ownerOpenId);
|
|
9427
10762
|
riffBackendConfig = Object.assign({}, cfg.backendConfig, { env: mergedEnv, resumeParentTaskId: cfg.riffParentTaskId });
|
|
9428
10763
|
// 复用本地仓库+分支:多仓只认会话上的显式 stamp(仓库选择卡多选流按用户
|
|
9429
10764
|
// 顺序写入 cfg.riffRepoDirs,首仓=primary);否则仅对 workingDir 本身做单仓
|
|
@@ -10321,6 +11656,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
10321
11656
|
else
|
|
10322
11657
|
delete childEnv.BOTMUX_API_ONLY;
|
|
10323
11658
|
childEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
|
|
11659
|
+
applySessionOwnerEnv(childEnv, cfg.ownerOpenId);
|
|
10324
11660
|
// This bot's resolved brandLabel template, injected so a SANDBOXED `botmux
|
|
10325
11661
|
// send` renders the role-name footer without reading bots.json (deny-by-
|
|
10326
11662
|
// default → EPERM → role footer would silently fall back to the default
|
|
@@ -11420,7 +12756,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11420
12756
|
if (effectiveCliSessionId) {
|
|
11421
12757
|
const rolloutPath = findCodexRolloutBySessionId(effectiveCliSessionId);
|
|
11422
12758
|
if (rolloutPath) {
|
|
11423
|
-
codexBridgeAttach(rolloutPath, 'baseline-existing');
|
|
12759
|
+
codexBridgeAttach(rolloutPath, effectiveResume ? 'baseline-existing' : 'fresh-empty');
|
|
11424
12760
|
}
|
|
11425
12761
|
else {
|
|
11426
12762
|
codexBridgePendingSessionId = effectiveCliSessionId;
|
|
@@ -11439,7 +12775,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11439
12775
|
if (effectiveCliSessionId) {
|
|
11440
12776
|
const rolloutPath = findTraexRolloutBySessionId(effectiveCliSessionId);
|
|
11441
12777
|
if (rolloutPath) {
|
|
11442
|
-
codexBridgeAttach(rolloutPath, 'baseline-existing');
|
|
12778
|
+
codexBridgeAttach(rolloutPath, effectiveResume ? 'baseline-existing' : 'fresh-empty');
|
|
11443
12779
|
}
|
|
11444
12780
|
else {
|
|
11445
12781
|
codexBridgePendingSessionId = effectiveCliSessionId;
|
|
@@ -11542,6 +12878,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11542
12878
|
];
|
|
11543
12879
|
log(`Ready gate skipped — preflight failed: ${reasons.join('; ')}`);
|
|
11544
12880
|
}
|
|
12881
|
+
ptyOutputGeneration.reset();
|
|
11545
12882
|
if (shouldArmReadyGate({
|
|
11546
12883
|
injectsReadyHook: cliAdapter.injectsReadyHook === true,
|
|
11547
12884
|
readySignalAvailable,
|
|
@@ -11608,8 +12945,19 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11608
12945
|
log('Screen settle barrier degraded after bounded retries; finalizing from the last successful snapshot');
|
|
11609
12946
|
}
|
|
11610
12947
|
}
|
|
11611
|
-
|
|
11612
|
-
|
|
12948
|
+
// Pi's transcript final (assistant_final) is persisted asynchronously
|
|
12949
|
+
// from the TUI clearing its `Working...` busy marker — either order
|
|
12950
|
+
// occurs. An external idle landing while the authoritative viewport
|
|
12951
|
+
// still shows busy must defer exactly like a screen idle, or the card
|
|
12952
|
+
// flips to 等待输入 mid-turn. Scoped to Pi: Grok/Codex own their idle
|
|
12953
|
+
// via reliableTurnTerminal / lifecycle blocking, and their busy markers
|
|
12954
|
+
// legitimately lag behind the transcript final. deferPromptReadyWhileBusy
|
|
12955
|
+
// itself fail-opens on non-authoritative screens (ZMX), so a stale
|
|
12956
|
+
// `Working...` in ZMX history can never block a structured terminal.
|
|
12957
|
+
const busyGuardedIdle = evidenceSource === 'screen'
|
|
12958
|
+
|| (evidenceSource === 'external' && structuredBridgeIsPi());
|
|
12959
|
+
if (busyGuardedIdle && idleBackend
|
|
12960
|
+
&& deferPromptReadyWhileBusy(`${cliName()} ${evidenceSource}-idle`, idleBackend))
|
|
11613
12961
|
return;
|
|
11614
12962
|
drainBridgesThenMarkReady(evidenceSource);
|
|
11615
12963
|
});
|
|
@@ -11938,6 +13286,9 @@ function killCli(opts = {}) {
|
|
|
11938
13286
|
idleDetector = null;
|
|
11939
13287
|
stopReattachIdleProbe();
|
|
11940
13288
|
stopBusyPatternIdleProbe();
|
|
13289
|
+
stopStructuredStartGraceRecheck();
|
|
13290
|
+
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
13291
|
+
ptyOutputGeneration.reset();
|
|
11941
13292
|
// Cancel any pending ready-gate fallback / settle timers; spawnCli re-arms on respawn.
|
|
11942
13293
|
if (readySignalTimer) {
|
|
11943
13294
|
clearTimeout(readySignalTimer);
|
|
@@ -12007,9 +13358,11 @@ function killCli(opts = {}) {
|
|
|
12007
13358
|
sandboxCleanup = null;
|
|
12008
13359
|
}
|
|
12009
13360
|
isPromptReady = false;
|
|
12010
|
-
|
|
12011
|
-
if (!opts.preservePending)
|
|
13361
|
+
forceClearSessionRenameInFlight();
|
|
13362
|
+
if (!opts.preservePending) {
|
|
12012
13363
|
pendingMessages.length = 0;
|
|
13364
|
+
pendingAdoptMessages.length = 0;
|
|
13365
|
+
}
|
|
12013
13366
|
// pendingRawInputs contains only commands that were accepted but never typed
|
|
12014
13367
|
// because /rename owned the TUI or an owned CLI restart was already fenced.
|
|
12015
13368
|
// Preserve them across restart; unlike an in-flight raw command, replaying
|
|
@@ -12034,6 +13387,10 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
12034
13387
|
log(`Restart ignored in adopt mode (${reason})`);
|
|
12035
13388
|
return;
|
|
12036
13389
|
}
|
|
13390
|
+
// Invalidate queued/deferred callbacks before async teardown begins. Waiting
|
|
13391
|
+
// until killCli/spawnCli would leave a window where an old timer can mutate
|
|
13392
|
+
// the next durable attempt while destroySession is still awaiting.
|
|
13393
|
+
cliSpawnGeneration += 1;
|
|
12037
13394
|
// Set before touching destroySession(): remote teardown can await for many
|
|
12038
13395
|
// seconds while the old backend object is still non-null and still capable
|
|
12039
13396
|
// of firing idle/task-done callbacks. Inputs accepted in that interval must
|
|
@@ -13391,13 +14748,13 @@ const emittedTurnTerminals = new TurnTerminalDeduper();
|
|
|
13391
14748
|
/** Report CLI processing completion independently from user-visible output.
|
|
13392
14749
|
* Keep a bounded worker-local dedup set because transcript watchers and app
|
|
13393
14750
|
* runners may surface the same final boundary more than once. */
|
|
13394
|
-
function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt) {
|
|
14751
|
+
function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt, outputDisposition) {
|
|
13395
14752
|
if (!sessionId || !turnId)
|
|
13396
14753
|
return;
|
|
13397
14754
|
if (!emittedTurnTerminals.claim(sessionId, turnId, dispatchAttempt))
|
|
13398
14755
|
return;
|
|
13399
14756
|
if (status !== 'completed') {
|
|
13400
|
-
const dropped = codexBridgeQueue.dropPendingTurn(turnId, dispatchAttempt);
|
|
14757
|
+
const dropped = codexBridgeQueue.dropPendingTurn(turnId, dispatchAttempt, true);
|
|
13401
14758
|
if (dropped) {
|
|
13402
14759
|
log(`Structured bridge retired terminal attempt turn=${turnId.slice(0, 12)} attempt=${dispatchAttempt ?? '-'} status=${status}`);
|
|
13403
14760
|
}
|
|
@@ -13412,6 +14769,7 @@ function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt) {
|
|
|
13412
14769
|
status,
|
|
13413
14770
|
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
13414
14771
|
...(errorCode ? { errorCode } : {}),
|
|
14772
|
+
...(outputDisposition ? { outputDisposition } : {}),
|
|
13415
14773
|
});
|
|
13416
14774
|
if (terminalReleasesDurableTurn({ turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt }, { turnId, dispatchAttempt })) {
|
|
13417
14775
|
// Durable replay ownership moves back to the receiver/hub at terminal.
|
|
@@ -13624,8 +14982,7 @@ process.on('message', async (raw) => {
|
|
|
13624
14982
|
sessionId = msg.sessionId;
|
|
13625
14983
|
refreshTerminalViewToken();
|
|
13626
14984
|
refreshTerminalWriteToken();
|
|
13627
|
-
|
|
13628
|
-
process.env.__OWNER_OPEN_ID = msg.ownerOpenId;
|
|
14985
|
+
applySessionOwnerEnv(process.env, msg.ownerOpenId);
|
|
13629
14986
|
// Pin this worker's i18n locale early so every t() call below resolves
|
|
13630
14987
|
// against the bot's chosen language without each callsite needing to
|
|
13631
14988
|
// re-thread it.
|
|
@@ -13758,6 +15115,11 @@ process.on('message', async (raw) => {
|
|
|
13758
15115
|
await prepareCodexNativeTitleGeneration(msg, codexRpcEngine);
|
|
13759
15116
|
if (codexRpcEngine)
|
|
13760
15117
|
armRpcStartupDialogDismiss(); // boundary #4: keep the --remote pane from freezing on a startup dialog
|
|
15118
|
+
if (deferredFreshRpcTurn) {
|
|
15119
|
+
const deferred = deferredFreshRpcTurn;
|
|
15120
|
+
deferredFreshRpcTurn = undefined;
|
|
15121
|
+
releaseRpcTurnTerminalDeferral(deferred.identity, deferred.generation);
|
|
15122
|
+
}
|
|
13761
15123
|
// Queue the initial prompt — flushed when CLI shows idle.
|
|
13762
15124
|
// Adapters with passesInitialPromptViaArgs (e.g. Gemini -i) bake the
|
|
13763
15125
|
// prompt into CLI args, so we normally skip queuing to avoid double-send.
|
|
@@ -13778,7 +15140,9 @@ process.on('message', async (raw) => {
|
|
|
13778
15140
|
// Mark it here before the CLI starts processing; late-attach is fine
|
|
13779
15141
|
// because CodexBridgeQueue is path-agnostic until ingest discovers the
|
|
13780
15142
|
// transcript file.
|
|
13781
|
-
codexBridgeMarkPendingTurn(msg.prompt, msg.turnId, msg.dispatchAttempt);
|
|
15143
|
+
const bridgeTurnId = codexBridgeMarkPendingTurn(msg.prompt, msg.turnId, msg.dispatchAttempt);
|
|
15144
|
+
if (bridgeTurnId)
|
|
15145
|
+
codexBridgeQueue.confirmPendingTurn(bridgeTurnId, undefined, msg.dispatchAttempt);
|
|
13782
15146
|
}
|
|
13783
15147
|
// Queue the initial prompt for flushPending (pure decision, unit-tested):
|
|
13784
15148
|
// - paste (no engine): normal queue.
|
|
@@ -13930,6 +15294,13 @@ process.on('message', async (raw) => {
|
|
|
13930
15294
|
// submission mutex is held so a queued message cannot steal attribution
|
|
13931
15295
|
// from the write/verification already in flight.
|
|
13932
15296
|
let turnSeq = usageLimitTracker.currentTurn();
|
|
15297
|
+
// Adopt handlers are async and serialized below, so their renderer/usage
|
|
15298
|
+
// turn begins inside writeAdoptMessage. Non-adopt keeps the immediate
|
|
15299
|
+
// baseline while the message waits for normal flush scheduling.
|
|
15300
|
+
if (!lastInitConfig?.adoptMode) {
|
|
15301
|
+
renderer?.markNewTurn();
|
|
15302
|
+
usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
15303
|
+
}
|
|
13933
15304
|
if (msg.nativeSessionTitlePrompt
|
|
13934
15305
|
&& msg.nativeSessionTitle
|
|
13935
15306
|
&& lastInitConfig?.cliId === 'codex'
|
|
@@ -13985,173 +15356,28 @@ process.on('message', async (raw) => {
|
|
|
13985
15356
|
}
|
|
13986
15357
|
}
|
|
13987
15358
|
if (lastInitConfig?.adoptMode) {
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
currentBotmuxTurnId = msg.turnId;
|
|
13998
|
-
currentBotmuxDispatchAttempt = msg.dispatchAttempt;
|
|
13999
|
-
currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
|
|
14000
|
-
writeCliPidMarker();
|
|
14001
|
-
publishSandboxRelayCapability();
|
|
14002
|
-
// Capture the transcript baseline only after this turn owns the
|
|
14003
|
-
// submission lock and immediately before its literal write.
|
|
14004
|
-
if (bridgeJsonlPath) {
|
|
14005
|
-
try {
|
|
14006
|
-
bridgeIngest();
|
|
14007
|
-
}
|
|
14008
|
-
catch { /* best effort */ }
|
|
14009
|
-
bridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
14010
|
-
}
|
|
14011
|
-
else if (codexBridgeFallbackActive()) {
|
|
14012
|
-
if (codexBridgeIsCursor()) {
|
|
14013
|
-
codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
14014
|
-
try {
|
|
14015
|
-
codexBridgeIngest();
|
|
14016
|
-
}
|
|
14017
|
-
catch { /* best effort */ }
|
|
14018
|
-
}
|
|
14019
|
-
else {
|
|
14020
|
-
try {
|
|
14021
|
-
codexBridgeIngest();
|
|
14022
|
-
}
|
|
14023
|
-
catch { /* best effort */ }
|
|
14024
|
-
codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
14025
|
-
}
|
|
14026
|
-
}
|
|
15359
|
+
const item = {
|
|
15360
|
+
content,
|
|
15361
|
+
turnId: msg.turnId,
|
|
15362
|
+
replyTurnId: msg.replyTurnId,
|
|
15363
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
15364
|
+
codexAppDispatchId: msg.codexAppDispatchId,
|
|
15365
|
+
queuedActivationToken: msg.queuedActivationToken,
|
|
15366
|
+
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
15367
|
+
codexAppInput,
|
|
14027
15368
|
};
|
|
14028
|
-
//
|
|
14029
|
-
//
|
|
14030
|
-
//
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
if (isStructuredBridgeAdoptInputCli(lastInitConfig?.cliId) && cliAdapter) {
|
|
14041
|
-
// writeInput is async but we're already inside an async
|
|
14042
|
-
// message handler. Errors are best-effort logged; attribution and
|
|
14043
|
-
// bridge baselines are prepared only after the submission lock is
|
|
14044
|
-
// held, immediately before the adapter write.
|
|
14045
|
-
const submissionBackend = backend;
|
|
14046
|
-
const submissionAdapter = cliAdapter;
|
|
14047
|
-
let recoveryFailureReason;
|
|
14048
|
-
const composerConflict = codexAdoptComposerConflict(submissionBackend);
|
|
14049
|
-
if (composerConflict) {
|
|
14050
|
-
log('Refused Codex adopt input because the local composer contains an unsubmitted draft');
|
|
14051
|
-
scheduleSubmitFailureNotify(content, undefined, 'submit history', undefined, composerConflict, turnSeq, msg, 'failed');
|
|
14052
|
-
return;
|
|
14053
|
-
}
|
|
14054
|
-
try {
|
|
14055
|
-
const transaction = await runAmbiguousSubmissionTransaction(submissionBackend, () => submissionAdapter.writeInput(adapterInputHandle(submissionBackend), content), settleVerifiableSubmissionForJournal, prepareAdoptWrite);
|
|
14056
|
-
const result = transaction.result;
|
|
14057
|
-
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
14058
|
-
if (result?.cliSessionId) {
|
|
14059
|
-
persistCliSessionId(result.cliSessionId);
|
|
14060
|
-
codexBridgeNotifyCliSessionId(result.cliSessionId);
|
|
14061
|
-
}
|
|
14062
|
-
if (result?.submitted === false || recoveryFailureReason) {
|
|
14063
|
-
if (recoveryFailureReason) {
|
|
14064
|
-
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, msg);
|
|
14065
|
-
}
|
|
14066
|
-
else {
|
|
14067
|
-
scheduleSubmitFailureNotify(content, result?.recheck, 'submit history', undefined, result?.failureReason, turnSeq, msg, 'failed');
|
|
14068
|
-
}
|
|
14069
|
-
}
|
|
14070
|
-
else {
|
|
14071
|
-
acknowledgeTurnInputCommitted(msg.turnId);
|
|
14072
|
-
}
|
|
14073
|
-
}
|
|
14074
|
-
catch (err) {
|
|
14075
|
-
recoveryFailureReason = err instanceof SubmissionWriteError
|
|
14076
|
-
? err.recoveryFailureReason
|
|
14077
|
-
: recoveryFailureReason;
|
|
14078
|
-
const blockedBeforeWrite = err instanceof SubmissionWriteError
|
|
14079
|
-
&& !!err.recoveryFailureReason
|
|
14080
|
-
&& !err.submissionStarted;
|
|
14081
|
-
log(`Adopt writeInput error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
14082
|
-
if (msg.turnId
|
|
14083
|
-
&& msg.dispatchAttempt !== undefined
|
|
14084
|
-
&& blockedBeforeWrite) {
|
|
14085
|
-
emitTurnTerminal(msg.turnId, 'failed', 'zmx_recovery_blocked_before_write', msg.dispatchAttempt);
|
|
14086
|
-
}
|
|
14087
|
-
else if (msg.turnId
|
|
14088
|
-
&& msg.dispatchAttempt !== undefined
|
|
14089
|
-
&& !recoveryFailureReason) {
|
|
14090
|
-
emitTurnTerminal(msg.turnId, 'ambiguous', 'write_input_threw', msg.dispatchAttempt);
|
|
14091
|
-
}
|
|
14092
|
-
if (backend) {
|
|
14093
|
-
if (recoveryFailureReason) {
|
|
14094
|
-
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, msg);
|
|
14095
|
-
}
|
|
14096
|
-
else {
|
|
14097
|
-
scheduleSubmitFailureNotify(content, undefined, 'submit history', undefined, undefined, turnSeq, msg, 'ambiguous');
|
|
14098
|
-
}
|
|
14099
|
-
}
|
|
14100
|
-
}
|
|
14101
|
-
}
|
|
14102
|
-
else if ('sendText' in backend && 'sendSpecialKeys' in backend) {
|
|
14103
|
-
const submissionBackend = backend;
|
|
14104
|
-
let recoveryFailureReason;
|
|
14105
|
-
try {
|
|
14106
|
-
const transaction = await runAmbiguousSubmissionTransaction(submissionBackend, async () => {
|
|
14107
|
-
const input = adapterInputHandle(submissionBackend);
|
|
14108
|
-
input.sendText(content);
|
|
14109
|
-
// Beat between text and Enter so the adopted CLI's input
|
|
14110
|
-
// layer registers the typed chars before submit.
|
|
14111
|
-
await new Promise(r => setTimeout(r, 200));
|
|
14112
|
-
input.sendSpecialKeys('Enter');
|
|
14113
|
-
}, undefined, prepareAdoptWrite);
|
|
14114
|
-
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
14115
|
-
if (recoveryFailureReason) {
|
|
14116
|
-
throw new SubmissionWriteError(`backend could not commit the adopt submission journal; ${recoveryFailureReason}`, recoveryFailureReason);
|
|
14117
|
-
}
|
|
14118
|
-
acknowledgeTurnInputCommitted(msg.turnId);
|
|
14119
|
-
}
|
|
14120
|
-
catch (err) {
|
|
14121
|
-
recoveryFailureReason = err instanceof SubmissionWriteError
|
|
14122
|
-
? err.recoveryFailureReason
|
|
14123
|
-
: recoveryFailureReason;
|
|
14124
|
-
const blockedBeforeWrite = err instanceof SubmissionWriteError
|
|
14125
|
-
&& !!err.recoveryFailureReason
|
|
14126
|
-
&& !err.submissionStarted;
|
|
14127
|
-
log(`Adopt raw input error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
14128
|
-
if (msg.turnId
|
|
14129
|
-
&& msg.dispatchAttempt !== undefined
|
|
14130
|
-
&& blockedBeforeWrite) {
|
|
14131
|
-
emitTurnTerminal(msg.turnId, 'failed', 'zmx_recovery_blocked_before_write', msg.dispatchAttempt);
|
|
14132
|
-
}
|
|
14133
|
-
else if (msg.turnId
|
|
14134
|
-
&& msg.dispatchAttempt !== undefined
|
|
14135
|
-
&& !recoveryFailureReason) {
|
|
14136
|
-
emitTurnTerminal(msg.turnId, 'ambiguous', 'write_input_threw', msg.dispatchAttempt);
|
|
14137
|
-
}
|
|
14138
|
-
if (backend) {
|
|
14139
|
-
if (recoveryFailureReason) {
|
|
14140
|
-
notifyAmbiguousSubmissionRecovery(recoveryFailureReason, msg);
|
|
14141
|
-
}
|
|
14142
|
-
else {
|
|
14143
|
-
scheduleSubmitFailureNotify(content, undefined, 'submit history', undefined, undefined, turnSeq, msg, 'ambiguous');
|
|
14144
|
-
}
|
|
14145
|
-
}
|
|
14146
|
-
}
|
|
14147
|
-
}
|
|
14148
|
-
else {
|
|
14149
|
-
prepareAdoptWrite();
|
|
14150
|
-
backend.write(content + '\r');
|
|
14151
|
-
acknowledgeTurnInputCommitted(msg.turnId);
|
|
14152
|
-
}
|
|
14153
|
-
isPromptReady = false;
|
|
14154
|
-
idleDetector?.reset();
|
|
15369
|
+
// process.on('message') does not serialize async listeners. Hold the
|
|
15370
|
+
// per-worker queue across transcript mark + complete adapter write so
|
|
15371
|
+
// two CoCo/Codex paste→wait→Enter/history cycles cannot overlap.
|
|
15372
|
+
if (cliRestartInProgress || rawInputRestartGate || !backend || sessionRenameInFlight()) {
|
|
15373
|
+
pendingAdoptMessages.push(item);
|
|
15374
|
+
log(`Deferred adopt message until the CLI generation/input gate settles (${content.length} chars)`);
|
|
15375
|
+
}
|
|
15376
|
+
else {
|
|
15377
|
+
await runAdoptMessageForCapturedGeneration(item, () => {
|
|
15378
|
+
pendingAdoptMessages.push(item);
|
|
15379
|
+
log(`Re-queued stale adopt message for the replacement CLI generation (${content.length} chars)`);
|
|
15380
|
+
});
|
|
14155
15381
|
}
|
|
14156
15382
|
}
|
|
14157
15383
|
else {
|
|
@@ -14193,7 +15419,7 @@ process.on('message', async (raw) => {
|
|
|
14193
15419
|
// shell。bareShellCheckInProgress 覆盖“检查进行中”、bareShellLaunchBlocked
|
|
14194
15420
|
// 覆盖“仍停在裸 shell 的安全 hold”两种状态,一并入队;若该进程随后出现
|
|
14195
15421
|
// 真实 PTY prompt 且 leaf 已变为非 shell,markPromptReady 会恢复排空。
|
|
14196
|
-
if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight
|
|
15422
|
+
if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight()
|
|
14197
15423
|
|| shouldHoldCodexRunnerInput(codexRunnerFreshness)
|
|
14198
15424
|
|| injectionFlushing || shouldDeferUserFlush(pendingInjections)
|
|
14199
15425
|
|| bareShellCheckInProgress || bareShellLaunchBlocked) {
|
|
@@ -14737,7 +15963,10 @@ process.on('message', async (raw) => {
|
|
|
14737
15963
|
pendingMessages: pendingMessages.length,
|
|
14738
15964
|
pendingRawInputs: pendingRawInputs.length,
|
|
14739
15965
|
pendingSessionRename: pendingSessionRename !== null,
|
|
14740
|
-
sessionRenameInFlight
|
|
15966
|
+
// sessionRenameInFlight became a function in this branch's rename
|
|
15967
|
+
// lifecycle rework; riffWorkerShutdownInputBlocker (added on master by
|
|
15968
|
+
// the RIFF two-phase shutdown) still consumes it as a boolean field.
|
|
15969
|
+
sessionRenameInFlight: sessionRenameInFlight(),
|
|
14741
15970
|
commandLineWritesPending,
|
|
14742
15971
|
});
|
|
14743
15972
|
if (preparedCloseRequestId || closeRequestInFlightId) {
|