botmux 2.111.0 → 2.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +19 -7
- package/README.md +19 -7
- package/dist/adapters/adopt-route.d.ts.map +1 -1
- package/dist/adapters/adopt-route.js +2 -1
- package/dist/adapters/adopt-route.js.map +1 -1
- package/dist/adapters/backend/herdr-backend.d.ts +29 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +167 -0
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/riff-backend.js +2 -2
- package/dist/adapters/backend/sandbox.d.ts +61 -6
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +235 -43
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +1 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts +2 -0
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +7 -0
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/codex-app-turn.d.ts +16 -0
- package/dist/adapters/cli/codex-app-turn.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app-turn.js +26 -0
- package/dist/adapters/cli/codex-app-turn.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +1 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts +8 -6
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +14 -6
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +40 -14
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +116 -19
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/runner-control-channel.d.ts +32 -0
- package/dist/adapters/cli/runner-control-channel.d.ts.map +1 -0
- package/dist/adapters/cli/runner-control-channel.js +104 -0
- package/dist/adapters/cli/runner-control-channel.js.map +1 -0
- package/dist/adapters/cli/shared-hints.d.ts +1 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +9 -0
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +100 -64
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +13 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +36 -27
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +354 -8
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/saved-workflow.d.ts +25 -0
- package/dist/cli/saved-workflow.d.ts.map +1 -0
- package/dist/cli/saved-workflow.js +338 -0
- package/dist/cli/saved-workflow.js.map +1 -0
- package/dist/cli/send-dispatch.d.ts +18 -3
- package/dist/cli/send-dispatch.d.ts.map +1 -1
- package/dist/cli/send-dispatch.js +22 -3
- package/dist/cli/send-dispatch.js.map +1 -1
- package/dist/cli/v3-run-cancel.d.ts +47 -0
- package/dist/cli/v3-run-cancel.d.ts.map +1 -0
- package/dist/cli/v3-run-cancel.js +96 -0
- package/dist/cli/v3-run-cancel.js.map +1 -0
- package/dist/cli/vc-agent.d.ts.map +1 -1
- package/dist/cli/vc-agent.js +54 -1
- package/dist/cli/vc-agent.js.map +1 -1
- package/dist/cli/workflow-migration.d.ts +39 -0
- package/dist/cli/workflow-migration.d.ts.map +1 -0
- package/dist/cli/workflow-migration.js +302 -0
- package/dist/cli/workflow-migration.js.map +1 -0
- package/dist/cli/workflow-run-archive.d.ts +29 -0
- package/dist/cli/workflow-run-archive.d.ts.map +1 -0
- package/dist/cli/workflow-run-archive.js +183 -0
- package/dist/cli/workflow-run-archive.js.map +1 -0
- package/dist/cli/workflow.d.ts +5 -10
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +85 -764
- package/dist/cli/workflow.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1018 -191
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +30 -21
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +21 -9
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/current-turn-provenance.d.ts +39 -0
- package/dist/core/current-turn-provenance.d.ts.map +1 -0
- package/dist/core/current-turn-provenance.js +122 -0
- package/dist/core/current-turn-provenance.js.map +1 -0
- package/dist/core/daemon-ipc-auth.d.ts +20 -0
- package/dist/core/daemon-ipc-auth.d.ts.map +1 -0
- package/dist/core/daemon-ipc-auth.js +44 -0
- package/dist/core/daemon-ipc-auth.js.map +1 -0
- package/dist/core/daemon-ipc-session-auth.d.ts +37 -0
- package/dist/core/daemon-ipc-session-auth.d.ts.map +1 -0
- package/dist/core/daemon-ipc-session-auth.js +35 -0
- package/dist/core/daemon-ipc-session-auth.js.map +1 -0
- package/dist/core/dashboard-command/index.d.ts +0 -2
- package/dist/core/dashboard-command/index.d.ts.map +1 -1
- package/dist/core/dashboard-command/index.js +2 -2
- package/dist/core/dashboard-command/index.js.map +1 -1
- package/dist/core/dashboard-command/stub.d.ts +1 -1
- package/dist/core/dashboard-command/stub.d.ts.map +1 -1
- package/dist/core/dashboard-command/stub.js +0 -1
- package/dist/core/dashboard-command/stub.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +11 -2
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +266 -41
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-secret.d.ts +3 -0
- package/dist/core/dashboard-secret.d.ts.map +1 -0
- package/dist/core/dashboard-secret.js +7 -0
- package/dist/core/dashboard-secret.js.map +1 -0
- package/dist/core/data-dir.d.ts +24 -0
- package/dist/core/data-dir.d.ts.map +1 -0
- package/dist/core/data-dir.js +48 -0
- package/dist/core/data-dir.js.map +1 -0
- package/dist/core/inflight-input-tracker.d.ts +4 -2
- package/dist/core/inflight-input-tracker.d.ts.map +1 -1
- package/dist/core/inflight-input-tracker.js +6 -5
- package/dist/core/inflight-input-tracker.js.map +1 -1
- package/dist/core/inject-queue-policy.d.ts +70 -0
- package/dist/core/inject-queue-policy.d.ts.map +1 -0
- package/dist/core/inject-queue-policy.js +30 -0
- package/dist/core/inject-queue-policy.js.map +1 -0
- package/dist/core/maintenance.d.ts +1 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +3 -2
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/managed-origin-capability.d.ts +30 -0
- package/dist/core/managed-origin-capability.d.ts.map +1 -0
- package/dist/core/managed-origin-capability.js +88 -0
- package/dist/core/managed-origin-capability.js.map +1 -0
- package/dist/core/role-library.d.ts +15 -0
- package/dist/core/role-library.d.ts.map +1 -0
- package/dist/core/role-library.js +75 -0
- package/dist/core/role-library.js.map +1 -0
- package/dist/core/session-cwd.d.ts +14 -0
- package/dist/core/session-cwd.d.ts.map +1 -0
- package/dist/core/session-cwd.js +17 -0
- package/dist/core/session-cwd.js.map +1 -0
- package/dist/core/session-manager.d.ts +4 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +27 -8
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-marker.d.ts +22 -0
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +185 -20
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/slash-inject.d.ts +15 -0
- package/dist/core/slash-inject.d.ts.map +1 -0
- package/dist/core/slash-inject.js +25 -0
- package/dist/core/slash-inject.js.map +1 -0
- package/dist/core/terminal-url.d.ts +1 -0
- package/dist/core/terminal-url.d.ts.map +1 -1
- package/dist/core/terminal-url.js +14 -8
- package/dist/core/terminal-url.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +26 -0
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +48 -3
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/trigger-session.d.ts +24 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +106 -15
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +26 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +65 -2
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +459 -39
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +249 -8
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +7383 -1195
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +1 -1
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +2 -2
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth.d.ts +3 -8
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +63 -22
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +5 -0
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +37 -0
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +42 -2
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/daemon-internal-api.d.ts +0 -2
- package/dist/dashboard/daemon-internal-api.d.ts.map +1 -1
- package/dist/dashboard/daemon-internal-api.js +26 -93
- package/dist/dashboard/daemon-internal-api.js.map +1 -1
- package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
- package/dist/dashboard/daemon-internal-client.js +2 -3
- package/dist/dashboard/daemon-internal-client.js.map +1 -1
- package/dist/dashboard/federation-api.js +1 -1
- package/dist/dashboard/federation-api.js.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +1 -1
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/history-senders.d.ts +3 -0
- package/dist/dashboard/history-senders.d.ts.map +1 -1
- package/dist/dashboard/history-senders.js.map +1 -1
- package/dist/dashboard/http.d.ts +4 -0
- package/dist/dashboard/http.d.ts.map +1 -0
- package/dist/dashboard/http.js +6 -0
- package/dist/dashboard/http.js.map +1 -0
- package/dist/dashboard/registry.d.ts +4 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/trigger-api.js +1 -1
- package/dist/dashboard/trigger-api.js.map +1 -1
- package/dist/dashboard/v3-runs-api.d.ts +13 -10
- package/dist/dashboard/v3-runs-api.d.ts.map +1 -1
- package/dist/dashboard/v3-runs-api.js +121 -4
- package/dist/dashboard/v3-runs-api.js.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.d.ts +105 -0
- package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -0
- package/dist/dashboard/vc-consumer-profiles-api.js +312 -0
- package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -0
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +7 -6
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +9 -14
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +7 -3
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/connectors-page.d.ts +5 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +16 -12
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/create-session-entry.d.ts +4 -0
- package/dist/dashboard/web/create-session-entry.d.ts.map +1 -0
- package/dist/dashboard/web/create-session-entry.js +17 -0
- package/dist/dashboard/web/create-session-entry.js.map +1 -0
- package/dist/dashboard/web/dashboard-components.d.ts +5 -0
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-components.js +29 -4
- package/dist/dashboard/web/dashboard-components.js.map +1 -1
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +0 -1
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +192 -382
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/preferences.d.ts +3 -0
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +18 -0
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +4 -1
- 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 +64 -18
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +2 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/v3-api.d.ts +12 -2
- package/dist/dashboard/web/v3-api.d.ts.map +1 -1
- package/dist/dashboard/web/v3-api.js +34 -6
- package/dist/dashboard/web/v3-api.js.map +1 -1
- package/dist/dashboard/web/v3-components.d.ts +6 -0
- package/dist/dashboard/web/v3-components.d.ts.map +1 -1
- package/dist/dashboard/web/v3-components.js +66 -23
- package/dist/dashboard/web/v3-components.js.map +1 -1
- package/dist/dashboard/web/v3-model.js +1 -1
- package/dist/dashboard/web/v3-model.js.map +1 -1
- package/dist/dashboard/web/v3-terminal.js +2 -2
- package/dist/dashboard/web/v3-terminal.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +19 -0
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -0
- package/dist/dashboard/web/vc-consumer-profiles-section.js +382 -0
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -0
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +29 -6
- 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-RYBL4ZYE.js +4 -0
- package/dist/dashboard-web/chunks/{chunk-OEJIT66L.js → chunk-CMDPZOPK.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +1 -0
- package/dist/dashboard-web/chunks/chunk-NYZHARIU.js +47 -0
- package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-YQFXGRSD.js → chunk-YBOHL7T7.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-YWCPDL4S.js +1 -0
- package/dist/dashboard-web/chunks/connectors-page-DAMP7TTV.js +1 -0
- package/dist/dashboard-web/chunks/{groups-page-5AJPPJH2.js → groups-page-BPFA356N.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-R3DFECX4.js → insights-page-SHOPHTWY.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-NLMKJLDY.js → monitor-room-PXPJGZCN.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-EHSOX4GV.js → monitoring-page-4DF54M5Y.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-OXJKFJ4R.js → office-page-6OHU6KMZ.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-GJHIVXJF.js → overview-page-IHIZ6F5M.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-6ZODQISW.js → roles-page-JYTBYKEF.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-DAGMHVCH.js → schedules-page-WJ72T7KV.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-QC56OIAI.js +3 -0
- package/dist/dashboard-web/chunks/settings-page-TCKWEXTN.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-J7EVUHXP.js → skills-page-RJJNIECW.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-YM5JBIII.js → team-federation-page-ZMDX3E7K.js} +1 -1
- package/dist/dashboard-web/chunks/v3-page-VLLAUQ33.js +94 -0
- package/dist/dashboard-web/chunks/{whiteboards-page-75N2U6W4.js → whiteboards-page-NAO53CKM.js} +1 -1
- package/dist/dashboard-web/style.css +363 -274
- package/dist/dashboard.js +194 -78
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -87
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +12 -87
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/brand-template.d.ts +15 -0
- package/dist/im/lark/brand-template.d.ts.map +1 -0
- package/dist/im/lark/brand-template.js +114 -0
- package/dist/im/lark/brand-template.js.map +1 -0
- package/dist/im/lark/card-handler.d.ts +6 -3
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +23 -39
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +8 -2
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +33 -31
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts +18 -0
- package/dist/im/lark/doc-comment.d.ts.map +1 -1
- package/dist/im/lark/doc-comment.js +119 -5
- package/dist/im/lark/doc-comment.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +18 -2
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +41 -19
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/merge-forward.d.ts.map +1 -1
- package/dist/im/lark/merge-forward.js +7 -2
- package/dist/im/lark/merge-forward.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +7 -0
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/overview-card.d.ts +1 -2
- package/dist/im/lark/overview-card.d.ts.map +1 -1
- package/dist/im/lark/overview-card.js +1 -48
- package/dist/im/lark/overview-card.js.map +1 -1
- package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card-handler.js +5 -5
- package/dist/im/lark/v3-blocked-card-handler.js.map +1 -1
- package/dist/im/lark/v3-blocked-card.d.ts +1 -0
- package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card.js +19 -2
- package/dist/im/lark/v3-blocked-card.js.map +1 -1
- package/dist/im/lark/v3-distillation-card-handler.d.ts +12 -0
- package/dist/im/lark/v3-distillation-card-handler.d.ts.map +1 -0
- package/dist/im/lark/v3-distillation-card-handler.js +107 -0
- package/dist/im/lark/v3-distillation-card-handler.js.map +1 -0
- package/dist/im/lark/v3-distillation-card.d.ts +42 -0
- package/dist/im/lark/v3-distillation-card.d.ts.map +1 -0
- package/dist/im/lark/v3-distillation-card.js +206 -0
- package/dist/im/lark/v3-distillation-card.js.map +1 -0
- package/dist/im/lark/v3-gate-card-handler.d.ts +1 -1
- package/dist/im/lark/v3-gate-card-handler.d.ts.map +1 -1
- package/dist/im/lark/v3-gate-card-handler.js +6 -5
- package/dist/im/lark/v3-gate-card-handler.js.map +1 -1
- package/dist/im/lark/v3-gate-card.d.ts +8 -1
- package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
- package/dist/im/lark/v3-gate-card.js +38 -7
- package/dist/im/lark/v3-gate-card.js.map +1 -1
- package/dist/im/lark/v3-loop-grant-card-handler.d.ts.map +1 -1
- package/dist/im/lark/v3-loop-grant-card-handler.js +3 -4
- package/dist/im/lark/v3-loop-grant-card-handler.js.map +1 -1
- package/dist/im/lark/v3-loop-grant-card.js +1 -1
- package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
- package/dist/im/lark/v3-progress-card-manager.d.ts +79 -0
- package/dist/im/lark/v3-progress-card-manager.d.ts.map +1 -0
- package/dist/im/lark/v3-progress-card-manager.js +444 -0
- package/dist/im/lark/v3-progress-card-manager.js.map +1 -0
- package/dist/im/lark/v3-progress-card.d.ts +26 -0
- package/dist/im/lark/v3-progress-card.d.ts.map +1 -0
- package/dist/im/lark/v3-progress-card.js +211 -0
- package/dist/im/lark/v3-progress-card.js.map +1 -0
- package/dist/im/lark/v3-revisit-grant-card-handler.d.ts.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card-handler.js +3 -4
- package/dist/im/lark/v3-revisit-grant-card-handler.js.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card.js +1 -1
- package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
- package/dist/im/lark/v3-run-save-card-handler.d.ts +12 -0
- package/dist/im/lark/v3-run-save-card-handler.d.ts.map +1 -0
- package/dist/im/lark/v3-run-save-card-handler.js +160 -0
- package/dist/im/lark/v3-run-save-card-handler.js.map +1 -0
- package/dist/im/lark/v3-run-save-card.d.ts +38 -0
- package/dist/im/lark/v3-run-save-card.d.ts.map +1 -0
- package/dist/im/lark/v3-run-save-card.js +125 -0
- package/dist/im/lark/v3-run-save-card.js.map +1 -0
- package/dist/im/lark/v3-saved-workflow-command.d.ts +36 -0
- package/dist/im/lark/v3-saved-workflow-command.d.ts.map +1 -0
- package/dist/im/lark/v3-saved-workflow-command.js +186 -0
- package/dist/im/lark/v3-saved-workflow-command.js.map +1 -0
- package/dist/im/lark/v3-saved-workflow-handler.d.ts +89 -0
- package/dist/im/lark/v3-saved-workflow-handler.d.ts.map +1 -0
- package/dist/im/lark/v3-saved-workflow-handler.js +212 -0
- package/dist/im/lark/v3-saved-workflow-handler.js.map +1 -0
- package/dist/im/lark/workflow-slash-command.d.ts +7 -110
- package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
- package/dist/im/lark/workflow-slash-command.js +16 -230
- package/dist/im/lark/workflow-slash-command.js.map +1 -1
- package/dist/index-daemon.js +1 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/mir-runner.js +11 -15
- package/dist/mir-runner.js.map +1 -1
- package/dist/mira-runner.js +8 -12
- package/dist/mira-runner.js.map +1 -1
- package/dist/services/bridge-turn-queue.d.ts +29 -12
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +59 -23
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/claude-transcript.d.ts +15 -0
- package/dist/services/claude-transcript.d.ts.map +1 -1
- package/dist/services/claude-transcript.js +24 -0
- package/dist/services/claude-transcript.js.map +1 -1
- package/dist/services/codex-bridge-queue.d.ts +13 -2
- package/dist/services/codex-bridge-queue.d.ts.map +1 -1
- package/dist/services/codex-bridge-queue.js +20 -3
- package/dist/services/codex-bridge-queue.js.map +1 -1
- package/dist/services/codex-transcript.d.ts +6 -0
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/grok-transcript.d.ts +3 -2
- package/dist/services/grok-transcript.d.ts.map +1 -1
- package/dist/services/grok-transcript.js +73 -22
- package/dist/services/grok-transcript.js.map +1 -1
- package/dist/services/hook-runner.d.ts.map +1 -1
- package/dist/services/hook-runner.js +26 -3
- package/dist/services/hook-runner.js.map +1 -1
- package/dist/services/schedule-store.d.ts +37 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +259 -141
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/send-policy.d.ts +50 -0
- package/dist/services/send-policy.d.ts.map +1 -1
- package/dist/services/send-policy.js +65 -0
- package/dist/services/send-policy.js.map +1 -1
- package/dist/services/session-lifecycle-hooks.d.ts.map +1 -1
- package/dist/services/session-lifecycle-hooks.js +12 -0
- package/dist/services/session-lifecycle-hooks.js.map +1 -1
- package/dist/services/traex-transcript.d.ts +11 -2
- package/dist/services/traex-transcript.d.ts.map +1 -1
- package/dist/services/traex-transcript.js +142 -8
- package/dist/services/traex-transcript.js.map +1 -1
- package/dist/services/trigger-types.d.ts +6 -1
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/services/turn-terminal-deduper.d.ts +14 -0
- package/dist/services/turn-terminal-deduper.d.ts.map +1 -0
- package/dist/services/turn-terminal-deduper.js +34 -0
- package/dist/services/turn-terminal-deduper.js.map +1 -0
- package/dist/services/vc-meeting-action-gate.d.ts +246 -0
- package/dist/services/vc-meeting-action-gate.d.ts.map +1 -0
- package/dist/services/vc-meeting-action-gate.js +756 -0
- package/dist/services/vc-meeting-action-gate.js.map +1 -0
- package/dist/services/vc-meeting-action-store.d.ts +222 -0
- package/dist/services/vc-meeting-action-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-action-store.js +806 -0
- package/dist/services/vc-meeting-action-store.js.map +1 -0
- package/dist/services/vc-meeting-consumer-isolation.d.ts +27 -0
- package/dist/services/vc-meeting-consumer-isolation.d.ts.map +1 -0
- package/dist/services/vc-meeting-consumer-isolation.js +37 -0
- package/dist/services/vc-meeting-consumer-isolation.js.map +1 -0
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts +72 -0
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts.map +1 -0
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js +343 -0
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js.map +1 -0
- package/dist/services/vc-meeting-consumer-profile-store.d.ts +39 -0
- package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-consumer-profile-store.js +190 -0
- package/dist/services/vc-meeting-consumer-profile-store.js.map +1 -0
- package/dist/services/vc-meeting-daemon-auth.d.ts +29 -0
- package/dist/services/vc-meeting-daemon-auth.d.ts.map +1 -0
- package/dist/services/vc-meeting-daemon-auth.js +117 -0
- package/dist/services/vc-meeting-daemon-auth.js.map +1 -0
- package/dist/services/vc-meeting-delivery-feed.d.ts +52 -0
- package/dist/services/vc-meeting-delivery-feed.d.ts.map +1 -0
- package/dist/services/vc-meeting-delivery-feed.js +231 -0
- package/dist/services/vc-meeting-delivery-feed.js.map +1 -0
- package/dist/services/vc-meeting-delivery-hub-store.d.ts +213 -0
- package/dist/services/vc-meeting-delivery-hub-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-delivery-hub-store.js +1230 -0
- package/dist/services/vc-meeting-delivery-hub-store.js.map +1 -0
- package/dist/services/vc-meeting-delivery-protocol.d.ts +159 -0
- package/dist/services/vc-meeting-delivery-protocol.d.ts.map +1 -0
- package/dist/services/vc-meeting-delivery-protocol.js +443 -0
- package/dist/services/vc-meeting-delivery-protocol.js.map +1 -0
- package/dist/services/vc-meeting-delivery-receiver.d.ts +80 -0
- package/dist/services/vc-meeting-delivery-receiver.d.ts.map +1 -0
- package/dist/services/vc-meeting-delivery-receiver.js +543 -0
- package/dist/services/vc-meeting-delivery-receiver.js.map +1 -0
- package/dist/services/vc-meeting-delivery-store.d.ts +339 -0
- package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-delivery-store.js +1024 -0
- package/dist/services/vc-meeting-delivery-store.js.map +1 -0
- package/dist/services/vc-meeting-feed-metadata-store.d.ts +58 -0
- package/dist/services/vc-meeting-feed-metadata-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-feed-metadata-store.js +356 -0
- package/dist/services/vc-meeting-feed-metadata-store.js.map +1 -0
- package/dist/services/vc-meeting-im-reply.d.ts +55 -0
- package/dist/services/vc-meeting-im-reply.d.ts.map +1 -0
- package/dist/services/vc-meeting-im-reply.js +279 -0
- package/dist/services/vc-meeting-im-reply.js.map +1 -0
- package/dist/services/vc-meeting-im-routing.d.ts +131 -0
- package/dist/services/vc-meeting-im-routing.d.ts.map +1 -0
- package/dist/services/vc-meeting-im-routing.js +449 -0
- package/dist/services/vc-meeting-im-routing.js.map +1 -0
- package/dist/services/vc-meeting-listener-message-store.d.ts +23 -0
- package/dist/services/vc-meeting-listener-message-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-listener-message-store.js +142 -0
- package/dist/services/vc-meeting-listener-message-store.js.map +1 -0
- package/dist/services/vc-meeting-member-policy.d.ts +37 -0
- package/dist/services/vc-meeting-member-policy.d.ts.map +1 -0
- package/dist/services/vc-meeting-member-policy.js +102 -0
- package/dist/services/vc-meeting-member-policy.js.map +1 -0
- package/dist/services/vc-meeting-profile-instructions.d.ts +22 -0
- package/dist/services/vc-meeting-profile-instructions.d.ts.map +1 -0
- package/dist/services/vc-meeting-profile-instructions.js +64 -0
- package/dist/services/vc-meeting-profile-instructions.js.map +1 -0
- package/dist/services/vc-meeting-runtime-store.d.ts +63 -0
- package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-runtime-store.js +605 -116
- package/dist/services/vc-meeting-runtime-store.js.map +1 -1
- package/dist/services/vc-meeting-send-policy.d.ts +89 -0
- package/dist/services/vc-meeting-send-policy.d.ts.map +1 -0
- package/dist/services/vc-meeting-send-policy.js +150 -0
- package/dist/services/vc-meeting-send-policy.js.map +1 -0
- package/dist/services/vc-meeting-terminal-reconciler.d.ts +93 -0
- package/dist/services/vc-meeting-terminal-reconciler.d.ts.map +1 -0
- package/dist/services/vc-meeting-terminal-reconciler.js +198 -0
- package/dist/services/vc-meeting-terminal-reconciler.js.map +1 -0
- package/dist/setup/open-platform-automation.d.ts +2 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +1 -0
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/open-platform-outcome.d.ts +50 -0
- package/dist/setup/open-platform-outcome.d.ts.map +1 -0
- package/dist/setup/open-platform-outcome.js +56 -0
- package/dist/setup/open-platform-outcome.js.map +1 -0
- package/dist/setup/setup-args.d.ts +7 -2
- package/dist/setup/setup-args.d.ts.map +1 -1
- package/dist/setup/setup-args.js +21 -5
- package/dist/setup/setup-args.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +66 -54
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +5 -4
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +19 -10
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/types.d.ts +187 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/canonical-input-hash.d.ts +5 -0
- package/dist/utils/canonical-input-hash.d.ts.map +1 -0
- package/dist/utils/canonical-input-hash.js +43 -0
- package/dist/utils/canonical-input-hash.js.map +1 -0
- package/dist/utils/canonical-json.d.ts +10 -0
- package/dist/utils/canonical-json.d.ts.map +1 -0
- package/dist/utils/canonical-json.js +23 -0
- package/dist/utils/canonical-json.js.map +1 -0
- package/dist/utils/child-env.d.ts +1 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +8 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/daemon-discovery.d.ts +4 -0
- package/dist/utils/daemon-discovery.d.ts.map +1 -1
- package/dist/utils/daemon-discovery.js +12 -6
- package/dist/utils/daemon-discovery.js.map +1 -1
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +696 -102
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/fs-durability.d.ts +20 -0
- package/dist/utils/fs-durability.d.ts.map +1 -0
- package/dist/utils/fs-durability.js +84 -0
- package/dist/utils/fs-durability.js.map +1 -0
- package/dist/utils/global-install.d.ts +5 -2
- package/dist/utils/global-install.d.ts.map +1 -1
- package/dist/utils/global-install.js +22 -4
- package/dist/utils/global-install.js.map +1 -1
- package/dist/utils/herdr-web-history.d.ts +12 -0
- package/dist/utils/herdr-web-history.d.ts.map +1 -0
- package/dist/utils/herdr-web-history.js +74 -0
- package/dist/utils/herdr-web-history.js.map +1 -0
- package/dist/utils/herdr-web-terminal-binding.d.ts +39 -0
- package/dist/utils/herdr-web-terminal-binding.d.ts.map +1 -0
- package/dist/utils/herdr-web-terminal-binding.js +56 -0
- package/dist/utils/herdr-web-terminal-binding.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +30 -1
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +45 -5
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/vc-agent/cards.d.ts +49 -4
- package/dist/vc-agent/cards.d.ts.map +1 -1
- package/dist/vc-agent/cards.js +460 -70
- package/dist/vc-agent/cards.js.map +1 -1
- package/dist/vc-agent/polling-source.d.ts +6 -1
- package/dist/vc-agent/polling-source.d.ts.map +1 -1
- package/dist/vc-agent/polling-source.js +17 -4
- package/dist/vc-agent/polling-source.js.map +1 -1
- package/dist/worker.js +1333 -283
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +28 -35
- package/dist/workflows/definition.d.ts.map +1 -1
- package/dist/workflows/definition.js +7 -24
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +28 -28
- package/dist/workflows/events/replay.d.ts +5 -0
- package/dist/workflows/events/replay.d.ts.map +1 -1
- package/dist/workflows/events/replay.js +6 -1
- package/dist/workflows/events/replay.js.map +1 -1
- package/dist/workflows/events/schema.d.ts +663 -663
- package/dist/workflows/events/schema.d.ts.map +1 -1
- package/dist/workflows/events/schema.js +1 -4
- package/dist/workflows/events/schema.js.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts +5 -4
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.js +80 -35
- package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
- package/dist/workflows/hostExecutors/feishu-im.d.ts +3 -3
- package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/feishu-im.js +3 -3
- package/dist/workflows/hostExecutors/feishu-im.js.map +1 -1
- package/dist/workflows/hostExecutors/feishu-reply.d.ts +1 -1
- package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/feishu-reply.js +1 -1
- package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -1
- package/dist/workflows/hostExecutors/feishu-send.d.ts +1 -1
- package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/feishu-send.js +1 -1
- package/dist/workflows/hostExecutors/feishu-send.js.map +1 -1
- package/dist/workflows/hostExecutors/index.d.ts +0 -1
- package/dist/workflows/hostExecutors/index.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/index.js +0 -1
- package/dist/workflows/hostExecutors/index.js.map +1 -1
- package/dist/workflows/hostExecutors/registry.d.ts +1 -1
- package/dist/workflows/hostExecutors/registry.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/types.d.ts +21 -29
- package/dist/workflows/hostExecutors/types.d.ts.map +1 -1
- package/dist/workflows/migration/v2-definition-paths.d.ts +3 -0
- package/dist/workflows/migration/v2-definition-paths.d.ts.map +1 -0
- package/dist/workflows/migration/v2-definition-paths.js +10 -0
- package/dist/workflows/migration/v2-definition-paths.js.map +1 -0
- package/dist/workflows/migration/v2-ledger.d.ts +138 -0
- package/dist/workflows/migration/v2-ledger.d.ts.map +1 -0
- package/dist/workflows/migration/v2-ledger.js +599 -0
- package/dist/workflows/migration/v2-ledger.js.map +1 -0
- package/dist/workflows/migration/v2-migration-service.d.ts +40 -0
- package/dist/workflows/migration/v2-migration-service.d.ts.map +1 -0
- package/dist/workflows/migration/v2-migration-service.js +454 -0
- package/dist/workflows/migration/v2-migration-service.js.map +1 -0
- package/dist/workflows/migration/v2-read-only-ids.d.ts +7 -0
- package/dist/workflows/migration/v2-read-only-ids.d.ts.map +1 -0
- package/dist/workflows/migration/v2-read-only-ids.js +9 -0
- package/dist/workflows/migration/v2-read-only-ids.js.map +1 -0
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +976 -0
- package/dist/workflows/migration/v2-run-archive-schema.d.ts.map +1 -0
- package/dist/workflows/migration/v2-run-archive-schema.js +113 -0
- package/dist/workflows/migration/v2-run-archive-schema.js.map +1 -0
- package/dist/workflows/migration/v2-run-archive.d.ts +92 -0
- package/dist/workflows/migration/v2-run-archive.d.ts.map +1 -0
- package/dist/workflows/migration/v2-run-archive.js +1220 -0
- package/dist/workflows/migration/v2-run-archive.js.map +1 -0
- package/dist/workflows/migration/v2-scanner.d.ts +22 -0
- package/dist/workflows/migration/v2-scanner.d.ts.map +1 -0
- package/dist/workflows/migration/v2-scanner.js +114 -0
- package/dist/workflows/migration/v2-scanner.js.map +1 -0
- package/dist/workflows/migration/v2-to-v3.d.ts +32 -0
- package/dist/workflows/migration/v2-to-v3.d.ts.map +1 -0
- package/dist/workflows/migration/v2-to-v3.js +494 -0
- package/dist/workflows/migration/v2-to-v3.js.map +1 -0
- package/dist/workflows/ops-projection.d.ts.map +1 -1
- package/dist/workflows/ops-projection.js +4 -3
- package/dist/workflows/ops-projection.js.map +1 -1
- package/dist/workflows/shared/idempotency-key.d.ts +19 -0
- package/dist/workflows/shared/idempotency-key.d.ts.map +1 -0
- package/dist/workflows/shared/idempotency-key.js +23 -0
- package/dist/workflows/shared/idempotency-key.js.map +1 -0
- package/dist/workflows/{params.d.ts → shared/params.d.ts} +42 -6
- package/dist/workflows/shared/params.d.ts.map +1 -0
- package/dist/workflows/{params.js → shared/params.js} +91 -4
- package/dist/workflows/shared/params.js.map +1 -0
- package/dist/workflows/shared/provider-reconciler.d.ts +47 -0
- package/dist/workflows/shared/provider-reconciler.d.ts.map +1 -0
- package/dist/workflows/shared/provider-reconciler.js +17 -0
- package/dist/workflows/shared/provider-reconciler.js.map +1 -0
- package/dist/workflows/{spawn-policy.d.ts → shared/sandbox-policy.d.ts} +2 -2
- package/dist/workflows/shared/sandbox-policy.d.ts.map +1 -0
- package/dist/workflows/{spawn-policy.js → shared/sandbox-policy.js} +1 -1
- package/dist/workflows/shared/sandbox-policy.js.map +1 -0
- package/dist/workflows/shared/worker-process.d.ts +29 -0
- package/dist/workflows/shared/worker-process.d.ts.map +1 -0
- package/dist/workflows/shared/worker-process.js +56 -0
- package/dist/workflows/shared/worker-process.js.map +1 -0
- package/dist/workflows/v3/architect.d.ts.map +1 -1
- package/dist/workflows/v3/architect.js +16 -4
- package/dist/workflows/v3/architect.js.map +1 -1
- package/dist/workflows/v3/attempt-ledger.d.ts +23 -0
- package/dist/workflows/v3/attempt-ledger.d.ts.map +1 -0
- package/dist/workflows/v3/attempt-ledger.js +74 -0
- package/dist/workflows/v3/attempt-ledger.js.map +1 -0
- package/dist/workflows/v3/bot-resolve.d.ts +19 -1
- package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
- package/dist/workflows/v3/bot-resolve.js +129 -3
- package/dist/workflows/v3/bot-resolve.js.map +1 -1
- package/dist/workflows/v3/cli-daemon-command-authority.d.ts +59 -0
- package/dist/workflows/v3/cli-daemon-command-authority.d.ts.map +1 -0
- package/dist/workflows/v3/cli-daemon-command-authority.js +175 -0
- package/dist/workflows/v3/cli-daemon-command-authority.js.map +1 -0
- package/dist/workflows/v3/cli-run.d.ts +27 -0
- package/dist/workflows/v3/cli-run.d.ts.map +1 -1
- package/dist/workflows/v3/cli-run.js +113 -29
- package/dist/workflows/v3/cli-run.js.map +1 -1
- package/dist/workflows/v3/contract.d.ts +16 -16
- package/dist/workflows/v3/contract.d.ts.map +1 -1
- package/dist/workflows/v3/contract.js +15 -11
- package/dist/workflows/v3/contract.js.map +1 -1
- package/dist/workflows/v3/daemon-ipc-auth.d.ts +113 -0
- package/dist/workflows/v3/daemon-ipc-auth.d.ts.map +1 -0
- package/dist/workflows/v3/daemon-ipc-auth.js +294 -0
- package/dist/workflows/v3/daemon-ipc-auth.js.map +1 -0
- package/dist/workflows/v3/daemon-ipc-body.d.ts +30 -0
- package/dist/workflows/v3/daemon-ipc-body.d.ts.map +1 -0
- package/dist/workflows/v3/daemon-ipc-body.js +65 -0
- package/dist/workflows/v3/daemon-ipc-body.js.map +1 -0
- package/dist/workflows/v3/daemon-ipc-client.d.ts +32 -0
- package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -0
- package/dist/workflows/v3/daemon-ipc-client.js +74 -0
- package/dist/workflows/v3/daemon-ipc-client.js.map +1 -0
- package/dist/workflows/v3/daemon-run.d.ts +87 -2
- package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
- package/dist/workflows/v3/daemon-run.js +863 -68
- package/dist/workflows/v3/daemon-run.js.map +1 -1
- package/dist/workflows/v3/dag.d.ts +22 -8
- package/dist/workflows/v3/dag.d.ts.map +1 -1
- package/dist/workflows/v3/dag.js +214 -22
- package/dist/workflows/v3/dag.js.map +1 -1
- package/dist/workflows/v3/distillation-compiler.d.ts +36 -0
- package/dist/workflows/v3/distillation-compiler.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-compiler.js +704 -0
- package/dist/workflows/v3/distillation-compiler.js.map +1 -0
- package/dist/workflows/v3/distillation-private-root.d.ts +10 -0
- package/dist/workflows/v3/distillation-private-root.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-private-root.js +34 -0
- package/dist/workflows/v3/distillation-private-root.js.map +1 -0
- package/dist/workflows/v3/distillation-runner.d.ts +64 -0
- package/dist/workflows/v3/distillation-runner.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-runner.js +1028 -0
- package/dist/workflows/v3/distillation-runner.js.map +1 -0
- package/dist/workflows/v3/distillation-schema.d.ts +63 -0
- package/dist/workflows/v3/distillation-schema.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-schema.js +184 -0
- package/dist/workflows/v3/distillation-schema.js.map +1 -0
- package/dist/workflows/v3/distillation-service.d.ts +78 -0
- package/dist/workflows/v3/distillation-service.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-service.js +416 -0
- package/dist/workflows/v3/distillation-service.js.map +1 -0
- package/dist/workflows/v3/distillation-source.d.ts +35 -0
- package/dist/workflows/v3/distillation-source.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-source.js +118 -0
- package/dist/workflows/v3/distillation-source.js.map +1 -0
- package/dist/workflows/v3/distillation-store.d.ts +185 -0
- package/dist/workflows/v3/distillation-store.d.ts.map +1 -0
- package/dist/workflows/v3/distillation-store.js +1127 -0
- package/dist/workflows/v3/distillation-store.js.map +1 -0
- package/dist/workflows/v3/drive-lease.d.ts +7 -0
- package/dist/workflows/v3/drive-lease.d.ts.map +1 -0
- package/dist/workflows/v3/drive-lease.js +10 -0
- package/dist/workflows/v3/drive-lease.js.map +1 -0
- package/dist/workflows/v3/ephemeral-pool.d.ts +1 -1
- package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.js +222 -37
- package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
- package/dist/workflows/v3/grill-state.d.ts +18 -3
- package/dist/workflows/v3/grill-state.d.ts.map +1 -1
- package/dist/workflows/v3/grill-state.js +22 -5
- package/dist/workflows/v3/grill-state.js.map +1 -1
- package/dist/workflows/v3/host-bindings.d.ts +40 -0
- package/dist/workflows/v3/host-bindings.d.ts.map +1 -0
- package/dist/workflows/v3/host-bindings.js +272 -0
- package/dist/workflows/v3/host-bindings.js.map +1 -0
- package/dist/workflows/v3/host-effect-ledger.d.ts +22 -0
- package/dist/workflows/v3/host-effect-ledger.d.ts.map +1 -0
- package/dist/workflows/v3/host-effect-ledger.js +110 -0
- package/dist/workflows/v3/host-effect-ledger.js.map +1 -0
- package/dist/workflows/v3/host-execution.d.ts +142 -0
- package/dist/workflows/v3/host-execution.d.ts.map +1 -0
- package/dist/workflows/v3/host-execution.js +424 -0
- package/dist/workflows/v3/host-execution.js.map +1 -0
- package/dist/workflows/v3/host.d.ts +46 -10
- package/dist/workflows/v3/host.d.ts.map +1 -1
- package/dist/workflows/v3/host.js +419 -140
- package/dist/workflows/v3/host.js.map +1 -1
- package/dist/workflows/v3/human-gate.d.ts +19 -1
- package/dist/workflows/v3/human-gate.d.ts.map +1 -1
- package/dist/workflows/v3/human-gate.js +145 -19
- package/dist/workflows/v3/human-gate.js.map +1 -1
- package/dist/workflows/v3/journal.d.ts +125 -7
- package/dist/workflows/v3/journal.d.ts.map +1 -1
- package/dist/workflows/v3/journal.js +154 -38
- package/dist/workflows/v3/journal.js.map +1 -1
- package/dist/workflows/v3/library-materialize.d.ts +88 -0
- package/dist/workflows/v3/library-materialize.d.ts.map +1 -0
- package/dist/workflows/v3/library-materialize.js +294 -0
- package/dist/workflows/v3/library-materialize.js.map +1 -0
- package/dist/workflows/v3/library-schema.d.ts +138 -0
- package/dist/workflows/v3/library-schema.d.ts.map +1 -0
- package/dist/workflows/v3/library-schema.js +573 -0
- package/dist/workflows/v3/library-schema.js.map +1 -0
- package/dist/workflows/v3/library-service.d.ts +126 -0
- package/dist/workflows/v3/library-service.d.ts.map +1 -0
- package/dist/workflows/v3/library-service.js +542 -0
- package/dist/workflows/v3/library-service.js.map +1 -0
- package/dist/workflows/v3/library-store.d.ts +124 -0
- package/dist/workflows/v3/library-store.d.ts.map +1 -0
- package/dist/workflows/v3/library-store.js +665 -0
- package/dist/workflows/v3/library-store.js.map +1 -0
- package/dist/workflows/v3/orchestrator.d.ts +9 -1
- package/dist/workflows/v3/orchestrator.d.ts.map +1 -1
- package/dist/workflows/v3/orchestrator.js +2 -1
- package/dist/workflows/v3/orchestrator.js.map +1 -1
- package/dist/workflows/v3/progress-projection.d.ts +99 -0
- package/dist/workflows/v3/progress-projection.d.ts.map +1 -0
- package/dist/workflows/v3/progress-projection.js +197 -0
- package/dist/workflows/v3/progress-projection.js.map +1 -0
- package/dist/workflows/v3/run-envelope.d.ts +1279 -0
- package/dist/workflows/v3/run-envelope.d.ts.map +1 -0
- package/dist/workflows/v3/run-envelope.js +681 -0
- package/dist/workflows/v3/run-envelope.js.map +1 -0
- package/dist/workflows/v3/runtime.d.ts +39 -7
- package/dist/workflows/v3/runtime.d.ts.map +1 -1
- package/dist/workflows/v3/runtime.js +1992 -100
- package/dist/workflows/v3/runtime.js.map +1 -1
- package/dist/workflows/v3/session-relay-client.d.ts +61 -0
- package/dist/workflows/v3/session-relay-client.d.ts.map +1 -0
- package/dist/workflows/v3/session-relay-client.js +92 -0
- package/dist/workflows/v3/session-relay-client.js.map +1 -0
- package/dist/workflows/v3/session-relay.d.ts +66 -0
- package/dist/workflows/v3/session-relay.d.ts.map +1 -0
- package/dist/workflows/v3/session-relay.js +157 -0
- package/dist/workflows/v3/session-relay.js.map +1 -0
- package/dist/workflows/v3/start-intent.d.ts +8 -0
- package/dist/workflows/v3/start-intent.d.ts.map +1 -0
- package/dist/workflows/v3/start-intent.js +34 -0
- package/dist/workflows/v3/start-intent.js.map +1 -0
- package/dist/workflows/v3/state.d.ts +11 -2
- package/dist/workflows/v3/state.d.ts.map +1 -1
- package/dist/workflows/v3/state.js +182 -10
- package/dist/workflows/v3/state.js.map +1 -1
- package/dist/workflows/v3/template-bindings.d.ts +22 -0
- package/dist/workflows/v3/template-bindings.d.ts.map +1 -0
- package/dist/workflows/v3/template-bindings.js +204 -0
- package/dist/workflows/v3/template-bindings.js.map +1 -0
- package/dist/workflows/v3/worker-fence.d.ts +158 -0
- package/dist/workflows/v3/worker-fence.d.ts.map +1 -0
- package/dist/workflows/v3/worker-fence.js +685 -0
- package/dist/workflows/v3/worker-fence.js.map +1 -0
- package/package.json +3 -2
- package/dist/core/dashboard-command/workflows.d.ts +0 -20
- package/dist/core/dashboard-command/workflows.d.ts.map +0 -1
- package/dist/core/dashboard-command/workflows.js +0 -45
- package/dist/core/dashboard-command/workflows.js.map +0 -1
- package/dist/dashboard/web/legacy-workflow-link.d.ts +0 -2
- package/dist/dashboard/web/legacy-workflow-link.d.ts.map +0 -1
- package/dist/dashboard/web/legacy-workflow-link.js +0 -12
- package/dist/dashboard/web/legacy-workflow-link.js.map +0 -1
- package/dist/dashboard/web/legacy-workflow-page.d.ts +0 -3
- package/dist/dashboard/web/legacy-workflow-page.d.ts.map +0 -1
- package/dist/dashboard/web/legacy-workflow-page.js +0 -717
- package/dist/dashboard/web/legacy-workflow-page.js.map +0 -1
- package/dist/dashboard/web/workflow-version-switch.d.ts +0 -8
- package/dist/dashboard/web/workflow-version-switch.d.ts.map +0 -1
- package/dist/dashboard/web/workflow-version-switch.js +0 -9
- package/dist/dashboard/web/workflow-version-switch.js.map +0 -1
- package/dist/dashboard/web/workflows.d.ts +0 -273
- package/dist/dashboard/web/workflows.d.ts.map +0 -1
- package/dist/dashboard/web/workflows.js +0 -436
- package/dist/dashboard/web/workflows.js.map +0 -1
- package/dist/dashboard/workflow-api.d.ts +0 -30
- package/dist/dashboard/workflow-api.d.ts.map +0 -1
- package/dist/dashboard/workflow-api.js +0 -397
- package/dist/dashboard/workflow-api.js.map +0 -1
- package/dist/dashboard/workflow-card-model.d.ts +0 -127
- package/dist/dashboard/workflow-card-model.d.ts.map +0 -1
- package/dist/dashboard/workflow-card-model.js +0 -186
- package/dist/dashboard/workflow-card-model.js.map +0 -1
- package/dist/dashboard/workflows-action-helpers.d.ts +0 -105
- package/dist/dashboard/workflows-action-helpers.d.ts.map +0 -1
- package/dist/dashboard/workflows-action-helpers.js +0 -196
- package/dist/dashboard/workflows-action-helpers.js.map +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-7T7DPOV6.js +0 -4
- package/dist/dashboard-web/chunks/chunk-IGXYYRYJ.js +0 -1
- package/dist/dashboard-web/chunks/chunk-ISOD7VDX.js +0 -1
- package/dist/dashboard-web/chunks/chunk-PNK3HQBI.js +0 -37
- package/dist/dashboard-web/chunks/chunk-SRIE4MLQ.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-UQ4QBE45.js +0 -1
- package/dist/dashboard-web/chunks/legacy-workflow-page-QMC7OZXH.js +0 -2
- package/dist/dashboard-web/chunks/sessions-page-NNZIDHZM.js +0 -3
- package/dist/dashboard-web/chunks/settings-page-LJIXX5LQ.js +0 -2
- package/dist/dashboard-web/chunks/v3-page-YLYVHGKD.js +0 -94
- package/dist/dashboard-web/terminal-replay.html +0 -227
- package/dist/im/lark/workflow-card-handler.d.ts +0 -51
- package/dist/im/lark/workflow-card-handler.d.ts.map +0 -1
- package/dist/im/lark/workflow-card-handler.js +0 -163
- package/dist/im/lark/workflow-card-handler.js.map +0 -1
- package/dist/im/lark/workflow-cards.d.ts +0 -47
- package/dist/im/lark/workflow-cards.d.ts.map +0 -1
- package/dist/im/lark/workflow-cards.js +0 -231
- package/dist/im/lark/workflow-cards.js.map +0 -1
- package/dist/im/lark/workflow-progress-card.d.ts +0 -81
- package/dist/im/lark/workflow-progress-card.d.ts.map +0 -1
- package/dist/im/lark/workflow-progress-card.js +0 -336
- package/dist/im/lark/workflow-progress-card.js.map +0 -1
- package/dist/im/lark/workflows-card.d.ts +0 -102
- package/dist/im/lark/workflows-card.d.ts.map +0 -1
- package/dist/im/lark/workflows-card.js +0 -737
- package/dist/im/lark/workflows-card.js.map +0 -1
- package/dist/workflows/attempt-resume.d.ts +0 -119
- package/dist/workflows/attempt-resume.d.ts.map +0 -1
- package/dist/workflows/attempt-resume.js +0 -392
- package/dist/workflows/attempt-resume.js.map +0 -1
- package/dist/workflows/blob.d.ts +0 -27
- package/dist/workflows/blob.d.ts.map +0 -1
- package/dist/workflows/blob.js +0 -40
- package/dist/workflows/blob.js.map +0 -1
- package/dist/workflows/cancel-run.d.ts +0 -45
- package/dist/workflows/cancel-run.d.ts.map +0 -1
- package/dist/workflows/cancel-run.js +0 -99
- package/dist/workflows/cancel-run.js.map +0 -1
- package/dist/workflows/cancel.d.ts +0 -111
- package/dist/workflows/cancel.d.ts.map +0 -1
- package/dist/workflows/cancel.js +0 -120
- package/dist/workflows/cancel.js.map +0 -1
- package/dist/workflows/catalog.d.ts +0 -60
- package/dist/workflows/catalog.d.ts.map +0 -1
- package/dist/workflows/catalog.js +0 -119
- package/dist/workflows/catalog.js.map +0 -1
- package/dist/workflows/cold-attach.d.ts +0 -30
- package/dist/workflows/cold-attach.d.ts.map +0 -1
- package/dist/workflows/cold-attach.js +0 -40
- package/dist/workflows/cold-attach.js.map +0 -1
- package/dist/workflows/cold-scan.d.ts +0 -21
- package/dist/workflows/cold-scan.d.ts.map +0 -1
- package/dist/workflows/cold-scan.js +0 -70
- package/dist/workflows/cold-scan.js.map +0 -1
- package/dist/workflows/daemon-spawn.d.ts +0 -119
- package/dist/workflows/daemon-spawn.d.ts.map +0 -1
- package/dist/workflows/daemon-spawn.js +0 -564
- package/dist/workflows/daemon-spawn.js.map +0 -1
- package/dist/workflows/effect-input.d.ts +0 -4
- package/dist/workflows/effect-input.d.ts.map +0 -1
- package/dist/workflows/effect-input.js +0 -19
- package/dist/workflows/effect-input.js.map +0 -1
- package/dist/workflows/events/append.d.ts +0 -77
- package/dist/workflows/events/append.d.ts.map +0 -1
- package/dist/workflows/events/append.js +0 -214
- package/dist/workflows/events/append.js.map +0 -1
- package/dist/workflows/events/idempotency.d.ts +0 -77
- package/dist/workflows/events/idempotency.d.ts.map +0 -1
- package/dist/workflows/events/idempotency.js +0 -116
- package/dist/workflows/events/idempotency.js.map +0 -1
- package/dist/workflows/events/index.d.ts +0 -7
- package/dist/workflows/events/index.d.ts.map +0 -1
- package/dist/workflows/events/index.js +0 -7
- package/dist/workflows/events/index.js.map +0 -1
- package/dist/workflows/fanout.d.ts +0 -36
- package/dist/workflows/fanout.d.ts.map +0 -1
- package/dist/workflows/fanout.js +0 -115
- package/dist/workflows/fanout.js.map +0 -1
- package/dist/workflows/hostExecutors/protocol.d.ts +0 -42
- package/dist/workflows/hostExecutors/protocol.d.ts.map +0 -1
- package/dist/workflows/hostExecutors/protocol.js +0 -181
- package/dist/workflows/hostExecutors/protocol.js.map +0 -1
- package/dist/workflows/loader.d.ts +0 -30
- package/dist/workflows/loader.d.ts.map +0 -1
- package/dist/workflows/loader.js +0 -84
- package/dist/workflows/loader.js.map +0 -1
- package/dist/workflows/loop.d.ts +0 -50
- package/dist/workflows/loop.d.ts.map +0 -1
- package/dist/workflows/loop.js +0 -408
- package/dist/workflows/loop.js.map +0 -1
- package/dist/workflows/orchestrator.d.ts +0 -171
- package/dist/workflows/orchestrator.d.ts.map +0 -1
- package/dist/workflows/orchestrator.js +0 -609
- package/dist/workflows/orchestrator.js.map +0 -1
- package/dist/workflows/output-binding.d.ts +0 -77
- package/dist/workflows/output-binding.d.ts.map +0 -1
- package/dist/workflows/output-binding.js +0 -329
- package/dist/workflows/output-binding.js.map +0 -1
- package/dist/workflows/params.d.ts.map +0 -1
- package/dist/workflows/params.js.map +0 -1
- package/dist/workflows/resume.d.ts +0 -263
- package/dist/workflows/resume.d.ts.map +0 -1
- package/dist/workflows/resume.js +0 -808
- package/dist/workflows/resume.js.map +0 -1
- package/dist/workflows/run-id.d.ts +0 -2
- package/dist/workflows/run-id.d.ts.map +0 -1
- package/dist/workflows/run-id.js +0 -7
- package/dist/workflows/run-id.js.map +0 -1
- package/dist/workflows/run-init.d.ts +0 -48
- package/dist/workflows/run-init.d.ts.map +0 -1
- package/dist/workflows/run-init.js +0 -100
- package/dist/workflows/run-init.js.map +0 -1
- package/dist/workflows/runs-dir.d.ts +0 -4
- package/dist/workflows/runs-dir.d.ts.map +0 -1
- package/dist/workflows/runs-dir.js +0 -15
- package/dist/workflows/runs-dir.js.map +0 -1
- package/dist/workflows/runtime.d.ts +0 -215
- package/dist/workflows/runtime.d.ts.map +0 -1
- package/dist/workflows/runtime.js +0 -630
- package/dist/workflows/runtime.js.map +0 -1
- package/dist/workflows/spawn-bot.d.ts +0 -165
- package/dist/workflows/spawn-bot.d.ts.map +0 -1
- package/dist/workflows/spawn-bot.js +0 -215
- package/dist/workflows/spawn-bot.js.map +0 -1
- package/dist/workflows/spawn-policy.d.ts.map +0 -1
- package/dist/workflows/spawn-policy.js.map +0 -1
- package/dist/workflows/system.d.ts +0 -49
- package/dist/workflows/system.d.ts.map +0 -1
- package/dist/workflows/system.js +0 -48
- package/dist/workflows/system.js.map +0 -1
- package/dist/workflows/trigger-from-envelope.d.ts +0 -13
- package/dist/workflows/trigger-from-envelope.d.ts.map +0 -1
- package/dist/workflows/trigger-from-envelope.js +0 -68
- package/dist/workflows/trigger-from-envelope.js.map +0 -1
- package/dist/workflows/trigger-run.d.ts +0 -70
- package/dist/workflows/trigger-run.d.ts.map +0 -1
- package/dist/workflows/trigger-run.js +0 -88
- package/dist/workflows/trigger-run.js.map +0 -1
- package/dist/workflows/wait.d.ts +0 -141
- package/dist/workflows/wait.d.ts.map +0 -1
- package/dist/workflows/wait.js +0 -203
- package/dist/workflows/wait.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -13,42 +13,46 @@
|
|
|
13
13
|
* 7. On 'restart', kills CLI and re-spawns with --resume
|
|
14
14
|
*/
|
|
15
15
|
import { randomBytes } from 'node:crypto';
|
|
16
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
|
|
16
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
|
|
17
17
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
18
18
|
import { join, basename } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
|
-
import { evaluateReadIsolationGate, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, } from './adapters/cli/read-isolation.js';
|
|
21
|
+
import { evaluateReadIsolationGate, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildReadIsolationProtectedWriteRules, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, isolationPaneMarkerContent, } from './adapters/cli/read-isolation.js';
|
|
22
22
|
import { killPersistentSession } from './core/persistent-backend.js';
|
|
23
|
+
import { readProcessStartIdentity } from './core/session-marker.js';
|
|
23
24
|
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
|
|
24
25
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
25
26
|
import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
26
27
|
import { shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
|
|
28
|
+
import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirst } from './core/inject-queue-policy.js';
|
|
27
29
|
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
28
30
|
import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
|
|
29
31
|
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
30
|
-
import { mergeQueuedCliInput } from './utils/pending-input-queue.js';
|
|
32
|
+
import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
|
|
31
33
|
import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
|
|
32
34
|
import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
|
|
33
35
|
import { sanitizePerBotEnv } from './core/per-bot-env.js';
|
|
36
|
+
import { evaluateVcMeetingManagedSend, } from './services/vc-meeting-send-policy.js';
|
|
37
|
+
import { TurnTerminalDeduper } from './services/turn-terminal-deduper.js';
|
|
34
38
|
import { ensureGatewayEntry } from './core/plugins/mcp/gateway-installer.js';
|
|
35
39
|
import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
|
|
36
40
|
import { loadBotConfigs } from './bot-registry.js';
|
|
37
41
|
import { readGlobalConfig } from './global-config.js';
|
|
38
|
-
import {
|
|
42
|
+
import { deriveTerminalViewToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
|
|
39
43
|
import { readPlatformBinding } from './platform/binding.js';
|
|
40
|
-
import { loadPersistedToken } from './dashboard/auth.js';
|
|
44
|
+
import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
41
45
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
42
46
|
import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, shouldHealAbsentBaseline, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
|
|
43
47
|
import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
|
|
44
|
-
import {
|
|
45
|
-
import { findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/traex-transcript.js';
|
|
48
|
+
import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn, codexSessionIdFromRolloutPath } from './services/codex-transcript.js';
|
|
49
|
+
import { drainTraexRollout, findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/traex-transcript.js';
|
|
46
50
|
import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
|
|
47
51
|
import { currentHermesStateOffset, drainHermesStateDb, resolveHermesStateDbPath } from './services/hermes-transcript.js';
|
|
48
52
|
import { filterHermesEventsForBotmuxSession } from './services/hermes-session-filter.js';
|
|
49
53
|
import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
|
|
50
54
|
import { drainPiTranscript } from './services/pi-transcript.js';
|
|
51
|
-
import { drainGrokUpdates, grokSessionIdFromPath, } from './services/grok-transcript.js';
|
|
55
|
+
import { drainGrokUpdates, findGrokSessionByPid, grokSessionIdFromPath, } from './services/grok-transcript.js';
|
|
52
56
|
import { resolveFileBridgePath } from './services/file-bridge-path.js';
|
|
53
57
|
import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, } from './services/structured-bridge-clis.js';
|
|
54
58
|
import { drainCursorTranscript, findCursorChatIdByPid, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
|
|
@@ -59,6 +63,7 @@ import { dirname } from 'node:path';
|
|
|
59
63
|
import { createServer as createHttpServer } from 'node:http';
|
|
60
64
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
61
65
|
import { listenWebTerminalWithFallback } from './utils/web-terminal-listen.js';
|
|
66
|
+
import { HerdrWebTerminalBinding } from './utils/herdr-web-terminal-binding.js';
|
|
62
67
|
import { TERMINAL_FAVICON_DATA_URI } from './utils/terminal-favicon.js';
|
|
63
68
|
import { t, setDefaultLocale } from './i18n/index.js';
|
|
64
69
|
import { TerminalRenderer } from './utils/terminal-renderer.js';
|
|
@@ -87,6 +92,7 @@ import { ScreenAnalyzer } from './utils/screen-analyzer.js';
|
|
|
87
92
|
import { captureToPng } from './utils/screenshot-renderer.js';
|
|
88
93
|
import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven } from './utils/transient-snapshot.js';
|
|
89
94
|
import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
|
|
95
|
+
import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web-history.js';
|
|
90
96
|
import { parseWorkerRequestUrl } from './utils/worker-http.js';
|
|
91
97
|
import { detectCliUsageLimit, usageLimitStateKey } from './utils/cli-usage-limit.js';
|
|
92
98
|
import { uploadImageBuffer } from './utils/lark-upload.js';
|
|
@@ -99,13 +105,20 @@ import { createHash } from 'node:crypto';
|
|
|
99
105
|
import { installHook } from './adapters/hook-installer.js';
|
|
100
106
|
import { hookCommandFor } from './adapters/hook-command.js';
|
|
101
107
|
import { withCodexAppContext } from './utils/codex-app-context.js';
|
|
108
|
+
import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
|
|
109
|
+
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
110
|
+
import { managedOriginCapabilityPath, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, } from './core/managed-origin-capability.js';
|
|
102
111
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
103
112
|
let cliAdapter = null;
|
|
104
113
|
let backend = null;
|
|
114
|
+
let intentionalRestartBackend = null;
|
|
105
115
|
let cliPidMarker = null; // path to .botmux-cli-pids/<pid>
|
|
106
116
|
let seatbeltProfilePath = null; // per-session Seatbelt .sb profile to rm at exit (external-wrapper read isolation)
|
|
107
117
|
let sandboxStopWatcher = null; // stop fn for the sandbox outbox watcher
|
|
108
118
|
let sandboxCleanup = null; // unmount overlays + rm the per-session sandbox tree
|
|
119
|
+
let sandboxRelayOutbox = null;
|
|
120
|
+
let sandboxRelayCapability = null;
|
|
121
|
+
let readIsolationOriginCapabilityFile = null;
|
|
109
122
|
let sandboxTeardownDone = false; // guards the exit-time best-effort teardown from double-running / running on suspend-for-resume
|
|
110
123
|
/** Counts consecutive in-worker restart cycles (see case 'restart'). Used by
|
|
111
124
|
* the SECONDARY guard so an adapter whose checkResumeTargetExists misses
|
|
@@ -349,12 +362,23 @@ const wsClients = new Set();
|
|
|
349
362
|
const authedClients = new WeakSet();
|
|
350
363
|
/** Per-WS-client tmux/zellij attach PTYs. */
|
|
351
364
|
const clientPtys = new Map();
|
|
365
|
+
/** Managed-Herdr viewers survive an in-worker /restart while backend changes. */
|
|
366
|
+
const herdrWebBindings = new Map();
|
|
352
367
|
const writeToken = randomBytes(16).toString('hex');
|
|
368
|
+
// Standalone/test fallback. Production replaces this after init with a stable
|
|
369
|
+
// per-session HMAC derived from the host-only dashboard secret.
|
|
370
|
+
let viewToken = randomBytes(32).toString('base64url');
|
|
353
371
|
// Active dashboard token, persisted by the dashboard process at this stable
|
|
354
372
|
// path (mirrors dashboard.ts TOKEN_PATH). The platform proxy injects it as the
|
|
355
373
|
// `botmux_dashboard_token` cookie on every request it fronts, so its presence
|
|
356
374
|
// proves a request traversed the platform's authenticated front door.
|
|
357
375
|
const DASHBOARD_TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
376
|
+
const DASHBOARD_SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
377
|
+
function refreshTerminalViewToken() {
|
|
378
|
+
const secret = loadDashboardSecret(DASHBOARD_SECRET_PATH);
|
|
379
|
+
if (secret && sessionId)
|
|
380
|
+
viewToken = deriveTerminalViewToken(secret, sessionId);
|
|
381
|
+
}
|
|
358
382
|
/**
|
|
359
383
|
* Resolve terminal write permission for one request. The platform-injected
|
|
360
384
|
* `X-Botmux-Role` header is trusted only when this machine is bound to a central
|
|
@@ -369,8 +393,8 @@ const DASHBOARD_TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
|
369
393
|
* `botmux bind`/unbind and dashboard token rotation are hot-reloaded without
|
|
370
394
|
* restarting this worker, so a cached value would go stale.
|
|
371
395
|
*/
|
|
372
|
-
function
|
|
373
|
-
return
|
|
396
|
+
function resolveTerminalAccessForReq(req, url) {
|
|
397
|
+
return resolveTerminalAccessForRequest(req.headers, safeTerminalTokenEqual(url.searchParams.get('token'), writeToken), safeTerminalTokenEqual(url.searchParams.get('viewToken'), viewToken), () => readPlatformBinding() !== null, () => loadPersistedToken(DASHBOARD_TOKEN_PATH));
|
|
374
398
|
}
|
|
375
399
|
/** Lazily-written locked-mode zellij config for per-WS web-terminal attach
|
|
376
400
|
* clients: cleared keybinds + locked mode so every keystroke passes straight
|
|
@@ -396,6 +420,16 @@ function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'C
|
|
|
396
420
|
let isPromptReady = false;
|
|
397
421
|
/** Mutex for async flushPending — prevents concurrent flush loops. */
|
|
398
422
|
let isFlushing = false;
|
|
423
|
+
/** True from the moment an owned CLI restart begins until the replacement
|
|
424
|
+
* backend has been synchronously installed. Async backends (notably Riff)
|
|
425
|
+
* keep the old backend object alive while destroySession() awaits remote
|
|
426
|
+
* cancellation; this gate prevents new input or an old idle callback from
|
|
427
|
+
* crossing that teardown fence. */
|
|
428
|
+
let cliRestartInProgress = false;
|
|
429
|
+
/** Raw slash commands require a real prompt and cannot use the ordinary
|
|
430
|
+
* type-ahead fallback. Keep them fenced across an owned restart until the
|
|
431
|
+
* replacement generation reaches its prompt. */
|
|
432
|
+
let rawInputRestartGate = false;
|
|
399
433
|
/** Per-spawn one-shot: have this spawn's bot.startupCommands been typed in yet?
|
|
400
434
|
* Reset in spawnCli so a restart/resume (which re-spawns the CLI) re-applies
|
|
401
435
|
* them — needed because session-only settings like `/effort ultracode` are lost
|
|
@@ -634,8 +668,9 @@ async function runStartupCommands() {
|
|
|
634
668
|
idleDetector?.reset();
|
|
635
669
|
}
|
|
636
670
|
const pendingMessages = [];
|
|
637
|
-
/** Literal commands that arrived while native /rename owned the TUI
|
|
638
|
-
*
|
|
671
|
+
/** Literal commands that arrived while native /rename owned the TUI or while
|
|
672
|
+
* an owned CLI restart was fenced. Normal raw_input commands are still
|
|
673
|
+
* delivered immediately (including while busy). */
|
|
639
674
|
const pendingRawInputs = [];
|
|
640
675
|
/** Latest requested canonical session title. Unlike a normal prompt this is an
|
|
641
676
|
* administrative TUI command: never type-ahead while the agent is busy, never
|
|
@@ -706,7 +741,9 @@ async function deliverRawInput(msg) {
|
|
|
706
741
|
// Enter lands. sendToPty also observes commandLineWritesPending, so another
|
|
707
742
|
// raw command's text -> Enter window cannot be interrupted by the follow-up.
|
|
708
743
|
if (sent && msg.followUpContent) {
|
|
709
|
-
sendToPty(msg.followUpContent, undefined,
|
|
744
|
+
sendToPty(msg.followUpContent, undefined, {
|
|
745
|
+
codexAppInput: msg.followUpCodexAppInput,
|
|
746
|
+
});
|
|
710
747
|
log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
|
|
711
748
|
}
|
|
712
749
|
// A pending /rename may have been held by the command-write mutex. It still
|
|
@@ -720,29 +757,192 @@ const inflightInputs = new InflightInputTracker();
|
|
|
720
757
|
* start work before their history/transcript submit marker is observable. */
|
|
721
758
|
let lastPtyActivityAtMs = 0;
|
|
722
759
|
let currentBotmuxTurnId;
|
|
723
|
-
|
|
760
|
+
let currentBotmuxDispatchAttempt;
|
|
761
|
+
let currentVcMeetingImTurnOrigin;
|
|
762
|
+
let durableTurnInFlight = false;
|
|
763
|
+
function publishSandboxRelayCapability(opts = {}) {
|
|
764
|
+
const capability = {
|
|
765
|
+
token: randomBytes(32).toString('hex'),
|
|
766
|
+
...(currentBotmuxTurnId ? { turnId: currentBotmuxTurnId } : {}),
|
|
767
|
+
...(currentBotmuxDispatchAttempt !== undefined
|
|
768
|
+
? { dispatchAttempt: currentBotmuxDispatchAttempt }
|
|
769
|
+
: {}),
|
|
770
|
+
};
|
|
771
|
+
const files = [
|
|
772
|
+
...(sandboxRelayOutbox
|
|
773
|
+
? [{
|
|
774
|
+
path: join(sandboxRelayOutbox, RELAY_ORIGIN_CAPABILITY_BASENAME),
|
|
775
|
+
body: JSON.stringify({ token: capability.token }),
|
|
776
|
+
}]
|
|
777
|
+
: []),
|
|
778
|
+
...(readIsolationOriginCapabilityFile && sessionId
|
|
779
|
+
? [{
|
|
780
|
+
path: readIsolationOriginCapabilityFile,
|
|
781
|
+
body: JSON.stringify({
|
|
782
|
+
sessionId,
|
|
783
|
+
capability: capability.token,
|
|
784
|
+
...(capability.turnId ? { turnId: capability.turnId } : {}),
|
|
785
|
+
...(capability.dispatchAttempt !== undefined
|
|
786
|
+
? { dispatchAttempt: capability.dispatchAttempt }
|
|
787
|
+
: {}),
|
|
788
|
+
}),
|
|
789
|
+
}]
|
|
790
|
+
: []),
|
|
791
|
+
];
|
|
792
|
+
for (const file of files) {
|
|
793
|
+
try {
|
|
794
|
+
replaceManagedOriginCapabilityFile(file.path, file.body);
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
log(`Failed to publish managed origin capability: ${err?.message ?? err}`);
|
|
798
|
+
if (opts.failClosed) {
|
|
799
|
+
unlinkManagedOriginCapabilityFiles();
|
|
800
|
+
throw err;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
sandboxRelayCapability = capability;
|
|
805
|
+
if (sessionId) {
|
|
806
|
+
send({
|
|
807
|
+
type: 'managed_turn_origin',
|
|
808
|
+
sessionId,
|
|
809
|
+
capability: capability.token,
|
|
810
|
+
...(capability.turnId ? { turnId: capability.turnId } : {}),
|
|
811
|
+
...(capability.dispatchAttempt !== undefined
|
|
812
|
+
? { dispatchAttempt: capability.dispatchAttempt }
|
|
813
|
+
: {}),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function unlinkManagedOriginCapabilityFiles() {
|
|
818
|
+
const files = [
|
|
819
|
+
sandboxRelayOutbox
|
|
820
|
+
? join(sandboxRelayOutbox, RELAY_ORIGIN_CAPABILITY_BASENAME)
|
|
821
|
+
: undefined,
|
|
822
|
+
readIsolationOriginCapabilityFile ?? undefined,
|
|
823
|
+
];
|
|
824
|
+
for (const file of new Set(files.filter((p) => !!p))) {
|
|
825
|
+
try {
|
|
826
|
+
unlinkSync(file);
|
|
827
|
+
}
|
|
828
|
+
catch { /* absent or teardown racing */ }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Read a host-owned isolation marker without following a child-planted
|
|
832
|
+
* symlink between lookup and open. */
|
|
833
|
+
function readRegularHostFileNoFollow(filePath) {
|
|
834
|
+
let fd;
|
|
724
835
|
try {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
|
|
730
|
-
return fields[19] || undefined;
|
|
836
|
+
fd = openSync(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
837
|
+
if (!fstatSync(fd).isFile())
|
|
838
|
+
return null;
|
|
839
|
+
return readFileSync(fd, 'utf8');
|
|
731
840
|
}
|
|
732
841
|
catch {
|
|
733
|
-
return
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
if (fd !== undefined) {
|
|
846
|
+
try {
|
|
847
|
+
closeSync(fd);
|
|
848
|
+
}
|
|
849
|
+
catch { /* best-effort */ }
|
|
850
|
+
}
|
|
734
851
|
}
|
|
735
852
|
}
|
|
853
|
+
function completeManagedTurnOriginRevocation(revoked, turnId, dispatchAttempt) {
|
|
854
|
+
// Clear local authority before queuing daemon IPC. A forked/delayed process
|
|
855
|
+
// can otherwise win the small window between terminal publication and
|
|
856
|
+
// revocation by submitting through the still-live host relay.
|
|
857
|
+
sandboxRelayCapability = null;
|
|
858
|
+
currentVcMeetingImTurnOrigin = undefined;
|
|
859
|
+
if (sessionId) {
|
|
860
|
+
send({
|
|
861
|
+
type: 'managed_turn_origin_revoked',
|
|
862
|
+
sessionId,
|
|
863
|
+
...(revoked ? { capability: revoked.token } : {}),
|
|
864
|
+
...(turnId ? { turnId } : {}),
|
|
865
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
unlinkManagedOriginCapabilityFiles();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Revoke this CLI generation's managed-send authority before an intentional
|
|
872
|
+
* in-worker restart starts tearing down its backend. The Node worker survives,
|
|
873
|
+
* so neither claude_exit nor worker-exit can perform the daemon-side cleanup.
|
|
874
|
+
*
|
|
875
|
+
* Keep currentBotmuxTurnId/currentBotmuxDispatchAttempt intact: the old
|
|
876
|
+
* backend's exit callback still needs that exact identity to emit its terminal
|
|
877
|
+
* edge. The relay token and explicit IM origin, however, become unusable
|
|
878
|
+
* synchronously; a later real turn publishes a fresh token in flushPending().
|
|
879
|
+
*/
|
|
880
|
+
function revokeManagedTurnOriginForRestart() {
|
|
881
|
+
const revoked = sandboxRelayCapability;
|
|
882
|
+
completeManagedTurnOriginRevocation(revoked, currentBotmuxTurnId, currentBotmuxDispatchAttempt);
|
|
883
|
+
}
|
|
884
|
+
/** Revoke only the capability generation bound to this exact terminal. A late
|
|
885
|
+
* terminal from turn N must not clear the token already rotated for turn N+1. */
|
|
886
|
+
function revokeManagedTurnOriginForTerminal(turnId, dispatchAttempt) {
|
|
887
|
+
const revoked = sandboxRelayCapability;
|
|
888
|
+
if (!revoked
|
|
889
|
+
|| revoked.turnId !== turnId
|
|
890
|
+
|| revoked.dispatchAttempt !== dispatchAttempt)
|
|
891
|
+
return;
|
|
892
|
+
completeManagedTurnOriginRevocation(revoked, turnId, dispatchAttempt);
|
|
893
|
+
}
|
|
894
|
+
function authorizeManagedSend(claim) {
|
|
895
|
+
if (!sessionId)
|
|
896
|
+
return { ok: false, error: 'VC policy cannot resolve session id' };
|
|
897
|
+
const dataDir = process.env.SESSION_DATA_DIR;
|
|
898
|
+
if (!dataDir)
|
|
899
|
+
return { ok: false, error: 'VC policy cannot resolve session data' };
|
|
900
|
+
// The sandbox controls every byte in outbox/*.req.json. A rotating host-issued
|
|
901
|
+
// capability binds request creation to this exact live turn; the request may
|
|
902
|
+
// not name an older loud receipt, and a delayed request crossing a turn
|
|
903
|
+
// boundary is rejected after capability rotation.
|
|
904
|
+
const capability = sandboxRelayCapability;
|
|
905
|
+
if (!capability || !claim.capability || claim.capability !== capability.token) {
|
|
906
|
+
return { ok: false, error: 'origin_mismatch: relay capability is stale or missing' };
|
|
907
|
+
}
|
|
908
|
+
const session = sessionStore.getSession(sessionId);
|
|
909
|
+
const currentImOrigin = currentVcMeetingImTurnOrigin;
|
|
910
|
+
const imOrigin = currentImOrigin?.larkMessageId === capability.turnId
|
|
911
|
+
&& currentImOrigin?.receiverSessionId === sessionId
|
|
912
|
+
? currentImOrigin
|
|
913
|
+
: undefined;
|
|
914
|
+
const decision = evaluateVcMeetingManagedSend(dataDir, {
|
|
915
|
+
receiverSessionId: sessionId,
|
|
916
|
+
receiverSession: !!session?.vcMeetingReceiver,
|
|
917
|
+
turnId: capability.turnId,
|
|
918
|
+
dispatchAttempt: capability.dispatchAttempt,
|
|
919
|
+
currentImTurnOrigin: imOrigin,
|
|
920
|
+
});
|
|
921
|
+
return decision.ok
|
|
922
|
+
? {
|
|
923
|
+
ok: true,
|
|
924
|
+
origin: {
|
|
925
|
+
...(capability.turnId ? { turnId: capability.turnId } : {}),
|
|
926
|
+
...(capability.dispatchAttempt !== undefined
|
|
927
|
+
? { dispatchAttempt: capability.dispatchAttempt }
|
|
928
|
+
: {}),
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
: { ok: false, error: `${decision.errorCode}: ${decision.error}` };
|
|
932
|
+
}
|
|
736
933
|
function writeCliPidMarker() {
|
|
737
934
|
if (!cliPidMarker || !sessionId)
|
|
738
935
|
return;
|
|
739
936
|
try {
|
|
740
937
|
// 原子写:daemon 侧(killStalePids 等)随时读这个 marker JSON。
|
|
741
938
|
const markerPid = Number(basename(cliPidMarker));
|
|
742
|
-
const procStart = Number.isInteger(markerPid) && markerPid > 0
|
|
939
|
+
const procStart = Number.isInteger(markerPid) && markerPid > 0
|
|
940
|
+
? readProcessStartIdentity(markerPid)
|
|
941
|
+
: undefined;
|
|
743
942
|
atomicWriteFileSync(cliPidMarker, JSON.stringify({
|
|
744
943
|
sessionId,
|
|
745
944
|
turnId: currentBotmuxTurnId ?? null,
|
|
945
|
+
dispatchAttempt: currentBotmuxDispatchAttempt ?? null,
|
|
746
946
|
...(procStart ? { procStart } : {}),
|
|
747
947
|
}));
|
|
748
948
|
}
|
|
@@ -910,6 +1110,12 @@ let codexAdoptPendingPid;
|
|
|
910
1110
|
* but BEFORE the rollout was located still reach the Lark thread. 5s
|
|
911
1111
|
* skew tolerance is applied on top, mirroring the Lark/Claude bridges. */
|
|
912
1112
|
let codexAdoptStartMs;
|
|
1113
|
+
/** Open-fd discovery is cheap via /proc on Linux but shells out to lsof on
|
|
1114
|
+
* macOS/BSD. Lark-driven Grok rotation is immediate through writeInput; this
|
|
1115
|
+
* throttled poll exists for direct terminal/adopt rotation and cold collision
|
|
1116
|
+
* recovery, where a few seconds of latency is acceptable. */
|
|
1117
|
+
let grokBridgePidProbeLastMs = 0;
|
|
1118
|
+
const GROK_BRIDGE_PID_PROBE_INTERVAL_MS = 5_000;
|
|
913
1119
|
/** Adopt-only: 一次性发送的 "/adopt 前最后一轮" preamble 是否已经触发过。
|
|
914
1120
|
* codexBridgeAttach 在 split-live 分支会查 history 取最后一对 user/assistant
|
|
915
1121
|
* 发给 daemon —— late-attach poller 也会反复走这条分支(每秒一次),所以
|
|
@@ -1785,6 +1991,10 @@ function bridgeIngest() {
|
|
|
1785
1991
|
if (result.events.length > 0)
|
|
1786
1992
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
1787
1993
|
bridgeQueue.ingest(result.events, bridgeJsonlPath);
|
|
1994
|
+
// Transcript terminal markers are authoritative and may settle a durable
|
|
1995
|
+
// turn immediately. Do not wait for the screen prompt: permission/AskUser
|
|
1996
|
+
// surfaces can resemble idle, while an explicit JSONL boundary cannot.
|
|
1997
|
+
emitReadyTurns({ explicitTerminalOnly: true });
|
|
1788
1998
|
}
|
|
1789
1999
|
function performBridgeIngestAndScheduleQuietEmit() {
|
|
1790
2000
|
const beforePath = bridgeJsonlPath;
|
|
@@ -1922,6 +2132,10 @@ function stopBridgeWatcher() {
|
|
|
1922
2132
|
bridgeStalePidStateSessionId = undefined;
|
|
1923
2133
|
bridgeSecondaryPaths.clear();
|
|
1924
2134
|
bridgeFingerprintScanLastMs.clear();
|
|
2135
|
+
// Attribution belongs to the CLI generation whose transcript we watched.
|
|
2136
|
+
// A fresh backend must not inherit a terminal/fingerprint from the exited
|
|
2137
|
+
// process; durable replay is owned by the receiver with a new attempt.
|
|
2138
|
+
bridgeQueue.clearPending();
|
|
1925
2139
|
bridgePreambleSent = false;
|
|
1926
2140
|
}
|
|
1927
2141
|
/**
|
|
@@ -1940,12 +2154,13 @@ function stopBridgeWatcher() {
|
|
|
1940
2154
|
* Lark turn.
|
|
1941
2155
|
*
|
|
1942
2156
|
* The turnId is returned so the writeInput failure path can call
|
|
1943
|
-
* `bridgeQueue.dropPendingTurn(turnId)` after deferred
|
|
1944
|
-
* fails — otherwise an Enter-eaten-by-TUI submit leaves
|
|
1945
|
-
* no jsonl line will ever match, and
|
|
1946
|
-
* CPU scanning all sibling jsonls for it
|
|
2157
|
+
* `bridgeQueue.dropPendingTurn(turnId, dispatchAttempt)` after deferred
|
|
2158
|
+
* recheck conclusively fails — otherwise an Enter-eaten-by-TUI submit leaves
|
|
2159
|
+
* a fingerprint that no jsonl line will ever match, and
|
|
2160
|
+
* `maybeSwitchBridgeJsonl` burns 99% CPU scanning all sibling jsonls for it
|
|
2161
|
+
* on every poll tick.
|
|
1947
2162
|
*/
|
|
1948
|
-
function bridgeMarkPendingTurn(messageText, preferredTurnId) {
|
|
2163
|
+
function bridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
|
|
1949
2164
|
if (!bridgeJsonlPath)
|
|
1950
2165
|
return undefined;
|
|
1951
2166
|
if (!bridgeBaselineDone) {
|
|
@@ -1982,7 +2197,7 @@ function bridgeMarkPendingTurn(messageText, preferredTurnId) {
|
|
|
1982
2197
|
const normalised = normaliseForFingerprint(messageText);
|
|
1983
2198
|
const contentNormalized = normalised.length > 0 ? normalised : undefined;
|
|
1984
2199
|
const turnId = preferredTurnId ?? randomBytes(8).toString('hex');
|
|
1985
|
-
bridgeQueue.mark(turnId, fingerprint, Date.now(), contentNormalized);
|
|
2200
|
+
bridgeQueue.mark(turnId, fingerprint, Date.now(), contentNormalized, dispatchAttempt);
|
|
1986
2201
|
return turnId;
|
|
1987
2202
|
}
|
|
1988
2203
|
function bridgeDrainAndMaybeEmit() {
|
|
@@ -2003,8 +2218,13 @@ function bridgeDrainAndMaybeEmit() {
|
|
|
2003
2218
|
*
|
|
2004
2219
|
* Caches per-path drains so a batch of turns from the same file only reads
|
|
2005
2220
|
* the transcript once (O(jsonl size) per distinct path). */
|
|
2006
|
-
function emitReadyTurns() {
|
|
2007
|
-
const ready = bridgeQueue.drainEmittable(
|
|
2221
|
+
function emitReadyTurns(opts = {}) {
|
|
2222
|
+
const ready = bridgeQueue.drainEmittable(opts.explicitTerminalOnly
|
|
2223
|
+
? { explicitTerminalOnly: true }
|
|
2224
|
+
: {
|
|
2225
|
+
terminalBoundary: true,
|
|
2226
|
+
requireExplicitTerminalForDurable: true,
|
|
2227
|
+
});
|
|
2008
2228
|
if (ready.length === 0)
|
|
2009
2229
|
return;
|
|
2010
2230
|
const adoptMode = lastInitConfig?.adoptMode === true;
|
|
@@ -2068,6 +2288,7 @@ function emitReadyTurns() {
|
|
|
2068
2288
|
content: fields.content,
|
|
2069
2289
|
lastUuid,
|
|
2070
2290
|
turnId: turn.turnId,
|
|
2291
|
+
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
2071
2292
|
kind: 'local-turn',
|
|
2072
2293
|
userText: fields.userText,
|
|
2073
2294
|
});
|
|
@@ -2082,12 +2303,25 @@ function emitReadyTurns() {
|
|
|
2082
2303
|
content: headlessContent,
|
|
2083
2304
|
lastUuid,
|
|
2084
2305
|
turnId: turn.turnId,
|
|
2306
|
+
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
2085
2307
|
kind: 'local-turn-headless',
|
|
2086
2308
|
});
|
|
2087
2309
|
continue;
|
|
2088
2310
|
}
|
|
2089
|
-
send({
|
|
2311
|
+
send({
|
|
2312
|
+
type: 'final_output',
|
|
2313
|
+
content: assistantText,
|
|
2314
|
+
lastUuid,
|
|
2315
|
+
turnId: turn.turnId,
|
|
2316
|
+
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
2317
|
+
});
|
|
2090
2318
|
}
|
|
2319
|
+
// A visible fallback is optional: it may be empty or deliberately suppressed
|
|
2320
|
+
// after the model used `botmux send`. Completion is not optional. Emit it
|
|
2321
|
+
// only after all corresponding final_output IPC messages have been queued so
|
|
2322
|
+
// the daemon observes a stable per-turn ordering.
|
|
2323
|
+
for (const turn of ready)
|
|
2324
|
+
emitTurnTerminal(turn.turnId, 'completed', undefined, turn.dispatchAttempt);
|
|
2091
2325
|
}
|
|
2092
2326
|
/** Drain `path` from `fromOffset` and feed the events to the bridge queue
|
|
2093
2327
|
* with that path as the source stamp. Pure side-effects on bridgeQueue +
|
|
@@ -2115,10 +2349,11 @@ function codexBridgeFallbackActive() {
|
|
|
2115
2349
|
// services/structured-bridge-clis.ts (single source of the allowlist).
|
|
2116
2350
|
return isStructuredBridgeFallbackActive(lastInitConfig?.cliId, lastInitConfig?.adoptMode === true);
|
|
2117
2351
|
}
|
|
2118
|
-
// Both Codex and TRAE share the same rollout JSONL layout (response_item
|
|
2119
|
-
// messages), so drainCodexRollout works for both.
|
|
2120
2352
|
function structuredBridgeIsCodex() {
|
|
2121
|
-
return lastInitConfig?.cliId === 'codex'
|
|
2353
|
+
return lastInitConfig?.cliId === 'codex';
|
|
2354
|
+
}
|
|
2355
|
+
function structuredBridgeIsTraex() {
|
|
2356
|
+
return lastInitConfig?.cliId === 'traex';
|
|
2122
2357
|
}
|
|
2123
2358
|
function structuredBridgeIsHermes() {
|
|
2124
2359
|
return lastInitConfig?.cliId === 'hermes';
|
|
@@ -2141,6 +2376,8 @@ function currentHermesBridgeDbPath() {
|
|
|
2141
2376
|
function structuredBridgeIngestPath(path, offset) {
|
|
2142
2377
|
if (structuredBridgeIsCodex())
|
|
2143
2378
|
return drainCodexRollout(path, offset);
|
|
2379
|
+
if (structuredBridgeIsTraex())
|
|
2380
|
+
return drainTraexRollout(path, offset);
|
|
2144
2381
|
if (codexBridgeIsCursor())
|
|
2145
2382
|
return drainCursorTranscript(path, offset);
|
|
2146
2383
|
if (structuredBridgeIsPi())
|
|
@@ -2222,6 +2459,17 @@ function codexBridgeStartTimer() {
|
|
|
2222
2459
|
emitReadyCodexTurns();
|
|
2223
2460
|
return;
|
|
2224
2461
|
}
|
|
2462
|
+
// Grok keeps one process alive across `/new` / `/clear` / `/resume`, but
|
|
2463
|
+
// moves its authoritative updates stream to a new session directory.
|
|
2464
|
+
// A Lark-driven next prompt reports the new sid through writeInput;
|
|
2465
|
+
// adopt/local terminal input has no such callback, so follow the pid's
|
|
2466
|
+
// currently-open Grok session on every bridge tick as well.
|
|
2467
|
+
maybeFollowGrokSessionRotationViaPid();
|
|
2468
|
+
// TRAE has the same adopt/local-input blind spot: a direct `/new` in
|
|
2469
|
+
// the observed pane produces no adapter writeInput result. Its rollout
|
|
2470
|
+
// fd is process-scoped, so following that fd cannot select a sibling
|
|
2471
|
+
// TRAE process merely because it shares the working directory.
|
|
2472
|
+
maybeFollowTraexSessionRotationViaPid();
|
|
2225
2473
|
if (!codexBridgeRolloutPath) {
|
|
2226
2474
|
// Late-attach: cliSessionId (writeInput / daemon probe) then adopt
|
|
2227
2475
|
// pid. Path lookup is centralized in resolveFileBridgePath so
|
|
@@ -2474,7 +2722,38 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
2474
2722
|
if (!codexBridgeFallbackActive())
|
|
2475
2723
|
return;
|
|
2476
2724
|
if (codexBridgeRolloutPath) {
|
|
2477
|
-
// Already attached — first-attach-wins for most CLIs.
|
|
2725
|
+
// Already attached — first-attach-wins for most CLIs. Exceptions: TRAE
|
|
2726
|
+
// and Grok can rotate their native session in the same process.
|
|
2727
|
+
if (structuredBridgeIsTraex()) {
|
|
2728
|
+
const currentSid = codexSessionIdFromRolloutPath(codexBridgeRolloutPath);
|
|
2729
|
+
if (currentSid && currentSid.toLowerCase() === cliSessionId.toLowerCase())
|
|
2730
|
+
return;
|
|
2731
|
+
const next = resolveFileBridgePath('traex', { sessionId: cliSessionId });
|
|
2732
|
+
// Close any terminal already committed to the retired rollout before
|
|
2733
|
+
// switching. A durable turn is a worker HOL barrier, so a legitimate
|
|
2734
|
+
// session rotation cannot overtake an unfinished durable delivery.
|
|
2735
|
+
try {
|
|
2736
|
+
codexBridgeIngest();
|
|
2737
|
+
emitReadyCodexTurns();
|
|
2738
|
+
}
|
|
2739
|
+
catch (err) {
|
|
2740
|
+
log(`TRAE pre-rotation bridge drain failed: ${err.message}`);
|
|
2741
|
+
}
|
|
2742
|
+
if (next && next !== codexBridgeRolloutPath) {
|
|
2743
|
+
log(`TRAE session rotated ${currentSid ?? '?'} → ${cliSessionId}; re-attaching bridge to ${next}`);
|
|
2744
|
+
codexBridgeDetachFile();
|
|
2745
|
+
codexBridgePendingSessionId = undefined;
|
|
2746
|
+
codexBridgeAttach(next, 'fresh-empty');
|
|
2747
|
+
}
|
|
2748
|
+
else if (!next) {
|
|
2749
|
+
log(`TRAE session rotated ${currentSid ?? '?'} → ${cliSessionId}; waiting for rollout`);
|
|
2750
|
+
codexBridgeDetachFile();
|
|
2751
|
+
codexBridgePendingSessionId = cliSessionId;
|
|
2752
|
+
codexBridgeStartTimer();
|
|
2753
|
+
}
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
// Grok's
|
|
2478
2757
|
// `/new` / `/clear` / `/resume` rotate to a NEW session directory at the
|
|
2479
2758
|
// SAME pid, and writeInput's prompt_history verify reports the new
|
|
2480
2759
|
// session id on the very next submit. Without a re-attach the bridge
|
|
@@ -2490,6 +2769,17 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
2490
2769
|
});
|
|
2491
2770
|
if (next && next !== codexBridgeRolloutPath) {
|
|
2492
2771
|
log(`Grok session rotated ${currentSid ?? '?'} → ${cliSessionId}; re-attaching bridge to ${next}`);
|
|
2772
|
+
// Drain the retired stream synchronously before closing its watcher.
|
|
2773
|
+
// A turn_completed may already be on disk while fs.watch / the 1s
|
|
2774
|
+
// poller is still queued; letting the new path replace it first would
|
|
2775
|
+
// strand the exact old turn until the durable lease expires.
|
|
2776
|
+
try {
|
|
2777
|
+
codexBridgeIngest();
|
|
2778
|
+
emitReadyCodexTurns();
|
|
2779
|
+
}
|
|
2780
|
+
catch (err) {
|
|
2781
|
+
log(`Grok pre-rotation bridge drain failed: ${err.message}`);
|
|
2782
|
+
}
|
|
2493
2783
|
codexBridgeDetachFile();
|
|
2494
2784
|
codexBridgePendingSessionId = undefined;
|
|
2495
2785
|
codexBridgeAttach(next, 'fresh-empty');
|
|
@@ -2498,6 +2788,13 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
2498
2788
|
// New session id reported but updates.jsonl not on disk yet —
|
|
2499
2789
|
// detach the retired session and arm the poller for late-attach.
|
|
2500
2790
|
log(`Grok session rotated ${currentSid ?? '?'} → ${cliSessionId}; waiting for updates.jsonl`);
|
|
2791
|
+
try {
|
|
2792
|
+
codexBridgeIngest();
|
|
2793
|
+
emitReadyCodexTurns();
|
|
2794
|
+
}
|
|
2795
|
+
catch (err) {
|
|
2796
|
+
log(`Grok pre-rotation bridge drain failed: ${err.message}`);
|
|
2797
|
+
}
|
|
2501
2798
|
codexBridgeDetachFile();
|
|
2502
2799
|
codexBridgePendingSessionId = cliSessionId;
|
|
2503
2800
|
codexBridgeStartTimer();
|
|
@@ -2544,7 +2841,52 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
2544
2841
|
codexBridgeStartTimer();
|
|
2545
2842
|
}
|
|
2546
2843
|
}
|
|
2547
|
-
|
|
2844
|
+
/** Follow a Grok in-process session rotation even when no botmux writeInput
|
|
2845
|
+
* occurred (notably direct terminal input in adopt mode). Open-fd discovery is
|
|
2846
|
+
* process-scoped, so sibling Grok sessions in the same cwd cannot steal this
|
|
2847
|
+
* bridge. `codexBridgeNotifyCliSessionId` owns the drain-before-detach switch. */
|
|
2848
|
+
function maybeFollowGrokSessionRotationViaPid() {
|
|
2849
|
+
if (!structuredBridgeIsGrok() || !codexBridgeRolloutPath || !backend)
|
|
2850
|
+
return;
|
|
2851
|
+
const now = Date.now();
|
|
2852
|
+
if (now - grokBridgePidProbeLastMs < GROK_BRIDGE_PID_PROBE_INTERVAL_MS)
|
|
2853
|
+
return;
|
|
2854
|
+
grokBridgePidProbeLastMs = now;
|
|
2855
|
+
const pid = backend.cliPid
|
|
2856
|
+
?? backend.getChildPid?.()
|
|
2857
|
+
?? codexAdoptPendingPid;
|
|
2858
|
+
if (!pid)
|
|
2859
|
+
return;
|
|
2860
|
+
const observed = findGrokSessionByPid(pid);
|
|
2861
|
+
if (!observed)
|
|
2862
|
+
return;
|
|
2863
|
+
const currentSid = grokSessionIdFromPath(codexBridgeRolloutPath);
|
|
2864
|
+
if (currentSid?.toLowerCase() === observed.sessionId.toLowerCase())
|
|
2865
|
+
return;
|
|
2866
|
+
persistCliSessionId(observed.sessionId);
|
|
2867
|
+
codexBridgeNotifyCliSessionId(observed.sessionId);
|
|
2868
|
+
}
|
|
2869
|
+
/** Follow a TRAE in-process session rotation for direct local input in an
|
|
2870
|
+
* adopted pane. `codexBridgeNotifyCliSessionId` performs the drain-before-
|
|
2871
|
+
* detach switch and persists the newly observed native session id. */
|
|
2872
|
+
function maybeFollowTraexSessionRotationViaPid() {
|
|
2873
|
+
if (!structuredBridgeIsTraex() || !codexBridgeRolloutPath || !backend)
|
|
2874
|
+
return;
|
|
2875
|
+
const pid = backend.cliPid
|
|
2876
|
+
?? backend.getChildPid?.()
|
|
2877
|
+
?? codexAdoptPendingPid;
|
|
2878
|
+
if (!pid)
|
|
2879
|
+
return;
|
|
2880
|
+
const observed = findTraexRolloutByPid(pid);
|
|
2881
|
+
if (!observed)
|
|
2882
|
+
return;
|
|
2883
|
+
const currentSid = codexSessionIdFromRolloutPath(codexBridgeRolloutPath);
|
|
2884
|
+
if (currentSid?.toLowerCase() === observed.cliSessionId.toLowerCase())
|
|
2885
|
+
return;
|
|
2886
|
+
persistCliSessionId(observed.cliSessionId);
|
|
2887
|
+
codexBridgeNotifyCliSessionId(observed.cliSessionId);
|
|
2888
|
+
}
|
|
2889
|
+
function codexBridgeIngest(opts = {}) {
|
|
2548
2890
|
if (structuredBridgeIsHermes()) {
|
|
2549
2891
|
hermesBridgeIngest();
|
|
2550
2892
|
return;
|
|
@@ -2567,26 +2909,26 @@ function codexBridgeIngest() {
|
|
|
2567
2909
|
// its own moving targets). Pushing idle here lets the bridge emit
|
|
2568
2910
|
// immediately instead of waiting for readyPattern + quiescence to
|
|
2569
2911
|
// converge. Idempotent — IdleDetector.fireIdle no-ops while already idle.
|
|
2570
|
-
if (result.events.some(e => e.kind === 'assistant_final')) {
|
|
2912
|
+
if (opts.signalIdle !== false && result.events.some(e => e.kind === 'assistant_final')) {
|
|
2571
2913
|
idleDetector?.fireIdle();
|
|
2572
2914
|
}
|
|
2573
2915
|
}
|
|
2574
2916
|
/** Mark a pending Lark turn for Codex. Crucially this works even before a
|
|
2575
2917
|
* rollout path is known — the queue is path-agnostic, and ingest after
|
|
2576
2918
|
* late-attach picks up the user_message and matches the fingerprint. */
|
|
2577
|
-
function codexBridgeMarkPendingTurn(messageText, preferredTurnId) {
|
|
2919
|
+
function codexBridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
|
|
2578
2920
|
if (!codexBridgeFallbackActive())
|
|
2579
2921
|
return false;
|
|
2580
2922
|
const turnId = preferredTurnId ?? `codex-${randomBytes(8).toString('hex')}`;
|
|
2581
|
-
codexBridgeQueue.mark(turnId, messageText);
|
|
2923
|
+
codexBridgeQueue.mark(turnId, messageText, Date.now(), dispatchAttempt);
|
|
2582
2924
|
return true;
|
|
2583
2925
|
}
|
|
2584
|
-
function codexBridgeDrainAndMaybeEmit() {
|
|
2926
|
+
function codexBridgeDrainAndMaybeEmit(opts = {}) {
|
|
2585
2927
|
if (!codexBridgeFallbackActive())
|
|
2586
2928
|
return;
|
|
2587
2929
|
if (structuredBridgeIsHermes() || structuredBridgeIsMtr() || (codexBridgeRolloutPath && codexBridgeBaselineDone)) {
|
|
2588
2930
|
try {
|
|
2589
|
-
codexBridgeIngest();
|
|
2931
|
+
codexBridgeIngest(opts);
|
|
2590
2932
|
}
|
|
2591
2933
|
catch (err) {
|
|
2592
2934
|
log(`Codex bridge ingest error: ${err.message}`);
|
|
@@ -2639,6 +2981,7 @@ function emitReadyCodexTurns() {
|
|
|
2639
2981
|
content: fields.content,
|
|
2640
2982
|
lastUuid: turn.turnId,
|
|
2641
2983
|
turnId: turn.turnId,
|
|
2984
|
+
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
2642
2985
|
kind: 'local-turn',
|
|
2643
2986
|
userText: fields.userText,
|
|
2644
2987
|
});
|
|
@@ -2650,8 +2993,12 @@ function emitReadyCodexTurns() {
|
|
|
2650
2993
|
content: turn.finalText,
|
|
2651
2994
|
lastUuid: turn.turnId,
|
|
2652
2995
|
turnId: turn.turnId,
|
|
2996
|
+
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
2653
2997
|
});
|
|
2654
2998
|
}
|
|
2999
|
+
for (const turn of ready) {
|
|
3000
|
+
emitTurnTerminal(turn.turnId, turn.terminalStatus ?? 'completed', turn.terminalErrorCode, turn.dispatchAttempt);
|
|
3001
|
+
}
|
|
2655
3002
|
}
|
|
2656
3003
|
function stopCodexBridge() {
|
|
2657
3004
|
if (codexBridgeWatcher) {
|
|
@@ -2680,6 +3027,7 @@ function stopCodexBridge() {
|
|
|
2680
3027
|
codexBridgePendingSessionId = undefined;
|
|
2681
3028
|
codexAdoptPendingPid = undefined;
|
|
2682
3029
|
codexAdoptStartMs = undefined;
|
|
3030
|
+
grokBridgePidProbeLastMs = 0;
|
|
2683
3031
|
}
|
|
2684
3032
|
/** When a rotation moves bridgeJsonlPath away from `oldPath`, queue turns
|
|
2685
3033
|
* whose sourceJsonlPath equals oldPath may still be waiting on assistant
|
|
@@ -2772,6 +3120,9 @@ const SCREEN_UPDATE_INTERVAL_MS = 2_000;
|
|
|
2772
3120
|
// ─── Scrollback Buffer (replay to late-connecting WS clients) ───────────────
|
|
2773
3121
|
const MAX_SCROLLBACK = 1_000_000; // chars (~1MB)
|
|
2774
3122
|
let scrollback = '';
|
|
3123
|
+
let herdrWebHistory = null;
|
|
3124
|
+
let herdrWebScrollDirection = null;
|
|
3125
|
+
let herdrWebCursor = null;
|
|
2775
3126
|
const WORKFLOW_TRANSCRIPT_MAX = 2_000_000; // chars (~2MB)
|
|
2776
3127
|
const WORKFLOW_OUTPUT_END_MARKER = '</WORKFLOW_OUTPUT>';
|
|
2777
3128
|
const CRASH_DIAGNOSTIC_RAW_MAX = 200_000; // enough scrollback for the web terminal without huge temp files
|
|
@@ -2787,6 +3138,68 @@ let workflowFinalOutputSent = false;
|
|
|
2787
3138
|
let altBufferActive = false;
|
|
2788
3139
|
const ALT_ENTER_RE = /\x1b\[\?(1049|1047|47)h/g;
|
|
2789
3140
|
const ALT_EXIT_RE = /\x1b\[\?(1049|1047|47)l/g;
|
|
3141
|
+
function usesHerdrSnapshotWebHistory() {
|
|
3142
|
+
return backend instanceof HerdrBackend;
|
|
3143
|
+
}
|
|
3144
|
+
function herdrWebCursorSequence(cursor = herdrWebCursor) {
|
|
3145
|
+
return cursor ? `\x1b[${cursor.row + 1};${cursor.col + 1}H` : '';
|
|
3146
|
+
}
|
|
3147
|
+
function relayHerdrWebCursor(cursor) {
|
|
3148
|
+
herdrWebCursor = cursor;
|
|
3149
|
+
const sequence = herdrWebCursorSequence(cursor);
|
|
3150
|
+
for (const ws of wsClients) {
|
|
3151
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
3152
|
+
ws.send(sequence);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
function relayHerdrWebSnapshot(snapshot) {
|
|
3156
|
+
const merged = mergeHerdrWebSnapshot(herdrWebHistory, snapshot, herdrWebScrollDirection, MAX_SCROLLBACK);
|
|
3157
|
+
herdrWebHistory = merged.state;
|
|
3158
|
+
herdrWebScrollDirection = null;
|
|
3159
|
+
scrollback = renderHerdrWebHistory(merged.state);
|
|
3160
|
+
const payload = `\x1b]1989;history;${merged.addedLines}\x07${scrollback}${herdrWebCursorSequence()}`;
|
|
3161
|
+
for (const ws of wsClients) {
|
|
3162
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
3163
|
+
ws.send(payload);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
function applyHerdrWebBindingResult(ws, result) {
|
|
3167
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
3168
|
+
return;
|
|
3169
|
+
const { backend: herdrWebBackend, initialSize, size } = result;
|
|
3170
|
+
if (initialSize) {
|
|
3171
|
+
ws.send(`\x1b]1989;follower;${initialSize.cols};${initialSize.rows}\x07`);
|
|
3172
|
+
}
|
|
3173
|
+
if (!herdrWebBackend || !size)
|
|
3174
|
+
return;
|
|
3175
|
+
for (const client of wsClients) {
|
|
3176
|
+
if (client.readyState === WebSocket.OPEN &&
|
|
3177
|
+
!herdrWebBackend.isWebTerminalOwner(client)) {
|
|
3178
|
+
client.send(`\x1b]1989;follower;${size.cols};${size.rows}\x07`);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* /restart replaces a managed Herdr backend without closing browser sockets.
|
|
3184
|
+
* Restore every viewer in connection order so the previous oldest surviving
|
|
3185
|
+
* owner remains authoritative, then re-apply its last browser grid.
|
|
3186
|
+
*/
|
|
3187
|
+
function restoreHerdrWebBindings() {
|
|
3188
|
+
if (!(backend instanceof HerdrBackend) || lastInitConfig?.adoptMode)
|
|
3189
|
+
return;
|
|
3190
|
+
for (const [ws, binding] of herdrWebBindings) {
|
|
3191
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
3192
|
+
binding.release();
|
|
3193
|
+
herdrWebBindings.delete(ws);
|
|
3194
|
+
continue;
|
|
3195
|
+
}
|
|
3196
|
+
applyHerdrWebBindingResult(ws, binding.restore());
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
function wireHerdrWebTerminalRelays(be) {
|
|
3200
|
+
be.onSnapshot(relayHerdrWebSnapshot);
|
|
3201
|
+
be.onWebTerminalCursor(relayHerdrWebCursor);
|
|
3202
|
+
}
|
|
2790
3203
|
function recentTerminalLogTail() {
|
|
2791
3204
|
const plain = stripAnsiForLog(tailChars(scrollback, CRASH_DIAGNOSTIC_RAW_MAX));
|
|
2792
3205
|
if (!plain)
|
|
@@ -2968,7 +3381,7 @@ function startScreenAnalyzer() {
|
|
|
2968
3381
|
onAnalyzing: () => { },
|
|
2969
3382
|
onTuiPrompt: (description, options, multiSelect) => {
|
|
2970
3383
|
tuiPromptBlocking = true;
|
|
2971
|
-
send({ type: 'tui_prompt', description, options, multiSelect, turnId: currentBotmuxTurnId });
|
|
3384
|
+
send({ type: 'tui_prompt', description, options, multiSelect, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
2972
3385
|
},
|
|
2973
3386
|
onTuiPromptResolved: (selectedText) => {
|
|
2974
3387
|
tuiPromptBlocking = false;
|
|
@@ -3254,6 +3667,78 @@ async function handleTuiKeys(keys, isFinal) {
|
|
|
3254
3667
|
}
|
|
3255
3668
|
log(`TUI keys: ${keys.join(' ')}${isFinal ? ' (final)' : ''}`);
|
|
3256
3669
|
}
|
|
3670
|
+
// 待注入的 TUI 命令队列。生命周期绑定当前 CLI 进程:killCli() 会清空它,
|
|
3671
|
+
// 防止 restart 后残留命令重放进新 CLI——新增清理状态时记得同步那里。
|
|
3672
|
+
// barrier=true 标记该注入携带 updateWorkingDir(cwd-move,如 /cd):markPromptReady
|
|
3673
|
+
// 里 barrier 注入必须先于本次 pending 用户消息落地,防止用户消息在「记录已是新
|
|
3674
|
+
// cwd、进程仍在旧 cwd」的窗口里被写进旧目录的 CLI。排队策略判定收敛到
|
|
3675
|
+
// core/inject-queue-policy.ts(shouldDeferUserFlush / shouldFlushInjectionsFirst)
|
|
3676
|
+
// 以获得可单测的纯函数单元——本文件只持有队列状态。
|
|
3677
|
+
const pendingInjections = [];
|
|
3678
|
+
let injectionFlushing = false;
|
|
3679
|
+
/** 排队注入一行 TUI 命令:idle(isPromptReady)时经 sendRawCommandLineSerially 敲入。 */
|
|
3680
|
+
async function flushPendingInjections() {
|
|
3681
|
+
// 不跨 restart 边界写入(与 flushPending 同款守卫):destroySession 异步期间
|
|
3682
|
+
// backend 可能仍指向旧 CLI。
|
|
3683
|
+
if (cliRestartInProgress)
|
|
3684
|
+
return;
|
|
3685
|
+
// 与其它 PTY 写入方的互斥判定收敛在 canStartInjectionFlush(纯函数,可单测):
|
|
3686
|
+
// - 用户消息 flush 持锁中(isFlushing):markPromptReady 没有 isFlushing 守卫,
|
|
3687
|
+
// 可能在 flush 中途被误 idle 触发(startup-command 的 quiescence 等待、慢
|
|
3688
|
+
// submit-verify 的 text→Enter 间隙都是多秒级窗口)——此时开始注入会把命令
|
|
3689
|
+
// 拼进已敲了半条用户消息的 composer。flushPending 反向检查 injectionFlushing,
|
|
3690
|
+
// 互斥必须对称才成立。
|
|
3691
|
+
// - /rename 原生同步在飞(sessionRenameInFlight)与字面命令行写入窗口
|
|
3692
|
+
// (commandLineWritesPending):raw-write 围栏,注入同为 raw 命令行写入方。
|
|
3693
|
+
// 被挡下的注入不丢:队列留存,竞争写入方结束后的下一次 markPromptReady 再踢。
|
|
3694
|
+
if (!canStartInjectionFlush({
|
|
3695
|
+
injectionFlushing,
|
|
3696
|
+
userFlushing: isFlushing,
|
|
3697
|
+
sessionRenameInFlight,
|
|
3698
|
+
commandLineWritesPending,
|
|
3699
|
+
bareShellLaunchBlocked,
|
|
3700
|
+
}))
|
|
3701
|
+
return;
|
|
3702
|
+
injectionFlushing = true;
|
|
3703
|
+
try {
|
|
3704
|
+
while (pendingInjections.length > 0 && backend && isPromptReady && !bareShellLaunchBlocked
|
|
3705
|
+
&& !sessionRenameInFlight) {
|
|
3706
|
+
// Mirror flushPending's one-shot launch-failure guard: when an injection is
|
|
3707
|
+
// the FIRST writer after a (re)spawn, flushPending may never have run
|
|
3708
|
+
// (pendingMessages empty ⇒ early return) so the bare-shell check must also
|
|
3709
|
+
// fire here before typing. Shares the same one-shot flag — whichever flush
|
|
3710
|
+
// path sends first runs the detection.
|
|
3711
|
+
if (!bareShellChecked) {
|
|
3712
|
+
bareShellChecked = true;
|
|
3713
|
+
if (detectBareShellLaunch())
|
|
3714
|
+
return; // finally{} releases the mutex; queue stays
|
|
3715
|
+
}
|
|
3716
|
+
const item = pendingInjections.shift();
|
|
3717
|
+
const cmd = item.command;
|
|
3718
|
+
isPromptReady = false;
|
|
3719
|
+
idleDetector?.reset();
|
|
3720
|
+
try {
|
|
3721
|
+
// Serially:与 startupCommands / raw_input / native-rename 共用同一条
|
|
3722
|
+
// 字面命令行写入互斥链(text → beat → Enter 窗口不被拼接)。
|
|
3723
|
+
await sendRawCommandLineSerially(backend, cmd);
|
|
3724
|
+
await awaitPtyQuiescence(STARTUP_CMD_QUIET_MS, STARTUP_CMD_CAP_MS);
|
|
3725
|
+
log(`Injected command: ${cmd}`);
|
|
3726
|
+
}
|
|
3727
|
+
catch (e) {
|
|
3728
|
+
log(`Inject command failed (${cmd}): ${e?.message ?? e}`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
finally {
|
|
3733
|
+
injectionFlushing = false;
|
|
3734
|
+
// 若注入期间有用户输入(消息 / raw input / rename)被 flushPending 的
|
|
3735
|
+
// injectionFlushing 守卫挡下、且此刻已重新 idle,补踢一次(flushPending
|
|
3736
|
+
// 自带全部守卫,最坏 no-op)。
|
|
3737
|
+
if (isPromptReady && (pendingMessages.length > 0 || pendingRawInputs.length > 0 || pendingSessionRename !== null)) {
|
|
3738
|
+
void flushPending();
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3257
3742
|
/**
|
|
3258
3743
|
* Handle atomic text-input: navigate to "Type something" (WITHOUT pressing Enter),
|
|
3259
3744
|
* then write text via cliAdapter (which adds its own Enter to submit).
|
|
@@ -3422,9 +3907,8 @@ function dismissAidenCodexUpdateDialog(data) {
|
|
|
3422
3907
|
// Codex App runner sends botmux control messages as OSC sequences so they do
|
|
3423
3908
|
// not pollute the visible terminal. Strip them before xterm rendering and
|
|
3424
3909
|
// translate them back into worker IPC.
|
|
3425
|
-
const CODEX_APP_OSC_PREFIX = '\x1b]777;botmux:';
|
|
3426
3910
|
const APP_RUNNER_OSC_CLI_IDS = new Set(['codex-app', 'mira', 'mir']);
|
|
3427
|
-
|
|
3911
|
+
const appRunnerControlDecoder = new RunnerControlDecoder();
|
|
3428
3912
|
let kiroSessionIdCaptureArmed = false;
|
|
3429
3913
|
let kiroSessionIdCaptureBuffer = '';
|
|
3430
3914
|
function decodeCodexAppPayload(payload) {
|
|
@@ -3450,20 +3934,45 @@ function handleCodexAppMarker(body) {
|
|
|
3450
3934
|
if (kind === 'final' && typeof payload.content === 'string') {
|
|
3451
3935
|
const startedAtMs = typeof payload.startedAtMs === 'number' ? payload.startedAtMs : undefined;
|
|
3452
3936
|
const completedAtMs = typeof payload.completedAtMs === 'number' ? payload.completedAtMs : Date.now();
|
|
3937
|
+
// Codex App keeps the app-server-generated id separately for diagnostics.
|
|
3938
|
+
// Routing must use its stable clientUserMessageId marker; legacy envelopes
|
|
3939
|
+
// intentionally omit it and fall back to the worker's frozen botmux turn.
|
|
3940
|
+
const identity = resolveCodexAppFinalTurnIdentity(payload, currentBotmuxTurnId, `${lastInitConfig?.cliId ?? 'app'}-${Date.now()}`);
|
|
3941
|
+
if (!identity.ok) {
|
|
3942
|
+
log(`${cliName()} rejected final marker with mismatched turn `
|
|
3943
|
+
+ `(marker=${identity.markerTurnId.substring(0, 12)}, `
|
|
3944
|
+
+ `current=${identity.currentBotmuxTurnId?.substring(0, 12) ?? '-'})`);
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
const { turnId, nativeTurnId } = identity;
|
|
3948
|
+
if (nativeTurnId && nativeTurnId !== turnId) {
|
|
3949
|
+
log(`${cliName()} native turn ${nativeTurnId.substring(0, 12)} mapped to botmux turn ${turnId.substring(0, 12)}`);
|
|
3950
|
+
}
|
|
3951
|
+
if (payload.dispatchAttempt !== undefined
|
|
3952
|
+
&& payload.dispatchAttempt !== currentBotmuxDispatchAttempt) {
|
|
3953
|
+
log(`${cliName()} rejected final marker with mismatched dispatch attempt `
|
|
3954
|
+
+ `(marker=${String(payload.dispatchAttempt)}, current=${currentBotmuxDispatchAttempt ?? '-'})`);
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
// Attempt authority is worker-owned. A runner marker may redundantly assert
|
|
3958
|
+
// equality for compatibility, but can never select another attempt.
|
|
3959
|
+
const dispatchAttempt = currentBotmuxDispatchAttempt;
|
|
3453
3960
|
if (startedAtMs !== undefined) {
|
|
3454
3961
|
const sentByModel = shouldSuppressBridgeEmit({ markTimeMs: startedAtMs, isLocal: false, finalText: payload.content }, completedAtMs + 5_001, readSendMarkers(), false);
|
|
3455
3962
|
if (sentByModel) {
|
|
3456
3963
|
log(`${cliName()} final_output suppressed (model already called botmux send)`);
|
|
3964
|
+
emitTurnTerminal(turnId, 'completed', undefined, dispatchAttempt);
|
|
3457
3965
|
return;
|
|
3458
3966
|
}
|
|
3459
3967
|
}
|
|
3460
|
-
const turnId = typeof payload.turnId === 'string' ? payload.turnId : (currentBotmuxTurnId ?? `${lastInitConfig?.cliId ?? 'app'}-${Date.now()}`);
|
|
3461
3968
|
send({
|
|
3462
3969
|
type: 'final_output',
|
|
3463
3970
|
content: payload.content,
|
|
3464
3971
|
lastUuid: turnId,
|
|
3465
3972
|
turnId,
|
|
3973
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
3466
3974
|
});
|
|
3975
|
+
emitTurnTerminal(turnId, 'completed', undefined, dispatchAttempt);
|
|
3467
3976
|
}
|
|
3468
3977
|
}
|
|
3469
3978
|
function maybeCaptureKiroSessionId(data) {
|
|
@@ -3478,36 +3987,7 @@ function maybeCaptureKiroSessionId(data) {
|
|
|
3478
3987
|
kiroSessionIdCaptureBuffer = '';
|
|
3479
3988
|
}
|
|
3480
3989
|
function splitCodexAppControl(data) {
|
|
3481
|
-
|
|
3482
|
-
return data;
|
|
3483
|
-
const input = codexAppOscPending + data;
|
|
3484
|
-
codexAppOscPending = '';
|
|
3485
|
-
let out = '';
|
|
3486
|
-
let cursor = 0;
|
|
3487
|
-
for (;;) {
|
|
3488
|
-
const start = input.indexOf(CODEX_APP_OSC_PREFIX, cursor);
|
|
3489
|
-
if (start < 0) {
|
|
3490
|
-
let tailStart = input.length;
|
|
3491
|
-
const tail = input.slice(cursor);
|
|
3492
|
-
for (let n = Math.min(CODEX_APP_OSC_PREFIX.length - 1, tail.length); n > 0; n--) {
|
|
3493
|
-
if (CODEX_APP_OSC_PREFIX.startsWith(tail.slice(tail.length - n))) {
|
|
3494
|
-
tailStart = input.length - n;
|
|
3495
|
-
break;
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
out += input.slice(cursor, tailStart);
|
|
3499
|
-
codexAppOscPending = input.slice(tailStart);
|
|
3500
|
-
return out;
|
|
3501
|
-
}
|
|
3502
|
-
out += input.slice(cursor, start);
|
|
3503
|
-
const end = input.indexOf('\x07', start + CODEX_APP_OSC_PREFIX.length);
|
|
3504
|
-
if (end < 0) {
|
|
3505
|
-
codexAppOscPending = input.slice(start);
|
|
3506
|
-
return out;
|
|
3507
|
-
}
|
|
3508
|
-
handleCodexAppMarker(input.slice(start + CODEX_APP_OSC_PREFIX.length, end));
|
|
3509
|
-
cursor = end + 1;
|
|
3510
|
-
}
|
|
3990
|
+
return appRunnerControlDecoder.push(data, APP_RUNNER_OSC_CLI_IDS.has(lastInitConfig?.cliId ?? ''), handleCodexAppMarker);
|
|
3511
3991
|
}
|
|
3512
3992
|
// ─── Prompt Detection ────────────────────────────────────────────────────────
|
|
3513
3993
|
function onPtyData(data) {
|
|
@@ -3522,7 +4002,7 @@ function onPtyData(data) {
|
|
|
3522
4002
|
// no relay needed. In non-tmux mode AND in pipe mode (adopt-bridge),
|
|
3523
4003
|
// broadcast through the shared scrollback so all connected web clients
|
|
3524
4004
|
// render the same byte stream.
|
|
3525
|
-
if (!isTmuxMode || isPipeMode) {
|
|
4005
|
+
if ((!isTmuxMode || isPipeMode) && !usesHerdrSnapshotWebHistory()) {
|
|
3526
4006
|
// Track alt-buffer state so we can restore it in the scrollback prefix.
|
|
3527
4007
|
// Scan for the *last* toggle in this chunk — that's the current state.
|
|
3528
4008
|
let lastToggleIdx = -1;
|
|
@@ -3583,6 +4063,12 @@ function onPtyData(data) {
|
|
|
3583
4063
|
lastPtyOutputAtMs = Date.now();
|
|
3584
4064
|
idleDetector?.feed(data);
|
|
3585
4065
|
}
|
|
4066
|
+
function releaseRawInputRestartGate() {
|
|
4067
|
+
if (!rawInputRestartGate)
|
|
4068
|
+
return;
|
|
4069
|
+
rawInputRestartGate = false;
|
|
4070
|
+
log('Replacement CLI prompt ready — releasing deferred passthrough commands');
|
|
4071
|
+
}
|
|
3586
4072
|
function markPromptReady() {
|
|
3587
4073
|
if (isPromptReady)
|
|
3588
4074
|
return; // guard against duplicate calls
|
|
@@ -3604,6 +4090,11 @@ function markPromptReady() {
|
|
|
3604
4090
|
}
|
|
3605
4091
|
isPromptReady = true;
|
|
3606
4092
|
clearSessionRenameInFlight();
|
|
4093
|
+
// An old backend can still report idle while its async teardown is running.
|
|
4094
|
+
// Only a prompt observed after the general restart fence drops may release
|
|
4095
|
+
// slash commands to the replacement generation.
|
|
4096
|
+
if (!cliRestartInProgress)
|
|
4097
|
+
releaseRawInputRestartGate();
|
|
3607
4098
|
// CLI 实际启动成功(回到 prompt):复位连续重启计数。
|
|
3608
4099
|
// 任何能到这一步的 spawn 都算"成功"——后续即便再崩溃(不是 resume 目标不存在
|
|
3609
4100
|
// 的问题),下一轮也该有新的 2 次重试预算,而不是被历史重启计数卡住。
|
|
@@ -3614,7 +4105,12 @@ function markPromptReady() {
|
|
|
3614
4105
|
// CLI is back at its prompt — every previously written input has been
|
|
3615
4106
|
// consumed, so nothing is in flight anymore. A later crash must not
|
|
3616
4107
|
// replay these.
|
|
3617
|
-
|
|
4108
|
+
// Screen-idle is sufficient for ordinary IM batching, but durable input is
|
|
4109
|
+
// cleared only by its exact turn_terminal. If this is a false idle and the
|
|
4110
|
+
// CLI exits, onCliExit still reports ambiguous while deliberately declining
|
|
4111
|
+
// worker-local replay of the durable attempt.
|
|
4112
|
+
if (!durableTurnInFlight)
|
|
4113
|
+
inflightInputs.onTurnComplete();
|
|
3618
4114
|
maybeEmitWorkflowTranscriptOutput();
|
|
3619
4115
|
if (awaitingFirstPrompt) {
|
|
3620
4116
|
awaitingFirstPrompt = false;
|
|
@@ -3628,9 +4124,23 @@ function markPromptReady() {
|
|
|
3628
4124
|
// (where the initial prompt is queued before the CLI becomes idle).
|
|
3629
4125
|
if (renderer && pendingMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null && !isFlushing) {
|
|
3630
4126
|
const { content } = renderer.snapshot();
|
|
3631
|
-
send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId });
|
|
4127
|
+
send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
4128
|
+
}
|
|
4129
|
+
// cwd-move 注入(barrier=true,如 /cd)必须先于本次 pending 用户消息落地:
|
|
4130
|
+
// 用户在 bot 发完「已切换角色」确认后立刻发下一条消息是最常见路径——若
|
|
4131
|
+
// flushPending 先跑,该消息会在 barrier 注入之前被写进旧 cwd 的 CLI(daemon
|
|
4132
|
+
// 记录已是新目录,实际执行仍在旧目录)。跳过本次 flushPending 不会饿死用户
|
|
4133
|
+
// 消息:flushPending 自身的 injectionFlushing 守卫会挡住并发写入,
|
|
4134
|
+
// flushPendingInjections 的 finally 会在注入完成、CLI 重新 idle 后补踢一次
|
|
4135
|
+
// flushPending 排空 pendingMessages。
|
|
4136
|
+
if (shouldFlushInjectionsFirst(pendingInjections)) {
|
|
4137
|
+
void flushPendingInjections();
|
|
4138
|
+
}
|
|
4139
|
+
else {
|
|
4140
|
+
flushPending();
|
|
4141
|
+
if (pendingInjections.length > 0)
|
|
4142
|
+
void flushPendingInjections();
|
|
3632
4143
|
}
|
|
3633
|
-
flushPending();
|
|
3634
4144
|
}
|
|
3635
4145
|
function persistCliSessionId(cliSessionId) {
|
|
3636
4146
|
if (!cliSessionId || !sessionId)
|
|
@@ -3709,20 +4219,25 @@ const SUBMIT_DEFERRED_RECHECK_MS = 20_000;
|
|
|
3709
4219
|
*
|
|
3710
4220
|
* `bridgeTurnId` is the BridgeTurnQueue mark created right before the
|
|
3711
4221
|
* failing writeInput. When the deferred recheck conclusively fails (= no
|
|
3712
|
-
* jsonl line will ever match this fingerprint), we drop
|
|
3713
|
-
* it would keep `maybeSwitchBridgeJsonl` doing
|
|
3714
|
-
* poll tick for a fingerprint that's permanently
|
|
3715
|
-
* this whole patch series is fixing. */
|
|
3716
|
-
function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn()) {
|
|
4222
|
+
* jsonl line will ever match this fingerprint), we drop that exact dispatch
|
|
4223
|
+
* attempt's mark — leaving it would keep `maybeSwitchBridgeJsonl` doing
|
|
4224
|
+
* full-directory scans every poll tick for a fingerprint that's permanently
|
|
4225
|
+
* dead, the 99% CPU bug this whole patch series is fixing. */
|
|
4226
|
+
function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn(), turnIdentity) {
|
|
3717
4227
|
const preview = msg.length > 60 ? msg.slice(0, 60) + '…' : msg;
|
|
4228
|
+
const emitDurableFailure = (errorCode) => {
|
|
4229
|
+
if (turnIdentity?.turnId && turnIdentity.dispatchAttempt !== undefined) {
|
|
4230
|
+
emitTurnTerminal(turnIdentity.turnId, 'failed', errorCode, turnIdentity.dispatchAttempt);
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
3718
4233
|
const dropBridgeMark = () => {
|
|
3719
4234
|
if (!bridgeTurnId)
|
|
3720
4235
|
return;
|
|
3721
|
-
const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId);
|
|
4236
|
+
const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId, turnIdentity?.dispatchAttempt);
|
|
3722
4237
|
if (dropped) {
|
|
3723
4238
|
if (dropped.contentFingerprint)
|
|
3724
4239
|
bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
|
|
3725
|
-
log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — rotation-fallback scan will stop spinning on this fingerprint.`);
|
|
4240
|
+
log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}, attempt=${turnIdentity?.dispatchAttempt ?? '-'}) — rotation-fallback scan will stop spinning on this fingerprint.`);
|
|
3726
4241
|
}
|
|
3727
4242
|
};
|
|
3728
4243
|
if (failureReason) {
|
|
@@ -3734,11 +4249,14 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
3734
4249
|
dropBridgeMark();
|
|
3735
4250
|
const reason = action.kind === 'notify-hard-failure' ? action.reason : failureReason;
|
|
3736
4251
|
log(`writeInput: submit impossible — notifying user immediately. reason="${reason}" preview="${preview}"`);
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4252
|
+
emitDurableFailure(`submit_impossible:${reason}`);
|
|
4253
|
+
if (turnIdentity?.dispatchAttempt === undefined) {
|
|
4254
|
+
send({
|
|
4255
|
+
type: 'user_notify',
|
|
4256
|
+
turnId: turnIdentity?.turnId ?? currentBotmuxTurnId,
|
|
4257
|
+
message: t('worker.submit_impossible', { cliName: cliName(), reason, preview }),
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
3742
4260
|
return;
|
|
3743
4261
|
}
|
|
3744
4262
|
const activityBaselineMs = Date.now();
|
|
@@ -3777,9 +4295,16 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
3777
4295
|
case 'suppress-usage-limit':
|
|
3778
4296
|
dropBridgeMark();
|
|
3779
4297
|
log(`Deferred recheck missing but usage limit was detected for this turn — suppressing submit warning. preview="${preview}"`);
|
|
4298
|
+
emitDurableFailure('submit_usage_limit');
|
|
3780
4299
|
return;
|
|
3781
4300
|
case 'suppress-active':
|
|
3782
4301
|
log(`Deferred recheck missing but later ${action.evidence} shows ${cliName()} is active — suppressing submit warning. preview="${preview}"`);
|
|
4302
|
+
// For a durable turn, activity is evidence of possible submission, not
|
|
4303
|
+
// a terminal result. Keep the arbiter closed and re-check until either
|
|
4304
|
+
// the transcript confirms it or a quiet interval proves it stuck.
|
|
4305
|
+
if (turnIdentity?.dispatchAttempt !== undefined) {
|
|
4306
|
+
scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity);
|
|
4307
|
+
}
|
|
3783
4308
|
return;
|
|
3784
4309
|
case 'notify-hard-failure':
|
|
3785
4310
|
// failureReason is handled synchronously above.
|
|
@@ -3789,11 +4314,14 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
3789
4314
|
}
|
|
3790
4315
|
dropBridgeMark();
|
|
3791
4316
|
log(`Deferred recheck still missing — notifying user. preview="${preview}"`);
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
4317
|
+
emitDurableFailure('submit_unconfirmed');
|
|
4318
|
+
if (turnIdentity?.dispatchAttempt === undefined) {
|
|
4319
|
+
send({
|
|
4320
|
+
type: 'user_notify',
|
|
4321
|
+
turnId: turnIdentity?.turnId ?? currentBotmuxTurnId,
|
|
4322
|
+
message: t('worker.submit_unconfirmed', { cliName: cliName(), secs: Math.round(SUBMIT_DEFERRED_RECHECK_MS / 1000), transcriptLabel, preview }),
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
3797
4325
|
}, SUBMIT_DEFERRED_RECHECK_MS);
|
|
3798
4326
|
}
|
|
3799
4327
|
/**
|
|
@@ -3856,7 +4384,7 @@ function detectBareShellLaunch() {
|
|
|
3856
4384
|
`可能原因:rc 文件启动过慢/报错,或 \`${cli}\` 的可执行文件不在 PATH 上(CLI 没找到)。\n` +
|
|
3857
4385
|
`建议:在 web 终端里手动敲一下启动命令看报什么错;确认 CLI 二进制能在 PATH 上找到;或精简 rc 启动逻辑后重启 daemon 再试。`;
|
|
3858
4386
|
}
|
|
3859
|
-
send({ type: 'user_notify', turnId: currentBotmuxTurnId, message });
|
|
4387
|
+
send({ type: 'user_notify', turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt, message });
|
|
3860
4388
|
return true;
|
|
3861
4389
|
}
|
|
3862
4390
|
/**
|
|
@@ -3866,6 +4394,11 @@ function detectBareShellLaunch() {
|
|
|
3866
4394
|
* Messages pushed during a flush are picked up by the while loop.
|
|
3867
4395
|
*/
|
|
3868
4396
|
async function flushPending() {
|
|
4397
|
+
// destroySession() may be asynchronous while `backend` still references the
|
|
4398
|
+
// old CLI. Never let a new flush (including one triggered by the old
|
|
4399
|
+
// backend's idle/task-done callback) write across that restart boundary.
|
|
4400
|
+
if (cliRestartInProgress)
|
|
4401
|
+
return;
|
|
3869
4402
|
if (isFlushing)
|
|
3870
4403
|
return; // while loop in active flush will pick up new messages
|
|
3871
4404
|
if (!backend || !cliAdapter)
|
|
@@ -3876,8 +4409,25 @@ async function flushPending() {
|
|
|
3876
4409
|
return; // wait for /rename to finish before any user input
|
|
3877
4410
|
if (commandLineWritesPending > 0)
|
|
3878
4411
|
return; // do not splice into text -> Enter
|
|
4412
|
+
// 注入进行中不得并发写 PTY(用户消息留在 pendingMessages,注入完成后的下一次
|
|
4413
|
+
// markPromptReady 自然排空)——防止 type-ahead 插进注入的 text→Enter 窗口。
|
|
4414
|
+
if (injectionFlushing)
|
|
4415
|
+
return;
|
|
4416
|
+
// cwd 切换是 barrier:在它执行前,任何用户消息都不得写入(type-ahead 路径也会
|
|
4417
|
+
// 走到这里,因为 supportsTypeAhead 的 CLI 从 sendToPty 直接调 flushPending,
|
|
4418
|
+
// 完全绕过 markPromptReady 的 barrier-first 分支)——否则消息(含 raw input 与
|
|
4419
|
+
// rename)会落进旧 cwd 的 CLI。消息留在 pendingMessages,待 markPromptReady →
|
|
4420
|
+
// flushPendingInjections 消费完 barrier 后由其 finally 的 re-kick 排空。
|
|
4421
|
+
if (shouldDeferUserFlush(pendingInjections))
|
|
4422
|
+
return;
|
|
3879
4423
|
if (bareShellLaunchBlocked)
|
|
3880
4424
|
return; // launch failed into a bare shell — don't type prompts into it
|
|
4425
|
+
// Screen-idle is not a durable receipt. A permission/AskUser prompt can look
|
|
4426
|
+
// idle while the logical delivery is unresolved, so no following IM or
|
|
4427
|
+
// meeting turn may cross this boundary until an explicit terminal releases
|
|
4428
|
+
// the active attempt.
|
|
4429
|
+
if (!pendingInputMayFlush(durableTurnInFlight))
|
|
4430
|
+
return;
|
|
3881
4431
|
// Ready-gate: hold the FIRST prompt until the SessionStart hook fires a true-
|
|
3882
4432
|
// ready signal. A cjadk-style startup selector's ❯ falsely matches readyPattern
|
|
3883
4433
|
// and would otherwise eat this message. releaseReadyGate() re-invokes us once
|
|
@@ -3918,7 +4468,7 @@ async function flushPending() {
|
|
|
3918
4468
|
// Codex on codex-cli 0.134.0.
|
|
3919
4469
|
const claudeBridgeActive = !!bridgeJsonlPath && !lastInitConfig?.adoptMode;
|
|
3920
4470
|
const codexBridgeActive = codexBridgeFallbackActive();
|
|
3921
|
-
const typeAheadAllowed = cliAdapter.supportsTypeAhead;
|
|
4471
|
+
const typeAheadAllowed = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, pendingMessages[0]);
|
|
3922
4472
|
// Native /rename is an administrative command, not a steer/queued model
|
|
3923
4473
|
// message. It must wait for a real prompt even on type-ahead CLIs. Normal
|
|
3924
4474
|
// pending messages can still drain while busy; the rename stays queued.
|
|
@@ -4001,12 +4551,18 @@ async function flushPending() {
|
|
|
4001
4551
|
}
|
|
4002
4552
|
while (pendingMessages.length > 0 && backend && cliAdapter) {
|
|
4003
4553
|
const item = pendingMessages.shift();
|
|
4554
|
+
const durableWrite = item.dispatchAttempt !== undefined;
|
|
4555
|
+
if (durableWrite)
|
|
4556
|
+
durableTurnInFlight = true;
|
|
4004
4557
|
// Track as in-flight until the CLI returns to idle (markPromptReady).
|
|
4005
4558
|
// If the CLI exits first, onExit stashes these for re-queue on respawn.
|
|
4006
4559
|
inflightInputs.onWrite(item);
|
|
4007
4560
|
const msg = item.content;
|
|
4008
4561
|
currentBotmuxTurnId = item.turnId;
|
|
4562
|
+
currentBotmuxDispatchAttempt = item.dispatchAttempt;
|
|
4563
|
+
currentVcMeetingImTurnOrigin = item.vcMeetingImTurnOrigin;
|
|
4009
4564
|
writeCliPidMarker();
|
|
4565
|
+
publishSandboxRelayCapability();
|
|
4010
4566
|
const turnSeq = usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
4011
4567
|
// Bridge fallback: mark immediately before writeInput. Doing it here
|
|
4012
4568
|
// (instead of at enqueue time) means markTimeMs anchors to the
|
|
@@ -4021,14 +4577,27 @@ async function flushPending() {
|
|
|
4021
4577
|
bridgeIngest();
|
|
4022
4578
|
}
|
|
4023
4579
|
catch { /* best-effort */ }
|
|
4024
|
-
bridgeTurnId = bridgeMarkPendingTurn(msg, item.turnId);
|
|
4580
|
+
bridgeTurnId = bridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
|
|
4025
4581
|
}
|
|
4026
4582
|
else if (codexBridgeActive) {
|
|
4027
4583
|
// Codex mark works even before the rollout path is known: the
|
|
4028
4584
|
// queue is path-agnostic, and the late-attach below will start
|
|
4029
4585
|
// ingest from offset 0 so the user_message that lands shortly
|
|
4030
4586
|
// after still fingerprint-matches this turn.
|
|
4031
|
-
codexBridgeMarkPendingTurn(msg, item.turnId);
|
|
4587
|
+
codexBridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
|
|
4588
|
+
}
|
|
4589
|
+
if (durableWrite
|
|
4590
|
+
&& cliAdapter.reliableTurnTerminal === true
|
|
4591
|
+
&& lastInitConfig?.cliId === 'claude-code'
|
|
4592
|
+
&& (!item.turnId || !bridgeTurnId)) {
|
|
4593
|
+
// The advertised Claude contract is transcript-backed. If we cannot
|
|
4594
|
+
// establish the mark before touching the PTY, fail without submitting;
|
|
4595
|
+
// otherwise this real delivery would have no attributable final edge.
|
|
4596
|
+
if (item.turnId) {
|
|
4597
|
+
emitTurnTerminal(item.turnId, 'failed', 'terminal_bridge_unavailable', item.dispatchAttempt);
|
|
4598
|
+
}
|
|
4599
|
+
log('Refused durable Claude submit: transcript terminal bridge is unavailable');
|
|
4600
|
+
break;
|
|
4032
4601
|
}
|
|
4033
4602
|
log(`Writing to PTY (flush): "${msg.substring(0, 80)}"`);
|
|
4034
4603
|
// Defense in depth: TmuxPipeBackend's send methods no longer throw on a
|
|
@@ -4046,12 +4615,18 @@ async function flushPending() {
|
|
|
4046
4615
|
}
|
|
4047
4616
|
catch (err) {
|
|
4048
4617
|
log(`writeInput threw: ${err?.message ?? err}`);
|
|
4618
|
+
if (durableWrite && item.turnId) {
|
|
4619
|
+
// A throwing backend cannot prove whether bytes reached the CLI.
|
|
4620
|
+
// Reconcile as ambiguous (not a definitive failure) so the receiver
|
|
4621
|
+
// can replay the same frozen delivery behind the action gate.
|
|
4622
|
+
emitTurnTerminal(item.turnId, 'ambiguous', 'write_input_threw', item.dispatchAttempt);
|
|
4623
|
+
}
|
|
4049
4624
|
// If the CLI exited mid-write the backend already fired onExit (which
|
|
4050
4625
|
// nulled `backend` and told the user the CLI exited) — nothing more to
|
|
4051
4626
|
// do. Otherwise surface it as a submit failure so the message isn't
|
|
4052
4627
|
// silently lost.
|
|
4053
4628
|
if (backend)
|
|
4054
|
-
scheduleSubmitFailureNotify(msg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq);
|
|
4629
|
+
scheduleSubmitFailureNotify(msg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
|
|
4055
4630
|
break;
|
|
4056
4631
|
}
|
|
4057
4632
|
// Persist any sessionId the adapter observed via authoritative sources
|
|
@@ -4070,7 +4645,7 @@ async function flushPending() {
|
|
|
4070
4645
|
// nulled backend) the user already got a "CLI exited" notice; don't also
|
|
4071
4646
|
// nag that the submit wasn't confirmed.
|
|
4072
4647
|
if (result && result.submitted === false && backend) {
|
|
4073
|
-
scheduleSubmitFailureNotify(msg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq);
|
|
4648
|
+
scheduleSubmitFailureNotify(msg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq, item);
|
|
4074
4649
|
}
|
|
4075
4650
|
// All structured bridges now drain every pending message in one flush:
|
|
4076
4651
|
// Claude's BridgeTurnQueue handles `attachment(queued_command)` events
|
|
@@ -4078,22 +4653,44 @@ async function flushPending() {
|
|
|
4078
4653
|
// and writes the user event at dequeue time (transcript stays interleaved);
|
|
4079
4654
|
// Codex parks them too but steers them into the active turn (which can
|
|
4080
4655
|
// merge into one final), and CodexBridgeQueue's HOL-block-drop attributes
|
|
4081
|
-
// that correctly.
|
|
4082
|
-
//
|
|
4656
|
+
// that correctly. Durable receiver attempts are the exception: they and
|
|
4657
|
+
// adjacent IM turns wait for separate idle edges so neither can be
|
|
4658
|
+
// HOL-dropped or steered into the other.
|
|
4659
|
+
if (shouldStopPendingBatch(item, pendingMessages[0]))
|
|
4660
|
+
break;
|
|
4083
4661
|
}
|
|
4084
4662
|
}
|
|
4085
4663
|
finally {
|
|
4086
4664
|
isFlushing = false;
|
|
4087
4665
|
}
|
|
4088
4666
|
}
|
|
4089
|
-
function sendToPty(content, turnId,
|
|
4090
|
-
if (!
|
|
4667
|
+
function sendToPty(content, turnId, opts = {}) {
|
|
4668
|
+
if (!cliAdapter)
|
|
4669
|
+
return;
|
|
4670
|
+
const next = {
|
|
4671
|
+
content,
|
|
4672
|
+
turnId,
|
|
4673
|
+
...(opts.codexAppInput ? { codexAppInput: opts.codexAppInput } : {}),
|
|
4674
|
+
...(opts.dispatchAttempt !== undefined ? { dispatchAttempt: opts.dispatchAttempt } : {}),
|
|
4675
|
+
...(opts.vcMeetingImTurnOrigin
|
|
4676
|
+
? { vcMeetingImTurnOrigin: opts.vcMeetingImTurnOrigin }
|
|
4677
|
+
: {}),
|
|
4678
|
+
};
|
|
4679
|
+
// During an exact lease-fenced CLI restart the worker stays alive while the
|
|
4680
|
+
// backend is rebuilt. Preserve incoming attempt N+1 in the worker queue; the
|
|
4681
|
+
// old early-return silently dropped it after receiver had already persisted
|
|
4682
|
+
// DISPATCHED.
|
|
4683
|
+
if (cliRestartInProgress || !backend) {
|
|
4684
|
+
pendingMessages.push(next);
|
|
4685
|
+
log(`Queued message while CLI backend is restarting (${pendingMessages.length} pending)`);
|
|
4091
4686
|
return;
|
|
4092
|
-
|
|
4093
|
-
const
|
|
4687
|
+
}
|
|
4688
|
+
const supportsTypeAhead = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, next);
|
|
4689
|
+
const shouldMergeQueued = opts.dispatchAttempt === undefined && !durableTurnInFlight
|
|
4690
|
+
&& !isFlushing && !shouldWriteNow({
|
|
4094
4691
|
isPromptReady,
|
|
4095
4692
|
isFlushing,
|
|
4096
|
-
supportsTypeAhead
|
|
4693
|
+
supportsTypeAhead,
|
|
4097
4694
|
awaitingFirstPrompt,
|
|
4098
4695
|
}) && cliAdapter.mergeQueuedInput === true;
|
|
4099
4696
|
const mergedQueued = shouldMergeQueued && mergeQueuedCliInput(pendingMessages, next);
|
|
@@ -4126,7 +4723,9 @@ function sendToPty(content, turnId, codexAppInput) {
|
|
|
4126
4723
|
// type-ahead write is dropped (no input box yet) — markPromptReady()'s flush
|
|
4127
4724
|
// delivers queued messages instead. See input-gate.ts; this fixes dispatch's
|
|
4128
4725
|
// brief reaching Codex before its first idle and never landing.
|
|
4129
|
-
if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({
|
|
4726
|
+
if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({
|
|
4727
|
+
isPromptReady, isFlushing, supportsTypeAhead, awaitingFirstPrompt,
|
|
4728
|
+
})) {
|
|
4130
4729
|
if (!mergedQueued)
|
|
4131
4730
|
log(`Writing to PTY: "${content.substring(0, 80)}"`);
|
|
4132
4731
|
flushPending(); // fire-and-forget async; no-op if already flushing
|
|
@@ -4209,7 +4808,7 @@ function startScreenUpdates() {
|
|
|
4209
4808
|
const usageAware = usageLimitTracker.classify(content, status);
|
|
4210
4809
|
if (changed || usageAware.status !== lastSentStatus) {
|
|
4211
4810
|
lastSentStatus = usageAware.status;
|
|
4212
|
-
send({ type: 'screen_update', content, ...usageAware, turnId: currentBotmuxTurnId });
|
|
4811
|
+
send({ type: 'screen_update', content, ...usageAware, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
4213
4812
|
}
|
|
4214
4813
|
})();
|
|
4215
4814
|
}, SCREEN_UPDATE_INTERVAL_MS);
|
|
@@ -4424,8 +5023,12 @@ function setupAdoptIdleDetection(cfg, label) {
|
|
|
4424
5023
|
function seedBackendScreen(source, be) {
|
|
4425
5024
|
try {
|
|
4426
5025
|
const initial = be.captureCurrentScreen?.() ?? '';
|
|
4427
|
-
if (initial.length > 0)
|
|
5026
|
+
if (initial.length > 0) {
|
|
4428
5027
|
onPtyData(initial);
|
|
5028
|
+
if (be instanceof HerdrBackend) {
|
|
5029
|
+
relayHerdrWebSnapshot(initial);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
4429
5032
|
}
|
|
4430
5033
|
catch (err) {
|
|
4431
5034
|
log(`${source} captureCurrentScreen failed: ${err.message}`);
|
|
@@ -4549,6 +5152,7 @@ function spawnCli(cfg) {
|
|
|
4549
5152
|
rows,
|
|
4550
5153
|
env: process.env,
|
|
4551
5154
|
});
|
|
5155
|
+
wireHerdrWebTerminalRelays(herdrBe);
|
|
4552
5156
|
seedBackendScreen('herdr adopt', herdrBe);
|
|
4553
5157
|
setupAdoptTranscriptBridges(cfg);
|
|
4554
5158
|
setupAdoptInputAdapter(cfg);
|
|
@@ -4562,7 +5166,7 @@ function spawnCli(cfg) {
|
|
|
4562
5166
|
backend = null;
|
|
4563
5167
|
isPromptReady = false;
|
|
4564
5168
|
stopBridgeWatcher();
|
|
4565
|
-
send({ type: 'claude_exit', code, signal });
|
|
5169
|
+
send({ type: 'claude_exit', code, signal, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
4566
5170
|
});
|
|
4567
5171
|
awaitingFirstPrompt = false;
|
|
4568
5172
|
renderer?.markNewTurn();
|
|
@@ -4615,7 +5219,7 @@ function spawnCli(cfg) {
|
|
|
4615
5219
|
backend = null;
|
|
4616
5220
|
isPromptReady = false;
|
|
4617
5221
|
stopBridgeWatcher();
|
|
4618
|
-
send({ type: 'claude_exit', code, signal });
|
|
5222
|
+
send({ type: 'claude_exit', code, signal, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
4619
5223
|
});
|
|
4620
5224
|
awaitingFirstPrompt = false;
|
|
4621
5225
|
renderer?.markNewTurn();
|
|
@@ -4852,6 +5456,7 @@ function spawnCli(cfg) {
|
|
|
4852
5456
|
}
|
|
4853
5457
|
}
|
|
4854
5458
|
let isolationBotHome;
|
|
5459
|
+
let isolatedCodexHome;
|
|
4855
5460
|
if (willReadIsolate) {
|
|
4856
5461
|
isolationBotHome = ownBotHome;
|
|
4857
5462
|
const isClaudeFam = !!claudeDataDir;
|
|
@@ -4871,6 +5476,17 @@ function spawnCli(cfg) {
|
|
|
4871
5476
|
if (report.warning)
|
|
4872
5477
|
log(`[mcp-gateway] WARN ${report.warning}`);
|
|
4873
5478
|
}
|
|
5479
|
+
if (!isClaudeFam) {
|
|
5480
|
+
isolatedCodexHome = join(isolationBotHome, 'codex');
|
|
5481
|
+
// The CLI child and its dedicated worker must resolve the same Codex data
|
|
5482
|
+
// root. Adapter submit confirmation, resume fallback, and transcript bridge
|
|
5483
|
+
// discovery all run in the worker and consult process.env.CODEX_HOME
|
|
5484
|
+
// dynamically. Setting only childEnv made successful submissions look
|
|
5485
|
+
// unconfirmed because the worker kept watching the global ~/.codex history.
|
|
5486
|
+
// A worker owns one session, so this process-local redirect cannot leak
|
|
5487
|
+
// between bots or sessions.
|
|
5488
|
+
process.env.CODEX_HOME = isolatedCodexHome;
|
|
5489
|
+
}
|
|
4874
5490
|
}
|
|
4875
5491
|
// Predict reattach vs fresh BEFORE the resume pre-flight. On a persistent
|
|
4876
5492
|
// backend (tmux/herdr/zellij) a daemon restart finds the CLI process still
|
|
@@ -4893,11 +5509,11 @@ function spawnCli(cfg) {
|
|
|
4893
5509
|
// survive a daemon restart still running a CLI that may NOT be isolated (e.g.
|
|
4894
5510
|
// spawned before isolation was enabled, or by an old build). Isolation is only
|
|
4895
5511
|
// injectable at spawn time, so reattaching such a pane would silently run
|
|
4896
|
-
// unisolated. We stamp a
|
|
4897
|
-
//
|
|
4898
|
-
// so the probe below sees no pane and we cold-spawn fresh isolated. A
|
|
4899
|
-
//
|
|
4900
|
-
//
|
|
5512
|
+
// unisolated. We stamp a versioned policy marker when we spawn an isolated
|
|
5513
|
+
// pane; if an existing pane was not spawned under the current policy, kill it
|
|
5514
|
+
// so the probe below sees no pane and we cold-spawn fresh isolated. A current-
|
|
5515
|
+
// policy pane survives daemon restarts and suspend→resume safely because the
|
|
5516
|
+
// confinement remains attached to the live process.
|
|
4901
5517
|
if ((willReadIsolate || willWriteSandbox) && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
4902
5518
|
const paneLive = effectiveBackendType === 'tmux'
|
|
4903
5519
|
? TmuxBackend.hasSession(persistentSessionName)
|
|
@@ -4906,20 +5522,17 @@ function spawnCli(cfg) {
|
|
|
4906
5522
|
: HerdrBackend.hasSession(persistentSessionName);
|
|
4907
5523
|
if (paneLive) {
|
|
4908
5524
|
let marker = null;
|
|
4909
|
-
|
|
4910
|
-
marker = readFileSync(join(process.env.SESSION_DATA_DIR ?? '', 'read-isolation', `${cfg.sessionId}.boot`), 'utf-8');
|
|
4911
|
-
}
|
|
4912
|
-
catch { /* no marker → pane was spawned WITHOUT isolation */ }
|
|
5525
|
+
marker = readRegularHostFileNoFollow(join(process.env.SESSION_DATA_DIR ?? '', 'read-isolation', `${cfg.sessionId}.boot`));
|
|
4913
5526
|
if (isolatedPaneReattachSafe(marker)) {
|
|
4914
|
-
// Pane was spawned
|
|
4915
|
-
// process
|
|
5527
|
+
// Pane was spawned under the current isolation policy → still confined
|
|
5528
|
+
// on the running process across daemon restarts; warm reattach preserves
|
|
4916
5529
|
// resume/context + tmux idle-suspend.
|
|
4917
5530
|
log(`[read-isolation] reattaching isolated persistent pane (${cfg.sessionId})`);
|
|
4918
5531
|
}
|
|
4919
5532
|
else {
|
|
4920
|
-
//
|
|
4921
|
-
//
|
|
4922
|
-
log(`[read-isolation] unmarked persistent pane for ${cfg.sessionId}
|
|
5533
|
+
// Missing/legacy marker → pane predates the current policy and may retain
|
|
5534
|
+
// obsolete permissions. Kill it before publishing any new capability.
|
|
5535
|
+
log(`[read-isolation] legacy/unmarked persistent pane for ${cfg.sessionId} — killing + cold-spawning with current policy`);
|
|
4923
5536
|
try {
|
|
4924
5537
|
killPersistentSession(effectiveBackendType, persistentSessionName);
|
|
4925
5538
|
}
|
|
@@ -5048,6 +5661,10 @@ function spawnCli(cfg) {
|
|
|
5048
5661
|
hasStartupCommands: !!cfg.startupCommands?.length,
|
|
5049
5662
|
adoptMode: cfg.adoptMode === true,
|
|
5050
5663
|
passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
|
|
5664
|
+
}) || shouldDeferArgsBakedDurablePrompt({
|
|
5665
|
+
passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
|
|
5666
|
+
adoptMode: cfg.adoptMode === true,
|
|
5667
|
+
dispatchAttempt: cfg.dispatchAttempt,
|
|
5051
5668
|
}) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true);
|
|
5052
5669
|
kiroSessionIdCaptureArmed = cfg.cliId === 'kiro-cli' && !effectiveCliSessionId && !willReattachPersistent;
|
|
5053
5670
|
kiroSessionIdCaptureBuffer = '';
|
|
@@ -5064,10 +5681,22 @@ function spawnCli(cfg) {
|
|
|
5064
5681
|
readIsolationCtx = {
|
|
5065
5682
|
homeDir: homedir(),
|
|
5066
5683
|
botmuxHome: dirname(sessionDataDir),
|
|
5684
|
+
defaultBotmuxHome: join(homedir(), '.botmux'),
|
|
5067
5685
|
sessionDataDir,
|
|
5068
5686
|
currentAppId: cfg.larkAppId,
|
|
5687
|
+
currentSessionId: cfg.sessionId,
|
|
5069
5688
|
extraDenyPaths: cfg.readDenyExtraPaths,
|
|
5070
5689
|
};
|
|
5690
|
+
readIsolationOriginCapabilityFile = process.platform === 'darwin'
|
|
5691
|
+
? managedOriginCapabilityPath(sessionDataDir, cfg.sessionId)
|
|
5692
|
+
: null;
|
|
5693
|
+
// Replace any legacy child-planted symlink before Seatbelt canonicalizes
|
|
5694
|
+
// carve-outs. A failure is fatal: spawning with a missing/ambiguous
|
|
5695
|
+
// capability transport would either break all IPC or reopen an attacker-
|
|
5696
|
+
// selected realpath in the generated profile.
|
|
5697
|
+
if (readIsolationOriginCapabilityFile) {
|
|
5698
|
+
publishSandboxRelayCapability({ failClosed: true });
|
|
5699
|
+
}
|
|
5071
5700
|
// Write this bot's OWN send-credential into its BOT_HOME (the same per-bot
|
|
5072
5701
|
// private storage as its CLI data; siblings' BOT_HOMEs are whole-denied).
|
|
5073
5702
|
// `botmux send` reads the secret from here instead of bots.json — so the
|
|
@@ -5198,6 +5827,10 @@ function spawnCli(cfg) {
|
|
|
5198
5827
|
// approver allowlist against session.owner. Missing env → exit 2.
|
|
5199
5828
|
childEnv.BOTMUX_SESSION_ID = cfg.sessionId;
|
|
5200
5829
|
childEnv.BOTMUX_CHAT_ID = cfg.chatId;
|
|
5830
|
+
if (cfg.chatType)
|
|
5831
|
+
childEnv.BOTMUX_CHAT_TYPE = cfg.chatType;
|
|
5832
|
+
else
|
|
5833
|
+
delete childEnv.BOTMUX_CHAT_TYPE;
|
|
5201
5834
|
childEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
|
|
5202
5835
|
childEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
|
|
5203
5836
|
// NOTE: under read isolation `botmux send` gets this bot's secret from the worker-
|
|
@@ -5216,6 +5849,9 @@ function spawnCli(cfg) {
|
|
|
5216
5849
|
// Initial value only; long-lived panes get the latest turn via the JSON pid marker.
|
|
5217
5850
|
if (cfg.turnId)
|
|
5218
5851
|
childEnv.BOTMUX_TURN_ID = cfg.turnId;
|
|
5852
|
+
if (cfg.dispatchAttempt !== undefined) {
|
|
5853
|
+
childEnv.BOTMUX_DISPATCH_ATTEMPT = String(cfg.dispatchAttempt);
|
|
5854
|
+
}
|
|
5219
5855
|
if (injectClaudeSandbox)
|
|
5220
5856
|
childEnv.IS_SANDBOX = '1';
|
|
5221
5857
|
if (claudeResumeTokenThreshold)
|
|
@@ -5235,7 +5871,7 @@ function spawnCli(cfg) {
|
|
|
5235
5871
|
if (claudeDataDir)
|
|
5236
5872
|
childEnv.CLAUDE_CONFIG_DIR = claudeDataDir; // = <BOT_HOME>/claude
|
|
5237
5873
|
else
|
|
5238
|
-
childEnv.CODEX_HOME =
|
|
5874
|
+
childEnv.CODEX_HOME = isolatedCodexHome;
|
|
5239
5875
|
}
|
|
5240
5876
|
// Per-bot env (bots.json `env`): extra vars for THIS bot's CLI only — e.g.
|
|
5241
5877
|
// ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN to run a bot on GLM/a 3rd-party
|
|
@@ -5283,18 +5919,25 @@ function spawnCli(cfg) {
|
|
|
5283
5919
|
// readable ~/.botmux + system creds; the own slice is re-opened via carve-outs.
|
|
5284
5920
|
// For a WRITE-sandbox-only session these stay empty → reads wide open (Linux parity).
|
|
5285
5921
|
let denyPaths = [], denyRegexes = [], allowPaths = [], finalDenyPaths = [], traverseDirs = [];
|
|
5922
|
+
let protectedWrites;
|
|
5286
5923
|
if (readIsolationCtx) {
|
|
5287
5924
|
const profileCtx = {
|
|
5288
5925
|
...readIsolationCtx,
|
|
5289
5926
|
homeDir: canonical(readIsolationCtx.homeDir),
|
|
5290
5927
|
botmuxHome: canonical(readIsolationCtx.botmuxHome),
|
|
5928
|
+
defaultBotmuxHome: canonical(readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux')),
|
|
5291
5929
|
sessionDataDir: canonical(readIsolationCtx.sessionDataDir),
|
|
5292
5930
|
};
|
|
5293
5931
|
denyPaths = buildV2DenyPaths(profileCtx).map(canonical);
|
|
5294
5932
|
denyRegexes = buildV2DenyRegexes(profileCtx);
|
|
5295
5933
|
const carve = buildV2CarveOuts(profileCtx);
|
|
5934
|
+
const capabilityCarvePath = managedOriginCapabilityPath(profileCtx.sessionDataDir, profileCtx.currentSessionId);
|
|
5296
5935
|
allowPaths = [
|
|
5297
|
-
|
|
5936
|
+
// Never realpath the capability leaf: another legacy sandbox can race
|
|
5937
|
+
// a symlink into that shared host directory while multiple persistent
|
|
5938
|
+
// sessions recover. Keeping the exact path may fail closed on a raced
|
|
5939
|
+
// symlink, but can never turn its target into a Seatbelt allow rule.
|
|
5940
|
+
...carve.allowPaths.map(path => path === capabilityCarvePath ? path : canonical(path)),
|
|
5298
5941
|
...buildCliExecutableReadCarveOuts({
|
|
5299
5942
|
homeDir: profileCtx.homeDir,
|
|
5300
5943
|
cliId: cliAdapter.id,
|
|
@@ -5303,6 +5946,16 @@ function spawnCli(cfg) {
|
|
|
5303
5946
|
];
|
|
5304
5947
|
finalDenyPaths = carve.finalDenyPaths.map(canonical);
|
|
5305
5948
|
traverseDirs = carve.traverseDirs.map(canonical);
|
|
5949
|
+
protectedWrites = buildReadIsolationProtectedWriteRules(profileCtx, {
|
|
5950
|
+
// Keep lexical roots as well as their canonical targets. Canonical
|
|
5951
|
+
// rules protect contents reached through a symlink; lexical literals
|
|
5952
|
+
// prevent replacing the symlink/root directory entry itself.
|
|
5953
|
+
dashboardRoots: [
|
|
5954
|
+
readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux'),
|
|
5955
|
+
readIsolationCtx.botmuxHome,
|
|
5956
|
+
],
|
|
5957
|
+
sessionDataDirs: [readIsolationCtx.sessionDataDir],
|
|
5958
|
+
});
|
|
5306
5959
|
}
|
|
5307
5960
|
// WRITE rules — the macOS file-sandbox (Linux-bwrap twin). Realpath the writable
|
|
5308
5961
|
// zones + crown-jewel re-denies for the same symlink-safety reason as reads.
|
|
@@ -5319,21 +5972,21 @@ function spawnCli(cfg) {
|
|
|
5319
5972
|
const profileDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
|
|
5320
5973
|
mkdirSync(profileDir, { recursive: true });
|
|
5321
5974
|
const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
|
|
5322
|
-
|
|
5975
|
+
replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(denyPaths, allowPaths, finalDenyPaths, traverseDirs, denyRegexes, writeRules, protectedWrites));
|
|
5323
5976
|
seatbeltProfilePath = profilePath;
|
|
5324
5977
|
spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
|
|
5325
5978
|
spawnBin = 'sandbox-exec';
|
|
5326
5979
|
log(`[file-sandbox] wrapping ${cliAdapter.id} in Seatbelt (read-isolation=${!!readIsolationCtx}, write-sandbox=${!!writeRules}): sandbox-exec -f ${profilePath}`);
|
|
5327
5980
|
}
|
|
5328
5981
|
// Fresh sandboxed spawn on a persistent backend: stamp the pane with this daemon's
|
|
5329
|
-
// boot id so a later suspend→resume reattach can be trusted (see the stale-pane
|
|
5982
|
+
// policy version + boot id so a later suspend→resume reattach can be trusted (see the stale-pane
|
|
5330
5983
|
// guard above). Applies to read-isolation AND write-sandbox panes (both carry the
|
|
5331
5984
|
// Seatbelt confinement on the live process). pty needs no marker (never reattached).
|
|
5332
5985
|
if ((readIsolationCtx || writeSandboxRules) && persistentSessionName && !willReattachPersistent) {
|
|
5333
5986
|
try {
|
|
5334
5987
|
const markerDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
|
|
5335
5988
|
mkdirSync(markerDir, { recursive: true });
|
|
5336
|
-
|
|
5989
|
+
replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? ''));
|
|
5337
5990
|
}
|
|
5338
5991
|
catch { /* non-fatal: worst case a same-lifetime reattach cold-spawns instead */ }
|
|
5339
5992
|
}
|
|
@@ -5373,6 +6026,7 @@ function spawnCli(cfg) {
|
|
|
5373
6026
|
...readIsolationCtx,
|
|
5374
6027
|
homeDir: canon(readIsolationCtx.homeDir),
|
|
5375
6028
|
botmuxHome: canon(readIsolationCtx.botmuxHome),
|
|
6029
|
+
defaultBotmuxHome: canon(readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux')),
|
|
5376
6030
|
sessionDataDir: canon(readIsolationCtx.sessionDataDir),
|
|
5377
6031
|
};
|
|
5378
6032
|
const ids = new Set();
|
|
@@ -5454,7 +6108,9 @@ function spawnCli(cfg) {
|
|
|
5454
6108
|
catch { /* */ }
|
|
5455
6109
|
}
|
|
5456
6110
|
sandboxCleanup = att.cleanup;
|
|
5457
|
-
|
|
6111
|
+
sandboxRelayOutbox = att.outbox;
|
|
6112
|
+
sandboxStopWatcher = startOutboxWatcher(att.outbox, childEnv, cfg.sessionId, { authorize: authorizeManagedSend });
|
|
6113
|
+
publishSandboxRelayCapability();
|
|
5458
6114
|
log(`Sandbox REATTACH (${cfg.cliId}): live pane CLI kept, re-wired outbox=${att.outbox} (no remount)`);
|
|
5459
6115
|
}
|
|
5460
6116
|
else {
|
|
@@ -5477,7 +6133,15 @@ function spawnCli(cfg) {
|
|
|
5477
6133
|
// sensitive set, keep the own BOT_HOME real+writable (authPaths), and re-
|
|
5478
6134
|
// expose the own attachments bucket read-only after the masks (readonlyRoots).
|
|
5479
6135
|
hidePaths: [...(cfg.sandboxHidePaths ?? []), ...(readIsoLinuxMasks?.hidePaths ?? [])],
|
|
5480
|
-
authPaths: [...(cliAdapter.authPaths ?? [])
|
|
6136
|
+
authPaths: [...(cliAdapter.authPaths ?? [])],
|
|
6137
|
+
// BOTMUX_HOME is credential-masked wholesale. Re-open only this
|
|
6138
|
+
// bot's daemon-derived private CLI home after that parent mask, then
|
|
6139
|
+
// re-mask its send credential so an untrusted receiver cannot bypass
|
|
6140
|
+
// the host-authorized relay with a direct Lark API call.
|
|
6141
|
+
trustedWritablePaths: readIsoLinuxMasks?.ownReadWritePaths ?? [],
|
|
6142
|
+
finalHidePaths: readIsoLinuxMasks
|
|
6143
|
+
? [sendCredFilePath(dataDir, cfg.larkAppId)]
|
|
6144
|
+
: [],
|
|
5481
6145
|
extraExecPaths: cliAdapter.sandboxExtraExecPaths?.(),
|
|
5482
6146
|
readonlyRoots: [...(cfg.skillReadonlyRoots ?? []), ...(readIsoLinuxMasks?.ownReadOnlyPaths ?? [])],
|
|
5483
6147
|
userReadonlyPaths: cfg.sandboxReadonlyPaths ?? [],
|
|
@@ -5503,8 +6167,10 @@ function spawnCli(cfg) {
|
|
|
5503
6167
|
catch { /* */ }
|
|
5504
6168
|
}
|
|
5505
6169
|
sandboxCleanup = sbx.cleanup;
|
|
6170
|
+
sandboxRelayOutbox = sbx.outbox;
|
|
5506
6171
|
// session-id is FORCED here so a relayed send can't target another session.
|
|
5507
|
-
sandboxStopWatcher = startOutboxWatcher(sbx.outbox, childEnv, cfg.sessionId);
|
|
6172
|
+
sandboxStopWatcher = startOutboxWatcher(sbx.outbox, childEnv, cfg.sessionId, { authorize: authorizeManagedSend });
|
|
6173
|
+
publishSandboxRelayCapability();
|
|
5508
6174
|
log(`Sandbox ON (${cfg.cliId}): upper=${sbx.workDir} outbox=${sbx.outbox}`);
|
|
5509
6175
|
}
|
|
5510
6176
|
else {
|
|
@@ -5862,6 +6528,7 @@ function spawnCli(cfg) {
|
|
|
5862
6528
|
});
|
|
5863
6529
|
}
|
|
5864
6530
|
backend.onData(onPtyData);
|
|
6531
|
+
const observedBackend = backend;
|
|
5865
6532
|
backend.onAccessUrl?.((url) => {
|
|
5866
6533
|
send({ type: 'riff_access_url', accessUrl: url });
|
|
5867
6534
|
});
|
|
@@ -5877,8 +6544,57 @@ function spawnCli(cfg) {
|
|
|
5877
6544
|
backend.onTaskId?.((taskId) => {
|
|
5878
6545
|
send({ type: 'riff_task_id', taskId });
|
|
5879
6546
|
});
|
|
6547
|
+
if (backend instanceof HerdrBackend) {
|
|
6548
|
+
wireHerdrWebTerminalRelays(backend);
|
|
6549
|
+
restoreHerdrWebBindings();
|
|
6550
|
+
}
|
|
5880
6551
|
backend.onExit((code, signal) => {
|
|
6552
|
+
const intentionalRestart = intentionalRestartBackend === observedBackend;
|
|
6553
|
+
if (intentionalRestart)
|
|
6554
|
+
intentionalRestartBackend = null;
|
|
6555
|
+
// destroySession()/kill() may report exit after the 500ms replacement spawn
|
|
6556
|
+
// has already installed a fresh backend. That callback belongs to the old
|
|
6557
|
+
// generation: apart from consuming its intentional-restart marker, it must
|
|
6558
|
+
// not clear the replacement backend/durable HOL state or emit claude_exit.
|
|
6559
|
+
if (backend !== observedBackend) {
|
|
6560
|
+
log(`Ignored stale backend exit (code: ${code}, signal: ${signal})`);
|
|
6561
|
+
return;
|
|
6562
|
+
}
|
|
6563
|
+
const exitedTurnId = currentBotmuxTurnId;
|
|
6564
|
+
const exitedDispatchAttempt = currentBotmuxDispatchAttempt;
|
|
6565
|
+
// Fail closed as soon as this CLI generation ends. The Node worker may
|
|
6566
|
+
// stay alive for auto-restart/crash diagnostics, but an old sandbox relay
|
|
6567
|
+
// token or explicit IM origin must not remain usable in that interval.
|
|
6568
|
+
completeManagedTurnOriginRevocation(sandboxRelayCapability, exitedTurnId, exitedDispatchAttempt);
|
|
5881
6569
|
log(`${cliName()} exited (code: ${code}, signal: ${signal})`);
|
|
6570
|
+
if (cliAdapter?.reliableTurnTerminal === true
|
|
6571
|
+
&& exitedTurnId
|
|
6572
|
+
&& exitedDispatchAttempt !== undefined) {
|
|
6573
|
+
// The CLI may have durably appended its terminal record immediately
|
|
6574
|
+
// before exiting while fs.watch/the 1s poller is still queued. Drain it
|
|
6575
|
+
// synchronously before claiming `cli_exit`; otherwise ambiguous wins the
|
|
6576
|
+
// deduper and needlessly replays a turn that actually completed.
|
|
6577
|
+
if (bridgeJsonlPath) {
|
|
6578
|
+
try {
|
|
6579
|
+
bridgeDrainAndMaybeEmit();
|
|
6580
|
+
}
|
|
6581
|
+
catch (err) {
|
|
6582
|
+
log(`Bridge exit drain failed: ${err.message}`);
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
if (codexBridgeFallbackActive()) {
|
|
6586
|
+
try {
|
|
6587
|
+
codexBridgeDrainAndMaybeEmit({ signalIdle: false });
|
|
6588
|
+
}
|
|
6589
|
+
catch (err) {
|
|
6590
|
+
log(`Codex bridge exit drain failed: ${err.message}`);
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
// Race-safe with transcript final / submit-failure: the worker-local
|
|
6594
|
+
// terminal deduper lets exactly one status win for this attempt.
|
|
6595
|
+
emitTurnTerminal(exitedTurnId, 'ambiguous', 'cli_exit', exitedDispatchAttempt);
|
|
6596
|
+
}
|
|
6597
|
+
durableTurnInFlight = false;
|
|
5882
6598
|
const logTail = recentTerminalLogTail();
|
|
5883
6599
|
// Don't park a diagnostic shell here: most exits are immediately
|
|
5884
6600
|
// auto-restarted by the daemon, so an inline park would just be torn down
|
|
@@ -5893,13 +6609,24 @@ function spawnCli(cfg) {
|
|
|
5893
6609
|
// the CLI — codex crashing mid-submit never records them, and the fresh
|
|
5894
6610
|
// respawn comes up empty. Stash them so the next spawnCli re-queues and
|
|
5895
6611
|
// re-delivers.
|
|
5896
|
-
|
|
6612
|
+
// Durable meeting replay has exactly one owner: the receiver receipt loop.
|
|
6613
|
+
// onCliExit tells the daemon to mark that receipt ambiguous; replaying the
|
|
6614
|
+
// same attempt locally as ordinary carry-over would race hub attempt N+1
|
|
6615
|
+
// and execute the prompt twice.
|
|
6616
|
+
const stashed = inflightInputs.onCliExit(item => item.dispatchAttempt === undefined);
|
|
5897
6617
|
if (stashed > 0) {
|
|
5898
6618
|
log(`CLI exited with ${stashed} in-flight message(s); will re-queue after restart`);
|
|
5899
6619
|
}
|
|
5900
6620
|
backend = null;
|
|
5901
6621
|
isPromptReady = false;
|
|
5902
|
-
|
|
6622
|
+
currentBotmuxTurnId = undefined;
|
|
6623
|
+
currentBotmuxDispatchAttempt = undefined;
|
|
6624
|
+
if (intentionalRestart) {
|
|
6625
|
+
log('Suppressed claude_exit for intentional in-worker restart');
|
|
6626
|
+
}
|
|
6627
|
+
else {
|
|
6628
|
+
send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt });
|
|
6629
|
+
}
|
|
5903
6630
|
});
|
|
5904
6631
|
if (isPipeMode && backend && 'isReattach' in backend && backend.isReattach) {
|
|
5905
6632
|
log(`Re-attached to existing ${effectiveBackendType} session via pipe backend: ${persistentSessionName}`);
|
|
@@ -5955,7 +6682,7 @@ function spawnCli(cfg) {
|
|
|
5955
6682
|
markPromptReady();
|
|
5956
6683
|
}
|
|
5957
6684
|
}
|
|
5958
|
-
function killCli() {
|
|
6685
|
+
function killCli(opts = {}) {
|
|
5959
6686
|
destroyCrashDiagnosticTerminal('killCli');
|
|
5960
6687
|
idleDetector?.dispose();
|
|
5961
6688
|
idleDetector = null;
|
|
@@ -5989,6 +6716,7 @@ function killCli() {
|
|
|
5989
6716
|
catch { /* already gone */ }
|
|
5990
6717
|
cliPidMarker = null;
|
|
5991
6718
|
}
|
|
6719
|
+
completeManagedTurnOriginRevocation(sandboxRelayCapability, currentBotmuxTurnId, currentBotmuxDispatchAttempt);
|
|
5992
6720
|
// Stop the sandbox outbox watcher, then unmount the overlays + remove the
|
|
5993
6721
|
// per-session sandbox tree. In the overlay model the upper layer (the
|
|
5994
6722
|
// changeset) must be landed BEFORE close — `/land` runs while the session is
|
|
@@ -6000,6 +6728,11 @@ function killCli() {
|
|
|
6000
6728
|
catch { /* */ }
|
|
6001
6729
|
sandboxStopWatcher = null;
|
|
6002
6730
|
}
|
|
6731
|
+
sandboxRelayOutbox = null;
|
|
6732
|
+
readIsolationOriginCapabilityFile = null;
|
|
6733
|
+
currentBotmuxTurnId = undefined;
|
|
6734
|
+
currentBotmuxDispatchAttempt = undefined;
|
|
6735
|
+
currentVcMeetingImTurnOrigin = undefined;
|
|
6003
6736
|
if (sandboxCleanup) {
|
|
6004
6737
|
try {
|
|
6005
6738
|
sandboxCleanup();
|
|
@@ -6009,15 +6742,112 @@ function killCli() {
|
|
|
6009
6742
|
}
|
|
6010
6743
|
isPromptReady = false;
|
|
6011
6744
|
clearSessionRenameInFlight();
|
|
6012
|
-
|
|
6745
|
+
if (!opts.preservePending)
|
|
6746
|
+
pendingMessages.length = 0;
|
|
6013
6747
|
// pendingRawInputs contains only commands that were accepted but never typed
|
|
6014
|
-
// because /rename owned the TUI
|
|
6015
|
-
// in-flight raw command, replaying
|
|
6748
|
+
// because /rename owned the TUI or an owned CLI restart was already fenced.
|
|
6749
|
+
// Preserve them across restart; unlike an in-flight raw command, replaying
|
|
6750
|
+
// these cannot duplicate a side effect.
|
|
6751
|
+
// pendingInjections 则无条件清空(不随 preservePending 保留):barrier /cd 的
|
|
6752
|
+
// 目录变更已固化进 lastInitConfig.workingDir 与 daemon 记录,respawn 本身就落在
|
|
6753
|
+
// 新目录,重放 /cd 反而多余;非 barrier 注入(如 /compact)是 best-effort,
|
|
6754
|
+
// 不跨进程重放。
|
|
6755
|
+
pendingInjections.length = 0;
|
|
6016
6756
|
scrollback = '';
|
|
6757
|
+
herdrWebHistory = null;
|
|
6758
|
+
herdrWebScrollDirection = null;
|
|
6759
|
+
herdrWebCursor = null;
|
|
6017
6760
|
altBufferActive = false;
|
|
6018
6761
|
trustHandled = false;
|
|
6019
6762
|
codexUpdateDialogGuard.reset();
|
|
6020
|
-
|
|
6763
|
+
appRunnerControlDecoder.reset();
|
|
6764
|
+
}
|
|
6765
|
+
async function restartCliProcess(reason, opts = {}) {
|
|
6766
|
+
if (lastInitConfig?.adoptMode) {
|
|
6767
|
+
log(`Restart ignored in adopt mode (${reason})`);
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6770
|
+
// Set before touching destroySession(): remote teardown can await for many
|
|
6771
|
+
// seconds while the old backend object is still non-null and still capable
|
|
6772
|
+
// of firing idle/task-done callbacks. Inputs accepted in that interval must
|
|
6773
|
+
// remain queued until a replacement backend has been installed.
|
|
6774
|
+
cliRestartInProgress = true;
|
|
6775
|
+
rawInputRestartGate = true;
|
|
6776
|
+
// The Node worker stays alive through this restart, so the daemon will see
|
|
6777
|
+
// neither claude_exit nor worker-exit. Explicitly revoke the old turn's
|
|
6778
|
+
// authority now, before jitter/async teardown leaves a stale-send window.
|
|
6779
|
+
// This deliberately preserves currentBotmuxTurnId/dispatchAttempt for the
|
|
6780
|
+
// old backend's terminal attribution.
|
|
6781
|
+
revokeManagedTurnOriginForRestart();
|
|
6782
|
+
log(`Restart requested (${reason})`);
|
|
6783
|
+
// Tier-2 guard: 2nd consecutive in-worker restart forces FRESH. Tier-1
|
|
6784
|
+
// adapter probing is still re-run on every spawn.
|
|
6785
|
+
consecutiveInWorkerRestarts++;
|
|
6786
|
+
log(`Restart count: ${consecutiveInWorkerRestarts} (>=2 forces FRESH)`);
|
|
6787
|
+
const restart = async () => {
|
|
6788
|
+
try {
|
|
6789
|
+
tmuxRestartTimer = null;
|
|
6790
|
+
const restartingBackend = backend;
|
|
6791
|
+
if (restartingBackend)
|
|
6792
|
+
intentionalRestartBackend = restartingBackend;
|
|
6793
|
+
// Riff teardown cancels a remote task asynchronously. Wait for the cancel
|
|
6794
|
+
// (bounded) before respawning, otherwise the old and replacement tasks can
|
|
6795
|
+
// overlap and both emit into the same Lark thread. Local backends normally
|
|
6796
|
+
// return void here and continue synchronously. A synchronous throw is
|
|
6797
|
+
// handled by the same fatal restart path below, so it cannot leave the
|
|
6798
|
+
// input gate permanently armed.
|
|
6799
|
+
const teardown = restartingBackend?.destroySession?.();
|
|
6800
|
+
if (teardown && typeof teardown.then === 'function') {
|
|
6801
|
+
try {
|
|
6802
|
+
await Promise.race([
|
|
6803
|
+
teardown,
|
|
6804
|
+
new Promise(resolve => setTimeout(resolve, 22_000)),
|
|
6805
|
+
]);
|
|
6806
|
+
}
|
|
6807
|
+
catch { /* destroySession logs its own failure details */ }
|
|
6808
|
+
}
|
|
6809
|
+
killCli({ preservePending: opts.preservePending });
|
|
6810
|
+
awaitingFirstPrompt = true;
|
|
6811
|
+
setTimeout(async () => {
|
|
6812
|
+
if (lastInitConfig) {
|
|
6813
|
+
startScreenUpdates();
|
|
6814
|
+
startScreenAnalyzer();
|
|
6815
|
+
try {
|
|
6816
|
+
spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
|
|
6817
|
+
}
|
|
6818
|
+
catch (err) {
|
|
6819
|
+
cliRestartInProgress = false;
|
|
6820
|
+
await sendFatalWorkerErrorAndExit(err);
|
|
6821
|
+
return;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
cliRestartInProgress = false;
|
|
6825
|
+
// Riff marks itself prompt-ready inside spawnCli(); that early flush is
|
|
6826
|
+
// intentionally held by the restart gate above. Release its raw-input
|
|
6827
|
+
// fence now; other backends keep it until their later markPromptReady().
|
|
6828
|
+
if (effectiveBackendType === 'riff' && isPromptReady)
|
|
6829
|
+
releaseRawInputRestartGate();
|
|
6830
|
+
void flushPending();
|
|
6831
|
+
}, 500);
|
|
6832
|
+
}
|
|
6833
|
+
catch (err) {
|
|
6834
|
+
cliRestartInProgress = false;
|
|
6835
|
+
try {
|
|
6836
|
+
await sendFatalWorkerErrorAndExit(err);
|
|
6837
|
+
}
|
|
6838
|
+
catch { /* sendFatalWorkerErrorAndExit is already best-effort */ }
|
|
6839
|
+
}
|
|
6840
|
+
};
|
|
6841
|
+
if (effectiveBackendType === 'tmux' && !opts.immediate) {
|
|
6842
|
+
const delayMs = tmuxRestartJitterMs(lastInitConfig?.sessionId ?? '', consecutiveInWorkerRestarts);
|
|
6843
|
+
log(`Staggering tmux teardown/restart by ${delayMs}ms to avoid shared-server probe storms`);
|
|
6844
|
+
if (tmuxRestartTimer)
|
|
6845
|
+
clearTimeout(tmuxRestartTimer);
|
|
6846
|
+
tmuxRestartTimer = setTimeout(() => { void restart(); }, delayMs);
|
|
6847
|
+
}
|
|
6848
|
+
else {
|
|
6849
|
+
await restart();
|
|
6850
|
+
}
|
|
6021
6851
|
}
|
|
6022
6852
|
// ─── HTTP + WebSocket Server ─────────────────────────────────────────────────
|
|
6023
6853
|
function startWebServer(host, preferredPort) {
|
|
@@ -6030,17 +6860,47 @@ function startWebServer(host, preferredPort) {
|
|
|
6030
6860
|
res.end('Bad Request');
|
|
6031
6861
|
return;
|
|
6032
6862
|
}
|
|
6033
|
-
const
|
|
6034
|
-
|
|
6863
|
+
const { hasRead, hasWrite, platformReadonly } = resolveTerminalAccessForReq(req, url);
|
|
6864
|
+
if (!hasRead) {
|
|
6865
|
+
res.writeHead(403, {
|
|
6866
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
6867
|
+
'Cache-Control': 'no-store',
|
|
6868
|
+
});
|
|
6869
|
+
res.end('Forbidden');
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
6035
6872
|
const loginHdr = req.headers['x-botmux-login-url'];
|
|
6036
6873
|
const loginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr) ? loginHdr : '';
|
|
6874
|
+
// Herdr snapshots contain screen cells but not terminal mode state. On a
|
|
6875
|
+
// refreshed page an alt-screen CLI would otherwise look like an empty
|
|
6876
|
+
// normal buffer until resize causes a redraw. Preserve that mode so
|
|
6877
|
+
// scroll gestures continue to target the CLI's own transcript.
|
|
6878
|
+
const forceRemoteScroll = effectiveBackendType === 'herdr' && cliAdapter?.altScreen === true;
|
|
6037
6879
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
6038
|
-
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl));
|
|
6880
|
+
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll));
|
|
6881
|
+
});
|
|
6882
|
+
wss = new WebSocketServer({
|
|
6883
|
+
server: httpServer,
|
|
6884
|
+
// Reject before the WebSocket handshake completes. Closing from the
|
|
6885
|
+
// `connection` callback is too late: an unauthenticated localhost scanner
|
|
6886
|
+
// briefly becomes a client and races the terminal history seed.
|
|
6887
|
+
verifyClient: ({ req }, done) => {
|
|
6888
|
+
const url = parseWorkerRequestUrl(req);
|
|
6889
|
+
if (!url) {
|
|
6890
|
+
done(false, 400, 'Bad Request');
|
|
6891
|
+
return;
|
|
6892
|
+
}
|
|
6893
|
+
if (!resolveTerminalAccessForReq(req, url).hasRead) {
|
|
6894
|
+
done(false, 403, 'Forbidden');
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
done(true);
|
|
6898
|
+
},
|
|
6039
6899
|
});
|
|
6040
|
-
wss = new WebSocketServer({ server: httpServer });
|
|
6041
6900
|
wss.on('connection', (ws, req) => {
|
|
6042
6901
|
wsClients.add(ws);
|
|
6043
|
-
//
|
|
6902
|
+
// Read access was already checked before the WebSocket handshake. Resolve
|
|
6903
|
+
// again here only to decide whether this client may write.
|
|
6044
6904
|
const url = parseWorkerRequestUrl(req);
|
|
6045
6905
|
if (!url) {
|
|
6046
6906
|
log(`Bad worker WS URL rejected: ${JSON.stringify(req.url ?? '')}`);
|
|
@@ -6048,8 +6908,7 @@ function startWebServer(host, preferredPort) {
|
|
|
6048
6908
|
ws.close(1008, 'Bad Request');
|
|
6049
6909
|
return;
|
|
6050
6910
|
}
|
|
6051
|
-
const
|
|
6052
|
-
const { hasWrite } = resolveTerminalWriteForReq(req, tokenMatches);
|
|
6911
|
+
const { hasWrite } = resolveTerminalAccessForReq(req, url);
|
|
6053
6912
|
if (hasWrite)
|
|
6054
6913
|
authedClients.add(ws);
|
|
6055
6914
|
log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
|
|
@@ -6078,7 +6937,13 @@ function startWebServer(host, preferredPort) {
|
|
|
6078
6937
|
const startAttach = (cols, rows) => {
|
|
6079
6938
|
if (cp)
|
|
6080
6939
|
return;
|
|
6081
|
-
|
|
6940
|
+
// Defense in depth: view-capability clients attach through tmux's
|
|
6941
|
+
// own read-only mode as well as the WebSocket input gate below.
|
|
6942
|
+
cp = pty.spawn('tmux', [
|
|
6943
|
+
'attach-session',
|
|
6944
|
+
...(!hasWrite ? ['-r'] : []),
|
|
6945
|
+
'-t', tmuxTarget,
|
|
6946
|
+
], {
|
|
6082
6947
|
name: 'xterm-256color',
|
|
6083
6948
|
cols,
|
|
6084
6949
|
rows,
|
|
@@ -6115,13 +6980,11 @@ function startWebServer(host, preferredPort) {
|
|
|
6115
6980
|
}
|
|
6116
6981
|
}
|
|
6117
6982
|
else if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
return;
|
|
6124
|
-
}
|
|
6983
|
+
// Mouse protocols carry clicks, releases, drags and wheel input;
|
|
6984
|
+
// a mouse-aware TUI may bind any of them to approvals/actions.
|
|
6985
|
+
// A view capability therefore forwards no input bytes at all.
|
|
6986
|
+
if (!authedClients.has(ws))
|
|
6987
|
+
return;
|
|
6125
6988
|
if (cp)
|
|
6126
6989
|
cp.write(msg.data);
|
|
6127
6990
|
else
|
|
@@ -6203,11 +7066,8 @@ function startWebServer(host, preferredPort) {
|
|
|
6203
7066
|
cp.resize(msg.cols, msg.rows);
|
|
6204
7067
|
}
|
|
6205
7068
|
else if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
6206
|
-
if (!authedClients.has(ws))
|
|
6207
|
-
|
|
6208
|
-
if (!/^\x1b\[([<M])/.test(msg.data))
|
|
6209
|
-
return;
|
|
6210
|
-
}
|
|
7069
|
+
if (!authedClients.has(ws))
|
|
7070
|
+
return;
|
|
6211
7071
|
if (cp)
|
|
6212
7072
|
cp.write(msg.data);
|
|
6213
7073
|
else
|
|
@@ -6235,6 +7095,12 @@ function startWebServer(host, preferredPort) {
|
|
|
6235
7095
|
}
|
|
6236
7096
|
else {
|
|
6237
7097
|
// ── Shared relay (PtyBackend OR tmux pipe mode) ──
|
|
7098
|
+
const herdrWebBinding = new HerdrWebTerminalBinding(ws, () => (backend instanceof HerdrBackend && !lastInitConfig?.adoptMode ? backend : null));
|
|
7099
|
+
herdrWebBindings.set(ws, herdrWebBinding);
|
|
7100
|
+
const initialHerdrSize = herdrWebBinding.sync().initialSize;
|
|
7101
|
+
if (initialHerdrSize) {
|
|
7102
|
+
ws.send(`\x1b]1989;follower;${initialHerdrSize.cols};${initialHerdrSize.rows}\x07`);
|
|
7103
|
+
}
|
|
6238
7104
|
// History seed: prefer tmux's authoritative capture-pane in pipe mode
|
|
6239
7105
|
// (clean grid + scrollback) over replaying the raw cumulative byte
|
|
6240
7106
|
// stream, which scrolls stale Ink redraw/spinner frames into scrollback
|
|
@@ -6252,29 +7118,37 @@ function startWebServer(host, preferredPort) {
|
|
|
6252
7118
|
if (sz && sz.cols > 0 && sz.rows > 0)
|
|
6253
7119
|
ws.send(`\x1b]1989;${sz.cols};${sz.rows}\x07`);
|
|
6254
7120
|
}
|
|
6255
|
-
const seed =
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
7121
|
+
const seed = usesHerdrSnapshotWebHistory() && scrollback.length > 0
|
|
7122
|
+
? scrollback
|
|
7123
|
+
: chooseWebTerminalSeed({
|
|
7124
|
+
canCapture: isPipeMode && isObserveBackend(backend),
|
|
7125
|
+
capture: () => backend.captureCurrentScreen(),
|
|
7126
|
+
scrollback,
|
|
7127
|
+
onError: log,
|
|
7128
|
+
});
|
|
6261
7129
|
if (seed.length > 0) {
|
|
6262
|
-
ws.send(seed);
|
|
7130
|
+
ws.send(seed + herdrWebCursorSequence());
|
|
6263
7131
|
}
|
|
6264
7132
|
ws.on('message', (raw) => {
|
|
6265
7133
|
try {
|
|
6266
7134
|
const msg = JSON.parse(String(raw));
|
|
6267
7135
|
if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
|
|
6268
|
-
|
|
7136
|
+
const result = herdrWebBinding.resize(msg.cols, msg.rows);
|
|
7137
|
+
applyHerdrWebBindingResult(ws, result);
|
|
7138
|
+
if (!result.backend) {
|
|
7139
|
+
backend?.resize(msg.cols, msg.rows);
|
|
7140
|
+
}
|
|
6269
7141
|
}
|
|
6270
7142
|
else if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
if (
|
|
6277
|
-
|
|
7143
|
+
// Mouse protocols can encode approvals/actions as well as wheel input.
|
|
7144
|
+
// A read-only view capability must never forward bytes to the backend.
|
|
7145
|
+
if (!authedClients.has(ws))
|
|
7146
|
+
return;
|
|
7147
|
+
if (usesHerdrSnapshotWebHistory()) {
|
|
7148
|
+
if (msg.data.includes('\x1b[<64;'))
|
|
7149
|
+
herdrWebScrollDirection = 'up';
|
|
7150
|
+
else if (msg.data.includes('\x1b[<65;'))
|
|
7151
|
+
herdrWebScrollDirection = 'down';
|
|
6278
7152
|
}
|
|
6279
7153
|
backend?.write(msg.data);
|
|
6280
7154
|
}
|
|
@@ -6283,6 +7157,11 @@ function startWebServer(host, preferredPort) {
|
|
|
6283
7157
|
});
|
|
6284
7158
|
ws.on('close', () => {
|
|
6285
7159
|
wsClients.delete(ws);
|
|
7160
|
+
herdrWebBindings.delete(ws);
|
|
7161
|
+
const promoted = herdrWebBinding.release();
|
|
7162
|
+
if (promoted?.readyState === WebSocket.OPEN) {
|
|
7163
|
+
promoted.send('\x1b]1989;owner\x07');
|
|
7164
|
+
}
|
|
6286
7165
|
});
|
|
6287
7166
|
}
|
|
6288
7167
|
});
|
|
@@ -6295,7 +7174,7 @@ function startWebServer(host, preferredPort) {
|
|
|
6295
7174
|
.then(resolve, reject);
|
|
6296
7175
|
});
|
|
6297
7176
|
}
|
|
6298
|
-
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '') {
|
|
7177
|
+
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false) {
|
|
6299
7178
|
const label = sessionId.substring(0, 8);
|
|
6300
7179
|
return `<!DOCTYPE html>
|
|
6301
7180
|
<html>
|
|
@@ -6307,8 +7186,8 @@ function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '') {
|
|
|
6307
7186
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
|
|
6308
7187
|
<style>
|
|
6309
7188
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
6310
|
-
html,body{height:100%;background:#1a1b26;overflow:hidden;overscroll-behavior:none}
|
|
6311
|
-
body{display:flex;flex-direction:column}
|
|
7189
|
+
html,body{width:100%;height:100%;background:#1a1b26;overflow:hidden;overscroll-behavior:none}
|
|
7190
|
+
body{display:flex;flex-direction:column;height:100vh;height:100dvh}
|
|
6312
7191
|
#safe-area-probe{position:fixed;visibility:hidden;pointer-events:none;
|
|
6313
7192
|
padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}
|
|
6314
7193
|
#toolbar-shell{--toolbar-scale:1;display:none;position:fixed;z-index:100;
|
|
@@ -6363,7 +7242,7 @@ body{display:flex;flex-direction:column}
|
|
|
6363
7242
|
background:rgba(36,40,59,0.74)!important;touch-action:none}
|
|
6364
7243
|
#toolbar.idle{opacity:.82}
|
|
6365
7244
|
@media(prefers-reduced-motion:reduce){#toolbar,#toolbar.collapsed{transition:opacity .12s linear}}
|
|
6366
|
-
#terminal{flex:1;min-height:0}
|
|
7245
|
+
#terminal{flex:1;min-width:0;min-height:0;width:100%;height:100%}
|
|
6367
7246
|
#terminal .xterm{height:100%}
|
|
6368
7247
|
/* Real scroll container is xterm's own viewport — kill iOS rubber-band bounce
|
|
6369
7248
|
and momentum here (not just on body), and reserve gestures for pinch-zoom so
|
|
@@ -6428,9 +7307,10 @@ ${loginUrl ? `<a id="login-banner" href="${loginUrl}" target="_top" rel="noopene
|
|
|
6428
7307
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-canvas@0/lib/addon-canvas.min.js"></script>
|
|
6429
7308
|
<script>
|
|
6430
7309
|
var isTouch='ontouchstart'in window||navigator.maxTouchPoints>0;
|
|
6431
|
-
if(isTouch){document.
|
|
7310
|
+
if(isTouch){document.body.classList.add('touch');}
|
|
6432
7311
|
var hasToken=${hasWrite};
|
|
6433
7312
|
var platformReadonly=${platformReadonly};
|
|
7313
|
+
var remoteScroll=${forceRemoteScroll};
|
|
6434
7314
|
if(!hasToken){
|
|
6435
7315
|
if(platformReadonly){var _lb=document.getElementById('login-banner');_lb.classList.add('show');}
|
|
6436
7316
|
else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
|
|
@@ -6536,9 +7416,9 @@ document.getElementById('terminal').addEventListener('contextmenu',function(e){e
|
|
|
6536
7416
|
var ws_=null,el=document.getElementById('status');
|
|
6537
7417
|
term.onData(function(d){
|
|
6538
7418
|
if(!hasToken){
|
|
6539
|
-
//
|
|
6540
|
-
//
|
|
6541
|
-
|
|
7419
|
+
// Mouse escape sequences are input too: a TUI can bind clicks or wheel
|
|
7420
|
+
// events to actions. View links never forward terminal bytes.
|
|
7421
|
+
_showReadonlyToast();return;
|
|
6542
7422
|
}
|
|
6543
7423
|
if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'input',data:d}));
|
|
6544
7424
|
});
|
|
@@ -6561,13 +7441,14 @@ function onViewportResize(){
|
|
|
6561
7441
|
}
|
|
6562
7442
|
window.addEventListener('resize',onViewportResize);
|
|
6563
7443
|
(function connect(){
|
|
6564
|
-
var t=new URLSearchParams(location.search).get('token')||'';
|
|
6565
7444
|
// Derive base from the current path so the WS connects to the same prefix the
|
|
6566
7445
|
// page was served under — works both directly (path '/') and behind the
|
|
6567
|
-
// per-daemon reverse proxy ('/s/{sessionId}').
|
|
7446
|
+
// per-daemon reverse proxy ('/s/{sessionId}'). Preserve the complete query:
|
|
7447
|
+
// write links carry token, while read-only links carry the distinct
|
|
7448
|
+
// viewToken capability. See terminal-proxy.ts.
|
|
6568
7449
|
var base=location.pathname.replace(/\\/+$/,'');
|
|
6569
7450
|
var proto=location.protocol==='https:'?'wss':'ws';
|
|
6570
|
-
var ws=new WebSocket(proto+'://'+location.host+base+'
|
|
7451
|
+
var ws=new WebSocket(proto+'://'+location.host+base+'/'+location.search);
|
|
6571
7452
|
ws_=ws;ws.binaryType='arraybuffer';
|
|
6572
7453
|
// Force a resize on every (re)connect: clear the dedup memory first. On
|
|
6573
7454
|
// reconnect the browser grid is usually unchanged, so without this the
|
|
@@ -6579,6 +7460,36 @@ window.addEventListener('resize',onViewportResize);
|
|
|
6579
7460
|
ws.onopen=function(){el.textContent='connected';el.className='ok';_lastC=_lastR=0;sendResize()};
|
|
6580
7461
|
ws.onmessage=function(e){
|
|
6581
7462
|
var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
|
|
7463
|
+
// Snapshot-aware Herdr history replaces the buffer instead of appending a
|
|
7464
|
+
// mostly-overlapping full screen. Preserve the reader's anchor when older
|
|
7465
|
+
// rows were prepended; otherwise preserve their current position/follow.
|
|
7466
|
+
var _hh=data.match(/^\x1b\]1989;history;([0-9]+)\x07/);
|
|
7467
|
+
if(_hh){
|
|
7468
|
+
var _ha=+_hh[1],_hb=term.buffer.active,_hy=_hb.viewportY,_hBottom=_hy===_hb.baseY;
|
|
7469
|
+
data=data.slice(_hh[0].length);_cancelInitialFollow();term.reset();term.clear();
|
|
7470
|
+
data='\\x1b[2J\\x1b[H'+data;
|
|
7471
|
+
term.write(data,function(){
|
|
7472
|
+
if(_ha>0)term.scrollToLine(_hy+_ha);
|
|
7473
|
+
else if(_hBottom)term.scrollToBottom();
|
|
7474
|
+
else term.scrollToLine(_hy);
|
|
7475
|
+
});
|
|
7476
|
+
return;
|
|
7477
|
+
}
|
|
7478
|
+
// Managed Herdr has one authoritative pane grid shared by every viewer.
|
|
7479
|
+
// Followers render at the owner's grid; a promoted owner re-fits to its
|
|
7480
|
+
// own viewport and reports the new size back to the worker.
|
|
7481
|
+
var _hf=data.match(/\\x1b\\]1989;follower;(\\d+);(\\d+)\\x07/);
|
|
7482
|
+
if(_hf){
|
|
7483
|
+
fixedSize=true;var _hc=+_hf[1],_hr=+_hf[2];
|
|
7484
|
+
if(_hc>0&&_hr>0){try{term.resize(_hc,_hr)}catch(ex){}_lastC=_hc;_lastR=_hr}
|
|
7485
|
+
data=data.replace(_hf[0],'');
|
|
7486
|
+
}
|
|
7487
|
+
var _ho=data.match(/\\x1b\\]1989;owner\\x07/);
|
|
7488
|
+
if(_ho){
|
|
7489
|
+
fixedSize=false;data=data.replace(_ho[0],'');
|
|
7490
|
+
try{fit.fit()}catch(ex){}
|
|
7491
|
+
_lastC=_lastR=0;sendResize();
|
|
7492
|
+
}
|
|
6582
7493
|
// botmux OSC 1989: pin the xterm to the adopted pane's fixed size (the pane
|
|
6583
7494
|
// can't be resized, so FitAddon-to-browser would wrap the snapshot lines).
|
|
6584
7495
|
var _fs=data.match(/\\x1b\\]1989;(\\d+);(\\d+)\\x07/);
|
|
@@ -6597,17 +7508,30 @@ window.addEventListener('resize',onViewportResize);
|
|
|
6597
7508
|
// tmux — their whole transcript is redrawn by the app inside the fixed alt-screen
|
|
6598
7509
|
// grid, so term.scrollLines() reveals nothing. In the alternate buffer we forward
|
|
6599
7510
|
// scrolling as SGR mouse-wheel events so the CLI scrolls its own transcript and
|
|
6600
|
-
// repaints
|
|
7511
|
+
// repaints. This is write-capability only; view links stay locally scrollable.
|
|
6601
7512
|
// Normal-buffer CLIs keep xterm's native scrollback scroll. Capture-phase +
|
|
6602
7513
|
// stopPropagation pre-empts xterm's own handler. Skipped for pure tmux/zellij
|
|
6603
7514
|
// ATTACH (gate), where the attach client owns scrolling via copy-mode.
|
|
6604
7515
|
//
|
|
6605
|
-
// Accumulate intended scroll DISTANCE (px) and emit one wheel tick per STEP px
|
|
6606
|
-
//
|
|
6607
|
-
//
|
|
6608
|
-
//
|
|
6609
|
-
|
|
6610
|
-
var
|
|
7516
|
+
// Accumulate intended scroll DISTANCE (px) and emit one wheel tick per STEP px.
|
|
7517
|
+
// A high-resolution trackpad emits dozens of wheel events for one gesture, so
|
|
7518
|
+
// cap the whole continuous burst — not each browser event — then require an idle
|
|
7519
|
+
// gap (or direction reversal) before loading the next history chunk.
|
|
7520
|
+
var _scrollAccum=0,_scrollBurstTicks=0,_scrollBurstDir=0,_scrollBurstT=0;
|
|
7521
|
+
var _SCROLL_STEP=33;var _SCROLL_BURST_MAX=6;var _SCROLL_BURST_IDLE_MS=250;
|
|
7522
|
+
function _endScrollBurst(){
|
|
7523
|
+
clearTimeout(_scrollBurstT);_scrollBurstT=0;
|
|
7524
|
+
_scrollAccum=0;_scrollBurstTicks=0;_scrollBurstDir=0;
|
|
7525
|
+
}
|
|
7526
|
+
// Snapshot-backed remote TUIs can still accumulate useful local xterm history.
|
|
7527
|
+
// Consume it first; request another remote chunk only when the user pushes past
|
|
7528
|
+
// the local top/bottom boundary in that direction.
|
|
7529
|
+
function _canScrollLocal(px){
|
|
7530
|
+
var b=term.buffer.active;
|
|
7531
|
+
if(b.type==='alternate'||!px)return false;
|
|
7532
|
+
if(!remoteScroll)return true;
|
|
7533
|
+
return px>0||b.viewportY>0;
|
|
7534
|
+
}
|
|
6611
7535
|
// Map a viewport pixel (clientX/Y) to a 1-based terminal cell "col;row", clamped to
|
|
6612
7536
|
// the grid. The forwarded SGR wheel event MUST carry the cell UNDER THE POINTER, the
|
|
6613
7537
|
// way a physical terminal reports it: zone-routed alt-screen TUIs — OpenCode (Bubble
|
|
@@ -6631,27 +7555,36 @@ function _cellAt(clientX,clientY){
|
|
|
6631
7555
|
return col+';'+row;
|
|
6632
7556
|
}
|
|
6633
7557
|
function _fwdScroll(px,coord){
|
|
6634
|
-
if(!ws_||ws_.readyState!==1)return;
|
|
7558
|
+
if(!hasToken||!ws_||ws_.readyState!==1||!px)return;
|
|
6635
7559
|
coord=coord||(((term.cols>>1)+1)+';'+((term.rows>>1)+1)); // never (1,1)
|
|
7560
|
+
var dir=px<0?-1:1;
|
|
7561
|
+
if(_scrollBurstDir&&dir!==_scrollBurstDir){_scrollAccum=0;_scrollBurstTicks=0;}
|
|
7562
|
+
_scrollBurstDir=dir;
|
|
7563
|
+
clearTimeout(_scrollBurstT);_scrollBurstT=setTimeout(_endScrollBurst,_SCROLL_BURST_IDLE_MS);
|
|
7564
|
+
if(_scrollBurstTicks>=_SCROLL_BURST_MAX)return;
|
|
6636
7565
|
_scrollAccum+=px;var data='',n=0;
|
|
6637
|
-
while(Math.abs(_scrollAccum)>=_SCROLL_STEP&&n<6){
|
|
7566
|
+
while(Math.abs(_scrollAccum)>=_SCROLL_STEP&&n<6&&_scrollBurstTicks<_SCROLL_BURST_MAX){
|
|
6638
7567
|
var up=_scrollAccum<0; // px<0 → wheel-up (history)
|
|
6639
7568
|
data+='\\x1b[<'+(up?64:65)+';'+coord+'M';
|
|
6640
|
-
_scrollAccum+=up?_SCROLL_STEP:-_SCROLL_STEP;n++;
|
|
7569
|
+
_scrollAccum+=up?_SCROLL_STEP:-_SCROLL_STEP;n++;_scrollBurstTicks++;
|
|
6641
7570
|
}
|
|
7571
|
+
if(_scrollBurstTicks>=_SCROLL_BURST_MAX)_scrollAccum=0;
|
|
6642
7572
|
if(data)ws_.send(JSON.stringify({type:'input',data:data}));
|
|
6643
7573
|
}
|
|
6644
7574
|
if(!${isTmuxMode && !isPipeMode}){
|
|
6645
7575
|
document.getElementById('terminal').addEventListener('wheel',function(e){
|
|
6646
|
-
|
|
7576
|
+
// Normalise deltaMode to px: line→~16px, page→~one screen.
|
|
7577
|
+
var px=e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*term.rows*16:e.deltaY;
|
|
7578
|
+
if(_canScrollLocal(px)){
|
|
6647
7579
|
// Normal buffer: xterm scrolls its own scrollback natively. In read-only a
|
|
6648
7580
|
// mouse-mode CLI could swallow the wheel, so drive scrollback directly.
|
|
6649
|
-
if(!hasToken){e.preventDefault();e.stopPropagation();term.scrollLines(
|
|
7581
|
+
if(!hasToken){e.preventDefault();e.stopPropagation();term.scrollLines(px>0?3:-3);}
|
|
6650
7582
|
return;
|
|
6651
7583
|
}
|
|
7584
|
+
if(!hasToken){
|
|
7585
|
+
e.preventDefault();e.stopPropagation();term.scrollLines(e.deltaY>0?3:-3);return;
|
|
7586
|
+
}
|
|
6652
7587
|
e.preventDefault();e.stopPropagation();
|
|
6653
|
-
// Normalise deltaMode to px: line→~16px, page→~one screen.
|
|
6654
|
-
var px=e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*term.rows*16:e.deltaY;
|
|
6655
7588
|
_fwdScroll(px,_cellAt(e.clientX,e.clientY)); // report the cell under the pointer
|
|
6656
7589
|
},{capture:true,passive:false});
|
|
6657
7590
|
}
|
|
@@ -6956,35 +7889,79 @@ if(isTouch&&hasToken){
|
|
|
6956
7889
|
_scheduleToolbarLayout();_wakeToolbar();
|
|
6957
7890
|
}
|
|
6958
7891
|
|
|
6959
|
-
// Single-finger touch scrolling: normal-buffer
|
|
6960
|
-
//
|
|
6961
|
-
//
|
|
6962
|
-
//
|
|
6963
|
-
//
|
|
6964
|
-
//
|
|
6965
|
-
// scrollTop. overscroll-behavior:none (see <style>) kills the iOS rubber-band.
|
|
7892
|
+
// Single-finger touch scrolling: drive normal-buffer scrollback explicitly.
|
|
7893
|
+
// Some embedded WebViews do not perform xterm/browser native touch scrolling
|
|
7894
|
+
// when touch-action is restricted for pinch zoom. Handling in capture phase and
|
|
7895
|
+
// stopping propagation also prevents xterm from double-driving the viewport.
|
|
7896
|
+
// Alt-screen CLIs have no xterm scrollback, so forward the drag as SGR wheel
|
|
7897
|
+
// events and let the CLI scroll its own transcript.
|
|
6966
7898
|
if(!${isTmuxMode && !isPipeMode}){
|
|
6967
7899
|
var _tTerm=document.getElementById('terminal');
|
|
7900
|
+
var _tViewport=document.querySelector('#terminal .xterm-viewport');
|
|
6968
7901
|
var _tLastY=null;
|
|
6969
7902
|
_tTerm.addEventListener('touchstart',function(e){
|
|
6970
7903
|
if(e.touches.length===1)_tLastY=e.touches[0].clientY;
|
|
6971
7904
|
},{capture:true,passive:true});
|
|
6972
7905
|
_tTerm.addEventListener('touchmove',function(e){
|
|
6973
|
-
//
|
|
6974
|
-
if(
|
|
7906
|
+
// View links never forward input; let xterm/browser handle local scrolling.
|
|
7907
|
+
if(!hasToken||_tLastY===null||e.touches.length!==1)return;
|
|
6975
7908
|
e.preventDefault();e.stopPropagation();
|
|
6976
7909
|
var y=e.touches[0].clientY;
|
|
7910
|
+
var px=_tLastY-y;
|
|
7911
|
+
if(_canScrollLocal(px)){
|
|
7912
|
+
if(_tViewport)_tViewport.scrollTop-=y-_tLastY;
|
|
7913
|
+
else term.scrollLines(y>_tLastY?-1:1);
|
|
7914
|
+
_tLastY=y;return;
|
|
7915
|
+
}
|
|
6977
7916
|
// finger drags down (y grows) → px<0 → scroll up (history); report the touched cell
|
|
6978
|
-
_fwdScroll(
|
|
7917
|
+
_fwdScroll(px,_cellAt(e.touches[0].clientX,y));
|
|
6979
7918
|
_tLastY=y;
|
|
6980
7919
|
},{capture:true,passive:false});
|
|
6981
|
-
_tTerm.addEventListener('touchend',function(){_tLastY=null;},{capture:true,passive:true});
|
|
7920
|
+
_tTerm.addEventListener('touchend',function(){_tLastY=null;_endScrollBurst()},{capture:true,passive:true});
|
|
7921
|
+
_tTerm.addEventListener('touchcancel',function(){_tLastY=null;_endScrollBurst()},{capture:true,passive:true});
|
|
6982
7922
|
}
|
|
6983
7923
|
</script>
|
|
6984
7924
|
</body>
|
|
6985
7925
|
</html>`;
|
|
6986
7926
|
}
|
|
6987
|
-
|
|
7927
|
+
const emittedTurnTerminals = new TurnTerminalDeduper();
|
|
7928
|
+
/** Report CLI processing completion independently from user-visible output.
|
|
7929
|
+
* Keep a bounded worker-local dedup set because transcript watchers and app
|
|
7930
|
+
* runners may surface the same final boundary more than once. */
|
|
7931
|
+
function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt) {
|
|
7932
|
+
if (!sessionId || !turnId)
|
|
7933
|
+
return;
|
|
7934
|
+
if (!emittedTurnTerminals.claim(sessionId, turnId, dispatchAttempt))
|
|
7935
|
+
return;
|
|
7936
|
+
if (status !== 'completed') {
|
|
7937
|
+
const dropped = codexBridgeQueue.dropPendingTurn(turnId, dispatchAttempt);
|
|
7938
|
+
if (dropped) {
|
|
7939
|
+
log(`Structured bridge retired terminal attempt turn=${turnId.slice(0, 12)} attempt=${dispatchAttempt ?? '-'} status=${status}`);
|
|
7940
|
+
}
|
|
7941
|
+
}
|
|
7942
|
+
// Revoke before publishing terminal. The daemon receives same-worker IPC in
|
|
7943
|
+
// order, and the worker-side relay becomes unusable synchronously.
|
|
7944
|
+
revokeManagedTurnOriginForTerminal(turnId, dispatchAttempt);
|
|
7945
|
+
send({
|
|
7946
|
+
type: 'turn_terminal',
|
|
7947
|
+
sessionId,
|
|
7948
|
+
turnId,
|
|
7949
|
+
status,
|
|
7950
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
7951
|
+
...(errorCode ? { errorCode } : {}),
|
|
7952
|
+
});
|
|
7953
|
+
if (terminalReleasesDurableTurn({ turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt }, { turnId, dispatchAttempt })) {
|
|
7954
|
+
// Durable replay ownership moves back to the receiver/hub at terminal.
|
|
7955
|
+
// Do not let a later CLI exit carry this already-settled attempt into the
|
|
7956
|
+
// fresh process behind the receiver's newer dispatch attempt.
|
|
7957
|
+
inflightInputs.onTurnComplete();
|
|
7958
|
+
durableTurnInFlight = false;
|
|
7959
|
+
// The terminal may be emitted from inside an active flush continuation
|
|
7960
|
+
// (for example a synchronous hard-submit failure). Wake after that
|
|
7961
|
+
// continuation releases the mutex so queued work cannot remain stranded.
|
|
7962
|
+
queueMicrotask(() => { void flushPending(); });
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
6988
7965
|
function workerIpcPayload(msg) {
|
|
6989
7966
|
return msg.type === 'final_output' && sessionId
|
|
6990
7967
|
? { ...msg, sessionId }
|
|
@@ -7021,7 +7998,7 @@ let fatalWorkerErrorPending = false;
|
|
|
7021
7998
|
* used both during init and after a previously-ready worker tries to recover
|
|
7022
7999
|
* from a stopped/crashed CLI, so those later paths cannot regress to a silent
|
|
7023
8000
|
* unhandled rejection/exception. */
|
|
7024
|
-
async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId) {
|
|
8001
|
+
async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId, dispatchAttempt = currentBotmuxDispatchAttempt) {
|
|
7025
8002
|
if (fatalWorkerErrorPending)
|
|
7026
8003
|
return;
|
|
7027
8004
|
fatalWorkerErrorPending = true;
|
|
@@ -7029,6 +8006,9 @@ async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId) {
|
|
|
7029
8006
|
type: 'error',
|
|
7030
8007
|
message: err instanceof Error ? err.message : String(err),
|
|
7031
8008
|
turnId,
|
|
8009
|
+
// Carry the durable attempt so the daemon can leave a meeting delivery
|
|
8010
|
+
// failure to the receipt/lease recovery path instead of replying out-of-band.
|
|
8011
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
7032
8012
|
});
|
|
7033
8013
|
process.exit(1);
|
|
7034
8014
|
}
|
|
@@ -7045,6 +8025,7 @@ process.on('message', async (raw) => {
|
|
|
7045
8025
|
return; // already initialized
|
|
7046
8026
|
lastInitConfig = msg;
|
|
7047
8027
|
sessionId = msg.sessionId;
|
|
8028
|
+
refreshTerminalViewToken();
|
|
7048
8029
|
if (msg.ownerOpenId)
|
|
7049
8030
|
process.env.__OWNER_OPEN_ID = msg.ownerOpenId;
|
|
7050
8031
|
// Pin this worker's i18n locale early so every t() call below resolves
|
|
@@ -7078,7 +8059,10 @@ process.on('message', async (raw) => {
|
|
|
7078
8059
|
try {
|
|
7079
8060
|
if (msg.turnId) {
|
|
7080
8061
|
currentBotmuxTurnId = msg.turnId;
|
|
8062
|
+
currentBotmuxDispatchAttempt = msg.dispatchAttempt;
|
|
8063
|
+
currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
|
|
7081
8064
|
writeCliPidMarker();
|
|
8065
|
+
publishSandboxRelayCapability();
|
|
7082
8066
|
}
|
|
7083
8067
|
let port = 0;
|
|
7084
8068
|
if (!isWorkflowWorker()) {
|
|
@@ -7111,6 +8095,10 @@ process.on('message', async (raw) => {
|
|
|
7111
8095
|
hasStartupCommands: !!msg.startupCommands?.length,
|
|
7112
8096
|
adoptMode: msg.adoptMode === true,
|
|
7113
8097
|
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
8098
|
+
}) || shouldDeferArgsBakedDurablePrompt({
|
|
8099
|
+
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
8100
|
+
adoptMode: msg.adoptMode === true,
|
|
8101
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
7114
8102
|
}) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true);
|
|
7115
8103
|
if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
|
|
7116
8104
|
// Args-baked first prompts (notably Pi) never pass through the normal
|
|
@@ -7119,12 +8107,14 @@ process.on('message', async (raw) => {
|
|
|
7119
8107
|
// Mark it here before the CLI starts processing; late-attach is fine
|
|
7120
8108
|
// because CodexBridgeQueue is path-agnostic until ingest discovers the
|
|
7121
8109
|
// transcript file.
|
|
7122
|
-
codexBridgeMarkPendingTurn(msg.prompt, msg.turnId);
|
|
8110
|
+
codexBridgeMarkPendingTurn(msg.prompt, msg.turnId, msg.dispatchAttempt);
|
|
7123
8111
|
}
|
|
7124
8112
|
if (msg.prompt && (!cliAdapter?.passesInitialPromptViaArgs || deferInitialPrompt)) {
|
|
7125
8113
|
pendingMessages.push({
|
|
7126
8114
|
content: msg.prompt,
|
|
7127
8115
|
turnId: msg.turnId,
|
|
8116
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
8117
|
+
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
7128
8118
|
codexAppInput: msg.promptCodexAppInput,
|
|
7129
8119
|
});
|
|
7130
8120
|
}
|
|
@@ -7136,7 +8126,14 @@ process.on('message', async (raw) => {
|
|
|
7136
8126
|
if (effectiveBackendType === 'riff' && pendingMessages.length > 0) {
|
|
7137
8127
|
flushPending();
|
|
7138
8128
|
}
|
|
7139
|
-
send({
|
|
8129
|
+
send({
|
|
8130
|
+
type: 'ready',
|
|
8131
|
+
port,
|
|
8132
|
+
token: writeToken,
|
|
8133
|
+
viewToken,
|
|
8134
|
+
turnId: currentBotmuxTurnId,
|
|
8135
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
8136
|
+
});
|
|
7140
8137
|
}
|
|
7141
8138
|
catch (err) {
|
|
7142
8139
|
await sendFatalWorkerErrorAndExit(err);
|
|
@@ -7161,8 +8158,6 @@ process.on('message', async (raw) => {
|
|
|
7161
8158
|
deferredPluginSkillCatalog = null;
|
|
7162
8159
|
log('Attached refreshed plugin Skill catalog to the first turn of this CLI generation');
|
|
7163
8160
|
}
|
|
7164
|
-
currentBotmuxTurnId = msg.turnId;
|
|
7165
|
-
writeCliPidMarker();
|
|
7166
8161
|
if (!backend && crashDiagnosticStopped && lastInitConfig && !lastInitConfig.adoptMode) {
|
|
7167
8162
|
log('Message received after crash-loop stop; retrying CLI start');
|
|
7168
8163
|
destroyCrashDiagnosticTerminal('retry after message');
|
|
@@ -7175,11 +8170,23 @@ process.on('message', async (raw) => {
|
|
|
7175
8170
|
spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
|
|
7176
8171
|
}
|
|
7177
8172
|
catch (err) {
|
|
7178
|
-
|
|
8173
|
+
// Pass the message's own attempt (not the stale currentBotmux* from a
|
|
8174
|
+
// prior IM turn) so a durable delivery relaunch failure carries the
|
|
8175
|
+
// right attribution for the daemon's receipt/lease gate.
|
|
8176
|
+
await sendFatalWorkerErrorAndExit(err, msg.turnId, msg.dispatchAttempt);
|
|
7179
8177
|
return;
|
|
7180
8178
|
}
|
|
7181
8179
|
}
|
|
7182
8180
|
if (lastInitConfig?.adoptMode) {
|
|
8181
|
+
// Adopt writes immediately below, so this turn really becomes current
|
|
8182
|
+
// now. Non-adopt messages update the marker only when flushPending
|
|
8183
|
+
// dequeues/writes them; doing it at IPC arrival lets a queued IM turn
|
|
8184
|
+
// steal the identity of a still-running durable delivery.
|
|
8185
|
+
currentBotmuxTurnId = msg.turnId;
|
|
8186
|
+
currentBotmuxDispatchAttempt = msg.dispatchAttempt;
|
|
8187
|
+
currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
|
|
8188
|
+
writeCliPidMarker();
|
|
8189
|
+
publishSandboxRelayCapability();
|
|
7183
8190
|
// Bridge mode: capture transcript baseline BEFORE writing to the pane,
|
|
7184
8191
|
// so any assistant uuids appended after this point are attributed to
|
|
7185
8192
|
// *this* Lark turn (not local user activity in the pane). Mark may
|
|
@@ -7190,7 +8197,7 @@ process.on('message', async (raw) => {
|
|
|
7190
8197
|
bridgeIngest();
|
|
7191
8198
|
}
|
|
7192
8199
|
catch { /* best effort */ }
|
|
7193
|
-
bridgeMarkPendingTurn(content, msg.turnId);
|
|
8200
|
+
bridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
7194
8201
|
}
|
|
7195
8202
|
else if (codexBridgeFallbackActive()) {
|
|
7196
8203
|
// Codex adopt: same idea, different bridge. ingest first so any
|
|
@@ -7202,7 +8209,7 @@ process.on('message', async (raw) => {
|
|
|
7202
8209
|
// before this IPC message is handled. Mark first so that preexisting
|
|
7203
8210
|
// current-line can still fingerprint-match instead of being marked
|
|
7204
8211
|
// seen as an unmatched event.
|
|
7205
|
-
codexBridgeMarkPendingTurn(content, msg.turnId);
|
|
8212
|
+
codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
7206
8213
|
try {
|
|
7207
8214
|
codexBridgeIngest();
|
|
7208
8215
|
}
|
|
@@ -7213,7 +8220,7 @@ process.on('message', async (raw) => {
|
|
|
7213
8220
|
codexBridgeIngest();
|
|
7214
8221
|
}
|
|
7215
8222
|
catch { /* best effort */ }
|
|
7216
|
-
codexBridgeMarkPendingTurn(content, msg.turnId);
|
|
8223
|
+
codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
|
|
7217
8224
|
}
|
|
7218
8225
|
}
|
|
7219
8226
|
// Adopt mode write:
|
|
@@ -7273,17 +8280,28 @@ process.on('message', async (raw) => {
|
|
|
7273
8280
|
// arrival. Marking now would race with a still-running previous
|
|
7274
8281
|
// turn whose `botmux send` could sneak its sentAtMs past this
|
|
7275
8282
|
// turn's markTimeMs and falsely suppress its fallback.
|
|
7276
|
-
sendToPty(content, msg.turnId,
|
|
8283
|
+
sendToPty(content, msg.turnId, {
|
|
8284
|
+
codexAppInput,
|
|
8285
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
8286
|
+
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
8287
|
+
});
|
|
7277
8288
|
}
|
|
7278
8289
|
break;
|
|
7279
8290
|
}
|
|
7280
8291
|
case 'raw_input': {
|
|
7281
8292
|
// Preserve legacy busy delivery (/btw and other steering commands). A
|
|
7282
|
-
// native /rename
|
|
7283
|
-
//
|
|
7284
|
-
|
|
8293
|
+
// native /rename and an owned CLI restart are the exceptions: never splice
|
|
8294
|
+
// into the rename UI, write through an old backend during async teardown,
|
|
8295
|
+
// or type into the replacement before its real prompt is ready.
|
|
8296
|
+
// TUI 注入是第三个例外:注入进行中(injectionFlushing——Serially 只互斥
|
|
8297
|
+
// text→Enter 短窗口,覆盖不了注入后的 quiescence 等待)或队列里有 cwd
|
|
8298
|
+
// barrier(shouldDeferUserFlush——/cd 未落地前任何用户输入都不得写入,
|
|
8299
|
+
// 否则 passthrough 会执行在旧 cwd 的 CLI 里)时入队。注入排空后由
|
|
8300
|
+
// flushPendingInjections 的 finally 补踢 flushPending 送达。
|
|
8301
|
+
if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight
|
|
8302
|
+
|| injectionFlushing || shouldDeferUserFlush(pendingInjections)) {
|
|
7285
8303
|
pendingRawInputs.push(msg);
|
|
7286
|
-
log(`Deferred passthrough slash command until
|
|
8304
|
+
log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
|
|
7287
8305
|
}
|
|
7288
8306
|
else {
|
|
7289
8307
|
await deliverRawInput(msg);
|
|
@@ -7310,71 +8328,99 @@ process.on('message', async (raw) => {
|
|
|
7310
8328
|
break;
|
|
7311
8329
|
}
|
|
7312
8330
|
case 'restart': {
|
|
7313
|
-
|
|
7314
|
-
|
|
8331
|
+
await restartCliProcess('daemon request', { preservePending: true });
|
|
8332
|
+
break;
|
|
8333
|
+
}
|
|
8334
|
+
case 'expire_durable_turn': {
|
|
8335
|
+
const acknowledge = (disposition) => send({
|
|
8336
|
+
type: 'durable_expiry_ready',
|
|
8337
|
+
sessionId,
|
|
8338
|
+
turnId: msg.turnId,
|
|
8339
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
8340
|
+
disposition,
|
|
8341
|
+
});
|
|
8342
|
+
const currentExact = durableTurnInFlight
|
|
8343
|
+
&& currentBotmuxTurnId === msg.turnId
|
|
8344
|
+
&& currentBotmuxDispatchAttempt === msg.dispatchAttempt;
|
|
8345
|
+
if (currentExact) {
|
|
8346
|
+
if (lastInitConfig?.adoptMode) {
|
|
8347
|
+
log('Refused durable expiry ACK for adopt-mode session');
|
|
8348
|
+
break;
|
|
8349
|
+
}
|
|
8350
|
+
log(`Expiring active durable turn=${msg.turnId.slice(0, 12)} attempt=${msg.dispatchAttempt}; `
|
|
8351
|
+
+ 'restarting CLI with queued follow-ups preserved');
|
|
8352
|
+
durableTurnInFlight = false;
|
|
8353
|
+
inflightInputs.onTurnComplete();
|
|
8354
|
+
// immediate=true bypasses tmux jitter. Await teardown (including Riff's
|
|
8355
|
+
// async remote cancellation) before ACKing so replay cannot overlap the
|
|
8356
|
+
// old owned CLI; the replacement spawn itself remains delayed by 500ms.
|
|
8357
|
+
await restartCliProcess('durable lease expiry', { immediate: true, preservePending: true });
|
|
8358
|
+
acknowledge('cli_fenced');
|
|
7315
8359
|
break;
|
|
7316
8360
|
}
|
|
7317
|
-
|
|
7318
|
-
//
|
|
7319
|
-
//
|
|
7320
|
-
//
|
|
7321
|
-
//
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
log(`Restart count: ${consecutiveInWorkerRestarts} (>=2 forces FRESH)`);
|
|
7329
|
-
// Must destroySession(), not kill(): for persistent backends (tmux/herdr)
|
|
7330
|
-
// kill() only detaches — the backing session + CLI process keep running,
|
|
7331
|
-
// so the resume:true spawnCli below would re-attach to the SAME live CLI
|
|
7332
|
-
// (selectSessionBackend reattaches whenever hasSession() is true) and the
|
|
7333
|
-
// process would never actually restart. destroySession() tears the session
|
|
7334
|
-
// down so the respawn starts a fresh CLI. (PTY has no destroySession, so
|
|
7335
|
-
// the ?. no-ops and killCli()'s kill() does the teardown.)
|
|
7336
|
-
const restart = async () => {
|
|
7337
|
-
tmuxRestartTimer = null;
|
|
7338
|
-
// riff:teardown=远端 task-cancel(异步)。必须等它完成再 spawn,否则
|
|
7339
|
-
// 旧任务与新任务并跑、双方都往话题里发消息。
|
|
7340
|
-
const t = backend?.destroySession?.();
|
|
7341
|
-
if (t && typeof t.then === 'function') {
|
|
7342
|
-
try {
|
|
7343
|
-
await Promise.race([t, new Promise((r) => setTimeout(r, 22_000))]);
|
|
7344
|
-
}
|
|
7345
|
-
catch { /* logged inside destroySession */ }
|
|
8361
|
+
// A receipt's lease begins at daemon dispatch claim, before worker IPC
|
|
8362
|
+
// necessarily reaches the PTY. It can therefore expire behind a long
|
|
8363
|
+
// ordinary IM turn. Remove that exact queued generation and ACK without
|
|
8364
|
+
// restarting the unrelated active turn; otherwise attempt N survives in
|
|
8365
|
+
// the queue and executes after the hub has already replayed N+1.
|
|
8366
|
+
let removedPending = 0;
|
|
8367
|
+
for (let i = pendingMessages.length - 1; i >= 0; i--) {
|
|
8368
|
+
const item = pendingMessages[i];
|
|
8369
|
+
if (item.turnId === msg.turnId && item.dispatchAttempt === msg.dispatchAttempt) {
|
|
8370
|
+
pendingMessages.splice(i, 1);
|
|
8371
|
+
removedPending++;
|
|
7346
8372
|
}
|
|
7347
|
-
killCli();
|
|
7348
|
-
awaitingFirstPrompt = true;
|
|
7349
|
-
setTimeout(async () => {
|
|
7350
|
-
if (lastInitConfig) {
|
|
7351
|
-
startScreenUpdates();
|
|
7352
|
-
startScreenAnalyzer();
|
|
7353
|
-
try {
|
|
7354
|
-
spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
|
|
7355
|
-
}
|
|
7356
|
-
catch (err) {
|
|
7357
|
-
await sendFatalWorkerErrorAndExit(err);
|
|
7358
|
-
}
|
|
7359
|
-
}
|
|
7360
|
-
}, 500);
|
|
7361
|
-
};
|
|
7362
|
-
if (effectiveBackendType === 'tmux') {
|
|
7363
|
-
const delayMs = tmuxRestartJitterMs(lastInitConfig?.sessionId ?? '', consecutiveInWorkerRestarts);
|
|
7364
|
-
log(`Staggering tmux teardown/restart by ${delayMs}ms to avoid shared-server probe storms`);
|
|
7365
|
-
if (tmuxRestartTimer)
|
|
7366
|
-
clearTimeout(tmuxRestartTimer);
|
|
7367
|
-
tmuxRestartTimer = setTimeout(restart, delayMs);
|
|
7368
8373
|
}
|
|
7369
|
-
|
|
7370
|
-
|
|
8374
|
+
if (removedPending > 0) {
|
|
8375
|
+
log(`Expired ${removedPending} queued durable input(s) turn=${msg.turnId.slice(0, 12)} `
|
|
8376
|
+
+ `attempt=${msg.dispatchAttempt}`);
|
|
8377
|
+
acknowledge('queued_removed');
|
|
8378
|
+
break;
|
|
7371
8379
|
}
|
|
8380
|
+
// Neither active nor queued means this worker cannot prove where the
|
|
8381
|
+
// attempt went. Deliberately withhold ACK so the daemon's short timeout
|
|
8382
|
+
// escalates to an owned-pane teardown before replay is admitted.
|
|
8383
|
+
log(`Cannot prove durable expiry turn=${msg.turnId.slice(0, 12)} `
|
|
8384
|
+
+ `attempt=${msg.dispatchAttempt}; withholding ACK for daemon fencing`);
|
|
8385
|
+
break;
|
|
8386
|
+
}
|
|
8387
|
+
case 'reset_ambiguous_receiver': {
|
|
8388
|
+
const receiver = sessionId ? sessionStore.getSession(sessionId)?.vcMeetingReceiver : undefined;
|
|
8389
|
+
if (!receiver) {
|
|
8390
|
+
log('Ignored boot recovery reset for a non-receiver session');
|
|
8391
|
+
break;
|
|
8392
|
+
}
|
|
8393
|
+
if (lastInitConfig?.adoptMode) {
|
|
8394
|
+
log('Refused boot recovery ACK for adopt-mode receiver identity');
|
|
8395
|
+
break;
|
|
8396
|
+
}
|
|
8397
|
+
log(`Boot recovery fencing ambiguous receiver turn=${msg.turnId.slice(0, 12)} `
|
|
8398
|
+
+ `attempt=${msg.dispatchAttempt}`);
|
|
8399
|
+
durableTurnInFlight = false;
|
|
8400
|
+
inflightInputs.onTurnComplete();
|
|
8401
|
+
await restartCliProcess('ambiguous receiver boot recovery', { immediate: true, preservePending: true });
|
|
8402
|
+
send({
|
|
8403
|
+
type: 'receiver_reset_ready',
|
|
8404
|
+
sessionId: sessionId,
|
|
8405
|
+
turnId: msg.turnId,
|
|
8406
|
+
dispatchAttempt: msg.dispatchAttempt,
|
|
8407
|
+
});
|
|
7372
8408
|
break;
|
|
7373
8409
|
}
|
|
7374
8410
|
case 'tui_keys': {
|
|
7375
8411
|
handleTuiKeys(msg.keys, msg.isFinal);
|
|
7376
8412
|
break;
|
|
7377
8413
|
}
|
|
8414
|
+
case 'inject_command': {
|
|
8415
|
+
// 会话内 /cd 移动后,worker 内部 respawn(claude_exit 自动重启 / IM /restart /
|
|
8416
|
+
// dashboard restart)必须收敛到新目录,而不是陈旧的 lastInitConfig.workingDir。
|
|
8417
|
+
if (msg.updateWorkingDir && lastInitConfig) {
|
|
8418
|
+
lastInitConfig.workingDir = msg.updateWorkingDir;
|
|
8419
|
+
}
|
|
8420
|
+
pendingInjections.push({ command: msg.command, barrier: !!msg.updateWorkingDir });
|
|
8421
|
+
void flushPendingInjections();
|
|
8422
|
+
break;
|
|
8423
|
+
}
|
|
7378
8424
|
case 'tui_text_input': {
|
|
7379
8425
|
handleTuiTextInput(msg.keys, msg.text);
|
|
7380
8426
|
break;
|
|
@@ -7473,6 +8519,7 @@ process.on('message', async (raw) => {
|
|
|
7473
8519
|
// forkWorker(resume=true) → a fresh `new-session --resume <cliSessionId>`
|
|
7474
8520
|
// that rebuilds context from the on-disk transcript (same path the daemon
|
|
7475
8521
|
// uses to recover sessions after a reboot kills the tmux server).
|
|
8522
|
+
revokeManagedTurnOriginForRestart();
|
|
7476
8523
|
try {
|
|
7477
8524
|
// riff:suspend 语义是「休眠待续」——绝不能 cancel 远端任务(血缘已持久化,
|
|
7478
8525
|
// 恢复时 follow-up 续上);只断流 detach。
|
|
@@ -7521,6 +8568,7 @@ function cleanup() {
|
|
|
7521
8568
|
for (const ws of wsClients)
|
|
7522
8569
|
ws.close();
|
|
7523
8570
|
wsClients.clear();
|
|
8571
|
+
herdrWebBindings.clear();
|
|
7524
8572
|
if (wss) {
|
|
7525
8573
|
wss.close();
|
|
7526
8574
|
wss = null;
|
|
@@ -7593,6 +8641,8 @@ function teardownSandboxBestEffort() {
|
|
|
7593
8641
|
}
|
|
7594
8642
|
catch { /* */ }
|
|
7595
8643
|
sandboxCleanup = null;
|
|
8644
|
+
unlinkManagedOriginCapabilityFiles();
|
|
8645
|
+
sandboxRelayCapability = null;
|
|
7596
8646
|
if (seatbeltProfilePath) {
|
|
7597
8647
|
try {
|
|
7598
8648
|
unlinkSync(seatbeltProfilePath);
|