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