botmux 3.13.0 → 3.14.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 +1 -1
- package/README.md +25 -1
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/adopt-route.d.ts +58 -0
- package/dist/adapters/adopt-route.d.ts.map +1 -1
- package/dist/adapters/adopt-route.js +136 -0
- package/dist/adapters/adopt-route.js.map +1 -1
- package/dist/adapters/backend/capabilities.js +1 -1
- package/dist/adapters/backend/capabilities.js.map +1 -1
- package/dist/adapters/backend/herdr-backend.d.ts +1 -1
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +2 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +97 -0
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +380 -66
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +8 -0
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +16 -2
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +12 -13
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +113 -53
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +4 -3
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +12 -9
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +2 -3
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +6 -13
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.d.ts +2 -1
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.js +6 -3
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zmx-backend.js +40 -18
- package/dist/adapters/backend/zmx-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +12 -1
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +3 -2
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/dsh.d.ts +4 -0
- package/dist/adapters/cli/dsh.d.ts.map +1 -0
- package/dist/adapters/cli/dsh.js +96 -0
- package/dist/adapters/cli/dsh.js.map +1 -0
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +5 -0
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts +52 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +117 -76
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/opencode2.d.ts +4 -0
- package/dist/adapters/cli/opencode2.d.ts.map +1 -0
- package/dist/adapters/cli/opencode2.js +137 -0
- package/dist/adapters/cli/opencode2.js.map +1 -0
- package/dist/adapters/cli/read-isolation.d.ts +1 -1
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +5 -2
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +7 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +19 -6
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +13 -0
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +24 -3
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-command.d.ts +8 -0
- package/dist/adapters/hook-command.d.ts.map +1 -1
- package/dist/adapters/hook-command.js +11 -0
- package/dist/adapters/hook-command.js.map +1 -1
- package/dist/adapters/hook-installer.d.ts +16 -1
- package/dist/adapters/hook-installer.d.ts.map +1 -1
- package/dist/adapters/hook-installer.js +232 -2
- package/dist/adapters/hook-installer.js.map +1 -1
- package/dist/bot-registry.d.ts +116 -8
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +52 -4
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/pm2-start-transaction.d.ts +14 -0
- package/dist/cli/pm2-start-transaction.d.ts.map +1 -1
- package/dist/cli/pm2-start-transaction.js +25 -9
- package/dist/cli/pm2-start-transaction.js.map +1 -1
- package/dist/cli/restart-failure-notification.d.ts +54 -0
- package/dist/cli/restart-failure-notification.d.ts.map +1 -0
- package/dist/cli/restart-failure-notification.js +142 -0
- package/dist/cli/restart-failure-notification.js.map +1 -0
- package/dist/cli/restart-failure-owner.d.ts +20 -0
- package/dist/cli/restart-failure-owner.d.ts.map +1 -0
- package/dist/cli/restart-failure-owner.js +27 -0
- package/dist/cli/restart-failure-owner.js.map +1 -0
- package/dist/cli/restart-shutdown-preflight.d.ts +24 -0
- package/dist/cli/restart-shutdown-preflight.d.ts.map +1 -0
- package/dist/cli/restart-shutdown-preflight.js +105 -0
- package/dist/cli/restart-shutdown-preflight.js.map +1 -0
- package/dist/cli/send-dispatch.d.ts +26 -0
- package/dist/cli/send-dispatch.d.ts.map +1 -1
- package/dist/cli/send-dispatch.js +30 -0
- package/dist/cli/send-dispatch.js.map +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +665 -399
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +38 -15
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/codex-rpc-lifecycle.d.ts +14 -2
- package/dist/codex-rpc-lifecycle.d.ts.map +1 -1
- package/dist/codex-rpc-lifecycle.js +13 -3
- package/dist/codex-rpc-lifecycle.js.map +1 -1
- package/dist/core/ask-broker.d.ts +3 -0
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +29 -7
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/ask-hook/registry.d.ts.map +1 -1
- package/dist/core/ask-hook/registry.js +4 -0
- package/dist/core/ask-hook/registry.js.map +1 -1
- package/dist/core/ask-types.d.ts +5 -1
- package/dist/core/ask-types.d.ts.map +1 -1
- package/dist/core/ask-types.js.map +1 -1
- package/dist/core/browser-restart.d.ts +112 -0
- package/dist/core/browser-restart.d.ts.map +1 -0
- package/dist/core/browser-restart.js +233 -0
- package/dist/core/browser-restart.js.map +1 -0
- package/dist/core/command-handler.d.ts +19 -11
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +237 -43
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/current-turn-provenance.d.ts.map +1 -1
- package/dist/core/current-turn-provenance.js +7 -27
- package/dist/core/current-turn-provenance.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +317 -10
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch-registry.d.ts.map +1 -1
- package/dist/core/dispatch-registry.js +6 -0
- package/dist/core/dispatch-registry.js.map +1 -1
- package/dist/core/dispatch-report-binding.d.ts +31 -0
- package/dist/core/dispatch-report-binding.d.ts.map +1 -0
- package/dist/core/dispatch-report-binding.js +88 -0
- package/dist/core/dispatch-report-binding.js.map +1 -0
- package/dist/core/doc-comment-prompt.d.ts +2 -0
- package/dist/core/doc-comment-prompt.d.ts.map +1 -1
- package/dist/core/doc-comment-prompt.js +6 -0
- package/dist/core/doc-comment-prompt.js.map +1 -1
- package/dist/core/host-overload-alert.d.ts +25 -1
- package/dist/core/host-overload-alert.d.ts.map +1 -1
- package/dist/core/host-overload-alert.js +69 -8
- package/dist/core/host-overload-alert.js.map +1 -1
- package/dist/core/local-terminal-opener.js +1 -1
- package/dist/core/local-terminal-opener.js.map +1 -1
- package/dist/core/managed-origin-capability.d.ts +7 -6
- package/dist/core/managed-origin-capability.d.ts.map +1 -1
- package/dist/core/managed-origin-capability.js +32 -1
- package/dist/core/managed-origin-capability.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +3 -2
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/plugins/materializer.d.ts.map +1 -1
- package/dist/core/plugins/materializer.js +1 -0
- package/dist/core/plugins/materializer.js.map +1 -1
- package/dist/core/producer-quiescence.d.ts +70 -0
- package/dist/core/producer-quiescence.d.ts.map +1 -0
- package/dist/core/producer-quiescence.js +90 -0
- package/dist/core/producer-quiescence.js.map +1 -0
- package/dist/core/raw-command-write-options.d.ts +10 -0
- package/dist/core/raw-command-write-options.d.ts.map +1 -0
- package/dist/core/raw-command-write-options.js +23 -0
- package/dist/core/raw-command-write-options.js.map +1 -0
- package/dist/core/raw-command-writer.d.ts +4 -0
- package/dist/core/raw-command-writer.d.ts.map +1 -1
- package/dist/core/raw-command-writer.js +15 -6
- package/dist/core/raw-command-writer.js.map +1 -1
- package/dist/core/reply-target.d.ts +11 -0
- package/dist/core/reply-target.d.ts.map +1 -1
- package/dist/core/reply-target.js +22 -7
- package/dist/core/reply-target.js.map +1 -1
- package/dist/core/report-session-relay.d.ts +53 -0
- package/dist/core/report-session-relay.d.ts.map +1 -0
- package/dist/core/report-session-relay.js +116 -0
- package/dist/core/report-session-relay.js.map +1 -0
- package/dist/core/session-discovery.d.ts +5 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +13 -0
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-group-birth.d.ts +7 -0
- package/dist/core/session-group-birth.d.ts.map +1 -0
- package/dist/core/session-group-birth.js +145 -0
- package/dist/core/session-group-birth.js.map +1 -0
- package/dist/core/session-manager.d.ts +16 -21
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +140 -21
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +18 -3
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +63 -11
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +241 -13
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +33 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +900 -137
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +20 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +86 -1
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +5 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +4 -1
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +27 -3
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/feedback-analytics-api.d.ts +6 -0
- package/dist/dashboard/feedback-analytics-api.d.ts.map +1 -0
- package/dist/dashboard/feedback-analytics-api.js +39 -0
- package/dist/dashboard/feedback-analytics-api.js.map +1 -0
- package/dist/dashboard/session-action-timeout.d.ts +3 -0
- package/dist/dashboard/session-action-timeout.d.ts.map +1 -0
- package/dist/dashboard/session-action-timeout.js +12 -0
- package/dist/dashboard/session-action-timeout.js.map +1 -0
- package/dist/dashboard/vc-consumer-profiles-api.d.ts +9 -3
- package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.js +2 -1
- package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +48 -5
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +4 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +273 -36
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +6 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +1 -0
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/feedback-page.d.ts +3 -0
- package/dist/dashboard/web/feedback-page.d.ts.map +1 -0
- package/dist/dashboard/web/feedback-page.js +34 -0
- package/dist/dashboard/web/feedback-page.js.map +1 -0
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +7 -2
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +114 -22
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
- package/dist/dashboard/web/team-federation-page.js +26 -2
- package/dist/dashboard/web/team-federation-page.js.map +1 -1
- package/dist/dashboard/web/team-federation.d.ts +19 -0
- package/dist/dashboard/web/team-federation.d.ts.map +1 -1
- package/dist/dashboard/web/team-federation.js +3 -0
- package/dist/dashboard/web/team-federation.js.map +1 -1
- package/dist/dashboard/web/update-action.d.ts +8 -0
- package/dist/dashboard/web/update-action.d.ts.map +1 -1
- package/dist/dashboard/web/update-action.js +10 -0
- package/dist/dashboard/web/update-action.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.js +58 -9
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-4RENHE7J.js +10 -0
- package/dist/dashboard-web/chunks/chunk-2WQDS65D.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-CU4SUKK3.js → chunk-6O7KXFD2.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-G5XGBCM6.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-6JTZJRQJ.js → chunk-Q2MT4VWH.js} +15 -15
- package/dist/dashboard-web/chunks/{chunk-5QJB24I2.js → chunk-U3ZFSO7E.js} +1 -1
- package/dist/dashboard-web/chunks/connectors-page-2KZO5KSJ.js +1 -0
- package/dist/dashboard-web/chunks/feedback-page-CLVTOLLO.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-HLASRTSY.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-EVGGJI5L.js → insights-page-4RPU2CRU.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-HRGR2ENP.js → monitor-room-OTKQ2CXK.js} +1 -1
- package/dist/dashboard-web/chunks/monitoring-page-IJPUB6JT.js +2 -0
- package/dist/dashboard-web/chunks/office-page-QSR3J44Y.js +1 -0
- package/dist/dashboard-web/chunks/overview-page-6OK63V4V.js +1 -0
- package/dist/dashboard-web/chunks/roles-page-6NWJMOB5.js +1 -0
- package/dist/dashboard-web/chunks/schedules-page-6I3LNXD4.js +1 -0
- package/dist/dashboard-web/chunks/{sessions-page-DXBCDWCS.js → sessions-page-B2TXGJDO.js} +1 -1
- package/dist/dashboard-web/chunks/settings-page-6KJADSC6.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-2QAVB2SA.js → skills-page-M2Q6L5CB.js} +1 -1
- package/dist/dashboard-web/chunks/team-federation-page-7I7FDE3B.js +1 -0
- package/dist/dashboard-web/chunks/{v3-page-AY35LKCX.js → v3-page-HQXRL65L.js} +1 -1
- package/dist/dashboard-web/chunks/whiteboards-page-LQKC4TAZ.js +1 -0
- package/dist/dashboard-web/style.css +35 -0
- package/dist/dashboard.js +199 -12
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/shared/shell-path-probes.d.ts +1 -1
- package/dist/desktop/shared/shell-path-probes.js +2 -2
- package/dist/desktop/shared/shell-path-probes.js.map +1 -1
- package/dist/dsh-runner.d.ts +3 -0
- package/dist/dsh-runner.d.ts.map +1 -0
- package/dist/dsh-runner.js +666 -0
- package/dist/dsh-runner.js.map +1 -0
- package/dist/global-config.d.ts +24 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +35 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +25 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +25 -6
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts +3 -1
- package/dist/im/lark/ask-card.d.ts.map +1 -1
- package/dist/im/lark/ask-card.js +61 -5
- package/dist/im/lark/ask-card.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +13 -6
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +2 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +145 -5
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +18 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +36 -9
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/md-card.d.ts +18 -0
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +21 -0
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/relay-target-routing.d.ts.map +1 -1
- package/dist/im/lark/relay-target-routing.js +3 -2
- package/dist/im/lark/relay-target-routing.js.map +1 -1
- package/dist/im/lark/reply-mode-command.d.ts.map +1 -1
- package/dist/im/lark/reply-mode-command.js +19 -8
- package/dist/im/lark/reply-mode-command.js.map +1 -1
- package/dist/im/lark/skill-feedback-card.d.ts +15 -0
- package/dist/im/lark/skill-feedback-card.d.ts.map +1 -0
- package/dist/im/lark/skill-feedback-card.js +149 -0
- package/dist/im/lark/skill-feedback-card.js.map +1 -0
- package/dist/services/bot-config-store.d.ts +10 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +103 -3
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +14 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +15 -0
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/codex-reasoning-effort.d.ts +9 -0
- package/dist/services/codex-reasoning-effort.d.ts.map +1 -0
- package/dist/services/codex-reasoning-effort.js +23 -0
- package/dist/services/codex-reasoning-effort.js.map +1 -0
- package/dist/services/codex-service-tier.d.ts +1 -1
- package/dist/services/codex-service-tier.d.ts.map +1 -1
- package/dist/services/codex-transcript.d.ts +9 -0
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +13 -6
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/feed-group-tagger.d.ts +15 -0
- package/dist/services/feed-group-tagger.d.ts.map +1 -0
- package/dist/services/feed-group-tagger.js +348 -0
- package/dist/services/feed-group-tagger.js.map +1 -0
- package/dist/services/feedback-analytics.d.ts +82 -0
- package/dist/services/feedback-analytics.d.ts.map +1 -0
- package/dist/services/feedback-analytics.js +95 -0
- package/dist/services/feedback-analytics.js.map +1 -0
- package/dist/services/feedback-outbox.d.ts +24 -0
- package/dist/services/feedback-outbox.d.ts.map +1 -0
- package/dist/services/feedback-outbox.js +12 -0
- package/dist/services/feedback-outbox.js.map +1 -0
- package/dist/services/feedback-policy-resolver.d.ts +56 -0
- package/dist/services/feedback-policy-resolver.d.ts.map +1 -0
- package/dist/services/feedback-policy-resolver.js +156 -0
- package/dist/services/feedback-policy-resolver.js.map +1 -0
- package/dist/services/feedback-policy.d.ts +41 -0
- package/dist/services/feedback-policy.d.ts.map +1 -0
- package/dist/services/feedback-policy.js +102 -0
- package/dist/services/feedback-policy.js.map +1 -0
- package/dist/services/feedback-webhook-dispatcher.d.ts +76 -0
- package/dist/services/feedback-webhook-dispatcher.d.ts.map +1 -0
- package/dist/services/feedback-webhook-dispatcher.js +237 -0
- package/dist/services/feedback-webhook-dispatcher.js.map +1 -0
- package/dist/services/grant-prefs-store.d.ts +3 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -1
- package/dist/services/grant-prefs-store.js +30 -1
- package/dist/services/grant-prefs-store.js.map +1 -1
- package/dist/services/groups-store.d.ts +14 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +23 -0
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +2 -0
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/prompt-context-store.d.ts +29 -0
- package/dist/services/prompt-context-store.d.ts.map +1 -0
- package/dist/services/prompt-context-store.js +205 -0
- package/dist/services/prompt-context-store.js.map +1 -0
- package/dist/services/session-group-avatar.d.ts +6 -0
- package/dist/services/session-group-avatar.d.ts.map +1 -0
- package/dist/services/session-group-avatar.js +96 -0
- package/dist/services/session-group-avatar.js.map +1 -0
- package/dist/services/session-group-title.d.ts +47 -0
- package/dist/services/session-group-title.d.ts.map +1 -0
- package/dist/services/session-group-title.js +226 -0
- package/dist/services/session-group-title.js.map +1 -0
- package/dist/services/session-groups-store.d.ts +30 -0
- package/dist/services/session-groups-store.d.ts.map +1 -0
- package/dist/services/session-groups-store.js +132 -0
- package/dist/services/session-groups-store.js.map +1 -0
- package/dist/services/session-store.d.ts +58 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +174 -0
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/skill-feedback-store.d.ts +252 -0
- package/dist/services/skill-feedback-store.d.ts.map +1 -0
- package/dist/services/skill-feedback-store.js +795 -0
- package/dist/services/skill-feedback-store.js.map +1 -0
- package/dist/services/skill-registry-store.d.ts +23 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +93 -7
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/team-store.d.ts +4 -0
- package/dist/services/team-store.d.ts.map +1 -1
- package/dist/services/team-store.js +16 -0
- package/dist/services/team-store.js.map +1 -1
- package/dist/services/traex-transcript.d.ts +32 -4
- package/dist/services/traex-transcript.d.ts.map +1 -1
- package/dist/services/traex-transcript.js +116 -8
- package/dist/services/traex-transcript.js.map +1 -1
- package/dist/services/trigger-types.d.ts +1 -1
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +2 -2
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/services/turn-completion-events.d.ts +57 -0
- package/dist/services/turn-completion-events.d.ts.map +1 -0
- package/dist/services/turn-completion-events.js +179 -0
- package/dist/services/turn-completion-events.js.map +1 -0
- package/dist/services/vc-meeting-consumer-isolation.d.ts +21 -12
- package/dist/services/vc-meeting-consumer-isolation.d.ts.map +1 -1
- package/dist/services/vc-meeting-consumer-isolation.js +25 -19
- package/dist/services/vc-meeting-consumer-isolation.js.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts +8 -2
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js +18 -10
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +6 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/cli-availability.d.ts +2 -1
- package/dist/setup/cli-availability.d.ts.map +1 -1
- package/dist/setup/cli-availability.js +2 -1
- package/dist/setup/cli-availability.js.map +1 -1
- package/dist/setup/cli-selection.d.ts +8 -0
- package/dist/setup/cli-selection.d.ts.map +1 -1
- package/dist/setup/cli-selection.js +31 -0
- package/dist/setup/cli-selection.js.map +1 -1
- package/dist/setup/ensure-fonts.d.ts +5 -0
- package/dist/setup/ensure-fonts.d.ts.map +1 -1
- package/dist/setup/ensure-fonts.js +26 -1
- package/dist/setup/ensure-fonts.js.map +1 -1
- package/dist/setup/lark-scopes.json +3 -1
- package/dist/setup/open-platform-automation.d.ts +1 -1
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +15 -3
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/skills/definitions.d.ts +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +9 -2
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +10 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/user-token.d.ts +38 -0
- package/dist/utils/user-token.d.ts.map +1 -1
- package/dist/utils/user-token.js +76 -1
- package/dist/utils/user-token.js.map +1 -1
- package/dist/utils/web-terminal-scroll.d.ts +27 -0
- package/dist/utils/web-terminal-scroll.d.ts.map +1 -0
- package/dist/utils/web-terminal-scroll.js +60 -0
- package/dist/utils/web-terminal-scroll.js.map +1 -0
- package/dist/vc-agent/polling-source.d.ts +70 -0
- package/dist/vc-agent/polling-source.d.ts.map +1 -1
- package/dist/vc-agent/polling-source.js +115 -0
- package/dist/vc-agent/polling-source.js.map +1 -1
- package/dist/worker.js +237 -76
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -22
- package/dist/workflows/events/payloads.d.ts +8 -8
- package/dist/workflows/events/schema.d.ts +20 -20
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +20 -20
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-NQJFJ45Z.js +0 -10
- package/dist/dashboard-web/chunks/chunk-KEEYQ3WF.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-HADHZXXZ.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-5TYF46RS.js +0 -8
- package/dist/dashboard-web/chunks/monitoring-page-LBU7TTTW.js +0 -2
- package/dist/dashboard-web/chunks/office-page-DF6RRBMN.js +0 -1
- package/dist/dashboard-web/chunks/overview-page-ZB6QH2EX.js +0 -1
- package/dist/dashboard-web/chunks/roles-page-ZBZGHIIF.js +0 -1
- package/dist/dashboard-web/chunks/schedules-page-NQVQC3QY.js +0 -1
- package/dist/dashboard-web/chunks/settings-page-E7NN4NVQ.js +0 -2
- package/dist/dashboard-web/chunks/team-federation-page-VGWL53AE.js +0 -1
- package/dist/dashboard-web/chunks/whiteboards-page-BNWUU4KG.js +0 -1
package/dist/daemon.js
CHANGED
|
@@ -17,6 +17,7 @@ import { writeHeartbeat } from './core/daemon-heartbeat.js';
|
|
|
17
17
|
import { botmuxWrapperFiles, resolveBotmuxWrapperBinDir } from './core/botmux-wrapper.js';
|
|
18
18
|
import { evaluateOverload, formatOverloadAlert, buildOverloadAlertCard, buildOverloadRecoveredCard, initialOverloadCardState, INITIAL_OVERLOAD_STATE, computeOverloadThresholds, isOverloadAlertTarget, } from './core/host-overload-alert.js';
|
|
19
19
|
import { registerOverloadNonce } from './im/lark/overload-nonce.js';
|
|
20
|
+
import { resolveBrowserTargets, detectRunningBrowsers } from './core/browser-restart.js';
|
|
20
21
|
import { countHostOverload } from './im/lark/card-handler.js';
|
|
21
22
|
import { startMaintenance, stopMaintenance } from './core/maintenance.js';
|
|
22
23
|
import { selectCodexRuntimeUpdateTargets, startCliRuntimeUpdateMonitor, stopCliRuntimeUpdateMonitor, } from './core/cli-runtime-update.js';
|
|
@@ -26,8 +27,11 @@ import { readSupervisorProcessStartIdentity } from './core/process-start-identit
|
|
|
26
27
|
import { statSync } from 'node:fs';
|
|
27
28
|
import { addReaction, deleteMessage, getChatContext, getChatMode, getChatNameAndMode, getMessageChatId, listChatMemberOpenIds, MessageWithdrawnError, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
|
|
28
29
|
import { resolveGroupJoinPrompt, waitForAllowedUserInChat } from './core/auto-start.js';
|
|
29
|
-
import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, setResolvedAllowedUsersRepublishHook, setAllowedUsersResolveRetryHook, vcMeetingAgentConfigActive, } from './bot-registry.js';
|
|
30
|
+
import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, setResolvedAllowedUsersRepublishHook, setAllowedUsersResolveRetryHook, vcMeetingAgentConfigActive, botAcceptsSlashFromBots, } from './bot-registry.js';
|
|
30
31
|
import { setDisplayNameRefresher, findConfigField, applyConfigField } from './services/bot-config-store.js';
|
|
32
|
+
import { getSkillFeedbackStore } from './services/skill-feedback-store.js';
|
|
33
|
+
import { enqueueTurnTerminal, drainTurnTerminalQueue } from './services/turn-completion-events.js';
|
|
34
|
+
import { FeedbackWebhookSecretStore, startFeedbackWebhookDispatcher } from './services/feedback-webhook-dispatcher.js';
|
|
31
35
|
import { resolveRegularGroupMode } from './services/chat-reply-mode-store.js';
|
|
32
36
|
import { renameBotOnOpenPlatform, changeBotAvatarOnOpenPlatform } from './services/open-platform-rename.js';
|
|
33
37
|
import { migrateSandboxConfigAtStartup } from './services/sandbox-migration.js';
|
|
@@ -73,13 +77,17 @@ import { isLocalCliOpenReady } from './services/local-cli-opener.js';
|
|
|
73
77
|
import { RECEIVED_REACTION_EMOJI_TYPE, SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE } from './core/pending-response.js';
|
|
74
78
|
import { t as tr, localeForBot } from './i18n/index.js';
|
|
75
79
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
76
|
-
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, forkAdoptWorker, sendWorkerInput, promoteQueuedActivationTail, admitQueuedActivationTail, reserveQueuedActivationTailAdmission, codexAppCleanInputAcceptedForSession, hasQueuedActivationAdmissionGate, sendWorkerSessionInput, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, getCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, setActiveSessionIfActive, rollbackRejectedSessionAndGetWinner, ensureCliEnv, sweepGlobalBotmuxSkills, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, findActiveBySessionId, withActiveSessionKeyLock, getDaemonBootId, getDaemonStreamingCardUsageSnapshot, postTurnStartingCard, isSessionTransferring, } from './core/worker-pool.js';
|
|
80
|
+
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, forkAdoptWorker, sendWorkerInput, promoteQueuedActivationTail, admitQueuedActivationTail, reserveQueuedActivationTailAdmission, codexAppCleanInputAcceptedForSession, hasQueuedActivationAdmissionGate, sendWorkerSessionInput, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, getCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, setActiveSessionIfActive, rollbackRejectedSessionAndGetWinner, ensureCliEnv, sweepGlobalBotmuxSkills, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, findActiveBySessionId, withActiveSessionKeyLock, getDaemonBootId, getDaemonStreamingCardUsageSnapshot, postTurnStartingCard, isSessionTransferring, snapshotCodexAppFinalSettlements, codexAppFinalSettlementCount, } from './core/worker-pool.js';
|
|
81
|
+
import { waitAllWithin, trackProducerQuiet, trackProcessExited } from './core/producer-quiescence.js';
|
|
77
82
|
import { AbortDeadlineError, hasExactSafeJsonKeys, ipcRoute, isTrustedHostIpcRequest, JsonBodyTooLargeError, jsonRes, readJsonBody, runWithAbortDeadline, setBotName, setLarkAppId, startIpcServer, setBotRenamer, setBotAvatarChanger, armCoreOnlyReadinessGate, setCoreOnlyReady, setSupervisorShutdownHandler } from './core/dashboard-ipc-server.js';
|
|
78
83
|
import { setDeviceIsolationDaemonIdentity } from './core/device-isolation-daemon.js';
|
|
79
84
|
import { cancelSessionReadyAck, waitForSessionReadyAck, } from './core/session-ready-handshake.js';
|
|
80
85
|
import { loadOrCreateDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
81
|
-
import { daemonIpcAuthHeaders, loadDaemonIpcSecret } from './core/daemon-ipc-auth.js';
|
|
86
|
+
import { daemonIpcAuthHeaders, fetchDaemonIpc, loadDaemonIpcSecret } from './core/daemon-ipc-auth.js';
|
|
82
87
|
import { authorizeSessionScopedIpc, bindSessionScopedIpcIdentity, } from './core/daemon-ipc-session-auth.js';
|
|
88
|
+
import { authorizeReportSessionRelayRequest, buildOrchestratorReportTrigger, REPORT_SESSION_RELAY_MAX_BYTES, REPORT_SESSION_RELAY_ROUTE, } from './core/report-session-relay.js';
|
|
89
|
+
import { createDispatchReportBinding, dispatchReportBindingSecretPath, DISPATCH_REPORT_REGISTER_MAX_BYTES, DISPATCH_REPORT_REGISTER_ROUTE, } from './core/dispatch-report-binding.js';
|
|
90
|
+
import { recordDispatchRegistryEntry } from './core/dispatch-registry.js';
|
|
83
91
|
import { saveFrozenCards } from './services/frozen-card-store.js';
|
|
84
92
|
import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, EXISTING_SESSION_ONLY_DAEMON_COMMANDS, resolvePassthroughCommands, resolveAdapterDefaultPassthroughCommands, handleCommand, handleCardCommand, handleTermLinkCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
|
|
85
93
|
import { docWatchCommandNeedsSession } from './core/doc-watch-command.js';
|
|
@@ -88,13 +96,13 @@ import { findInheritablePeer } from './core/inherit-peer.js';
|
|
|
88
96
|
import { isCallbackUrl, handleCallbackUrl } from './utils/user-token.js';
|
|
89
97
|
import { consumeQuota, removeChatGrant, removeGlobalGrant } from './services/grant-store.js';
|
|
90
98
|
import { abortCharge, commitCharge, beginCharge } from './services/quota-dedup.js';
|
|
91
|
-
import { getSessionWorkingDir, getProjectScanDirs, getProjectScanDirsForBot, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicCliInput, buildFollowUpCliInput, buildBridgeInputContent, buildReforkCliInput, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, ensureTerminalWorkerPort, ensureSessionWhiteboard, closeCliMismatchedSessionsForBot, } from './core/session-manager.js';
|
|
92
|
-
import { triggerSessionTurn, reconcileIdempotencyLeasesOnBoot, convergeIdempotentAsyncTurnOnWorkerExit } from './core/trigger-session.js';
|
|
99
|
+
import { getSessionWorkingDir, getProjectScanDirs, getProjectScanDirsForBot, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicCliInput, buildFollowUpCliInput, buildBridgeInputContent, buildReforkCliInput, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, ensureTerminalWorkerPort, ensureSessionWhiteboard, resumeSession, closeCliMismatchedSessionsForBot, } from './core/session-manager.js';
|
|
100
|
+
import { triggerSessionTurn, reconcileIdempotencyLeasesOnBoot, convergeIdempotentAsyncTurnOnWorkerExit, externalEventOpensOwnTopic } from './core/trigger-session.js';
|
|
93
101
|
import { runDetachedBotTurnMutation, tryWithBotTurnMutation, withBotTurnAdmission, withBotTurnMutation, } from './core/bot-turn-mutation-gate.js';
|
|
94
102
|
import { claimInitialUserTurn, isInitialUserTurnPending, releaseInitialUserTurn } from './core/initial-user-turn.js';
|
|
95
103
|
import { applyQueuedCodexAppLegacyFallback, mergeQueuedCodexAppTurn } from './core/session-create.js';
|
|
96
104
|
import { findOnlineDaemon, listOnlineDaemons } from './utils/daemon-discovery.js';
|
|
97
|
-
import { beginReplyTargetTurn, buildTurnParticipantsFrom, fallbackTurnId, isSubstituteTurn, resolveSessionReplyTarget, syncReplyTargetState } from './core/reply-target.js';
|
|
105
|
+
import { beginReplyTargetTurn, buildTurnParticipantsFrom, fallbackTurnId, isSubstituteTurn, resolveInboundReplyTarget, resolveSessionReplyTarget, syncReplyTargetState } from './core/reply-target.js';
|
|
98
106
|
import { readDeferredTopicBinding } from './core/deferred-topic-binding.js';
|
|
99
107
|
import { buildBotmuxLarkNativeSessionTitle, extractBotmuxLarkNativeSessionTitlePrompt, } from './core/session-title.js';
|
|
100
108
|
import { settleDeferredScheduleRun } from './core/deferred-schedule-settlement.js';
|
|
@@ -187,11 +195,14 @@ import { computeCocoPickerKeys } from './core/coco-picker-keys.js';
|
|
|
187
195
|
import { createLarkAskCardDispatcher } from './im/lark/ask-card.js';
|
|
188
196
|
import { normalizeVcMeetingEvents } from './vc-agent/normalizer.js';
|
|
189
197
|
import { beginVcIngestionPass, collectStableTranscriptItems, createVcMeetingSessionState, ingestNormalizedVcMeetingItems, markVcTranscriptItemsFlushed, } from './vc-agent/meeting-state.js';
|
|
190
|
-
import { fetchMeetingEventsAsBot, joinMeetingAsBot, sendMeetingTextMessageAsBot, } from './vc-agent/polling-source.js';
|
|
198
|
+
import { ensureLarkCliBotProfile, fetchMeetingEventsAsBot, joinMeetingAsBot, sendMeetingTextMessageAsBot, } from './vc-agent/polling-source.js';
|
|
191
199
|
import { buildVcMeetingConfirmCard, buildVcMeetingConsumerCard, buildVcMeetingConsumerRecoveryCard, buildVcMeetingListenerRejoinCard, buildVcMeetingOutputReviewCard, } from './vc-agent/cards.js';
|
|
192
200
|
import { connectRealtimeVoiceTransport, createProtoRealtimeVoiceProtocol, fetchRealtimeVoiceEndpoint, RealtimeVoiceSession, } from './vc-agent/realtime/index.js';
|
|
193
201
|
import { createGroupWithBots } from './services/group-creator.js';
|
|
194
202
|
import { addBotToChat, isInChat } from './services/groups-store.js';
|
|
203
|
+
import { initSessionGroups, isSessionGroup, getSessionGroup, touchSessionGroup } from './services/session-groups-store.js';
|
|
204
|
+
import { maybeBirthSessionGroup } from './core/session-group-birth.js';
|
|
205
|
+
import { scheduleSessionGroupTitle } from './services/session-group-title.js';
|
|
195
206
|
import { setChatReplyMode } from './services/chat-reply-mode-store.js';
|
|
196
207
|
import { hasVcMeetingEndedTombstone, listVcMeetingRuntimeSessionsByListenerAndAgent, listVcMeetingRuntimeSessions, pruneExpiredVcMeetingRuntimeSessions, recordVcMeetingEndedTombstone, recordVcMeetingRuntimeSession, removeVcMeetingRuntimeSession, } from './services/vc-meeting-runtime-store.js';
|
|
197
208
|
import { computeVcMeetingConsumerProfileHash, normalizeVcMeetingProfileInstructions, } from './services/vc-meeting-profile-instructions.js';
|
|
@@ -1085,6 +1096,17 @@ const VC_MEETING_ACTIVATION_CONTEXT_FIELD = 'vc_meeting_activation_context';
|
|
|
1085
1096
|
const VC_MEETING_ACTIVATION_CONTEXT_MAX_CHARS = 2_000;
|
|
1086
1097
|
const vcMeetingEndedTombstones = new BoundedMap(2_000);
|
|
1087
1098
|
const vcMeetingPendingInvites = new BoundedMap(2_000);
|
|
1099
|
+
/**
|
|
1100
|
+
* De-dupes the owner DM for an eager-join failure that happens BEFORE a session
|
|
1101
|
+
* exists (invite carries only a meeting_no, no meeting.id — so there is no
|
|
1102
|
+
* session to hang `inviteFailureNotified` on). Keyed by `${larkAppId}:${no}`;
|
|
1103
|
+
* bounded and best-effort. Cleared once the meeting is successfully joined so a
|
|
1104
|
+
* later genuine failure can still notify. */
|
|
1105
|
+
const vcMeetingEagerJoinFailureNotified = new BoundedMap(2_000);
|
|
1106
|
+
const VC_MEETING_EAGER_JOIN_FAILURE_TTL_MS = 30 * 60 * 1000;
|
|
1107
|
+
function vcMeetingEagerJoinFailureKey(larkAppId, meetingNo) {
|
|
1108
|
+
return `${larkAppId}:${meetingNo}`;
|
|
1109
|
+
}
|
|
1088
1110
|
function vcMeetingSessionKey(larkAppId, meetingId) {
|
|
1089
1111
|
return `${larkAppId}:${meetingId}`;
|
|
1090
1112
|
}
|
|
@@ -1268,6 +1290,70 @@ function vcMeetingConfiguredBotConfigs() {
|
|
|
1268
1290
|
function vcMeetingConfiguredBotConfig(larkAppId) {
|
|
1269
1291
|
return vcMeetingConfiguredBotConfigs().find(cfg => cfg.larkAppId === larkAppId);
|
|
1270
1292
|
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Ensure the lark-cli bot profile used for `--as bot` meeting join actually
|
|
1295
|
+
* exists, provisioning it from the bot's stored appSecret when absent.
|
|
1296
|
+
*
|
|
1297
|
+
* Historically `larkCliProfile` defaulted to the bot's own appId (see the
|
|
1298
|
+
* dashboard managed-listener bootstrap) but nothing ever registered that
|
|
1299
|
+
* profile with lark-cli, so the first real invite failed with "profile not
|
|
1300
|
+
* found" and the bot was stuck ringing. This closes the gap in the trusted
|
|
1301
|
+
* daemon process (the secret only reaches lark-cli's encrypted store, never a
|
|
1302
|
+
* worker/sandbox and never argv). Idempotent: a present profile is a no-op.
|
|
1303
|
+
*/
|
|
1304
|
+
function ensureVcMeetingJoinProfile(larkAppId, cfg) {
|
|
1305
|
+
const profile = cfg.larkCliProfile?.trim();
|
|
1306
|
+
if (!profile) {
|
|
1307
|
+
return { ok: false, reason: 'no_profile', error: '缺少 vcMeetingAgent.larkCliProfile' };
|
|
1308
|
+
}
|
|
1309
|
+
const botCfg = vcMeetingLocalBotConfig(larkAppId) ?? vcMeetingConfiguredBotConfig(larkAppId);
|
|
1310
|
+
const result = ensureLarkCliBotProfile({
|
|
1311
|
+
profileName: profile,
|
|
1312
|
+
appId: larkAppId,
|
|
1313
|
+
appSecret: botCfg?.larkAppSecret ?? '',
|
|
1314
|
+
...(botCfg?.brand ? { brand: botCfg.brand } : {}),
|
|
1315
|
+
});
|
|
1316
|
+
if (result.ok) {
|
|
1317
|
+
if (result.created) {
|
|
1318
|
+
logger.info(`[vc-agent] auto-provisioned lark-cli profile ${profile} for ${larkAppId} before join`);
|
|
1319
|
+
}
|
|
1320
|
+
return { ok: true, profile, created: result.created };
|
|
1321
|
+
}
|
|
1322
|
+
return { ok: false, reason: result.reason, profile, error: result.error };
|
|
1323
|
+
}
|
|
1324
|
+
/** Actionable, operator-facing text for a failed join-profile provision. */
|
|
1325
|
+
function vcMeetingJoinProfileErrorText(larkAppId, result) {
|
|
1326
|
+
switch (result.reason) {
|
|
1327
|
+
case 'no_profile':
|
|
1328
|
+
return '缺少 vcMeetingAgent.larkCliProfile,拒绝使用 lark-cli 默认 profile 入会';
|
|
1329
|
+
case 'missing_secret':
|
|
1330
|
+
return `无法为 ${larkAppId} 自动创建 lark-cli profile「${result.profile}」:配置里没有该 bot 的 appSecret(apiOnly bot 无法入会)`;
|
|
1331
|
+
case 'add_failed':
|
|
1332
|
+
return `自动创建 lark-cli profile「${result.profile}」失败:${result.error}`;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* DM the meeting-agent owner when an invite fails, so a broken join no longer
|
|
1337
|
+
* shows up only as a silently-ringing bot + one log line. Best-effort and
|
|
1338
|
+
* de-duped per session so retries don't spam. `errorText` is already the
|
|
1339
|
+
* actionable operator-facing message from startVcMeetingMonitoring.
|
|
1340
|
+
*/
|
|
1341
|
+
function notifyVcMeetingInviteFailure(larkAppId, cfg, session, meeting, errorText) {
|
|
1342
|
+
if (session) {
|
|
1343
|
+
if (session.inviteFailureNotified)
|
|
1344
|
+
return;
|
|
1345
|
+
session.inviteFailureNotified = true;
|
|
1346
|
+
}
|
|
1347
|
+
const target = vcMeetingTargetOpenId(larkAppId, cfg);
|
|
1348
|
+
if (!target)
|
|
1349
|
+
return;
|
|
1350
|
+
const title = vcMeetingTitle(meeting);
|
|
1351
|
+
void sendUserMessage(larkAppId, target, `⚠️ 机器人未能加入会议「${title}」。\n\n原因:${errorText}\n\n`
|
|
1352
|
+
+ '请到 dashboard 检查该会议 bot 的 lark-cli profile / appSecret / VC 权限后重新邀请。', 'text').catch((err) => {
|
|
1353
|
+
logger.warn(`[vc-agent] invite failure notice failed meeting=${meeting.id || '?'}: `
|
|
1354
|
+
+ `${err instanceof Error ? err.message : String(err)}`);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1271
1357
|
function vcMeetingConsumerBotOnline(larkAppId) {
|
|
1272
1358
|
return !!vcMeetingLocalBotConfig(larkAppId) || !!findOnlineDaemon(larkAppId);
|
|
1273
1359
|
}
|
|
@@ -1459,6 +1545,12 @@ async function ensureVcMeetingReceiverSession(request, existingSessionId, receiv
|
|
|
1459
1545
|
if (!isolation.decision.ok) {
|
|
1460
1546
|
throw new Error(`receiver agent ${selfAppId} has no managed side-effect isolation: ${isolation.decision.error}`);
|
|
1461
1547
|
}
|
|
1548
|
+
// Plan B: the receiver runs sandboxed only when the bot actually opted in and
|
|
1549
|
+
// the boundary is deliverable. An unsandboxed opt-out (isolated:false) must
|
|
1550
|
+
// spawn WITHOUT the sandbox — hardcoding sandbox=true here would confine a bot
|
|
1551
|
+
// whose environment/working dir was never set up for bwrap, and the CLI would
|
|
1552
|
+
// fail to spawn or be unable to work (the "joined but never replies" bug).
|
|
1553
|
+
const receiverSandboxed = isolation.decision.isolated;
|
|
1462
1554
|
const rawWorkingDir = findOncallChat(selfAppId, chatId)?.workingDir
|
|
1463
1555
|
?? effectiveDefaultWorkingDir(bot.config)
|
|
1464
1556
|
?? bot.config.workingDir
|
|
@@ -1481,11 +1573,12 @@ async function ensureVcMeetingReceiverSession(request, existingSessionId, receiv
|
|
|
1481
1573
|
session.cliId = bot.config.cliId;
|
|
1482
1574
|
// Freeze the security-critical launch decision at receiver creation. A
|
|
1483
1575
|
// later live Bot-config edit must neither weaken this session nor make an
|
|
1484
|
-
// old unisolated receiver appear eligible retroactively.
|
|
1485
|
-
|
|
1486
|
-
session.
|
|
1487
|
-
session.
|
|
1488
|
-
session.
|
|
1576
|
+
// old unisolated receiver appear eligible retroactively. Under plan B the
|
|
1577
|
+
// frozen value follows the bot's opt-in, not a hardcoded true.
|
|
1578
|
+
session.sandbox = receiverSandboxed;
|
|
1579
|
+
session.sandboxHidePaths = receiverSandboxed ? (bot.config.sandboxHidePaths ?? []) : [];
|
|
1580
|
+
session.sandboxReadonlyPaths = receiverSandboxed ? (bot.config.sandboxReadonlyPaths ?? []) : [];
|
|
1581
|
+
session.sandboxNetwork = receiverSandboxed ? (bot.config.sandboxNetwork !== false) : true;
|
|
1489
1582
|
session.backendType = isolation.backendType;
|
|
1490
1583
|
sessionStore.updateSession(session);
|
|
1491
1584
|
const ds = {
|
|
@@ -2052,23 +2145,6 @@ function receivedReactionEmojiFor(ds) {
|
|
|
2052
2145
|
return RECEIVED_REACTION_EMOJI_TYPE;
|
|
2053
2146
|
}
|
|
2054
2147
|
}
|
|
2055
|
-
function readSessionFreshFromDisk(sessionId, larkAppId) {
|
|
2056
|
-
const paths = [
|
|
2057
|
-
join(config.session.dataDir, `sessions-${larkAppId}.json`),
|
|
2058
|
-
join(config.session.dataDir, 'sessions.json'),
|
|
2059
|
-
];
|
|
2060
|
-
for (const fp of paths) {
|
|
2061
|
-
if (!existsSync(fp))
|
|
2062
|
-
continue;
|
|
2063
|
-
try {
|
|
2064
|
-
const data = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
2065
|
-
if (data[sessionId])
|
|
2066
|
-
return data[sessionId];
|
|
2067
|
-
}
|
|
2068
|
-
catch { /* ignore corrupt/racing session file */ }
|
|
2069
|
-
}
|
|
2070
|
-
return undefined;
|
|
2071
|
-
}
|
|
2072
2148
|
export async function noteTurnReceived(ds, triggerMessageId, _prompt, _sender, _turnId, receivedReactionEmoji) {
|
|
2073
2149
|
// Replaces the old 「处理中」 placeholder card. That card existed only to be
|
|
2074
2150
|
// PATCHed with the final answer, and `im.v1.message.patch` is silent (no Feishu
|
|
@@ -2239,7 +2315,7 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId, turnId
|
|
|
2239
2315
|
return replyWithHookPolicy(root.rootMessageId, content, msgType, true, opts.uuid);
|
|
2240
2316
|
}
|
|
2241
2317
|
if (ds?.scope === 'chat') {
|
|
2242
|
-
const fresh =
|
|
2318
|
+
const fresh = sessionStore.readSessionRowFromDisk(ds.session.sessionId, ds.larkAppId);
|
|
2243
2319
|
if (fresh)
|
|
2244
2320
|
syncReplyTargetState(ds, fresh);
|
|
2245
2321
|
// Resolve through fallbackTurnId so daemon-side sends that carry no turn of
|
|
@@ -3127,6 +3203,7 @@ async function prewarmDocCommentSession(ds, sub) {
|
|
|
3127
3203
|
botCliPathOverride: botCfg.cliPathOverride,
|
|
3128
3204
|
sender,
|
|
3129
3205
|
mode: 'live',
|
|
3206
|
+
turnId,
|
|
3130
3207
|
});
|
|
3131
3208
|
rememberLastCliInput(ds, promptContent, cliInput);
|
|
3132
3209
|
sessionStore.updateSession(ds.session);
|
|
@@ -3145,6 +3222,7 @@ async function prewarmDocCommentSession(ds, sub) {
|
|
|
3145
3222
|
botIdentity: { name: bot.botName, openId: bot.botOpenId },
|
|
3146
3223
|
sender,
|
|
3147
3224
|
mode: 'refork',
|
|
3225
|
+
turnId,
|
|
3148
3226
|
});
|
|
3149
3227
|
rememberLastCliInput(ds, promptContent, wrappedInput);
|
|
3150
3228
|
sessionStore.updateSession(ds.session);
|
|
@@ -3161,6 +3239,24 @@ const commandDeps = {
|
|
|
3161
3239
|
lastRepoScan,
|
|
3162
3240
|
prewarmDocCommentSession,
|
|
3163
3241
|
};
|
|
3242
|
+
/** Bind every reply produced by one slash-command invocation to the routing
|
|
3243
|
+
* decision already made for its inbound Lark message. Commands return before
|
|
3244
|
+
* the normal turn-registration path, and lifecycle commands may remove or
|
|
3245
|
+
* replace their session before replying, so mutable session state cannot be
|
|
3246
|
+
* the authority for placement here. */
|
|
3247
|
+
function commandDepsForInvocation(input) {
|
|
3248
|
+
const replyTarget = resolveInboundReplyTarget({
|
|
3249
|
+
scope: input.scope,
|
|
3250
|
+
chatId: input.chatId,
|
|
3251
|
+
threadRootId: input.anchor,
|
|
3252
|
+
replyRootId: input.replyRootId,
|
|
3253
|
+
});
|
|
3254
|
+
return {
|
|
3255
|
+
...commandDeps,
|
|
3256
|
+
invocationReplyTarget: replyTarget,
|
|
3257
|
+
sessionReply: (rootId, content, msgType, larkAppId, turnId, opts) => sessionReply(rootId, content, msgType, larkAppId, turnId ?? input.messageId, opts?.replyTarget ? opts : { ...opts, replyTarget }),
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3164
3260
|
/**
|
|
3165
3261
|
* Fire a session-less daemon command (`/group`, `/g`) WITHOUT blocking the Lark
|
|
3166
3262
|
* event ACK on its slow work — the fast-ACK path.
|
|
@@ -3181,8 +3277,8 @@ const commandDeps = {
|
|
|
3181
3277
|
* wraps its whole body in try/catch and replies failures to the chat; the `.catch`
|
|
3182
3278
|
* here is only a last-resort backstop for anything that somehow escapes.
|
|
3183
3279
|
*/
|
|
3184
|
-
function fireSessionlessCommandDetached(cmd, anchor, message, larkAppId) {
|
|
3185
|
-
void handleCommand(cmd, anchor, message,
|
|
3280
|
+
function fireSessionlessCommandDetached(cmd, anchor, message, larkAppId, deps) {
|
|
3281
|
+
void handleCommand(cmd, anchor, message, deps, larkAppId).catch((err) => logger.error(`[sessionless ${cmd}] ${anchor.substring(0, 12)} failed: ${err?.message ?? err}`));
|
|
3186
3282
|
}
|
|
3187
3283
|
// Dependencies passed to card-handler
|
|
3188
3284
|
// v3 run-level progress is a best-effort IM projection. journal.ndjson remains
|
|
@@ -3975,6 +4071,187 @@ workflowDaemonMutationRoute('grant', async (reply, params, body, identity) => {
|
|
|
3975
4071
|
v3GateRunner.driveDetached(runId);
|
|
3976
4072
|
return reply(202, { ok: true, runId, ...outcome });
|
|
3977
4073
|
});
|
|
4074
|
+
// ─── report session relay:隔离 CLI 的 dispatch 完成回注 ──────────────────
|
|
4075
|
+
//
|
|
4076
|
+
// Linux credential-only bwrap / macOS read isolation 都
|
|
4077
|
+
// 不允许 CLI 读取 `.dashboard-secret`。这里验证 source session 当前轮换的
|
|
4078
|
+
// capability,并把 dispatch root 与 daemon 自己的 live session 绑定;目标
|
|
4079
|
+
// app/session 只从宿主签名的 dispatch binding 派生,再由本 daemon 用 HMAC 转发。
|
|
4080
|
+
ipcRoute('POST', DISPATCH_REPORT_REGISTER_ROUTE, async (req, res) => {
|
|
4081
|
+
let raw;
|
|
4082
|
+
try {
|
|
4083
|
+
raw = await readJsonBody(req, DISPATCH_REPORT_REGISTER_MAX_BYTES);
|
|
4084
|
+
}
|
|
4085
|
+
catch (error) {
|
|
4086
|
+
if (error instanceof JsonBodyTooLargeError) {
|
|
4087
|
+
return jsonRes(res, 413, { ok: false, error: 'body_too_large' });
|
|
4088
|
+
}
|
|
4089
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
4090
|
+
}
|
|
4091
|
+
const body = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
4092
|
+
? raw
|
|
4093
|
+
: undefined;
|
|
4094
|
+
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
4095
|
+
const seedText = typeof body?.seedText === 'string' ? body.seedText.trim() : '';
|
|
4096
|
+
const targetChatId = typeof body?.targetChatId === 'string' ? body.targetChatId.trim() : '';
|
|
4097
|
+
const title = typeof body?.title === 'string' ? body.title.trim().slice(0, 200) : '';
|
|
4098
|
+
if (!sessionId)
|
|
4099
|
+
return jsonRes(res, 400, { ok: false, error: 'missing_session_id' });
|
|
4100
|
+
if (!seedText) {
|
|
4101
|
+
return jsonRes(res, 400, { ok: false, error: 'missing_seed_text' });
|
|
4102
|
+
}
|
|
4103
|
+
if (!/^oc_[A-Za-z0-9_-]{1,128}$/.test(targetChatId)) {
|
|
4104
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_target_chat_id' });
|
|
4105
|
+
}
|
|
4106
|
+
const ds = findActiveBySessionId(sessionId);
|
|
4107
|
+
const verified = authorizeSessionScopedIpc({
|
|
4108
|
+
trustedHost: isTrustedHostIpcRequest(req),
|
|
4109
|
+
sessionExists: !!ds && ds.session.sessionId === sessionId,
|
|
4110
|
+
receiverSession: !!ds?.session.vcMeetingReceiver,
|
|
4111
|
+
allowReceiver: false,
|
|
4112
|
+
sessionId,
|
|
4113
|
+
...(ds?.managedTurnOrigin ? { liveOrigin: ds.managedTurnOrigin } : {}),
|
|
4114
|
+
...(typeof body?.originCapability === 'string'
|
|
4115
|
+
? { claimedCapability: body.originCapability }
|
|
4116
|
+
: {}),
|
|
4117
|
+
...(typeof body?.originTurnId === 'string' ? { claimedTurnId: body.originTurnId } : {}),
|
|
4118
|
+
...(typeof body?.originDispatchAttempt === 'number'
|
|
4119
|
+
? { claimedDispatchAttempt: body.originDispatchAttempt }
|
|
4120
|
+
: {}),
|
|
4121
|
+
});
|
|
4122
|
+
if (!verified.ok)
|
|
4123
|
+
return jsonRes(res, 403, { ok: false, error: verified.error });
|
|
4124
|
+
if (!ds || !ds.larkAppId || ds.larkAppId !== selfDaemonLarkAppId) {
|
|
4125
|
+
return jsonRes(res, 403, { ok: false, error: 'session_identity_incomplete' });
|
|
4126
|
+
}
|
|
4127
|
+
let dispatchRoot;
|
|
4128
|
+
try {
|
|
4129
|
+
dispatchRoot = await sendMessage(ds.larkAppId, targetChatId, seedText, 'text');
|
|
4130
|
+
}
|
|
4131
|
+
catch (error) {
|
|
4132
|
+
return jsonRes(res, 502, {
|
|
4133
|
+
ok: false,
|
|
4134
|
+
error: 'dispatch_seed_send_failed',
|
|
4135
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
4138
|
+
const bindingSecret = loadOrCreateDashboardSecret(dispatchReportBindingSecretPath(config.session.dataDir));
|
|
4139
|
+
const issuedAt = new Date().toISOString();
|
|
4140
|
+
const reportBinding = createDispatchReportBinding(bindingSecret, {
|
|
4141
|
+
dispatchRoot,
|
|
4142
|
+
targetLarkAppId: ds.larkAppId,
|
|
4143
|
+
targetSessionId: ds.session.sessionId,
|
|
4144
|
+
sourceName: title || 'dispatched subtask',
|
|
4145
|
+
issuedAt,
|
|
4146
|
+
});
|
|
4147
|
+
const stringArray = (value) => Array.isArray(value)
|
|
4148
|
+
? value.filter((item) => typeof item === 'string')
|
|
4149
|
+
.map(item => item.trim()).filter(Boolean).slice(0, 64)
|
|
4150
|
+
: [];
|
|
4151
|
+
try {
|
|
4152
|
+
await recordDispatchRegistryEntry(join(config.session.dataDir, 'orchestrate-dispatch.json'), dispatchRoot, {
|
|
4153
|
+
orchRoot: ds.session.rootMessageId ?? '',
|
|
4154
|
+
orchChatId: ds.session.chatId,
|
|
4155
|
+
orchScope: ds.scope ?? ds.session.scope ?? 'thread',
|
|
4156
|
+
orchAppId: ds.larkAppId,
|
|
4157
|
+
orchSessionId: ds.session.sessionId,
|
|
4158
|
+
targetChatId,
|
|
4159
|
+
targetAppIds: stringArray(body?.targetAppIds),
|
|
4160
|
+
title,
|
|
4161
|
+
bots: stringArray(body?.bots),
|
|
4162
|
+
createdAt: issuedAt,
|
|
4163
|
+
reportBinding,
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
4166
|
+
catch (error) {
|
|
4167
|
+
return jsonRes(res, 500, {
|
|
4168
|
+
ok: false,
|
|
4169
|
+
error: 'dispatch_registry_write_failed',
|
|
4170
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
return jsonRes(res, 201, { ok: true, dispatchRoot });
|
|
4174
|
+
});
|
|
4175
|
+
ipcRoute('POST', REPORT_SESSION_RELAY_ROUTE, async (req, res) => {
|
|
4176
|
+
let raw;
|
|
4177
|
+
try {
|
|
4178
|
+
raw = await readJsonBody(req, REPORT_SESSION_RELAY_MAX_BYTES);
|
|
4179
|
+
}
|
|
4180
|
+
catch (error) {
|
|
4181
|
+
if (error instanceof JsonBodyTooLargeError) {
|
|
4182
|
+
return jsonRes(res, 413, { ok: false, error: 'body_too_large' });
|
|
4183
|
+
}
|
|
4184
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
4185
|
+
}
|
|
4186
|
+
const claimedSessionId = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
4187
|
+
? raw.sessionId
|
|
4188
|
+
: undefined;
|
|
4189
|
+
const ds = typeof claimedSessionId === 'string'
|
|
4190
|
+
? findActiveBySessionId(claimedSessionId)
|
|
4191
|
+
: undefined;
|
|
4192
|
+
let registry = {};
|
|
4193
|
+
try {
|
|
4194
|
+
const parsed = JSON.parse(readFileSync(join(config.session.dataDir, 'orchestrate-dispatch.json'), 'utf8'));
|
|
4195
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
4196
|
+
registry = parsed;
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
catch { /* missing/malformed registry fails closed below */ }
|
|
4200
|
+
const decision = authorizeReportSessionRelayRequest({
|
|
4201
|
+
raw,
|
|
4202
|
+
trustedHost: isTrustedHostIpcRequest(req),
|
|
4203
|
+
session: ds
|
|
4204
|
+
? {
|
|
4205
|
+
sessionId: ds.session.sessionId,
|
|
4206
|
+
larkAppId: ds.larkAppId,
|
|
4207
|
+
receiver: !!ds.session.vcMeetingReceiver,
|
|
4208
|
+
scope: ds.scope ?? ds.session.scope,
|
|
4209
|
+
rootMessageId: ds.session.rootMessageId,
|
|
4210
|
+
...(ds.managedTurnOrigin ? { liveOrigin: ds.managedTurnOrigin } : {}),
|
|
4211
|
+
...(ds.session.quoteTargetId ? { quoteTargetId: ds.session.quoteTargetId } : {}),
|
|
4212
|
+
...(ds.session.replyTargets ? { replyTargets: ds.session.replyTargets } : {}),
|
|
4213
|
+
...((ds.currentReplyTarget ?? ds.session.currentReplyTarget)
|
|
4214
|
+
? { currentReplyTarget: ds.currentReplyTarget ?? ds.session.currentReplyTarget }
|
|
4215
|
+
: {}),
|
|
4216
|
+
}
|
|
4217
|
+
: undefined,
|
|
4218
|
+
selfLarkAppId: selfDaemonLarkAppId,
|
|
4219
|
+
registry,
|
|
4220
|
+
bindingSecret: loadOrCreateDashboardSecret(dispatchReportBindingSecretPath(config.session.dataDir)),
|
|
4221
|
+
});
|
|
4222
|
+
if (!decision.ok) {
|
|
4223
|
+
return jsonRes(res, decision.status, { ok: false, error: decision.error });
|
|
4224
|
+
}
|
|
4225
|
+
const targetDaemon = findOnlineDaemon(decision.target.larkAppId);
|
|
4226
|
+
if (!targetDaemon) {
|
|
4227
|
+
return jsonRes(res, 503, { ok: false, error: 'orchestrator_daemon_offline' });
|
|
4228
|
+
}
|
|
4229
|
+
const trigger = buildOrchestratorReportTrigger(decision, {
|
|
4230
|
+
requestId: `report:${decision.source.sessionId}:${Date.now()}`,
|
|
4231
|
+
receivedAt: new Date().toISOString(),
|
|
4232
|
+
});
|
|
4233
|
+
try {
|
|
4234
|
+
const response = await fetchDaemonIpc(targetDaemon.ipcPort, '/api/trigger', {
|
|
4235
|
+
method: 'POST',
|
|
4236
|
+
headers: { 'content-type': 'application/json' },
|
|
4237
|
+
body: JSON.stringify(trigger),
|
|
4238
|
+
});
|
|
4239
|
+
const responseBody = await response.json().catch(() => ({}));
|
|
4240
|
+
return jsonRes(res, response.status, {
|
|
4241
|
+
...(responseBody && typeof responseBody === 'object' && !Array.isArray(responseBody)
|
|
4242
|
+
? responseBody
|
|
4243
|
+
: {}),
|
|
4244
|
+
reportTarget: decision.target,
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
catch (error) {
|
|
4248
|
+
return jsonRes(res, 502, {
|
|
4249
|
+
ok: false,
|
|
4250
|
+
error: 'orchestrator_daemon_unreachable',
|
|
4251
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
3978
4255
|
// ─── v3 session relay:sandbox / read-isolation 会话的 workflow 变更通道 ─────
|
|
3979
4256
|
//
|
|
3980
4257
|
// 沙盒(Linux bwrap)/ read-isolation(macOS)里的 chat CLI 读不到宿主进程树
|
|
@@ -4532,6 +4809,57 @@ ipcRoute('GET', '/api/adopt-session/:pid', (_req, res, params) => {
|
|
|
4532
4809
|
}
|
|
4533
4810
|
return jsonRes(res, 404, { ok: false, error: 'no_adopt_session' });
|
|
4534
4811
|
});
|
|
4812
|
+
/**
|
|
4813
|
+
* 按 (cliId, cliSessionId) 在活跃会话里反查 CLI 原生会话绑定。
|
|
4814
|
+
*
|
|
4815
|
+
* 反查 identity 必须是组合键:OpenCode V1→V2 迁移会原样保留 ses_* id(同一
|
|
4816
|
+
* id 同时存在于 V1 session 与 V2 session_v2),只按 cliSessionId 匹配会把
|
|
4817
|
+
* V1 会话(cliId=opencode)当成唯一 hit,错投到 V1 话题。本查询只服务
|
|
4818
|
+
* opencode2 共享托管 service,命中必须恰好一个:0 个 → miss,≥2 个
|
|
4819
|
+
* (两个话题/机器人并发导入同一外部会话的重复绑定)→ conflict。
|
|
4820
|
+
*/
|
|
4821
|
+
export function matchCliSession(activeSessions, cliId, cliSessionId) {
|
|
4822
|
+
const matches = [...activeSessions].filter((ds) => ds.session.cliId === cliId && ds.session.cliSessionId === cliSessionId);
|
|
4823
|
+
if (matches.length === 0)
|
|
4824
|
+
return { kind: 'miss' };
|
|
4825
|
+
if (matches.length > 1)
|
|
4826
|
+
return { kind: 'conflict' };
|
|
4827
|
+
return { kind: 'hit', session: matches[0] };
|
|
4828
|
+
}
|
|
4829
|
+
// GET /api/session-by-cli/:cliId/:cliSessionId — 按 (cliId, cliSessionId) 反查
|
|
4830
|
+
// 活跃 botmux 会话路由。托管 service 场景(opencode2 的 ask 插件运行在所有
|
|
4831
|
+
// 客户端共用的 service 里):hook 子进程继承的是启动该 service 时固化的 ambient
|
|
4832
|
+
// env,与当前会话无关;必须用 payload 携带的 native sessionID 显式反查,否则
|
|
4833
|
+
// 跨会话错投(多 bot/多会话会放行或错投到首个启动 service 的会话)。
|
|
4834
|
+
// 反查 identity 必须是 (cliId, cliSessionId):OpenCode V1→V2 迁移会原样保留
|
|
4835
|
+
// ses_* id(真实环境验证同一 id 同时存在于 V1 session 与 V2 session_v2),
|
|
4836
|
+
// 只按 cliSessionId 匹配会把 V1 会话当成唯一 hit,V2 hook 的问题被投到 V1 话题。
|
|
4837
|
+
// 本端点只服务 opencode2 共享托管 service 的反查,强制 cliId === 'opencode2';
|
|
4838
|
+
// 多命中返回 409 conflict,让反查侧 fail closed(歧义时 passthrough)。
|
|
4839
|
+
ipcRoute('GET', '/api/session-by-cli/:cliId/:cliSessionId', (_req, res, params) => {
|
|
4840
|
+
const cliId = params.cliId;
|
|
4841
|
+
const cliSessionId = params.cliSessionId;
|
|
4842
|
+
if (cliId !== 'opencode2') {
|
|
4843
|
+
return jsonRes(res, 400, { ok: false, error: 'unsupported_cli' });
|
|
4844
|
+
}
|
|
4845
|
+
if (!cliSessionId || cliSessionId.length > 512) {
|
|
4846
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_cli_session_id' });
|
|
4847
|
+
}
|
|
4848
|
+
const match = matchCliSession(activeSessions.values(), cliId, cliSessionId);
|
|
4849
|
+
if (match.kind === 'miss') {
|
|
4850
|
+
return jsonRes(res, 404, { ok: false, error: 'no_session' });
|
|
4851
|
+
}
|
|
4852
|
+
if (match.kind === 'conflict') {
|
|
4853
|
+
return jsonRes(res, 409, { ok: false, error: 'ambiguous_cli_session' });
|
|
4854
|
+
}
|
|
4855
|
+
const ds = match.session;
|
|
4856
|
+
return jsonRes(res, 200, {
|
|
4857
|
+
sessionId: ds.session.sessionId,
|
|
4858
|
+
chatId: ds.chatId,
|
|
4859
|
+
larkAppId: ds.larkAppId,
|
|
4860
|
+
rootMessageId: sessionAnchorId(ds),
|
|
4861
|
+
});
|
|
4862
|
+
});
|
|
4535
4863
|
for (const path of ['/api/vc-meetings/members/register', '/api/vc-meetings/members/update']) {
|
|
4536
4864
|
ipcRoute('POST', path, async (req, res) => {
|
|
4537
4865
|
if (!guardVcMeetingDaemonControlRoute(req, res))
|
|
@@ -6375,6 +6703,7 @@ async function applyVcMeetingTemporaryAuthCommand(active, parsed, input) {
|
|
|
6375
6703
|
await sendMessage(session.larkAppId, listenerChatId, vcMeetingTemporaryAuthUsage(), 'text');
|
|
6376
6704
|
return true;
|
|
6377
6705
|
}
|
|
6706
|
+
input.onMutating?.();
|
|
6378
6707
|
const changed = [];
|
|
6379
6708
|
const unchanged = [];
|
|
6380
6709
|
for (const target of parsed.targets) {
|
|
@@ -6527,6 +6856,7 @@ async function handleVcMeetingTemporaryAuthCommand(input) {
|
|
|
6527
6856
|
mentions: input.mentions,
|
|
6528
6857
|
senderOpenId: input.senderOpenId,
|
|
6529
6858
|
senderUnionId: input.senderUnionId,
|
|
6859
|
+
onMutating: input.onMutating,
|
|
6530
6860
|
});
|
|
6531
6861
|
}
|
|
6532
6862
|
function clearVcMeetingOutputRequestTimer(req) {
|
|
@@ -11234,13 +11564,14 @@ async function startVcMeetingMonitoring(input) {
|
|
|
11234
11564
|
throw new Error('meeting already ended');
|
|
11235
11565
|
}
|
|
11236
11566
|
if (input.forceJoin || !session.joined || session.listenerPresenceStale) {
|
|
11237
|
-
|
|
11238
|
-
|
|
11567
|
+
const provisioned = ensureVcMeetingJoinProfile(input.larkAppId, input.cfg);
|
|
11568
|
+
if (!provisioned.ok) {
|
|
11569
|
+
throw new Error(vcMeetingJoinProfileErrorText(input.larkAppId, provisioned));
|
|
11239
11570
|
}
|
|
11240
11571
|
if (!meeting.meetingNo) {
|
|
11241
11572
|
throw new Error('会议事件没有 meeting_no,无法执行 BotJoinMeeting');
|
|
11242
11573
|
}
|
|
11243
|
-
const joined = joinMeetingAsBot({ meetingNumber: meeting.meetingNo, profile:
|
|
11574
|
+
const joined = joinMeetingAsBot({ meetingNumber: meeting.meetingNo, profile: provisioned.profile });
|
|
11244
11575
|
if (joined.meetingId && joined.meetingId !== meeting.id) {
|
|
11245
11576
|
if (input.forceJoin || session.listenerPresenceStale) {
|
|
11246
11577
|
throw new Error(`rejoin meeting id mismatch expected=${meeting.id} joined=${joined.meetingId}`);
|
|
@@ -12542,15 +12873,43 @@ async function handleVcMeetingPush(ctx) {
|
|
|
12542
12873
|
if (ctx.kind === 'meeting_invited') {
|
|
12543
12874
|
let meeting = ctx.meeting;
|
|
12544
12875
|
let joinedByInvite = false;
|
|
12545
|
-
|
|
12876
|
+
// Eager join: an invite that carries only a meeting_no (no meeting.id) must
|
|
12877
|
+
// be joined here to learn the id. A failure in THIS block used to only WARN
|
|
12878
|
+
// and then fall through to the silent "no meeting id yet" return below — so
|
|
12879
|
+
// the original "profile not found → bot rings forever" bug produced no user
|
|
12880
|
+
// signal at all (there is no session yet for the manual-invite DM path). We
|
|
12881
|
+
// now DM the owner here too, deduped per meeting_no so redelivered invites
|
|
12882
|
+
// don't spam.
|
|
12883
|
+
if (!meeting.id && meeting.meetingNo) {
|
|
12884
|
+
const eagerNo = meeting.meetingNo;
|
|
12885
|
+
const notifyEagerFailure = (errorText) => {
|
|
12886
|
+
const dmKey = vcMeetingEagerJoinFailureKey(ctx.larkAppId, eagerNo);
|
|
12887
|
+
const last = vcMeetingEagerJoinFailureNotified.get(dmKey);
|
|
12888
|
+
const now = Date.now();
|
|
12889
|
+
if (last !== undefined && now - last <= VC_MEETING_EAGER_JOIN_FAILURE_TTL_MS)
|
|
12890
|
+
return;
|
|
12891
|
+
vcMeetingEagerJoinFailureNotified.set(dmKey, now);
|
|
12892
|
+
// session is undefined here → notify skips per-session dedup and sends.
|
|
12893
|
+
notifyVcMeetingInviteFailure(ctx.larkAppId, cfg, undefined, meeting, errorText);
|
|
12894
|
+
};
|
|
12546
12895
|
try {
|
|
12547
|
-
|
|
12896
|
+
// A missing larkCliProfile previously skipped this whole block (the old
|
|
12897
|
+
// `&& cfg.larkCliProfile` guard) and died silently; surface it instead.
|
|
12898
|
+
const provisioned = ensureVcMeetingJoinProfile(ctx.larkAppId, cfg);
|
|
12899
|
+
if (!provisioned.ok)
|
|
12900
|
+
throw new Error(vcMeetingJoinProfileErrorText(ctx.larkAppId, provisioned));
|
|
12901
|
+
const joined = joinMeetingAsBot({ meetingNumber: eagerNo, profile: provisioned.profile });
|
|
12548
12902
|
meeting = { ...meeting, id: joined.meetingId };
|
|
12549
12903
|
joinedByInvite = true;
|
|
12550
|
-
|
|
12904
|
+
// Success → clear any prior failure tombstone so a later real failure
|
|
12905
|
+
// (e.g. rotated secret on a subsequent meeting) can notify again.
|
|
12906
|
+
vcMeetingEagerJoinFailureNotified.delete(vcMeetingEagerJoinFailureKey(ctx.larkAppId, eagerNo));
|
|
12907
|
+
logger.info(`[vc-agent] manual invite joined before session create meeting=${meeting.id} meetingNo=${eagerNo} profile=${provisioned.profile}`);
|
|
12551
12908
|
}
|
|
12552
12909
|
catch (err) {
|
|
12553
|
-
|
|
12910
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12911
|
+
logger.warn(`[vc-agent] manual invite join failed meetingNo=${eagerNo}: ${message}`);
|
|
12912
|
+
notifyEagerFailure(message);
|
|
12554
12913
|
}
|
|
12555
12914
|
}
|
|
12556
12915
|
if (!meeting.id) {
|
|
@@ -12575,6 +12934,7 @@ async function handleVcMeetingPush(ctx) {
|
|
|
12575
12934
|
});
|
|
12576
12935
|
if (!result.ok) {
|
|
12577
12936
|
logger.warn(`[vc-agent] manual invite start failed meeting=${meeting.id}: ${result.error}`);
|
|
12937
|
+
notifyVcMeetingInviteFailure(ctx.larkAppId, cfg, session, meeting, result.error);
|
|
12578
12938
|
}
|
|
12579
12939
|
else {
|
|
12580
12940
|
deleteVcMeetingPendingInvite(key);
|
|
@@ -12871,6 +13231,7 @@ export const __vcMeetingAgentTest = {
|
|
|
12871
13231
|
vcMeetingEndedTombstones.clear();
|
|
12872
13232
|
for (const key of [...vcMeetingPendingInvites.keys()])
|
|
12873
13233
|
deleteVcMeetingPendingInvite(key);
|
|
13234
|
+
vcMeetingEagerJoinFailureNotified.clear();
|
|
12874
13235
|
},
|
|
12875
13236
|
};
|
|
12876
13237
|
// ─── Event handling ──────────────────────────────────────────────────────────
|
|
@@ -13219,6 +13580,10 @@ function releaseQueuedActivationReservation(ds, acknowledgedToken) {
|
|
|
13219
13580
|
const bot = getBot(ds.larkAppId);
|
|
13220
13581
|
const cliId = ds.session.cliId ?? bot.config.cliId;
|
|
13221
13582
|
const rawCodexText = (ds.pendingCodexAppFollowUps ?? []).join('\n\n');
|
|
13583
|
+
const bufferedTurnIds = ds.pendingFollowUpTurnIds ?? [];
|
|
13584
|
+
const turnId = bufferedTurnIds[bufferedTurnIds.length - 1]
|
|
13585
|
+
?? ds.currentReplyTarget?.turnId
|
|
13586
|
+
?? `queued-activation-followup-${randomUUID()}`;
|
|
13222
13587
|
const followUp = buildFollowUpCliInput(buffered.join('\n\n'), ds.session.sessionId, {
|
|
13223
13588
|
cliId,
|
|
13224
13589
|
cliPathOverride: ds.session.cliPathOverride ?? bot.config.cliPathOverride,
|
|
@@ -13227,15 +13592,13 @@ function releaseQueuedActivationReservation(ds, acknowledgedToken) {
|
|
|
13227
13592
|
chatId: ds.chatId,
|
|
13228
13593
|
whiteboardId: ds.session.whiteboardId,
|
|
13229
13594
|
codexAppText: rawCodexText || buffered.join('\n\n'),
|
|
13595
|
+
sessionBackendType: ds.session.backendType,
|
|
13596
|
+
turnId,
|
|
13230
13597
|
codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
|
|
13231
13598
|
codexAppMessageContext: (ds.pendingCodexAppFollowUpContexts ?? [])
|
|
13232
13599
|
.filter(Boolean)
|
|
13233
13600
|
.join('\n\n'),
|
|
13234
13601
|
});
|
|
13235
|
-
const bufferedTurnIds = ds.pendingFollowUpTurnIds ?? [];
|
|
13236
|
-
const turnId = bufferedTurnIds[bufferedTurnIds.length - 1]
|
|
13237
|
-
?? ds.currentReplyTarget?.turnId
|
|
13238
|
-
?? `queued-activation-followup-${randomUUID()}`;
|
|
13239
13602
|
const reservation = reserveQueuedActivationTailAdmission(ds);
|
|
13240
13603
|
const bufferedGateDecisions = ds.pendingCodexAppFollowUpGateAccepted ?? [];
|
|
13241
13604
|
// Runtime buffers are rolling-upgrade compatibility only. If they carry an
|
|
@@ -13526,6 +13889,7 @@ function deliverPassthroughToExistingSession(ds, cmd, commandContent, anchor, la
|
|
|
13526
13889
|
content: commandContent,
|
|
13527
13890
|
turnId: turn.messageId,
|
|
13528
13891
|
});
|
|
13892
|
+
turn.onDelivered?.();
|
|
13529
13893
|
markSessionActivity(ds);
|
|
13530
13894
|
logger.info(`[${anchor.substring(0, 12)}] Passthrough ${cmd} → worker`);
|
|
13531
13895
|
return;
|
|
@@ -13534,7 +13898,7 @@ function deliverPassthroughToExistingSession(ds, cmd, commandContent, anchor, la
|
|
|
13534
13898
|
}
|
|
13535
13899
|
export const __testOnly_deliverPassthroughToExistingSession = deliverPassthroughToExistingSession;
|
|
13536
13900
|
async function startInitialPassthroughSession(args) {
|
|
13537
|
-
const { larkAppId, chatId, chatType, scope, anchor, messageId, replyRootId, parsed, cmd, commandContent, senderOpenId, substitute, senderUnionId, memberUnionId, ownerOpenId, ownerUnionId, creatorOpenId, botSender, senderIsBot, cardlessForceTopicSeed, routeToCanonicalOwner, } = args;
|
|
13901
|
+
const { larkAppId, chatId, chatType, scope, anchor, messageId, replyRootId, parsed, cmd, commandContent, senderOpenId, substitute, senderUnionId, memberUnionId, ownerOpenId, ownerUnionId, creatorOpenId, botSender, senderIsBot, cardlessForceTopicSeed, onDurablyAdmitted, routeToCanonicalOwner, } = args;
|
|
13538
13902
|
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, senderUnionId, memberUnionId, chatType, botSender)) {
|
|
13539
13903
|
return;
|
|
13540
13904
|
}
|
|
@@ -13625,9 +13989,14 @@ async function startInitialPassthroughSession(args) {
|
|
|
13625
13989
|
}
|
|
13626
13990
|
messageQueue.ensureQueue(anchor);
|
|
13627
13991
|
messageQueue.appendMessage(anchor, { ...parsed, content: commandContent });
|
|
13992
|
+
// 接纳标记按分支下沉,不绑在 appendMessage 上:queues/*.jsonl 仅供 dashboard
|
|
13993
|
+
// 预览,无投递重放语义;且 replyInvalidWorkingDirs 是「放弃本轮」分支(关闭
|
|
13994
|
+
// 会话后才回复),放弃前的失败必须保持「请重发」提示。
|
|
13628
13995
|
if (pinnedWorkingDir && autoWt) {
|
|
13629
13996
|
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
13630
13997
|
return;
|
|
13998
|
+
// 已 durable staging(auto_worktree)且通过放弃闸:detached 建库流程接管投递。
|
|
13999
|
+
onDurablyAdmitted?.();
|
|
13631
14000
|
ds.initialStartPending = false; // pendingRepo/worktree now owns buffering
|
|
13632
14001
|
// 挂起态提交:worktree 建好后经 runAutoWorktreeCommit → commitRepoSelection 拉起
|
|
13633
14002
|
// pendingRawInput 冷启动会话。detach → 立即返回,不阻塞本条消息处理。
|
|
@@ -13645,6 +14014,8 @@ async function startInitialPassthroughSession(args) {
|
|
|
13645
14014
|
});
|
|
13646
14015
|
const availableBots = await getAvailableBots(larkAppId, chatId);
|
|
13647
14016
|
forkReservedInitialRawSession(ds, availableBots);
|
|
14017
|
+
// fork 成功即开场已交给 CLI;fork 抛错则本轮只存在于内存,保持重发提示。
|
|
14018
|
+
onDurablyAdmitted?.();
|
|
13648
14019
|
const reason = oncallEntry
|
|
13649
14020
|
? `oncall-bound chat ${chatId}`
|
|
13650
14021
|
: inheritedFrom
|
|
@@ -13660,6 +14031,9 @@ async function startInitialPassthroughSession(args) {
|
|
|
13660
14031
|
if (projects.length > 0) {
|
|
13661
14032
|
ds.initialStartPending = false; // pendingRepo/card now owns buffering
|
|
13662
14033
|
lastRepoScan.set(chatId, projects);
|
|
14034
|
+
// 已 durable staging(picker)且通过放弃闸;接下来卡片发送失败不得诱导重发
|
|
14035
|
+
//(重发会在 pendingRepo 缓冲里再入一份)。
|
|
14036
|
+
onDurablyAdmitted?.();
|
|
13663
14037
|
const cardJson = buildRepoSelectCard(projects, getSessionWorkingDir(ds), anchor, localeForBot(larkAppId), getBot(larkAppId).config.worktreeMultiPicker);
|
|
13664
14038
|
ds.repoCardMessageId = await sessionReply(anchor, cardJson, 'interactive', larkAppId);
|
|
13665
14039
|
persistPendingRepoCardMessageId(ds, ds.repoCardMessageId);
|
|
@@ -13667,6 +14041,9 @@ async function startInitialPassthroughSession(args) {
|
|
|
13667
14041
|
logger.info(`[${tag(ds)}] Waiting for repo selection before initial raw passthrough (${projects.length} projects)`);
|
|
13668
14042
|
return;
|
|
13669
14043
|
}
|
|
14044
|
+
// 已 durable staging(picker,含 rawInput)且通过放弃闸:fork 失败后本条 raw
|
|
14045
|
+
// passthrough 仍 durable 挂在会话上,重发会重复执行非幂等命令——先打接纳标。
|
|
14046
|
+
onDurablyAdmitted?.();
|
|
13670
14047
|
ds.pendingRepo = false;
|
|
13671
14048
|
await maybeSeedCardlessForceTopicTurn({
|
|
13672
14049
|
ds,
|
|
@@ -13694,8 +14071,49 @@ function mergeVcMeetingApplicationContext(existing, incoming) {
|
|
|
13694
14071
|
return undefined;
|
|
13695
14072
|
return `${[...new Set(lines)].join('\n')}\n`;
|
|
13696
14073
|
}
|
|
14074
|
+
/** 本轮 inbound 已被真正接纳后打标:durable 写入(queuedActivationTail /
|
|
14075
|
+
* pendingRepo staging)且已通过 replyInvalidWorkingDirs 之类的放弃闸,或已实际
|
|
14076
|
+
* 送达 worker(live send 被接受 / refork·初次冷 fork 成功 / PTY raw input 已写入 /
|
|
14077
|
+
* vc-auth 真实变更开始)。标记落在 ctx.ingressAdmission 共享 box 上(见其字段
|
|
14078
|
+
* 注释),notifyOrdinaryIngressFailure 据此区分「没送达请重发」与「已接纳勿重发」。
|
|
14079
|
+
* messageQueue.appendMessage 不算接纳——queues/*.jsonl 仅供 dashboard 预览,
|
|
14080
|
+
* 没有投递重放语义;handleCommand 命令路径不参与(其内部 catch 吞掉一切异常)。 */
|
|
14081
|
+
function markIngressAdmitted(ctx) {
|
|
14082
|
+
if (ctx.ingressAdmission)
|
|
14083
|
+
ctx.ingressAdmission.admitted = true;
|
|
14084
|
+
else
|
|
14085
|
+
ctx.ingressAdmission = { admitted: true };
|
|
14086
|
+
}
|
|
14087
|
+
/**
|
|
14088
|
+
* 普通消息处理链的终态失败收口:transport 已 ACK(用户看到消息发出去了),但
|
|
14089
|
+
* 异常把整条投递链掀翻——此前只剩 dispatcher 的 log-only catch,用户视角就是
|
|
14090
|
+
* 机器人吞消息。best-effort 回一条可行动提示后原样重抛,dispatcher 既有的错误
|
|
14091
|
+
* 日志与调用方语义不变。rejected 类失败(quota/权限拦截)由各自路径回复后正常
|
|
14092
|
+
* return,不会走到这里。
|
|
14093
|
+
*
|
|
14094
|
+
* 提示文案按接纳阶段二选一:本轮一旦被 durable queue / worker 接纳
|
|
14095
|
+
* (ctx.ingressAdmission.admitted,各接纳点经 markIngressAdmitted 翻转),失败
|
|
14096
|
+
* 只可能出在接纳之后的状态回复等 UX 支路——此时绝不能提示「请重发」,否则一次
|
|
14097
|
+
* Lark 瞬时发送故障会被放大成同一任务再次入队、CLI 重复执行;改为「已接收,
|
|
14098
|
+
* 勿重发」的澄清。未接纳的失败保持原有重发提示。
|
|
14099
|
+
*/
|
|
14100
|
+
async function notifyOrdinaryIngressFailure(ctx, err) {
|
|
14101
|
+
const replyAnchor = ctx.scope === 'thread' ? ctx.anchor : ctx.chatId;
|
|
14102
|
+
const noticeKey = ctx.ingressAdmission?.admitted
|
|
14103
|
+
? 'daemon.ordinary_ingress_admitted_reply_failed'
|
|
14104
|
+
: 'daemon.ordinary_ingress_failed';
|
|
14105
|
+
try {
|
|
14106
|
+
await sessionReply(replyAnchor, tr(noticeKey, undefined, localeForBot(ctx.larkAppId)), 'text', ctx.larkAppId);
|
|
14107
|
+
}
|
|
14108
|
+
catch (noticeErr) {
|
|
14109
|
+
logger.warn(`[${ctx.larkAppId}] ordinary ingress failure notice for ${ctx.messageId.substring(0, 12)} `
|
|
14110
|
+
+ `could not be delivered: ${noticeErr instanceof Error ? noticeErr.message : String(noticeErr)}`);
|
|
14111
|
+
}
|
|
14112
|
+
throw err;
|
|
14113
|
+
}
|
|
13697
14114
|
async function handleNewTopic(data, ctx) {
|
|
13698
|
-
|
|
14115
|
+
ctx.ingressAdmission ??= { admitted: false };
|
|
14116
|
+
return withBotTurnAdmission(ctx.larkAppId, () => handleNewTopicAdmitted(data, ctx)).catch(err => notifyOrdinaryIngressFailure(ctx, err));
|
|
13699
14117
|
}
|
|
13700
14118
|
/**
|
|
13701
14119
|
* Codex App steer authorization (Blocking 1, decision A) from INBOUND SOURCE
|
|
@@ -13724,10 +14142,53 @@ function computeCodexAppSteerable(facts) {
|
|
|
13724
14142
|
}
|
|
13725
14143
|
async function handleNewTopicAdmitted(data, ctx) {
|
|
13726
14144
|
const { chatId, messageId, chatType, larkAppId, replyRootId, substituteTrigger, messageListener } = ctx;
|
|
14145
|
+
// Session-group birth re-homes the turn into the new group: replies/quotes
|
|
14146
|
+
// anchor on the in-group intro message, while `messageId` (the ORIGINAL
|
|
14147
|
+
// inbound message) keeps serving resource downloads / merge-forward
|
|
14148
|
+
// expansion / dedup — their keys belong to the source message.
|
|
14149
|
+
const replyAnchorId = ctx.replyAnchorMessageId ?? messageId;
|
|
13727
14150
|
// scope/anchor are mutable here: `/t` / `/topic` may flip a 普通群 chat-scope
|
|
13728
14151
|
// routing into thread-scope so the bot's first reply seeds a Lark thread.
|
|
13729
14152
|
let scope = ctx.scope;
|
|
13730
14153
|
let anchor = ctx.anchor;
|
|
14154
|
+
// ─── p2pMode='group':会话群出生 ────────────────────────────────────────
|
|
14155
|
+
// group 模式下,顶层 DM 消息在任何会话机制启动前先改道:建一个专属会话群
|
|
14156
|
+
//(bot 保留群主),把本轮 context 重写到新群(chat-scope)后递归。返回 null
|
|
14157
|
+
// 表示跳过/失败(斜杠命令、建群失败已私聊提示),落回旧的 DM 话题行为。
|
|
14158
|
+
// sessionGroupBirth 防递归;scope/anchor 形状约束保证只有全新顶层种子会出生
|
|
14159
|
+
//(回复旧 DM 话题的消息仍归原会话)。thread_id 检查兜住「话题内发送 + 同时
|
|
14160
|
+
// 发送到会话」的消息形态:它的事件 root_id 为空(顶层形态)但携带 thread_id
|
|
14161
|
+
// ——语境属于既有话题,绝不能当全新会话去建群;放行后按旧 thread 行为在
|
|
14162
|
+
// 原 DM 落话题(用户实测反馈的误建群场景)。
|
|
14163
|
+
if (chatType === 'p2p'
|
|
14164
|
+
&& getBot(larkAppId).config.p2pMode === 'group'
|
|
14165
|
+
&& !ctx.sessionGroupBirth
|
|
14166
|
+
&& !substituteTrigger
|
|
14167
|
+
&& scope === 'thread'
|
|
14168
|
+
&& anchor === messageId
|
|
14169
|
+
&& !data?.message?.thread_id) {
|
|
14170
|
+
const reborn = await maybeBirthSessionGroup(data, ctx);
|
|
14171
|
+
if (reborn)
|
|
14172
|
+
return handleNewTopic(data, reborn);
|
|
14173
|
+
}
|
|
14174
|
+
// ─── 会话群同群续聊 ─────────────────────────────────────────────────────
|
|
14175
|
+
// 会话群里的新消息发现登记的会话已关闭时,自动 resume 原会话(与话题内续聊
|
|
14176
|
+
// 同款体验),而不是新开 CLI。恢复失败则照常走全新会话。
|
|
14177
|
+
if (chatType === 'group' && scope === 'chat' && !ctx.sessionGroupBirth) {
|
|
14178
|
+
const sgEntry = getSessionGroup(chatId);
|
|
14179
|
+
if (sgEntry?.lastSessionId) {
|
|
14180
|
+
const prevSession = sessionStore.getSession(sgEntry.lastSessionId);
|
|
14181
|
+
if (prevSession?.status === 'closed') {
|
|
14182
|
+
const resumed = await resumeSession(sgEntry.lastSessionId, activeSessions);
|
|
14183
|
+
if (resumed.ok) {
|
|
14184
|
+
touchSessionGroup(chatId);
|
|
14185
|
+
logger.info(`[session-group] resumed session=${sgEntry.lastSessionId.substring(0, 8)} in chat=${chatId.substring(0, 12)}`);
|
|
14186
|
+
return handleThreadReply(data, ctx);
|
|
14187
|
+
}
|
|
14188
|
+
logger.warn(`[session-group] resume failed (${resumed.error}) chat=${chatId.substring(0, 12)}; spawning fresh session`);
|
|
14189
|
+
}
|
|
14190
|
+
}
|
|
14191
|
+
}
|
|
13731
14192
|
const numberer = createImgNumberer();
|
|
13732
14193
|
let forwardSeedContent = '';
|
|
13733
14194
|
let forwardSeedResources = [];
|
|
@@ -13883,17 +14344,31 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13883
14344
|
return;
|
|
13884
14345
|
}
|
|
13885
14346
|
// Intercept daemon commands in new topics (no session needed for some commands)
|
|
13886
|
-
|
|
14347
|
+
// acceptSlashFromBots gate: a bot sender's slash command is only routed as a
|
|
14348
|
+
// command when this bot opts in (default on). When off, fall through to
|
|
14349
|
+
// ordinary message handling — the peer bot can still talk, it just can't drive
|
|
14350
|
+
// /clear /model /close … into this bot. Human senders are never gated here.
|
|
14351
|
+
const senderIsBotForSlashGate = isBotSenderType || isForeignBotSender;
|
|
14352
|
+
const invocation = (senderIsBotForSlashGate && !botAcceptsSlashFromBots(larkAppId))
|
|
14353
|
+
? null
|
|
14354
|
+
: parseSlashCommandInvocation(cmdContent);
|
|
13887
14355
|
if (invocation) {
|
|
13888
14356
|
const { cmd, content: commandContent } = invocation;
|
|
14357
|
+
const invocationDeps = commandDepsForInvocation({
|
|
14358
|
+
scope,
|
|
14359
|
+
chatId,
|
|
14360
|
+
anchor,
|
|
14361
|
+
messageId: parsed.messageId,
|
|
14362
|
+
replyRootId,
|
|
14363
|
+
});
|
|
13889
14364
|
const restrictedText = grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
13890
14365
|
if (restrictedText) {
|
|
13891
|
-
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
14366
|
+
await invocationDeps.sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
13892
14367
|
return;
|
|
13893
14368
|
}
|
|
13894
14369
|
if (cmd === '/vc-auth') {
|
|
13895
14370
|
if (!canOperate(larkAppId, chatId, senderOpenId, teamTrustUnionId)) {
|
|
13896
|
-
await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
14371
|
+
await invocationDeps.sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13897
14372
|
return;
|
|
13898
14373
|
}
|
|
13899
14374
|
await handleVcMeetingTemporaryAuthCommand({
|
|
@@ -13904,6 +14379,9 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13904
14379
|
mentions: parsed.mentions,
|
|
13905
14380
|
senderOpenId,
|
|
13906
14381
|
senderUnionId,
|
|
14382
|
+
// help/invalid/无监听等纯拒绝分支不打接纳标(回复失败应保持「请重发」);
|
|
14383
|
+
// 只有真正开始改临时授权状态后,结果回复失败才不得诱导重发。
|
|
14384
|
+
onMutating: () => markIngressAdmitted(ctx),
|
|
13907
14385
|
});
|
|
13908
14386
|
return;
|
|
13909
14387
|
}
|
|
@@ -13911,14 +14389,14 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13911
14389
|
// summon has nothing to show in a brand-new topic. Route here so the generic
|
|
13912
14390
|
// daemon-command block below does not pre-create a worker=null session.
|
|
13913
14391
|
if (cmd === '/card') {
|
|
13914
|
-
await handleCardCommand(anchor, larkAppId, chatId, senderOpenId, commandContent,
|
|
14392
|
+
await handleCardCommand(anchor, larkAppId, chatId, senderOpenId, commandContent, invocationDeps);
|
|
13915
14393
|
return;
|
|
13916
14394
|
}
|
|
13917
14395
|
// /term needs a live session's terminal; in a brand-new topic there's none.
|
|
13918
14396
|
// Route here (own owner-gate inside) so the generic block below doesn't
|
|
13919
14397
|
// pre-create a worker=null phantom session just to reply "no session".
|
|
13920
14398
|
if (cmd === '/term') {
|
|
13921
|
-
await handleTermLinkCommand(anchor, larkAppId, chatId, senderOpenId, commandContent,
|
|
14399
|
+
await handleTermLinkCommand(anchor, larkAppId, chatId, senderOpenId, commandContent, invocationDeps);
|
|
13922
14400
|
return;
|
|
13923
14401
|
}
|
|
13924
14402
|
if (resolvePassthroughCommands(larkAppId).has(cmd)) {
|
|
@@ -13949,6 +14427,7 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13949
14427
|
ownerOpenId: senderOpenId,
|
|
13950
14428
|
ownerUnionId: senderUnionId,
|
|
13951
14429
|
creatorOpenId: senderOpenId,
|
|
14430
|
+
onDurablyAdmitted: () => markIngressAdmitted(ctx),
|
|
13952
14431
|
routeToCanonicalOwner: () => handleThreadReplyAdmitted(data, {
|
|
13953
14432
|
...ctx,
|
|
13954
14433
|
scope,
|
|
@@ -13957,7 +14436,7 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13957
14436
|
});
|
|
13958
14437
|
return;
|
|
13959
14438
|
}
|
|
13960
|
-
await sessionReply(anchor, tr('daemon.cmd_requires_session', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
14439
|
+
await invocationDeps.sessionReply(anchor, tr('daemon.cmd_requires_session', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13961
14440
|
return;
|
|
13962
14441
|
}
|
|
13963
14442
|
if (DAEMON_COMMANDS.has(cmd)) {
|
|
@@ -13969,7 +14448,7 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13969
14448
|
// canRunDaemonCommand = canOperate ∪(cmd ∈ canTalkDaemonCommands && canTalk):
|
|
13970
14449
|
// bot 可通过名单把选定命令(如 /status)降到 canTalk;未配置时与 canOperate 全等。
|
|
13971
14450
|
if (!canRunDaemonCommand(larkAppId, chatId, senderOpenId, teamTrustUnionId, cmd, senderUnionId, chatType, isBotSenderType, isBotSenderType)) {
|
|
13972
|
-
await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
14451
|
+
await invocationDeps.sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13973
14452
|
return;
|
|
13974
14453
|
}
|
|
13975
14454
|
// `/group` (`/g`) doesn't open a conversation — creating a sessionStore
|
|
@@ -13980,7 +14459,7 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13980
14459
|
// Fast-ACK: run detached so the WS event ack isn't blocked on /group's
|
|
13981
14460
|
// slow Lark API work → no Feishu redelivery → no duplicate group.
|
|
13982
14461
|
// See fireSessionlessCommandDetached.
|
|
13983
|
-
fireSessionlessCommandDetached(cmd, anchor, { ...parsed, content: commandContent, chatId }, larkAppId);
|
|
14462
|
+
fireSessionlessCommandDetached(cmd, anchor, { ...parsed, content: commandContent, chatId }, larkAppId, invocationDeps);
|
|
13984
14463
|
return;
|
|
13985
14464
|
}
|
|
13986
14465
|
// These commands operate on an EXISTING session; a brand-new topic has none. Route
|
|
@@ -13991,7 +14470,7 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
13991
14470
|
// permission gates inside their handlers) this branch MUST stay after
|
|
13992
14471
|
// the canOperate gate above — their handlers do not repeat that gate.
|
|
13993
14472
|
if (EXISTING_SESSION_ONLY_DAEMON_COMMANDS.has(cmd)) {
|
|
13994
|
-
await handleCommand(cmd, anchor, { ...parsed, content: commandContent },
|
|
14473
|
+
await handleCommand(cmd, anchor, { ...parsed, content: commandContent }, invocationDeps, larkAppId);
|
|
13995
14474
|
return;
|
|
13996
14475
|
}
|
|
13997
14476
|
// Same rootMessageId reasoning as below in the main spawn path:
|
|
@@ -14051,11 +14530,13 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14051
14530
|
else if (cmdDs.pendingRepo) {
|
|
14052
14531
|
stageClaimedPendingRepoSetup(activeSessions, cmdDs, {
|
|
14053
14532
|
mode: 'picker',
|
|
14054
|
-
|
|
14533
|
+
// Turn identity unified on the reply anchor (slash commands never
|
|
14534
|
+
// birth a session group, so this always equals messageId today).
|
|
14535
|
+
turnId: replyAnchorId,
|
|
14055
14536
|
});
|
|
14056
14537
|
}
|
|
14057
14538
|
// Pass mention-stripped content so /command argument parsing works.
|
|
14058
|
-
await handleCommand(cmd, anchor, { ...parsed, content: commandContent },
|
|
14539
|
+
await handleCommand(cmd, anchor, { ...parsed, content: commandContent }, invocationDeps, larkAppId);
|
|
14059
14540
|
return;
|
|
14060
14541
|
}
|
|
14061
14542
|
}
|
|
@@ -14169,9 +14650,16 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14169
14650
|
// disconnect click silently no-ops.
|
|
14170
14651
|
// For chat-scope, rootMessageId stores the seed message_id (audit only);
|
|
14171
14652
|
// routing keys off chatId via sessionAnchorId(), so any value works.
|
|
14172
|
-
|
|
14653
|
+
// Session-group births store the in-group intro (reply anchor) here so the
|
|
14654
|
+
// audit trail points at a message that actually lives in the session's chat.
|
|
14655
|
+
const rootIdForStore = scope === 'thread' ? anchor : replyAnchorId;
|
|
14173
14656
|
const initialTurnTitle = (messageListener?.replyCardTitle ?? (ctx.forwardSeedData ? followupContent : content)).substring(0, 50);
|
|
14174
14657
|
const session = sessionStore.createSession(chatId, rootIdForStore, initialTurnTitle, chatType);
|
|
14658
|
+
// Session-group registry: point the group at its (new) resident session so
|
|
14659
|
+
// same-group resume and the async AI title can find it.
|
|
14660
|
+
if (chatType === 'group' && isSessionGroup(chatId)) {
|
|
14661
|
+
touchSessionGroup(chatId, session.sessionId);
|
|
14662
|
+
}
|
|
14175
14663
|
const now = Date.now();
|
|
14176
14664
|
setDirectChatDisplayNameFromSender(session, chatType, newTopicSender);
|
|
14177
14665
|
const groupChatName = await groupChatNamePromise;
|
|
@@ -14189,7 +14677,11 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14189
14677
|
// `botmux send` can --mention-back / 引用 the triggering message (chat scope).
|
|
14190
14678
|
// Without this the first reply hits hasQuoteTargetSender=false (exit 2) and
|
|
14191
14679
|
// chat-scope首条不引用. Use the event's sender open_id (correct app scope).
|
|
14192
|
-
|
|
14680
|
+
// Keyed off the REPLY anchor (== messageId except for session-group births,
|
|
14681
|
+
// where it is the in-group intro message so quotes stay in the group instead
|
|
14682
|
+
// of leaking to the source DM — while messageId itself keeps serving
|
|
14683
|
+
// resource/merge-forward lookups against the original inbound message).
|
|
14684
|
+
session.quoteTargetId = replyAnchorId;
|
|
14193
14685
|
session.quoteTargetSenderOpenId = senderOpenId;
|
|
14194
14686
|
session.quoteTargetSenderIsBot = isForeignBotSender || parsed.senderType === 'app' || parsed.senderType === 'bot';
|
|
14195
14687
|
session.lastMessageAt = new Date(now).toISOString();
|
|
@@ -14219,7 +14711,10 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14219
14711
|
initialStartPending: true,
|
|
14220
14712
|
pendingRepo: !pinnedWorkingDir || autoWt,
|
|
14221
14713
|
pendingPrompt: promptContent,
|
|
14222
|
-
|
|
14714
|
+
// Turn identity must equal the reply anchor so current-turn provenance
|
|
14715
|
+
// (quoteTargetId === marker.turnId) holds on session-group first turns.
|
|
14716
|
+
// Outside a birth replyAnchorId === messageId, so plain paths are unchanged.
|
|
14717
|
+
pendingTurnId: replyAnchorId,
|
|
14223
14718
|
pendingCodexAppText: codexAppVisibleText,
|
|
14224
14719
|
pendingCodexAppApplicationContext: codexAppApplicationContext || undefined,
|
|
14225
14720
|
pendingCodexAppMessageContext: codexAppMessageContext,
|
|
@@ -14267,7 +14762,10 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14267
14762
|
// exact same set (a race-losing scratch's seed @s must not vanish).
|
|
14268
14763
|
const newTopicPostAt = collectPostAtMentions(data?.message, ctx.forwardSeedData?.message);
|
|
14269
14764
|
const newTopicWindow = buildTurnParticipants(larkAppId, senderOpenId, senderIsBotTriState(parsed.senderType, isForeignBotSender), parsed.mentions, newTopicSender?.name, newTopicPostAt);
|
|
14270
|
-
|
|
14765
|
+
// Turn key is the reply anchor (== messageId outside session-group births) so
|
|
14766
|
+
// the per-turn reply context and currentReplyTarget.turnId line up with the
|
|
14767
|
+
// worker's turn id — current-turn provenance requires that equality.
|
|
14768
|
+
beginReplyTargetTurn(ds, replyRootId, replyAnchorId, new Date().toISOString(), { quoteOnly: substituteReplyMode === 'quote', substitute: !!substituteTrigger, senderOpenId, participants: newTopicWindow.participants, participantsIncomplete: newTopicWindow.incomplete });
|
|
14271
14769
|
sessionStore.updateSession(ds.session);
|
|
14272
14770
|
const registration = await claimNewDaemonSession(activeSessions, ds);
|
|
14273
14771
|
if (!registration.accepted) {
|
|
@@ -14298,17 +14796,25 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14298
14796
|
stageClaimedPendingRepoSetup(activeSessions, ds, {
|
|
14299
14797
|
mode: autoWt ? 'auto_worktree' : 'picker',
|
|
14300
14798
|
...(autoWt && pinnedWorkingDir ? { baseDir: pinnedWorkingDir } : {}),
|
|
14301
|
-
turnId
|
|
14799
|
+
// Persisted turn identity feeds the eventual fork's turnId; it must be the
|
|
14800
|
+
// reply anchor so provenance (quoteTargetId === marker.turnId) holds on
|
|
14801
|
+
// session-group first turns that go through the repo picker / worktree.
|
|
14802
|
+
turnId: replyAnchorId,
|
|
14302
14803
|
});
|
|
14303
14804
|
}
|
|
14304
14805
|
messageQueue.ensureQueue(anchor);
|
|
14305
14806
|
messageQueue.appendMessage(anchor, parsed);
|
|
14807
|
+
// 接纳标记按分支下沉,不绑在 appendMessage 上:queues/*.jsonl 仅供 dashboard
|
|
14808
|
+
// 预览,无投递重放语义;且 replyInvalidWorkingDirs 是「放弃本轮」分支(关闭
|
|
14809
|
+
// 会话后才回复),放弃前的失败必须保持「请重发」提示。
|
|
14306
14810
|
// Auto-worktree: session is registered PENDING; build the worktree off the
|
|
14307
14811
|
// critical path, then commitRepoSelection pins it + forks (folding in any
|
|
14308
14812
|
// messages buffered during creation). detach → return immediately.
|
|
14309
14813
|
if (pinnedWorkingDir && autoWt) {
|
|
14310
14814
|
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
14311
14815
|
return;
|
|
14816
|
+
// 已 durable staging(auto_worktree)且通过放弃闸:detached 建库流程接管投递。
|
|
14817
|
+
markIngressAdmitted(ctx);
|
|
14312
14818
|
ds.initialStartPending = false; // pendingRepo/worktree now owns buffering
|
|
14313
14819
|
startAutoWorktreePending(ds, { anchor, baseDir: pinnedWorkingDir, title: session.title, prompt: promptContent, operatorOpenId: senderOpenId });
|
|
14314
14820
|
return;
|
|
@@ -14325,8 +14831,12 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14325
14831
|
messageId,
|
|
14326
14832
|
});
|
|
14327
14833
|
const availableBots = await getAvailableBots(larkAppId, chatId);
|
|
14328
|
-
|
|
14834
|
+
// Ack reaction targets the ORIGINAL inbound message (2nd arg); the turn id
|
|
14835
|
+
// (5th arg) is the reply anchor so provenance holds on session-group births.
|
|
14836
|
+
await noteTurnReceived(ds, messageId, content, newTopicSender, replyAnchorId, substituteTrigger ? SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE : undefined);
|
|
14329
14837
|
forkReservedInitialSession(ds, availableBots);
|
|
14838
|
+
// fork 成功即开场已交给 CLI;fork 抛错则开场只存在于内存,保持重发提示。
|
|
14839
|
+
markIngressAdmitted(ctx);
|
|
14330
14840
|
const reason = oncallEntry
|
|
14331
14841
|
? `oncall-bound chat ${chatId}`
|
|
14332
14842
|
: inheritedFrom
|
|
@@ -14348,6 +14858,9 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14348
14858
|
if (projects.length > 0) {
|
|
14349
14859
|
ds.initialStartPending = false; // pendingRepo/card now owns buffering
|
|
14350
14860
|
lastRepoScan.set(chatId, projects);
|
|
14861
|
+
// 已 durable staging(picker)且通过放弃闸;接下来卡片发送失败不得诱导重发
|
|
14862
|
+
//(重发会在 pendingRepo 缓冲里再入一份)。
|
|
14863
|
+
markIngressAdmitted(ctx);
|
|
14351
14864
|
const currentCwd = getSessionWorkingDir(ds);
|
|
14352
14865
|
const cardJson = buildRepoSelectCard(projects, currentCwd, anchor, localeForBot(larkAppId), getBot(larkAppId).config.worktreeMultiPicker);
|
|
14353
14866
|
ds.repoCardMessageId = await sessionReply(anchor, cardJson, 'interactive', larkAppId);
|
|
@@ -14356,7 +14869,11 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14356
14869
|
logger.info(`[${tag(ds)}] Waiting for repo selection (${projects.length} projects)`);
|
|
14357
14870
|
}
|
|
14358
14871
|
else {
|
|
14359
|
-
// No projects found — skip repo selection, spawn directly
|
|
14872
|
+
// No projects found — skip repo selection, spawn directly.
|
|
14873
|
+
// 已 durable staging(picker)且通过放弃闸:即使下面 fork 失败,opening 仍由
|
|
14874
|
+
// 会话 durable 持有(pre-init 回滚恢复 queued journal,重启复活),重发会
|
|
14875
|
+
// 重复执行——先打接纳标。
|
|
14876
|
+
markIngressAdmitted(ctx);
|
|
14360
14877
|
ds.pendingRepo = false;
|
|
14361
14878
|
ensureSessionWhiteboard(ds);
|
|
14362
14879
|
await maybeSeedCardlessForceTopicTurn({
|
|
@@ -14366,7 +14883,9 @@ async function handleNewTopicAdmitted(data, ctx) {
|
|
|
14366
14883
|
messageId,
|
|
14367
14884
|
});
|
|
14368
14885
|
const availableBots = await getAvailableBots(larkAppId, chatId);
|
|
14369
|
-
|
|
14886
|
+
// Ack reaction targets the ORIGINAL inbound message (2nd arg); the turn id
|
|
14887
|
+
// (5th arg) is the reply anchor so provenance holds on session-group births.
|
|
14888
|
+
await noteTurnReceived(ds, messageId, content, newTopicSender, replyAnchorId, substituteTrigger ? SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE : undefined);
|
|
14370
14889
|
forkReservedInitialSession(ds, availableBots);
|
|
14371
14890
|
logger.info(`Session ${session.sessionId} ready (no projects to select), total active: ${getActiveCount()}`);
|
|
14372
14891
|
}
|
|
@@ -14555,11 +15074,16 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId, opts) {
|
|
|
14555
15074
|
// synthetic instruction. Codex App still needs a non-blank visible
|
|
14556
15075
|
// UserMessage; reuse the localized session title only in its clean sidecar.
|
|
14557
15076
|
const codexAppText = promptBody || title;
|
|
14558
|
-
// Pick scope + anchor. 话题群 → seed a topic and
|
|
14559
|
-
//
|
|
15077
|
+
// Pick scope + anchor. 话题群 或 普通群 new-topic 模式 → seed a topic and
|
|
15078
|
+
// anchor thread-scope there;其余普通群 → chat-scope anchored at chatId。
|
|
15079
|
+
// 复用入站 @ 的路由判据(externalEventOpensOwnTopic):否则「自动进群主动开工」
|
|
15080
|
+
// 会无视群配的 new-topic,把首轮平铺进群顶层 chat-scope,而不是像用户手动 @
|
|
15081
|
+
// 那样开一个独立话题。
|
|
15082
|
+
const regularGroupMode = resolveRegularGroupMode(larkAppId, chatId);
|
|
15083
|
+
const opensOwnTopic = externalEventOpensOwnTopic(mode, regularGroupMode);
|
|
14560
15084
|
let scope;
|
|
14561
15085
|
let anchor;
|
|
14562
|
-
if (
|
|
15086
|
+
if (opensOwnTopic) {
|
|
14563
15087
|
const seedText = tr('daemon.auto_start_join_seed', undefined, localeForBot(larkAppId));
|
|
14564
15088
|
anchor = await sendMessage(larkAppId, chatId, seedText, 'text');
|
|
14565
15089
|
scope = 'thread';
|
|
@@ -14574,7 +15098,7 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId, opts) {
|
|
|
14574
15098
|
return;
|
|
14575
15099
|
}
|
|
14576
15100
|
const needsSharedReply = mode === 'group'
|
|
14577
|
-
&&
|
|
15101
|
+
&& regularGroupMode === 'shared';
|
|
14578
15102
|
const { pinnedWorkingDir, pinnedFromBotDefault } = await resolvePinnedWorkingDir({ scope, anchor, chatId, chatType, larkAppId });
|
|
14579
15103
|
const autoWt = willAutoWorktree(larkAppId, pinnedWorkingDir, pinnedFromBotDefault);
|
|
14580
15104
|
refreshCliVersion(botCfg);
|
|
@@ -14852,7 +15376,8 @@ async function handleThreadReply(data, ctx, prepared) {
|
|
|
14852
15376
|
// quota/resource/sender preparation awaits. Synthetic keys share the same
|
|
14853
15377
|
// lock registry without occupying or mutating an active-session slot.
|
|
14854
15378
|
const deliveryKey = `\u0000thread-delivery:${ctx.larkAppId}:${ctx.scope}:${ctx.anchor}`;
|
|
14855
|
-
|
|
15379
|
+
ctx.ingressAdmission ??= { admitted: false };
|
|
15380
|
+
return withActiveSessionKeyLock(activeSessions, deliveryKey, () => withBotTurnAdmission(ctx.larkAppId, () => handleThreadReplyAdmitted(data, ctx, prepared))).catch(err => notifyOrdinaryIngressFailure(ctx, err));
|
|
14856
15381
|
}
|
|
14857
15382
|
async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
14858
15383
|
const { chatId: ctxChatId, chatType: ctxChatType, scope, anchor, larkAppId, replyRootId, substituteTrigger } = ctx;
|
|
@@ -14869,6 +15394,15 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
14869
15394
|
}
|
|
14870
15395
|
if (!prepared)
|
|
14871
15396
|
learnFromMentions(larkAppId, parsed.mentions);
|
|
15397
|
+
// 会话群自愈命名:出生时 AI 命名失败(CLI 抖动/超时/当时无模板)的群会停在
|
|
15398
|
+
// 截断占位名——任意后续文本消息触发一次补跑(title 服务内 in-flight 去重 +
|
|
15399
|
+
// titled 标记幂等),把偶发失败自动治愈,而不是永远留疤。
|
|
15400
|
+
if (ctxChatType === 'group' && isSessionGroup(ctxChatId)) {
|
|
15401
|
+
const sgEntry = getSessionGroup(ctxChatId);
|
|
15402
|
+
if (sgEntry && !sgEntry.titled && parsed.content.trim() && !parsed.content.trim().startsWith('/')) {
|
|
15403
|
+
scheduleSessionGroupTitle({ larkAppId, chatId: ctxChatId, userText: parsed.content });
|
|
15404
|
+
}
|
|
15405
|
+
}
|
|
14872
15406
|
// Foreign bot @mention prefix: when sender is another botmux bot,把内容包成
|
|
14873
15407
|
// [来自 X 的 @mention]\n<原文> 喂给 worker,让 CLI 知道这是另一个 bot 发的——
|
|
14874
15408
|
// 不是用户直接发的——后续不需要按"对话用户"的方式处理。signal-file 路径
|
|
@@ -15035,19 +15569,31 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15035
15569
|
return;
|
|
15036
15570
|
}
|
|
15037
15571
|
// Intercept daemon commands
|
|
15038
|
-
|
|
15572
|
+
// acceptSlashFromBots gate (mirror of the new-topic path): a bot sender's
|
|
15573
|
+
// slash is only routed as a command when this bot opts in (default on); when
|
|
15574
|
+
// off it falls through to ordinary message handling. Human senders unaffected.
|
|
15575
|
+
const invocation = ((isBotSenderType || isForeignBot) && !botAcceptsSlashFromBots(larkAppId))
|
|
15576
|
+
? null
|
|
15577
|
+
: parseSlashCommandInvocation(cmdContent);
|
|
15039
15578
|
if (invocation) {
|
|
15040
15579
|
const { cmd, content: commandContent } = invocation;
|
|
15580
|
+
const invocationDeps = commandDepsForInvocation({
|
|
15581
|
+
scope,
|
|
15582
|
+
chatId: ctxChatId,
|
|
15583
|
+
anchor,
|
|
15584
|
+
messageId: parsed.messageId,
|
|
15585
|
+
replyRootId,
|
|
15586
|
+
});
|
|
15041
15587
|
const existingDs = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
15042
15588
|
const effectiveThreadChatId = existingDs?.chatId ?? threadChatId;
|
|
15043
15589
|
const restrictedText = grantRestrictedSlashCommandText(larkAppId, effectiveThreadChatId, threadSenderOpenId, cmd);
|
|
15044
15590
|
if (restrictedText) {
|
|
15045
|
-
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
15591
|
+
await invocationDeps.sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
15046
15592
|
return;
|
|
15047
15593
|
}
|
|
15048
15594
|
if (cmd === '/vc-auth') {
|
|
15049
15595
|
if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId)) {
|
|
15050
|
-
await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15596
|
+
await invocationDeps.sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15051
15597
|
return;
|
|
15052
15598
|
}
|
|
15053
15599
|
await handleVcMeetingTemporaryAuthCommand({
|
|
@@ -15058,10 +15604,17 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15058
15604
|
mentions: parsed.mentions,
|
|
15059
15605
|
senderOpenId: threadSenderOpenId,
|
|
15060
15606
|
senderUnionId: threadSenderUnionId,
|
|
15607
|
+
// 同 new-topic 路径:仅在真正开始改状态后打接纳标。
|
|
15608
|
+
onMutating: () => markIngressAdmitted(ctx),
|
|
15061
15609
|
});
|
|
15062
15610
|
return;
|
|
15063
15611
|
}
|
|
15064
|
-
|
|
15612
|
+
// Existing sessions freeze their CLI independently from the bot's current
|
|
15613
|
+
// config. Route passthrough capability from that frozen runtime so changing
|
|
15614
|
+
// `/botconfig cli` cannot make an old Codex App session receive raw_input
|
|
15615
|
+
// (or make an old interactive TUI lose its native slash commands).
|
|
15616
|
+
const passthroughCliId = existingDs?.session.cliId ?? getBot(larkAppId).config.cliId;
|
|
15617
|
+
if (resolvePassthroughCommands(larkAppId, passthroughCliId).has(cmd)) {
|
|
15065
15618
|
if (!existingDs && threadChatId && isInitialSessionPassthrough(larkAppId, cmd)) {
|
|
15066
15619
|
await startInitialPassthroughSession({
|
|
15067
15620
|
larkAppId,
|
|
@@ -15089,6 +15642,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15089
15642
|
ownerOpenId: isForeignBot ? undefined : threadSenderOpenId,
|
|
15090
15643
|
ownerUnionId: isForeignBot ? undefined : data?.sender?.sender_id?.union_id,
|
|
15091
15644
|
creatorOpenId: threadSenderOpenId,
|
|
15645
|
+
onDurablyAdmitted: () => markIngressAdmitted(ctx),
|
|
15092
15646
|
routeToCanonicalOwner: () => handleThreadReplyAdmitted(data, {
|
|
15093
15647
|
...ctx,
|
|
15094
15648
|
scope,
|
|
@@ -15106,7 +15660,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15106
15660
|
// 收紧到与 daemon 命令同档;这会同时改变真人 oncall 成员的现有行为,应单独评估。
|
|
15107
15661
|
const ds = existingDs;
|
|
15108
15662
|
if (ds?.worker && !ds.worker.killed && hasQueuedActivationAdmissionGate(ds)) {
|
|
15109
|
-
sessionReply(anchor, tr('daemon.cmd_activation_pending', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15663
|
+
invocationDeps.sessionReply(anchor, tr('daemon.cmd_activation_pending', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15110
15664
|
logger.warn(`[${anchor.substring(0, 12)}] Refused passthrough ${cmd} before acceptance `
|
|
15111
15665
|
+ 'while queued activation owns raw submission order');
|
|
15112
15666
|
return;
|
|
@@ -15117,7 +15671,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15117
15671
|
// a clear message rather than deliver a silent no-op (or spawn junk Riff
|
|
15118
15672
|
// tasks). Other passthrough commands are unaffected.
|
|
15119
15673
|
if (cmd === '/fast' && fastToggleUnsupportedBackend(ds)) {
|
|
15120
|
-
await sessionReply(anchor, tr('daemon.fast_unsupported_backend', undefined, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15674
|
+
await invocationDeps.sessionReply(anchor, tr('daemon.fast_unsupported_backend', undefined, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15121
15675
|
return;
|
|
15122
15676
|
}
|
|
15123
15677
|
deliverPassthroughToExistingSession(ds, cmd, commandContent, anchor, larkAppId, {
|
|
@@ -15126,10 +15680,11 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15126
15680
|
senderOpenId: threadSenderOpenId,
|
|
15127
15681
|
senderIsBot: isForeignBot,
|
|
15128
15682
|
substitute: !!substituteTrigger,
|
|
15683
|
+
onDelivered: () => markIngressAdmitted(ctx),
|
|
15129
15684
|
});
|
|
15130
15685
|
}
|
|
15131
15686
|
else
|
|
15132
|
-
void sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15687
|
+
void invocationDeps.sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15133
15688
|
return;
|
|
15134
15689
|
}
|
|
15135
15690
|
if (DAEMON_COMMANDS.has(cmd)) {
|
|
@@ -15140,7 +15695,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15140
15695
|
// /term in a thread with no existingDs would spawn a worker:null phantom
|
|
15141
15696
|
// session and pollute the dashboard before replying not_ready/owner_only.
|
|
15142
15697
|
if (cmd === '/term') {
|
|
15143
|
-
await handleTermLinkCommand(anchor, larkAppId, threadChatId ?? '', threadSenderOpenId, commandContent,
|
|
15698
|
+
await handleTermLinkCommand(anchor, larkAppId, threadChatId ?? '', threadSenderOpenId, commandContent, invocationDeps);
|
|
15144
15699
|
return;
|
|
15145
15700
|
}
|
|
15146
15701
|
// canOperate gate for thread-reply daemon commands — required in every chat
|
|
@@ -15148,7 +15703,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15148
15703
|
// canRunDaemonCommand:canTalkDaemonCommands 名单内的命令降到 canTalk,
|
|
15149
15704
|
// 与 new-topic 路径的统一闸同款(未配置时与 canOperate 全等)。
|
|
15150
15705
|
if (!canRunDaemonCommand(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId, cmd, threadSenderUnionId, ctxChatType, isBotSenderType || isForeignBot, isBotSenderType)) {
|
|
15151
|
-
sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15706
|
+
invocationDeps.sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
15152
15707
|
return;
|
|
15153
15708
|
}
|
|
15154
15709
|
// First message of a fresh thread carrying a session-needing daemon command
|
|
@@ -15215,10 +15770,12 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15215
15770
|
const cmdMessage = { ...parsed, content: commandContent, chatId: threadChatId };
|
|
15216
15771
|
if (isSessionlessCommandInvocation(cmd, commandContent)) {
|
|
15217
15772
|
// Fast-ACK for /group invoked mid-thread. See fireSessionlessCommandDetached.
|
|
15218
|
-
fireSessionlessCommandDetached(cmd, anchor, cmdMessage, larkAppId);
|
|
15773
|
+
fireSessionlessCommandDetached(cmd, anchor, cmdMessage, larkAppId, invocationDeps);
|
|
15219
15774
|
return;
|
|
15220
15775
|
}
|
|
15221
|
-
|
|
15776
|
+
// 命令路径不打接纳标:handleCommand 内部 catch 吞掉一切异常并记日志
|
|
15777
|
+
//(command-handler.ts 收口),异常到不了 ingress catch,标记永远读不到。
|
|
15778
|
+
await handleCommand(cmd, anchor, cmdMessage, invocationDeps, larkAppId);
|
|
15222
15779
|
return;
|
|
15223
15780
|
}
|
|
15224
15781
|
}
|
|
@@ -15440,6 +15997,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15440
15997
|
codexAppText: parsed.content,
|
|
15441
15998
|
codexAppApplicationContext,
|
|
15442
15999
|
codexAppMessageContext,
|
|
16000
|
+
sessionBackendType: ds.session.backendType,
|
|
16001
|
+
turnId: parsed.messageId,
|
|
15443
16002
|
});
|
|
15444
16003
|
// R5-B1-1: freeze the admission-time steer authorization onto this earliest
|
|
15445
16004
|
// (initialStartPending follower) tail entry — the strip-proof admit rebuild
|
|
@@ -15451,6 +16010,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15451
16010
|
cliInput: followUp,
|
|
15452
16011
|
turnId: parsed.messageId,
|
|
15453
16012
|
}, tailReservation);
|
|
16013
|
+
markIngressAdmitted(ctx);
|
|
15454
16014
|
}
|
|
15455
16015
|
finally {
|
|
15456
16016
|
settleAsyncQueuedActivationTailAdmission(ds);
|
|
@@ -15579,6 +16139,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15579
16139
|
codexAppText: parsed.content,
|
|
15580
16140
|
codexAppApplicationContext,
|
|
15581
16141
|
codexAppMessageContext,
|
|
16142
|
+
sessionBackendType: ds.session.backendType,
|
|
16143
|
+
turnId: parsed.messageId,
|
|
15582
16144
|
});
|
|
15583
16145
|
ds.session.queuedPrompt ??= ds.pendingPrompt;
|
|
15584
16146
|
ds.session.queuedCodexAppText ??= ds.pendingCodexAppText;
|
|
@@ -15592,6 +16154,9 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15592
16154
|
turnId: parsed.messageId,
|
|
15593
16155
|
}, durableTailReservation);
|
|
15594
16156
|
}
|
|
16157
|
+
// 本轮已持久化接纳(durable opening 或 durable tail);下面的状态回复再
|
|
16158
|
+
// 失败也不得诱导重发(PR #846 review)。
|
|
16159
|
+
markIngressAdmitted(ctx);
|
|
15595
16160
|
const pendingReplyKey = ds.worktreeCreating
|
|
15596
16161
|
? 'daemon.worktree_building_wait'
|
|
15597
16162
|
: 'daemon.choose_repo_first';
|
|
@@ -15818,10 +16383,13 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15818
16383
|
// creator re-enters the canonical route above, which appends exactly once.
|
|
15819
16384
|
messageQueue.ensureQueue(anchor);
|
|
15820
16385
|
messageQueue.appendMessage(anchor, parsed);
|
|
16386
|
+
// 接纳标记按分支下沉(同 handleNewTopicAdmitted):append 仅供 dashboard 预览,
|
|
16387
|
+
// replyInvalidWorkingDirs 是放弃分支,放弃前的失败必须保持「请重发」提示。
|
|
15821
16388
|
// Auto-worktree: register PENDING, build worktree off-path, commit+fork later.
|
|
15822
16389
|
if (pinnedWorkingDir && autoWt) {
|
|
15823
16390
|
if (await replyInvalidWorkingDirs(anchor, larkAppId, newDs))
|
|
15824
16391
|
return;
|
|
16392
|
+
markIngressAdmitted(ctx);
|
|
15825
16393
|
newDs.initialStartPending = false; // pendingRepo/worktree now owns buffering
|
|
15826
16394
|
startAutoWorktreePending(newDs, { anchor, baseDir: pinnedWorkingDir, title: parsed.content.substring(0, 50), prompt: promptContent, operatorOpenId: ownerOpenId });
|
|
15827
16395
|
return;
|
|
@@ -15835,6 +16403,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15835
16403
|
const availableBots = await getAvailableBots(larkAppId, autoCreateChatId);
|
|
15836
16404
|
await noteTurnReceived(newDs, parsed.messageId, parsed.content, autoCreateSender, parsed.messageId, substituteTrigger ? SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE : undefined);
|
|
15837
16405
|
forkReservedInitialSession(newDs, availableBots);
|
|
16406
|
+
// fork 成功即开场已交给 CLI;fork 抛错则开场只存在于内存,保持重发提示。
|
|
16407
|
+
markIngressAdmitted(ctx);
|
|
15838
16408
|
const reason = oncallEntry
|
|
15839
16409
|
? `oncall-bound chat ${autoCreateChatId}`
|
|
15840
16410
|
: inheritedFrom
|
|
@@ -15854,6 +16424,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15854
16424
|
if (projects.length > 0) {
|
|
15855
16425
|
newDs.initialStartPending = false; // pendingRepo/card now owns buffering
|
|
15856
16426
|
lastRepoScan.set(autoCreateChatId, projects);
|
|
16427
|
+
// 已 durable staging(picker)且通过放弃闸;卡片发送失败不得诱导重发。
|
|
16428
|
+
markIngressAdmitted(ctx);
|
|
15857
16429
|
const currentCwd = getSessionWorkingDir(newDs);
|
|
15858
16430
|
const cardJson = buildRepoSelectCard(projects, currentCwd, anchor, localeForBot(larkAppId), getBot(larkAppId).config.worktreeMultiPicker);
|
|
15859
16431
|
newDs.repoCardMessageId = await sessionReply(anchor, cardJson, 'interactive', larkAppId);
|
|
@@ -15862,7 +16434,10 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15862
16434
|
logger.info(`[${tag(newDs)}] Waiting for repo selection (${projects.length} projects)`);
|
|
15863
16435
|
}
|
|
15864
16436
|
else {
|
|
15865
|
-
// No projects found — skip repo selection, spawn directly
|
|
16437
|
+
// No projects found — skip repo selection, spawn directly.
|
|
16438
|
+
// 已 durable staging(picker):fork 失败后 opening 仍由会话 durable 持有,
|
|
16439
|
+
// 先打接纳标(同 handleNewTopicAdmitted 的 no-projects 分支)。
|
|
16440
|
+
markIngressAdmitted(ctx);
|
|
15866
16441
|
newDs.pendingRepo = false;
|
|
15867
16442
|
ensureSessionWhiteboard(newDs);
|
|
15868
16443
|
const availableBots = await getAvailableBots(larkAppId, autoCreateChatId);
|
|
@@ -15872,6 +16447,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15872
16447
|
return;
|
|
15873
16448
|
}
|
|
15874
16449
|
// Existing-owner route: append once after all pending-repo early returns.
|
|
16450
|
+
// 接纳标记不打在这里:append 仅供 dashboard 预览,真正的接纳在下方 live worker
|
|
16451
|
+
// 送达 / durable tail staging / refork 成功之后。
|
|
15875
16452
|
messageQueue.ensureQueue(anchor);
|
|
15876
16453
|
messageQueue.appendMessage(anchor, parsed);
|
|
15877
16454
|
publishSessionMessagePreviewPatch(ds);
|
|
@@ -15934,6 +16511,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15934
16511
|
codexAppText: parsed.content,
|
|
15935
16512
|
codexAppApplicationContext,
|
|
15936
16513
|
codexAppMessageContext,
|
|
16514
|
+
sessionBackendType: ds.session.backendType,
|
|
16515
|
+
turnId: parsed.messageId,
|
|
15937
16516
|
});
|
|
15938
16517
|
beginNewTurn(ds, parsed.content, parsed.messageId);
|
|
15939
16518
|
await noteTurnReceived(ds, parsed.messageId, parsed.content, turnSender, parsed.messageId, substituteTrigger ? SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE : undefined);
|
|
@@ -15948,8 +16527,11 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
15948
16527
|
// reached the CLI; a rejected send then left that poison behind, and the
|
|
15949
16528
|
// next message's worker-null refork would read it as `hadPriorCliInput`
|
|
15950
16529
|
// and wrongly `--resume` a CLI that never took a real turn.
|
|
15951
|
-
if (accepted)
|
|
16530
|
+
if (accepted) {
|
|
16531
|
+
// 消息已实际送入 live worker——之后的收尾失败不得再诱导重发。
|
|
16532
|
+
markIngressAdmitted(ctx);
|
|
15952
16533
|
rememberLastCliInput(ds, promptContent, cliInput);
|
|
16534
|
+
}
|
|
15953
16535
|
else
|
|
15954
16536
|
logger.warn(`[${tag(ds)}] Inbound ${parsed.messageId} was not accepted by the live worker`);
|
|
15955
16537
|
}
|
|
@@ -16025,6 +16607,8 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16025
16607
|
codexAppText: parsed.content,
|
|
16026
16608
|
codexAppApplicationContext,
|
|
16027
16609
|
codexAppMessageContext,
|
|
16610
|
+
sessionBackendType: ds.session.backendType,
|
|
16611
|
+
turnId: parsed.messageId,
|
|
16028
16612
|
});
|
|
16029
16613
|
// R4-B1: freeze the admission-time steer authorization onto the queued
|
|
16030
16614
|
// opening payload so the worker-null re-fork path carries it exactly like
|
|
@@ -16037,6 +16621,9 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16037
16621
|
cliInput: currentFollowUp,
|
|
16038
16622
|
turnId: parsed.messageId,
|
|
16039
16623
|
}, tailReservation);
|
|
16624
|
+
// 本轮已进 durable tail——之后 fork/回执失败可由重启或下一条 inbound 恢复,
|
|
16625
|
+
// 不得再诱导重发。
|
|
16626
|
+
markIngressAdmitted(ctx);
|
|
16040
16627
|
}
|
|
16041
16628
|
finally {
|
|
16042
16629
|
settleAsyncQueuedActivationTailAdmission(ds);
|
|
@@ -16134,35 +16721,12 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16134
16721
|
// its normal `--resume` instead of being cold-spawned over.
|
|
16135
16722
|
const hadPriorCliInput = !!(ds.lastCliInput ?? ds.session.lastCliInput);
|
|
16136
16723
|
const openingTurn = wantsOpening && claimInitialUserTurn(ds);
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
selfMention: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
16143
|
-
sender: queuedHasDurableTail ? undefined : reforkSender,
|
|
16144
|
-
substituteTrigger: queuedHasDurableTail ? undefined : substituteTrigger,
|
|
16145
|
-
codexAppText: reforkCodexApp.text,
|
|
16146
|
-
codexAppApplicationContext: queuedHasDurableTail ? undefined : codexAppApplicationContext,
|
|
16147
|
-
codexAppMessageContext: reforkCodexApp.messageContext,
|
|
16148
|
-
});
|
|
16149
|
-
let wrappedInput = applyQueuedCodexAppLegacyFallback(builtReforkInput, {
|
|
16150
|
-
queued: queuedDashboardTurn,
|
|
16151
|
-
queuedText: queuedCodexAppText,
|
|
16152
|
-
});
|
|
16153
|
-
if (wrappedInput !== builtReforkInput && dsBotCfgForFork.codexAppCleanInput === true) {
|
|
16154
|
-
// Backlog sessions persisted before clean-input have no raw queued text.
|
|
16155
|
-
// Keep this activation entirely legacy: reforkContent already contains
|
|
16156
|
-
// queuedPrompt + the current reply, whereas a structured turn could only
|
|
16157
|
-
// contain the reply and would silently discard the original task.
|
|
16158
|
-
logger.warn(`[${tag(ds)}] Legacy queued dashboard task has no clean-input text; using the full legacy activation prompt`);
|
|
16159
|
-
}
|
|
16724
|
+
// 先选 builder:opening 轮用 buildNewTopicCliInput,非 opening 才 buildRefork。
|
|
16725
|
+
// 不能无条件先跑 buildRefork——它现在有副作用(写 follow-up sidecar),opening 时
|
|
16726
|
+
// 结果会被 buildNewTopicCliInput 覆盖丢弃,但 sidecar 已写入 opening 的 turnId,
|
|
16727
|
+
// opening 的 hook 会领到这份从没发出去的 speculative reminder → 双注入。
|
|
16728
|
+
let wrappedInput;
|
|
16160
16729
|
if (openingTurn) {
|
|
16161
|
-
// Replace the follow-up envelope built above with the real opening. The
|
|
16162
|
-
// discarded build is pure string assembly (no side effects) — keeping the
|
|
16163
|
-
// refork statement unconditional keeps the queued/substitute wiring, and
|
|
16164
|
-
// its guard test, on a single code path. (openingTurn is mutually
|
|
16165
|
-
// exclusive with queuedDashboardTurn/queuedHasDurableTail.)
|
|
16166
16730
|
wrappedInput = buildNewTopicCliInput(reforkContent, ds.session.sessionId, ds.session.cliId ?? dsBotCfgForFork.cliId, ds.session.cliPathOverride ?? dsBotCfgForFork.cliPathOverride, attachments, parsed.mentions, openingBots, undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), reforkSender, {
|
|
16167
16731
|
larkAppId,
|
|
16168
16732
|
chatId: ds.session.chatId,
|
|
@@ -16173,9 +16737,36 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16173
16737
|
codexAppMessageContext: reforkCodexApp.messageContext,
|
|
16174
16738
|
});
|
|
16175
16739
|
}
|
|
16740
|
+
else {
|
|
16741
|
+
const builtReforkInput = buildReforkCliInput(ds, reforkContent, {
|
|
16742
|
+
attachments: queuedHasDurableTail ? undefined : attachments,
|
|
16743
|
+
mentions: queuedHasDurableTail ? undefined : parsed.mentions,
|
|
16744
|
+
cliId: ds.session.cliId ?? dsBotCfgForFork.cliId,
|
|
16745
|
+
cliPathOverride: ds.session.cliPathOverride ?? dsBotCfgForFork.cliPathOverride,
|
|
16746
|
+
selfMention: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
16747
|
+
sender: queuedHasDurableTail ? undefined : reforkSender,
|
|
16748
|
+
substituteTrigger: queuedHasDurableTail ? undefined : substituteTrigger,
|
|
16749
|
+
codexAppText: reforkCodexApp.text,
|
|
16750
|
+
codexAppApplicationContext: queuedHasDurableTail ? undefined : codexAppApplicationContext,
|
|
16751
|
+
codexAppMessageContext: reforkCodexApp.messageContext,
|
|
16752
|
+
turnId: parsed.messageId,
|
|
16753
|
+
});
|
|
16754
|
+
wrappedInput = applyQueuedCodexAppLegacyFallback(builtReforkInput, {
|
|
16755
|
+
queued: queuedDashboardTurn,
|
|
16756
|
+
queuedText: queuedCodexAppText,
|
|
16757
|
+
});
|
|
16758
|
+
if (wrappedInput !== builtReforkInput && dsBotCfgForFork.codexAppCleanInput === true) {
|
|
16759
|
+
// Backlog sessions persisted before clean-input have no raw queued text.
|
|
16760
|
+
// Keep this activation entirely legacy: reforkContent already contains
|
|
16761
|
+
// queuedPrompt + the current reply, whereas a structured turn could only
|
|
16762
|
+
// contain the reply and would silently discard the original task.
|
|
16763
|
+
logger.warn(`[${tag(ds)}] Legacy queued dashboard task has no clean-input text; using the full legacy activation prompt`);
|
|
16764
|
+
}
|
|
16765
|
+
}
|
|
16176
16766
|
if (!queuedHasDurableTail) {
|
|
16177
16767
|
await noteTurnReceived(ds, parsed.messageId, parsed.content, reforkSender, parsed.messageId, substituteTrigger ? SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE : undefined);
|
|
16178
16768
|
}
|
|
16769
|
+
let reforkAccepted = true;
|
|
16179
16770
|
try {
|
|
16180
16771
|
// Adopt sessions must re-fork via forkAdoptWorker, NOT forkWorker: the
|
|
16181
16772
|
// latter would spawn a fresh botmux-managed bmx-* CLI in the adopt cwd,
|
|
@@ -16198,7 +16789,7 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16198
16789
|
if (codexAppSteerable && !queuedDashboardTurn && !queuedHasDurableTail) {
|
|
16199
16790
|
wrappedInput.codexAppSteerable = true;
|
|
16200
16791
|
}
|
|
16201
|
-
forkWorker(ds, wrappedInput, {
|
|
16792
|
+
reforkAccepted = forkWorker(ds, wrappedInput, {
|
|
16202
16793
|
// See `hadPriorCliInput` above — an opening on a CLI that never took any
|
|
16203
16794
|
// input cold-spawns rather than `--resume`-ing an empty session.
|
|
16204
16795
|
resume: ds.hasHistory && !(openingTurn && !hadPriorCliInput),
|
|
@@ -16213,6 +16804,11 @@ async function handleThreadReplyAdmitted(data, ctx, prepared) {
|
|
|
16213
16804
|
releaseInitialUserTurn(ds);
|
|
16214
16805
|
throw e;
|
|
16215
16806
|
}
|
|
16807
|
+
// refork 成功才算本轮已交给 CLI。fork 抛错(rethrow)或返回 false(quarantine /
|
|
16808
|
+
// 会话已非 active 的不抛错拒绝腿)时,本轮只存在于内存、无 durable 兜底,
|
|
16809
|
+
// 必须保持「请重发」提示——与 live 分支按 accepted 打标对称。
|
|
16810
|
+
if (reforkAccepted)
|
|
16811
|
+
markIngressAdmitted(ctx);
|
|
16216
16812
|
// Record the input as the session's last real CLI turn ONLY after the fork
|
|
16217
16813
|
// succeeded. Recording before the fork (the old order) persisted lastCliInput
|
|
16218
16814
|
// for a turn that never launched when forkWorker threw; the retry then read
|
|
@@ -16515,6 +17111,7 @@ async function handleDocCommentAdmitted(ctx) {
|
|
|
16515
17111
|
botIdentity: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
16516
17112
|
sender,
|
|
16517
17113
|
mode: 'live',
|
|
17114
|
+
turnId,
|
|
16518
17115
|
});
|
|
16519
17116
|
beginNewTurn(ds, text, turnId);
|
|
16520
17117
|
(ds.session.docCommentTargets ??= {})[turnId] = docTarget; // per-turn map,不覆盖其他并发轮
|
|
@@ -16560,6 +17157,7 @@ async function handleDocCommentAdmitted(ctx) {
|
|
|
16560
17157
|
botIdentity: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
16561
17158
|
sender,
|
|
16562
17159
|
mode: 'refork',
|
|
17160
|
+
turnId,
|
|
16563
17161
|
});
|
|
16564
17162
|
(ds.session.docCommentTargets ??= {})[turnId] = docTarget; // per-turn map,不覆盖其他并发轮
|
|
16565
17163
|
rememberLastCliInput(ds, promptContent, wrappedInput);
|
|
@@ -16970,6 +17568,28 @@ export async function startDaemon(botIndex) {
|
|
|
16970
17568
|
}
|
|
16971
17569
|
registerBot(cfg);
|
|
16972
17570
|
selfDaemonLarkAppId = cfg.larkAppId;
|
|
17571
|
+
// The final-answer feedback subsystem is OPTIONAL: a bot with feedback
|
|
17572
|
+
// disabled still opens the shared feedback DB here for turn-completion
|
|
17573
|
+
// indexing, but a bootstrap failure (shared-dataDir lock storm, corruption,
|
|
17574
|
+
// disk full) must NOT abort daemon startup for that bot — that would let one
|
|
17575
|
+
// optional subsystem take down every PM2 bot sharing the dataDir. Fail soft:
|
|
17576
|
+
// log and continue; delivery-time paths degrade to best-effort on their own.
|
|
17577
|
+
let feedbackWebhookDispatcher;
|
|
17578
|
+
try {
|
|
17579
|
+
const feedbackWebhookStore = await getSkillFeedbackStore(config.session.dataDir);
|
|
17580
|
+
const feedbackWebhookSecrets = new FeedbackWebhookSecretStore(config.session.dataDir);
|
|
17581
|
+
feedbackWebhookDispatcher = startFeedbackWebhookDispatcher({
|
|
17582
|
+
store: feedbackWebhookStore,
|
|
17583
|
+
readSecret: ref => feedbackWebhookSecrets.get(ref),
|
|
17584
|
+
onError: error => logger.warn(`[feedback-webhook] ${error instanceof Error ? error.message : String(error)}`),
|
|
17585
|
+
});
|
|
17586
|
+
await feedbackWebhookDispatcher.ready;
|
|
17587
|
+
}
|
|
17588
|
+
catch (error) {
|
|
17589
|
+
logger.error(`[feedback] subsystem bootstrap failed; continuing without feedback indexing/webhooks: `
|
|
17590
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
17591
|
+
feedbackWebhookDispatcher = undefined;
|
|
17592
|
+
}
|
|
16973
17593
|
// Establish the target-scoped daemon control credential before publishing
|
|
16974
17594
|
// the daemon descriptor or accepting IPC traffic. Corruption fails startup
|
|
16975
17595
|
// closed; silently rotating here could strand peers on mismatched tokens.
|
|
@@ -17033,6 +17653,7 @@ export async function startDaemon(botIndex) {
|
|
|
17033
17653
|
}
|
|
17034
17654
|
sessionStore.init(cfg.larkAppId);
|
|
17035
17655
|
chatFirstSeenStore.init(cfg.larkAppId);
|
|
17656
|
+
initSessionGroups(cfg.larkAppId);
|
|
17036
17657
|
const ambiguousOnBoot = reconcileVcMeetingDeliveriesOnBoot(config.session.dataDir, { receiverBootId: getDaemonBootId(), agentAppId: cfg.larkAppId });
|
|
17037
17658
|
if (ambiguousOnBoot.length > 0) {
|
|
17038
17659
|
logger.warn(`[vc-delivery] reconciled ${ambiguousOnBoot.length} stale dispatched receipt(s) as ambiguous`);
|
|
@@ -17304,12 +17925,32 @@ export async function startDaemon(botIndex) {
|
|
|
17304
17925
|
},
|
|
17305
17926
|
enforceLiveSessionCap: () => enforceLiveSessionCap('session_change'),
|
|
17306
17927
|
onQueuedActivationSubmitted,
|
|
17307
|
-
onTurnTerminal(ds, terminal, context) {
|
|
17928
|
+
async onTurnTerminal(ds, terminal, context) {
|
|
17929
|
+
// VC reconcile first: it is in-memory and latency-sensitive, and must not
|
|
17930
|
+
// sit behind a synchronous SQLite write. (Master did only this enqueue.)
|
|
17308
17931
|
const enqueued = vcMeetingTerminalReconciler?.enqueue(terminal, context);
|
|
17309
17932
|
if (terminal.dispatchAttempt !== undefined && enqueued?.accepted) {
|
|
17310
17933
|
logger.info(`[vc-delivery] terminal queued ${terminal.status} turn=${terminal.turnId.slice(0, 12)} `
|
|
17311
17934
|
+ `session=${ds.session.sessionId.slice(0, 8)} attempt=${terminal.dispatchAttempt}`);
|
|
17312
17935
|
}
|
|
17936
|
+
// Feedback turn-completion persistence is a synchronous node:sqlite write.
|
|
17937
|
+
// Route it through the nonblocking, tracked retry queue so a cross-process
|
|
17938
|
+
// write-lock (a `botmux send --response-kind final` subprocess) can never
|
|
17939
|
+
// stall the daemon event loop up to busy_timeout — the queue fails fast on
|
|
17940
|
+
// contention and retries on a timer that yields the loop. apiOnly bots
|
|
17941
|
+
// never produce a feedback delivery, so skip them outright; everyone else
|
|
17942
|
+
// enqueues (a hosted-team may have enabled feedback with no bot-level
|
|
17943
|
+
// config, so we must not gate on bot config alone).
|
|
17944
|
+
if (!getBot(ds.larkAppId).config.apiOnly) {
|
|
17945
|
+
void enqueueTurnTerminal({
|
|
17946
|
+
dataDir: config.session.dataDir,
|
|
17947
|
+
botAppId: ds.larkAppId,
|
|
17948
|
+
sessionId: ds.session.sessionId,
|
|
17949
|
+
terminal: { turnId: terminal.turnId, dispatchAttempt: terminal.dispatchAttempt, status: terminal.status },
|
|
17950
|
+
onError: error => logger.error(`[turn-completion] persistence failed turn=${terminal.turnId.slice(0, 12)}: `
|
|
17951
|
+
+ `${error instanceof Error ? error.message : String(error)}`),
|
|
17952
|
+
});
|
|
17953
|
+
}
|
|
17313
17954
|
},
|
|
17314
17955
|
onDeferredScheduleTurnSettled(ds, context) {
|
|
17315
17956
|
scheduleDeferredScheduleSettlement(ds, context);
|
|
@@ -17420,6 +18061,11 @@ export async function startDaemon(botIndex) {
|
|
|
17420
18061
|
// host-only shared secret on every daemon IPC route except the tiny
|
|
17421
18062
|
// capability-gated receiver/readiness apertures in dashboard-ipc-server.
|
|
17422
18063
|
loadOrCreateDashboardSecret(join(homedir(), '.botmux', '.dashboard-secret'));
|
|
18064
|
+
// Create the dispatch-binding key before any credential-only child spawns.
|
|
18065
|
+
// Linux bwrap snapshots `.dashboard-secret.*` authority leaves at launch;
|
|
18066
|
+
// creating this later would leave the new key visible through its writable
|
|
18067
|
+
// host-root bind until the pane cold-started.
|
|
18068
|
+
loadOrCreateDashboardSecret(dispatchReportBindingSecretPath(config.session.dataDir));
|
|
17423
18069
|
let markIpcReady;
|
|
17424
18070
|
const ipcReady = new Promise((resolve) => { markIpcReady = resolve; });
|
|
17425
18071
|
const coreOnly = process.env.BOTMUX_CORE_ONLY === '1';
|
|
@@ -18029,7 +18675,20 @@ export async function startDaemon(botIndex) {
|
|
|
18029
18675
|
catch (err) {
|
|
18030
18676
|
logger.warn(`[overload] count failed, showing 0: ${err instanceof Error ? err.message : String(err)}`);
|
|
18031
18677
|
}
|
|
18032
|
-
|
|
18678
|
+
// Probe which configured browsers are running + holding memory so the
|
|
18679
|
+
// card can offer a per-browser「♻️ 重启」button (the real memory hog on
|
|
18680
|
+
// a dev Mac is usually a browser, not botmux sessions). Best-effort:
|
|
18681
|
+
// any probe failure just yields no browser buttons.
|
|
18682
|
+
let browsers = [];
|
|
18683
|
+
try {
|
|
18684
|
+
const targets = resolveBrowserTargets(alertCfg.browserRestartTargets);
|
|
18685
|
+
const running = await detectRunningBrowsers(targets);
|
|
18686
|
+
browsers = running.map(b => ({ bundleId: b.bundleId, label: b.label, memMB: b.memMB }));
|
|
18687
|
+
}
|
|
18688
|
+
catch (err) {
|
|
18689
|
+
logger.warn(`[overload] browser probe failed, no restart buttons: ${err instanceof Error ? err.message : String(err)}`);
|
|
18690
|
+
}
|
|
18691
|
+
cardJson = buildOverloadAlertCard(initialOverloadCardState(action, counts, nonce, browsers));
|
|
18033
18692
|
}
|
|
18034
18693
|
else {
|
|
18035
18694
|
cardJson = buildOverloadRecoveredCard(action);
|
|
@@ -18164,6 +18823,13 @@ export async function startDaemon(botIndex) {
|
|
|
18164
18823
|
riffRetiredWorkers.push(result.worker);
|
|
18165
18824
|
}
|
|
18166
18825
|
scheduler.stopScheduler();
|
|
18826
|
+
// NOTE: turn-terminal queue drain + webhook dispatcher stop are deliberately
|
|
18827
|
+
// deferred until AFTER workers have stopped producing (see below). Draining
|
|
18828
|
+
// here would close queue admission while worker terminal IPC handlers are
|
|
18829
|
+
// still alive, so a turn completing during shutdown would have its terminal
|
|
18830
|
+
// refused and never persisted (delivery cannot be reconstructed to
|
|
18831
|
+
// completed/failed/cancelled after the fact). Keep the queue and dispatcher
|
|
18832
|
+
// live through worker teardown.
|
|
18167
18833
|
stopMaintenance();
|
|
18168
18834
|
vcMeetingTerminalReconciler?.stop();
|
|
18169
18835
|
clearInterval(vcMeetingDeliveryLeaseTimer);
|
|
@@ -18200,32 +18866,65 @@ export async function startDaemon(botIndex) {
|
|
|
18200
18866
|
ipcHandle.close().catch(() => { });
|
|
18201
18867
|
if (terminalProxy)
|
|
18202
18868
|
terminalProxy.close().catch(() => { });
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18869
|
+
// Producer-closed fence. We must not close the turn-terminal queue admission
|
|
18870
|
+
// until every worker's IPC channel is truly closed — a worker whose 'exit'
|
|
18871
|
+
// Producer-quiescence fence. A worker whose 'exit' fired can still have
|
|
18872
|
+
// queued 'message' terminals in flight; the IPC `disconnect` event is the
|
|
18873
|
+
// real "no more terminals from this producer" signal (see producer-quiescence
|
|
18874
|
+
// helper — it deliberately keys on disconnect, not 'close', which stdio
|
|
18875
|
+
// inherited by grandchildren can delay). Dedup by ChildProcess.
|
|
18876
|
+
// Two INDEPENDENT shutdown concerns, tracked over the same worker set but
|
|
18877
|
+
// never conflated (conflating them regresses process reaping):
|
|
18878
|
+
// • reaping (workersToReap / workerExited): every live worker PROCESS must
|
|
18879
|
+
// exit or be SIGKILLed — orphan prevention. Keys on process death, NOT
|
|
18880
|
+
// on `connected`.
|
|
18881
|
+
// • producer fence (producerClosed): every worker's IPC channel must
|
|
18882
|
+
// disconnect — the "no more terminal messages" signal that gates closing
|
|
18883
|
+
// the feedback queue admission. Keys on `disconnect`, NOT on exit.
|
|
18884
|
+
const producerClosed = [];
|
|
18885
|
+
const workerExited = [];
|
|
18886
|
+
const reapCandidates = [];
|
|
18887
|
+
const trackedProducers = new Set();
|
|
18888
|
+
const trackedReap = new Set();
|
|
18889
|
+
const trackWorker = (w) => {
|
|
18890
|
+
// Producer fence: disconnect-based, gates feedback admission only.
|
|
18891
|
+
if (!trackedProducers.has(w)) {
|
|
18892
|
+
trackedProducers.add(w);
|
|
18893
|
+
const q = trackProducerQuiet(w);
|
|
18894
|
+
if (!q.alreadyQuiet && q.done)
|
|
18895
|
+
producerClosed.push(q.done);
|
|
18896
|
+
}
|
|
18897
|
+
// Reaping: exit-based, orphan prevention. Registered BEFORE any signal so
|
|
18898
|
+
// a fast exit is not missed; dedup by ChildProcess.
|
|
18899
|
+
if (!trackedReap.has(w)) {
|
|
18900
|
+
trackedReap.add(w);
|
|
18901
|
+
const e = trackProcessExited(w);
|
|
18902
|
+
if (!e.alreadyExited && e.done) {
|
|
18903
|
+
workerExited.push(e.done);
|
|
18904
|
+
reapCandidates.push(w);
|
|
18905
|
+
}
|
|
18906
|
+
}
|
|
18216
18907
|
};
|
|
18217
18908
|
for (const worker of riffRetiredWorkers)
|
|
18218
|
-
|
|
18909
|
+
trackWorker(worker);
|
|
18219
18910
|
for (const ds of currentShutdownFleet.sessions) {
|
|
18220
|
-
|
|
18911
|
+
const w = ds.worker;
|
|
18912
|
+
if (!w)
|
|
18913
|
+
continue;
|
|
18914
|
+
// Track EVERY non-null worker (both concerns) unconditionally. `killed`
|
|
18915
|
+
// only means a signal was already sent — NOT that the process exited or
|
|
18916
|
+
// the IPC channel disconnected (a worker can be `killed=true &&
|
|
18917
|
+
// connected=true` right after .kill(), e.g. the promoted-activation
|
|
18918
|
+
// IPC-failure path that kills but defers ds.worker cleanup). `killed` only
|
|
18919
|
+
// decides whether we still need to SEND a stop signal; it never decides
|
|
18920
|
+
// whether the worker is tracked for reaping or the producer fence.
|
|
18921
|
+
trackWorker(w);
|
|
18922
|
+
if (!w.killed) {
|
|
18221
18923
|
logger.info(`Shutting down worker for session ${ds.session.sessionId}`);
|
|
18222
|
-
const w = ds.worker;
|
|
18223
18924
|
const disposition = shutdownBackendDisposition(ds);
|
|
18224
18925
|
if (disposition === 'riff-drain-detach') {
|
|
18225
18926
|
throw new Error(`undrained Riff generation after atomic commit: ${ds.session.sessionId}`);
|
|
18226
18927
|
}
|
|
18227
|
-
// Capture the exit promise BEFORE killWorker nulls ds.worker.
|
|
18228
|
-
trackWorkerExit(w);
|
|
18229
18928
|
// Branch by the session's FROZEN backend (stamped on Session.backendType
|
|
18230
18929
|
// at spawn), NOT the bot's live config — a dashboard backendType edit must
|
|
18231
18930
|
// not change how a running session is torn down, or we'd e.g. try to
|
|
@@ -18253,12 +18952,34 @@ export async function startDaemon(botIndex) {
|
|
|
18253
18952
|
}
|
|
18254
18953
|
}
|
|
18255
18954
|
}
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18955
|
+
// --- Staged producer quiescence before touching the feedback queue -------
|
|
18956
|
+
// Contract (fail-closed): the turn-terminal queue admission may be closed
|
|
18957
|
+
// (inside drainTurnTerminalQueue) ONLY after BOTH fences confirm quiescent
|
|
18958
|
+
// within the shared absolute deadline:
|
|
18959
|
+
// (1) every worker IPC channel disconnected — no NEW terminal message can
|
|
18960
|
+
// be delivered; and
|
|
18961
|
+
// (2) every in-flight Codex App final-settlement resolved — an already
|
|
18962
|
+
// delivered final_output whose handler is awaiting network delivery
|
|
18963
|
+
// has finished its cb.onTurnTerminal (which synchronously enqueues).
|
|
18964
|
+
// If either fence is not quiescent by the deadline we DO NOT close admission
|
|
18965
|
+
// (closing it would refuse a terminal a still-live producer may yet emit —
|
|
18966
|
+
// strictly worse than the pre-feature behaviour). We keep admission open,
|
|
18967
|
+
// warn loudly, and let the process exit on its budget.
|
|
18968
|
+
// Every wait shares the single absolute `shutdownDeadlineMs`; a ref'd
|
|
18969
|
+
// keepalive (waitAllWithin) holds the loop so a fire-and-forget SIGTERM
|
|
18970
|
+
// handler cannot exit mid-wait once workers/services have stopped.
|
|
18971
|
+
const remainingBudget = () => Math.max(0, shutdownDeadlineMs - Date.now());
|
|
18972
|
+
// --- Phase 1: process reaping (orphan prevention) ------------------------
|
|
18973
|
+
// Give every live worker PROCESS a normal grace to exit, then SIGKILL any
|
|
18974
|
+
// still alive, then confirm exit within the shared deadline. This is keyed
|
|
18975
|
+
// on process death (exitCode/signalCode), independent of IPC state, so a
|
|
18976
|
+
// worker that ignores SIGTERM — or that already disconnected its IPC but is
|
|
18977
|
+
// still running — is still reaped and cannot become a ppid=1 orphan.
|
|
18978
|
+
if (workerExited.length > 0) {
|
|
18979
|
+
const exitGraceDeadline = Math.min(shutdownDeadlineMs, Date.now() + DAEMON_WORKER_EXIT_GRACE_MS);
|
|
18980
|
+
await waitAllWithin(workerExited, exitGraceDeadline);
|
|
18260
18981
|
let stragglers = 0;
|
|
18261
|
-
for (const w of
|
|
18982
|
+
for (const w of reapCandidates) {
|
|
18262
18983
|
if (w.exitCode === null && w.signalCode === null) {
|
|
18263
18984
|
stragglers++;
|
|
18264
18985
|
try {
|
|
@@ -18268,9 +18989,51 @@ export async function startDaemon(botIndex) {
|
|
|
18268
18989
|
}
|
|
18269
18990
|
}
|
|
18270
18991
|
if (stragglers > 0) {
|
|
18271
|
-
logger.warn(`${stragglers}/${
|
|
18992
|
+
logger.warn(`${stragglers}/${reapCandidates.length} worker(s) didn't exit within grace — SIGKILL'd to prevent ppid=1 orphans.`);
|
|
18993
|
+
// Confirm the SIGKILLed processes actually exit, still within the deadline.
|
|
18994
|
+
await waitAllWithin(workerExited, shutdownDeadlineMs);
|
|
18995
|
+
}
|
|
18996
|
+
}
|
|
18997
|
+
// --- Phase 2: producer disconnect fence (gates feedback admission) -------
|
|
18998
|
+
// Separate from reaping: this only decides whether it is safe to close the
|
|
18999
|
+
// feedback queue admission. A late (but in-deadline) disconnect still counts
|
|
19000
|
+
// as quiescent — we do not force it via SIGKILL here (Phase 1 already
|
|
19001
|
+
// handled process liveness). By this point most workers have exited (which
|
|
19002
|
+
// also disconnects their IPC), so producerClosed is typically already
|
|
19003
|
+
// settled; we still await it explicitly under the shared deadline.
|
|
19004
|
+
const disconnectQuiesced = await waitAllWithin(producerClosed, shutdownDeadlineMs);
|
|
19005
|
+
// Settlement fence: only meaningful once IPC is confirmed disconnected (no
|
|
19006
|
+
// new settlement can be created). Await the snapshot, then re-read the count
|
|
19007
|
+
// — 0 confirms every in-flight settlement (and its enqueue) has completed.
|
|
19008
|
+
let settlementQuiesced = false;
|
|
19009
|
+
if (disconnectQuiesced) {
|
|
19010
|
+
await waitAllWithin(snapshotCodexAppFinalSettlements(), shutdownDeadlineMs);
|
|
19011
|
+
settlementQuiesced = codexAppFinalSettlementCount() === 0;
|
|
19012
|
+
}
|
|
19013
|
+
if (disconnectQuiesced && settlementQuiesced) {
|
|
19014
|
+
// Both fences quiescent: safe to close admission and drain the last
|
|
19015
|
+
// terminals into the durable outbox, bounded by the remaining budget.
|
|
19016
|
+
const drainBudget = remainingBudget();
|
|
19017
|
+
if (drainBudget > 0) {
|
|
19018
|
+
const undrained = await drainTurnTerminalQueue(drainBudget);
|
|
19019
|
+
if (undrained > 0)
|
|
19020
|
+
logger.warn(`[turn-completion] shutdown drain timed out with ${undrained} pending`);
|
|
19021
|
+
}
|
|
19022
|
+
else {
|
|
19023
|
+
logger.error('[turn-completion] shutdown deadline exhausted before drain — in-memory queued turn terminals were NOT persisted and are LOST (they are not yet in the durable store)');
|
|
18272
19024
|
}
|
|
18273
19025
|
}
|
|
19026
|
+
else {
|
|
19027
|
+
// Fail-closed: a producer may still emit a terminal. Do NOT close
|
|
19028
|
+
// admission (that would refuse and lose it). Hold open, warn, and let the
|
|
19029
|
+
// process exit on its budget; unfinished work stays in the durable store.
|
|
19030
|
+
logger.error(`[turn-completion] shutdown producer quiescence NOT confirmed `
|
|
19031
|
+
+ `(ipcDisconnected=${disconnectQuiesced} settlementsSettled=${settlementQuiesced}); `
|
|
19032
|
+
+ `leaving feedback queue admission OPEN to avoid refusing a late terminal`);
|
|
19033
|
+
}
|
|
19034
|
+
// Dispatcher stop always receives the hard-clamped remaining budget (never
|
|
19035
|
+
// its internal 5s default), success or failure path alike.
|
|
19036
|
+
await feedbackWebhookDispatcher?.stop(remainingBudget());
|
|
18274
19037
|
// Flush any pending identity-cache writes before exit. The cache uses a
|
|
18275
19038
|
// 2s debounce on disk persistence to dedupe writes from chatty groups; on
|
|
18276
19039
|
// SIGTERM we want anything learned since the last flush to land.
|