botmux 3.7.1 → 3.8.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/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/capabilities.d.ts +17 -0
- package/dist/adapters/backend/capabilities.d.ts.map +1 -0
- package/dist/adapters/backend/capabilities.js +22 -0
- package/dist/adapters/backend/capabilities.js.map +1 -0
- package/dist/adapters/backend/critical-control-key.d.ts +19 -0
- package/dist/adapters/backend/critical-control-key.d.ts.map +1 -0
- package/dist/adapters/backend/critical-control-key.js +34 -0
- package/dist/adapters/backend/critical-control-key.js.map +1 -0
- package/dist/adapters/backend/herdr-backend.d.ts +9 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +23 -4
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +3 -2
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +61 -6
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +173 -9
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +10 -3
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +25 -8
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +3 -2
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +45 -4
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js +11 -0
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +3 -2
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts +286 -0
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zmx-backend.js +2396 -0
- package/dist/adapters/backend/zmx-backend.js.map +1 -0
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +3 -0
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +5 -1
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +26 -1
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/fs-policy.d.ts +79 -1
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +290 -13
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +4 -1
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
- package/dist/adapters/cli/oh-my-pi.js +5 -3
- package/dist/adapters/cli/oh-my-pi.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +7 -4
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +52 -35
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/strict-input-handle.d.ts +13 -0
- package/dist/adapters/cli/strict-input-handle.d.ts.map +1 -0
- package/dist/adapters/cli/strict-input-handle.js +39 -0
- package/dist/adapters/cli/strict-input-handle.js.map +1 -0
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +6 -4
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +16 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +137 -8
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +298 -12
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/session-list-liveness.d.ts +1 -1
- package/dist/cli/session-list-liveness.js +1 -1
- package/dist/cli/zmx-managed-attach.d.ts +41 -0
- package/dist/cli/zmx-managed-attach.d.ts.map +1 -0
- package/dist/cli/zmx-managed-attach.js +72 -0
- package/dist/cli/zmx-managed-attach.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +780 -131
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +90 -2
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts.map +1 -1
- package/dist/codex-rpc-engine.js +22 -6
- package/dist/codex-rpc-engine.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -1
- package/dist/config.js.map +1 -1
- package/dist/core/ask-broker.d.ts +22 -1
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +12 -5
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/botmux-wrapper.d.ts +18 -0
- package/dist/core/botmux-wrapper.d.ts.map +1 -1
- package/dist/core/botmux-wrapper.js +25 -1
- package/dist/core/botmux-wrapper.js.map +1 -1
- package/dist/core/chat-rename.d.ts +27 -0
- package/dist/core/chat-rename.d.ts.map +1 -0
- package/dist/core/chat-rename.js +53 -0
- package/dist/core/chat-rename.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +160 -12
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +25 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +102 -4
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +16 -2
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +611 -62
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +22 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +61 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +6 -0
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +11 -1
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/device-isolation-daemon.d.ts +28 -6
- package/dist/core/device-isolation-daemon.d.ts.map +1 -1
- package/dist/core/device-isolation-daemon.js +96 -13
- package/dist/core/device-isolation-daemon.js.map +1 -1
- package/dist/core/host-overload-alert.d.ts +75 -0
- package/dist/core/host-overload-alert.d.ts.map +1 -1
- package/dist/core/host-overload-alert.js +103 -0
- package/dist/core/host-overload-alert.js.map +1 -1
- package/dist/core/idle-worker-sweeper.js +1 -1
- package/dist/core/idle-worker-sweeper.js.map +1 -1
- package/dist/core/inflight-input-tracker.d.ts +3 -0
- package/dist/core/inflight-input-tracker.d.ts.map +1 -1
- package/dist/core/inflight-input-tracker.js +9 -0
- package/dist/core/inflight-input-tracker.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +10 -0
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/persistent-backend.d.ts +32 -4
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +75 -6
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +2 -2
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/runtime.d.ts +3 -0
- package/dist/core/plugins/runtime.d.ts.map +1 -1
- package/dist/core/plugins/runtime.js.map +1 -1
- package/dist/core/plugins/service-manager.d.ts +4 -1
- package/dist/core/plugins/service-manager.d.ts.map +1 -1
- package/dist/core/plugins/service-manager.js +11 -6
- package/dist/core/plugins/service-manager.js.map +1 -1
- package/dist/core/role-library.d.ts +48 -1
- package/dist/core/role-library.d.ts.map +1 -1
- package/dist/core/role-library.js +111 -3
- package/dist/core/role-library.js.map +1 -1
- package/dist/core/role-resolver.d.ts +14 -2
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +47 -4
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/core/session-activity.d.ts +14 -0
- package/dist/core/session-activity.d.ts.map +1 -1
- package/dist/core/session-activity.js +46 -1
- package/dist/core/session-activity.js.map +1 -1
- package/dist/core/session-board.d.ts +5 -2
- package/dist/core/session-board.d.ts.map +1 -1
- package/dist/core/session-board.js +5 -2
- package/dist/core/session-board.js.map +1 -1
- package/dist/core/session-liveness.d.ts +6 -5
- package/dist/core/session-liveness.d.ts.map +1 -1
- package/dist/core/session-liveness.js +34 -13
- package/dist/core/session-liveness.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +397 -103
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-marker.d.ts +11 -2
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +50 -11
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/session-message-preview.d.ts +20 -0
- package/dist/core/session-message-preview.d.ts.map +1 -0
- package/dist/core/session-message-preview.js +188 -0
- package/dist/core/session-message-preview.js.map +1 -0
- package/dist/core/session-title.d.ts +5 -1
- package/dist/core/session-title.d.ts.map +1 -1
- package/dist/core/session-title.js +20 -3
- package/dist/core/session-title.js.map +1 -1
- package/dist/core/startup-commands.d.ts +1 -1
- package/dist/core/startup-commands.js +1 -1
- package/dist/core/terminal-url.d.ts.map +1 -1
- package/dist/core/terminal-url.js +3 -2
- package/dist/core/terminal-url.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +75 -12
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +23 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-ipc.d.ts +11 -0
- package/dist/core/worker-ipc.d.ts.map +1 -0
- package/dist/core/worker-ipc.js +26 -0
- package/dist/core/worker-ipc.js.map +1 -0
- package/dist/core/worker-pool.d.ts +179 -29
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2118 -331
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +132 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +1327 -449
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +2 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +9 -0
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +4 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +5 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/federation-api.d.ts.map +1 -1
- package/dist/dashboard/federation-api.js +4 -0
- package/dist/dashboard/federation-api.js.map +1 -1
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +32 -5
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/public-redact.d.ts +8 -2
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +34 -7
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/session-card-model.d.ts.map +1 -1
- package/dist/dashboard/session-card-model.js +10 -2
- package/dist/dashboard/session-card-model.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +35 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +88 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +0 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +15 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +212 -58
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +3 -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/bot-onboarding.d.ts +2 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/clipboard.d.ts +2 -0
- package/dist/dashboard/web/clipboard.d.ts.map +1 -0
- package/dist/dashboard/web/clipboard.js +42 -0
- package/dist/dashboard/web/clipboard.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +12 -8
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/dashboard-components.d.ts +17 -16
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-components.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +7 -3
- 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 +205 -12
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -1
- package/dist/dashboard/web/icon-tooltip.js +16 -1
- package/dist/dashboard/web/icon-tooltip.js.map +1 -1
- package/dist/dashboard/web/listener-filters.d.ts +23 -0
- package/dist/dashboard/web/listener-filters.d.ts.map +1 -0
- package/dist/dashboard/web/listener-filters.js +36 -0
- package/dist/dashboard/web/listener-filters.js.map +1 -0
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/plugin-page.d.ts.map +1 -1
- package/dist/dashboard/web/plugin-page.js.map +1 -1
- package/dist/dashboard/web/preview-markdown.d.ts +5 -0
- package/dist/dashboard/web/preview-markdown.d.ts.map +1 -0
- package/dist/dashboard/web/preview-markdown.js +52 -0
- package/dist/dashboard/web/preview-markdown.js.map +1 -0
- package/dist/dashboard/web/roles-page.d.ts.map +1 -1
- package/dist/dashboard/web/roles-page.js +535 -10
- package/dist/dashboard/web/roles-page.js.map +1 -1
- package/dist/dashboard/web/roles.d.ts +77 -0
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +98 -0
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/session-terminal.d.ts.map +1 -1
- package/dist/dashboard/web/session-terminal.js +6 -4
- package/dist/dashboard/web/session-terminal.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts +4 -1
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-kanban.js +1 -1
- package/dist/dashboard/web/sessions-kanban.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts +3 -2
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +163 -28
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +25 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +58 -7
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +28 -3
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +61 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts +6 -5
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +2 -0
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/v3-components.d.ts +3 -2
- package/dist/dashboard/web/v3-components.d.ts.map +1 -1
- package/dist/dashboard/web/v3-components.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +12 -2
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.js +0 -9
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
- package/dist/dashboard/web/webhook-logs-page.d.ts +2 -1
- package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -1
- package/dist/dashboard/web/webhook-logs-page.js +4 -1
- package/dist/dashboard/web/webhook-logs-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-FAPIMQV3.js +10 -0
- package/dist/dashboard-web/chunks/{chunk-QFMH6ITB.js → chunk-5CJMHTT4.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-KQB42ML5.js → chunk-DLZK52JU.js} +14 -12
- package/dist/dashboard-web/chunks/chunk-FDZ7WOBW.js +16 -0
- package/dist/dashboard-web/chunks/{chunk-3QAUJXAS.js → chunk-GTAJV5YC.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FNPGOZMX.js → chunk-IHNG6KE6.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FLGXZJTD.js → chunk-VA6QWSGQ.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
- package/dist/dashboard-web/chunks/chunk-ZJVTGU5X.js +1 -0
- package/dist/dashboard-web/chunks/connectors-page-LKWRT3GS.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-V6654ICQ.js +8 -0
- package/dist/dashboard-web/chunks/insights-page-237RMFQ5.js +7 -0
- package/dist/dashboard-web/chunks/{monitor-room-7IVIS75W.js → monitor-room-OO5Y4FRQ.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-5SSJRQZF.js → monitoring-page-OUEITM5P.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-EOND3L5D.js → office-page-4KNAHQ4Q.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-NISBVBBJ.js → overview-page-QDP4YK2S.js} +1 -1
- package/dist/dashboard-web/chunks/{plugin-page-JXC5F5YV.js → plugin-page-VEGTCEAS.js} +1 -1
- package/dist/dashboard-web/chunks/roles-page-PGXNZZNP.js +1 -0
- package/dist/dashboard-web/chunks/{schedules-page-AVOOKLUH.js → schedules-page-BJNC5TID.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-M2ZYYCMO.js +6 -0
- package/dist/dashboard-web/chunks/settings-page-IJFZKUUW.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-5MW54PCC.js → skills-page-YRAIRZOQ.js} +3 -3
- package/dist/dashboard-web/chunks/{team-federation-page-2SNIMVXI.js → team-federation-page-DEU27GRU.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-W6SBXXQC.js → v3-page-6KHEW2YL.js} +5 -5
- package/dist/dashboard-web/chunks/{whiteboards-page-STC54KTP.js → whiteboards-page-VDECAALH.js} +1 -1
- package/dist/dashboard-web/style.css +12545 -11432
- package/dist/dashboard.js +215 -36
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +41 -2
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +47 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +71 -19
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +71 -19
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/callback-button-marker.d.ts +34 -0
- package/dist/im/lark/callback-button-marker.d.ts.map +1 -0
- package/dist/im/lark/callback-button-marker.js +99 -0
- package/dist/im/lark/callback-button-marker.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +46 -10
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +299 -78
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +3 -4
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +380 -148
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +24 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +101 -8
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts.map +1 -1
- package/dist/im/lark/doc-comment.js +6 -1
- package/dist/im/lark/doc-comment.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +115 -3
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +534 -147
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +16 -12
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +48 -151
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +12 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +31 -8
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/im/lark/invite-command.d.ts +20 -0
- package/dist/im/lark/invite-command.d.ts.map +1 -0
- package/dist/im/lark/invite-command.js +239 -0
- package/dist/im/lark/invite-command.js.map +1 -0
- package/dist/im/lark/md-card.d.ts +66 -4
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +201 -31
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/mention-targets.d.ts +43 -0
- package/dist/im/lark/mention-targets.d.ts.map +1 -0
- package/dist/im/lark/mention-targets.js +190 -0
- package/dist/im/lark/mention-targets.js.map +1 -0
- package/dist/im/lark/message-parser.d.ts +31 -0
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +305 -32
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/reply-card-footer-signature.d.ts +7 -0
- package/dist/im/lark/reply-card-footer-signature.d.ts.map +1 -0
- package/dist/im/lark/reply-card-footer-signature.js +7 -0
- package/dist/im/lark/reply-card-footer-signature.js.map +1 -0
- package/dist/im/lark/sessions-card.d.ts.map +1 -1
- package/dist/im/lark/sessions-card.js +4 -1
- package/dist/im/lark/sessions-card.js.map +1 -1
- package/dist/im/lark/substitute-command.d.ts.map +1 -1
- package/dist/im/lark/substitute-command.js +8 -0
- package/dist/im/lark/substitute-command.js.map +1 -1
- package/dist/im/lark/topic-root-context.d.ts +35 -0
- package/dist/im/lark/topic-root-context.d.ts.map +1 -0
- package/dist/im/lark/topic-root-context.js +37 -0
- package/dist/im/lark/topic-root-context.js.map +1 -0
- package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card.js +2 -1
- package/dist/im/lark/v3-blocked-card.js.map +1 -1
- package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
- package/dist/im/lark/v3-gate-card.js +2 -1
- package/dist/im/lark/v3-gate-card.js.map +1 -1
- package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -1
- package/dist/im/lark/v3-loop-grant-card.js +2 -1
- package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
- package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
- package/dist/im/lark/v3-progress-card.js +2 -1
- package/dist/im/lark/v3-progress-card.js.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card.js +2 -1
- package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
- package/dist/index-core-only.d.ts +3 -0
- package/dist/index-core-only.d.ts.map +1 -0
- package/dist/index-core-only.js +107 -0
- package/dist/index-core-only.js.map +1 -0
- package/dist/services/async-trigger-state.d.ts +8 -0
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +3 -2
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +10 -2
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +3 -2
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/backend-availability.d.ts +3 -1
- package/dist/services/backend-availability.d.ts.map +1 -1
- package/dist/services/backend-availability.js +14 -1
- package/dist/services/backend-availability.js.map +1 -1
- package/dist/services/backend-type-store.d.ts.map +1 -1
- package/dist/services/backend-type-store.js +1 -1
- package/dist/services/backend-type-store.js.map +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +2 -1
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +14 -1
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +65 -1
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +7 -2
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +30 -4
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/codex-app-runner-protocol.d.ts +9 -0
- package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
- package/dist/services/codex-app-runner-protocol.js +17 -0
- package/dist/services/codex-app-runner-protocol.js.map +1 -1
- package/dist/services/codex-app-token-usage.d.ts +93 -0
- package/dist/services/codex-app-token-usage.d.ts.map +1 -0
- package/dist/services/codex-app-token-usage.js +190 -0
- package/dist/services/codex-app-token-usage.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +6 -1
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +103 -32
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/federation-roster.d.ts +4 -0
- package/dist/services/federation-roster.d.ts.map +1 -1
- package/dist/services/federation-roster.js +3 -0
- package/dist/services/federation-roster.js.map +1 -1
- package/dist/services/federation-store.d.ts +6 -0
- package/dist/services/federation-store.d.ts.map +1 -1
- package/dist/services/federation-store.js.map +1 -1
- package/dist/services/grant-policy.d.ts +13 -0
- package/dist/services/grant-policy.d.ts.map +1 -0
- package/dist/services/grant-policy.js +40 -0
- package/dist/services/grant-policy.js.map +1 -0
- package/dist/services/grant-store.d.ts +24 -3
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +196 -12
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/groups-store.d.ts +36 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +123 -1
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/jsonl-cursor.d.ts.map +1 -1
- package/dist/services/jsonl-cursor.js +8 -2
- package/dist/services/jsonl-cursor.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +32 -0
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/message-listener-run-preview-store.d.ts +43 -0
- package/dist/services/message-listener-run-preview-store.d.ts.map +1 -0
- package/dist/services/message-listener-run-preview-store.js +152 -0
- package/dist/services/message-listener-run-preview-store.js.map +1 -0
- package/dist/services/message-listener-store.d.ts +26 -0
- package/dist/services/message-listener-store.d.ts.map +1 -0
- package/dist/services/message-listener-store.js +138 -0
- package/dist/services/message-listener-store.js.map +1 -0
- package/dist/services/message-listener.d.ts +112 -0
- package/dist/services/message-listener.d.ts.map +1 -0
- package/dist/services/message-listener.js +391 -0
- package/dist/services/message-listener.js.map +1 -0
- package/dist/services/overload-alert-migration.d.ts +15 -0
- package/dist/services/overload-alert-migration.d.ts.map +1 -0
- package/dist/services/overload-alert-migration.js +115 -0
- package/dist/services/overload-alert-migration.js.map +1 -0
- package/dist/services/send-policy.d.ts +38 -0
- package/dist/services/send-policy.d.ts.map +1 -1
- package/dist/services/send-policy.js +34 -0
- package/dist/services/send-policy.js.map +1 -1
- package/dist/services/session-store.d.ts +12 -1
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +44 -2
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +87 -12
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/substitute-mode-normalize.d.ts +3 -0
- package/dist/services/substitute-mode-normalize.d.ts.map +1 -1
- package/dist/services/substitute-mode-normalize.js +8 -0
- package/dist/services/substitute-mode-normalize.js.map +1 -1
- package/dist/services/substitute-mode-store.d.ts.map +1 -1
- package/dist/services/substitute-mode-store.js +8 -1
- package/dist/services/substitute-mode-store.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +6 -1
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +145 -29
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/services/trigger-types.d.ts +16 -0
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +6 -0
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/setup/bot-config-editor.js +2 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-zmx.d.ts +31 -0
- package/dist/setup/ensure-zmx.d.ts.map +1 -0
- package/dist/setup/ensure-zmx.js +161 -0
- package/dist/setup/ensure-zmx.js.map +1 -0
- package/dist/setup/index.d.ts +1 -1
- package/dist/setup/index.js +1 -1
- package/dist/setup/open-platform-automation.d.ts +9 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +15 -2
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/setup-args.d.ts +1 -1
- package/dist/setup/setup-args.d.ts.map +1 -1
- package/dist/setup/setup-args.js +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +32 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +6 -2
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +10 -5
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/types.d.ts +79 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +1 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +4 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/effort-confirm-dialog.d.ts +40 -0
- package/dist/utils/effort-confirm-dialog.d.ts.map +1 -0
- package/dist/utils/effort-confirm-dialog.js +74 -0
- package/dist/utils/effort-confirm-dialog.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +56 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +80 -0
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/utils/render-dimensions.d.ts +8 -0
- package/dist/utils/render-dimensions.d.ts.map +1 -1
- package/dist/utils/render-dimensions.js +10 -0
- package/dist/utils/render-dimensions.js.map +1 -1
- package/dist/utils/stuck-key-guard.d.ts +3 -2
- package/dist/utils/stuck-key-guard.d.ts.map +1 -1
- package/dist/utils/stuck-key-guard.js +3 -3
- package/dist/utils/stuck-key-guard.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +6 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +15 -0
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/tui-input-delivery.d.ts +32 -0
- package/dist/utils/tui-input-delivery.d.ts.map +1 -0
- package/dist/utils/tui-input-delivery.js +53 -0
- package/dist/utils/tui-input-delivery.js.map +1 -0
- package/dist/utils/xml.d.ts +19 -0
- package/dist/utils/xml.d.ts.map +1 -0
- package/dist/utils/xml.js +26 -0
- package/dist/utils/xml.js.map +1 -0
- package/dist/worker.js +1852 -374
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +8 -8
- package/dist/workflows/events/payloads.d.ts +8 -8
- package/dist/workflows/events/schema.d.ts +160 -160
- package/dist/workflows/v3/session-relay-client.js +1 -1
- package/dist/workflows/v3/session-relay-client.js.map +1 -1
- package/dist/workflows/v3/worker-fence.d.ts.map +1 -1
- package/dist/workflows/v3/worker-fence.js +15 -10
- package/dist/workflows/v3/worker-fence.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-UKA7K4JV.js +0 -10
- package/dist/dashboard-web/chunks/chunk-DFLNXQNT.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-A77YAB5Y.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-RUZNVXQB.js +0 -8
- package/dist/dashboard-web/chunks/insights-page-RF7U7RTA.js +0 -22
- package/dist/dashboard-web/chunks/roles-page-BUMKD7Y4.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-6HLSEAB7.js +0 -2
- package/dist/dashboard-web/chunks/settings-page-MJDZHQ4X.js +0 -2
package/dist/daemon.js
CHANGED
|
@@ -14,17 +14,17 @@ import { buildDashboardUrls } from './core/dashboard-url.js';
|
|
|
14
14
|
import { resolveBotmuxDataDir } from './core/data-dir.js';
|
|
15
15
|
import { reloadExactDaemonBotConfig } from './core/daemon-config-fence.js';
|
|
16
16
|
import { writeHeartbeat } from './core/daemon-heartbeat.js';
|
|
17
|
-
import { botmuxWrapperFiles } from './core/botmux-wrapper.js';
|
|
18
|
-
import { evaluateOverload, formatOverloadAlert, buildOverloadAlertCard, buildOverloadRecoveredCard, initialOverloadCardState,
|
|
17
|
+
import { botmuxWrapperFiles, resolveBotmuxWrapperBinDir } from './core/botmux-wrapper.js';
|
|
18
|
+
import { evaluateOverload, formatOverloadAlert, buildOverloadAlertCard, buildOverloadRecoveredCard, initialOverloadCardState, INITIAL_OVERLOAD_STATE, computeOverloadThresholds, isOverloadAlertTarget, } from './core/host-overload-alert.js';
|
|
19
19
|
import { registerOverloadNonce } from './im/lark/overload-nonce.js';
|
|
20
20
|
import { countHostOverload } from './im/lark/card-handler.js';
|
|
21
21
|
import { startMaintenance, stopMaintenance } from './core/maintenance.js';
|
|
22
22
|
import { selectCodexRuntimeUpdateTargets, startCliRuntimeUpdateMonitor, stopCliRuntimeUpdateMonitor, } from './core/cli-runtime-update.js';
|
|
23
23
|
import { sendRestartReportIfPending } from './core/restart-report.js';
|
|
24
24
|
import { statSync } from 'node:fs';
|
|
25
|
-
import { addReaction, getChatMode, getChatNameAndMode, getMessageChatId, listChatMemberOpenIds, MessageWithdrawnError, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
|
|
25
|
+
import { addReaction, deleteMessage, getChatMode, getChatNameAndMode, getMessageChatId, listChatMemberOpenIds, MessageWithdrawnError, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
|
|
26
26
|
import { resolveGroupJoinPrompt, waitForAllowedUserInChat } from './core/auto-start.js';
|
|
27
|
-
import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, setResolvedAllowedUsersRepublishHook, setAllowedUsersResolveRetryHook, } from './bot-registry.js';
|
|
27
|
+
import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, setResolvedAllowedUsersRepublishHook, setAllowedUsersResolveRetryHook, vcMeetingAgentConfigActive, } from './bot-registry.js';
|
|
28
28
|
import { setDisplayNameRefresher, findConfigField, applyConfigField } from './services/bot-config-store.js';
|
|
29
29
|
import { resolveRegularGroupMode } from './services/chat-reply-mode-store.js';
|
|
30
30
|
import { renameBotOnOpenPlatform, changeBotAvatarOnOpenPlatform } from './services/open-platform-rename.js';
|
|
@@ -34,6 +34,7 @@ import * as chatFirstSeenStore from './services/chat-first-seen-store.js';
|
|
|
34
34
|
import { ensureDefaultOncallBound } from './services/oncall-store.js';
|
|
35
35
|
import * as scheduleStore from './services/schedule-store.js';
|
|
36
36
|
import { migrateSharedSchedulesAtStartup } from './services/schedule-split-migration.js';
|
|
37
|
+
import { migrateOverloadAlertAtStartup } from './services/overload-alert-migration.js';
|
|
37
38
|
import * as messageQueue from './services/message-queue.js';
|
|
38
39
|
import { emitHookEvent, emitHookEventLocal, HOOK_EVENTS } from './services/hook-runner.js';
|
|
39
40
|
import { setSessionLifecycleShutdown } from './services/session-lifecycle-hooks.js';
|
|
@@ -41,6 +42,7 @@ import { createImgNumberer, parseEventMessage, resolveNonsupportMessage, stripLe
|
|
|
41
42
|
import { expandMergeForward } from './im/lark/merge-forward.js';
|
|
42
43
|
import { bindResourcesToMessage, composeForwardFollowupContent, mergeMessageMentions } from './im/lark/forward-followup-content.js';
|
|
43
44
|
import { buildQuoteHint } from './im/lark/quote-hint.js';
|
|
45
|
+
import { buildTopicThreadContext } from './im/lark/topic-root-context.js';
|
|
44
46
|
import { logger } from './utils/logger.js';
|
|
45
47
|
import { applyAllowedUsersResolve } from './utils/allowed-users-apply.js';
|
|
46
48
|
import { withFileLock, withFileLockSync } from './utils/file-lock.js';
|
|
@@ -52,8 +54,8 @@ import { scrubTmuxServerGlobalEnv } from './setup/ensure-tmux.js';
|
|
|
52
54
|
import { entryNeedsContactResolve } from './setup/bot-config-editor.js';
|
|
53
55
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
54
56
|
import { validateWorkingDir } from './core/working-dir.js';
|
|
55
|
-
import { activeSessionKey, sessionKey, sessionAnchorId, storedSessionAnchorId } from './core/types.js';
|
|
56
|
-
import {
|
|
57
|
+
import { activeSessionKey, sessionKey, sessionAnchorId, storedSessionAnchorId, larkTransportEnabled } from './core/types.js';
|
|
58
|
+
import { setTerminalProxyPort, setTerminalExternalPort } from './core/terminal-url.js';
|
|
57
59
|
import { startTerminalProxy } from './core/terminal-proxy.js';
|
|
58
60
|
import * as scheduler from './core/scheduler.js';
|
|
59
61
|
import { scanMultipleProjects } from './services/project-scanner.js';
|
|
@@ -62,8 +64,8 @@ import { isLocalCliOpenReady } from './services/local-cli-opener.js';
|
|
|
62
64
|
import { RECEIVED_REACTION_EMOJI_TYPE, SUBSTITUTE_RECEIVED_REACTION_EMOJI_TYPE } from './core/pending-response.js';
|
|
63
65
|
import { t as tr, localeForBot } from './i18n/index.js';
|
|
64
66
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
65
|
-
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, forkAdoptWorker, sendWorkerInput, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, getCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, sweepGlobalBotmuxSkills, writableTerminalLinkFor, findActiveBySessionId, getDaemonBootId, } from './core/worker-pool.js';
|
|
66
|
-
import { AbortDeadlineError, hasExactSafeJsonKeys, ipcRoute, isTrustedHostIpcRequest, JsonBodyTooLargeError, jsonRes, readJsonBody, runWithAbortDeadline, setBotName, setLarkAppId, startIpcServer, setBotRenamer, setBotAvatarChanger } from './core/dashboard-ipc-server.js';
|
|
67
|
+
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, forkAdoptWorker, sendWorkerInput, sendWorkerSessionInput, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, getCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, setActiveSessionIfActive, rollbackRejectedSessionAndGetWinner, ensureCliEnv, sweepGlobalBotmuxSkills, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, findActiveBySessionId, getDaemonBootId, getDaemonStreamingCardUsageSnapshot, isSessionTransferring, } from './core/worker-pool.js';
|
|
68
|
+
import { AbortDeadlineError, hasExactSafeJsonKeys, ipcRoute, isTrustedHostIpcRequest, JsonBodyTooLargeError, jsonRes, readJsonBody, runWithAbortDeadline, setBotName, setLarkAppId, startIpcServer, setBotRenamer, setBotAvatarChanger, armCoreOnlyReadinessGate, setCoreOnlyReady } from './core/dashboard-ipc-server.js';
|
|
67
69
|
import { setDeviceIsolationDaemonIdentity } from './core/device-isolation-daemon.js';
|
|
68
70
|
import { cancelSessionReadyAck, waitForSessionReadyAck, } from './core/session-ready-handshake.js';
|
|
69
71
|
import { loadOrCreateDashboardSecret } from './dashboard/auth.js';
|
|
@@ -86,10 +88,12 @@ import { beginReplyTargetTurn, fallbackTurnId, isSubstituteTurn, resolveSessionR
|
|
|
86
88
|
import { readDeferredTopicBinding } from './core/deferred-topic-binding.js';
|
|
87
89
|
import { buildBotmuxLarkNativeSessionTitle, extractBotmuxLarkNativeSessionTitlePrompt, } from './core/session-title.js';
|
|
88
90
|
import { settleDeferredScheduleRun } from './core/deferred-schedule-settlement.js';
|
|
91
|
+
import { renderMessageListenerPrompt } from './services/message-listener.js';
|
|
89
92
|
import { sweepOrphanSandboxes } from './adapters/backend/sandbox.js';
|
|
90
93
|
import { TmuxBackend } from './adapters/backend/tmux-backend.js';
|
|
91
94
|
import { HerdrBackend } from './adapters/backend/herdr-backend.js';
|
|
92
95
|
import { ZellijBackend } from './adapters/backend/zellij-backend.js';
|
|
96
|
+
import { ZmxBackend } from './adapters/backend/zmx-backend.js';
|
|
93
97
|
import { sweepIdleWorkers, DEFAULT_MAX_LIVE_WORKERS } from './core/idle-worker-sweeper.js';
|
|
94
98
|
import { getSessionPersistentBackendType, killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget, probePersistentSession, resolvePersistentBackendTarget, resolvePairedSpawnBackendType, } from './core/persistent-backend.js';
|
|
95
99
|
import { handleCardAction, runAutoWorktreeCommit } from './im/lark/card-handler.js';
|
|
@@ -148,9 +152,9 @@ function republishResolvedAllowedUsers(larkAppId, resolved) {
|
|
|
148
152
|
catch { /* best effort */ }
|
|
149
153
|
}
|
|
150
154
|
let vcMeetingTerminalReconciler;
|
|
151
|
-
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, markForwardFollowupsSessionsReady, writeBotInfoFile, canOperate, canRunDaemonCommand, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
152
|
-
import { getDocSubscription, listAllDocSubscriptions, removeDocSubscription, setDocCommentPollCursor } from './services/doc-subs-store.js';
|
|
153
|
-
import { BOT_REPLY_SENTINEL, subscribeDocFile, unsubscribeDocFile, addCommentReaction, hasBotSentinel, isBotAuthoredReply, listDocComments } from './im/lark/doc-comment.js';
|
|
155
|
+
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, markForwardFollowupsSessionsReady, writeBotInfoFile, canOperate, canRunDaemonCommand, evaluateTalk, evaluateBotTalk, evaluateAskAnswerTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
156
|
+
import { getDocSubscription, listAllDocSubscriptions, putDocSubscription, removeDocSubscription, setDocCommentPollCursor } from './services/doc-subs-store.js';
|
|
157
|
+
import { BOT_REPLY_SENTINEL, subscribeDocFile, unsubscribeDocFile, addCommentReaction, removeCommentReaction, hasBotSentinel, isBotAuthoredReply, listDocComments } from './im/lark/doc-comment.js';
|
|
154
158
|
import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
|
|
155
159
|
import { normalizeBrand } from './im/lark/lark-hosts.js';
|
|
156
160
|
import { buildDocCommentTurnInput, buildDocWatchWarmupTurnInput } from './core/doc-comment-prompt.js';
|
|
@@ -158,7 +162,7 @@ import { advanceDocCommentCursor, docCommentRepliesAfterCursor, latestDocComment
|
|
|
158
162
|
import { renderBufferedSenderBlock } from './core/session-manager.js';
|
|
159
163
|
import { shutdownBackendDisposition } from './core/persistent-backend.js';
|
|
160
164
|
import { evaluateVcMeetingConsumerIsolation } from './services/vc-meeting-consumer-isolation.js';
|
|
161
|
-
import { markSessionActivity, announcePendingRepoSession, publishAttentionPatch, publishLastInputFromBotPatch, clearAgentAttention, } from './core/session-activity.js';
|
|
165
|
+
import { markSessionActivity, announcePendingRepoSession, publishAttentionPatch, publishLastInputFromBotPatch, publishSessionMessagePreviewPatch, publishClosedSessionPatch, clearAgentAttention, } from './core/session-activity.js';
|
|
162
166
|
import { emitSessionLifecycleHook } from './services/session-lifecycle-hooks.js';
|
|
163
167
|
import { botAutoWorktreeEnabled } from './services/default-worktree.js';
|
|
164
168
|
import { setCardDispatcher as setAskCardDispatcher, setCanTalkChecker as setAskCanTalkChecker, registerAsk as registerAskBroker, findPendingAskByAnchor, submitCustomReply, } from './core/ask-broker.js';
|
|
@@ -197,7 +201,7 @@ const VC_MEETING_DELIVERY_LEASE_SCAN_MS = 60_000;
|
|
|
197
201
|
const VC_MEETING_RUNTIME_EXPIRY_ACK_TIMEOUT_MS = 3_000;
|
|
198
202
|
const VC_MEETING_RUNTIME_EXPIRY_TEARDOWN_MS = 8_000;
|
|
199
203
|
const VC_MEETING_RUNTIME_EXPIRY_REPROBE_MS = 5_000;
|
|
200
|
-
const VC_MEETING_PERSISTENT_BACKENDS = ['tmux', 'herdr', 'zellij'];
|
|
204
|
+
const VC_MEETING_PERSISTENT_BACKENDS = ['tmux', 'herdr', 'zellij', 'zmx'];
|
|
201
205
|
function vcMeetingDeliveryStreamKey(scope) {
|
|
202
206
|
return [scope.listenerAppId, scope.meetingId, scope.memberId, scope.memberEpoch].join('\u0000');
|
|
203
207
|
}
|
|
@@ -268,7 +272,9 @@ function vcMeetingPersistentBackendAvailable(backendType) {
|
|
|
268
272
|
return TmuxBackend.isAvailable();
|
|
269
273
|
if (backendType === 'herdr')
|
|
270
274
|
return HerdrBackend.isAvailable();
|
|
271
|
-
|
|
275
|
+
if (backendType === 'zellij')
|
|
276
|
+
return ZellijBackend.isAvailable();
|
|
277
|
+
return ZmxBackend.isAvailable();
|
|
272
278
|
}
|
|
273
279
|
let testOnlyVcMeetingBootBackingMissing;
|
|
274
280
|
function vcMeetingBootBackingMissing(sessionId, destroy) {
|
|
@@ -289,7 +295,8 @@ function vcMeetingBootBackingMissing(sessionId, destroy) {
|
|
|
289
295
|
const persistedBackend = liveBackend ?? persistedSession?.backendType;
|
|
290
296
|
if (persistedBackend === 'pty')
|
|
291
297
|
return true;
|
|
292
|
-
const backendTypes = persistedBackend === 'tmux' || persistedBackend === 'herdr'
|
|
298
|
+
const backendTypes = persistedBackend === 'tmux' || persistedBackend === 'herdr'
|
|
299
|
+
|| persistedBackend === 'zellij' || persistedBackend === 'zmx'
|
|
293
300
|
? [persistedBackend]
|
|
294
301
|
: VC_MEETING_PERSISTENT_BACKENDS.filter(vcMeetingPersistentBackendAvailable);
|
|
295
302
|
let allMissing = true;
|
|
@@ -298,7 +305,7 @@ function vcMeetingBootBackingMissing(sessionId, destroy) {
|
|
|
298
305
|
const name = target.sessionName;
|
|
299
306
|
if (destroy) {
|
|
300
307
|
try {
|
|
301
|
-
killPersistentBackendTarget(target);
|
|
308
|
+
killPersistentBackendTarget(target, sessionId);
|
|
302
309
|
}
|
|
303
310
|
catch (err) {
|
|
304
311
|
logger.warn(`[vc-delivery] boot recovery backing kill failed backend=${backendType} `
|
|
@@ -480,7 +487,7 @@ function createVcMeetingRuntimeLeaseRecovery(deps) {
|
|
|
480
487
|
: persistentScope === 'unknown'
|
|
481
488
|
// An unavailable client binary cannot own/reach a live mux session on
|
|
482
489
|
// this host. Excluding it avoids permanent unknown fences on machines
|
|
483
|
-
// that only install one of the
|
|
490
|
+
// that only install one of the supported persistent backends.
|
|
484
491
|
? VC_MEETING_PERSISTENT_BACKENDS.filter(deps.backendAvailable)
|
|
485
492
|
: [persistentScope];
|
|
486
493
|
let allMissing = true;
|
|
@@ -489,7 +496,7 @@ function createVcMeetingRuntimeLeaseRecovery(deps) {
|
|
|
489
496
|
const target = resolvePersistentBackendTarget(backendType, fence.receiverSessionId, persistedTarget);
|
|
490
497
|
const name = target.sessionName;
|
|
491
498
|
try {
|
|
492
|
-
deps.killPersistent(backendType, name, target);
|
|
499
|
+
deps.killPersistent(backendType, name, fence.receiverSessionId, target);
|
|
493
500
|
}
|
|
494
501
|
catch (err) {
|
|
495
502
|
deps.warn(`[vc-delivery] runtime lease backing kill failed backend=${backendType} `
|
|
@@ -721,7 +728,8 @@ const vcMeetingRuntimeLeaseRecovery = createVcMeetingRuntimeLeaseRecovery({
|
|
|
721
728
|
resolveMissingPersistentScope(sessionId) {
|
|
722
729
|
const persisted = sessionStore.getSession(sessionId);
|
|
723
730
|
const backendType = persisted?.backendType;
|
|
724
|
-
if (backendType === 'tmux' || backendType === 'herdr'
|
|
731
|
+
if (backendType === 'tmux' || backendType === 'herdr'
|
|
732
|
+
|| backendType === 'zellij' || backendType === 'zmx') {
|
|
725
733
|
return backendType;
|
|
726
734
|
}
|
|
727
735
|
return backendType === 'pty' ? 'none' : 'unknown';
|
|
@@ -731,15 +739,144 @@ const vcMeetingRuntimeLeaseRecovery = createVcMeetingRuntimeLeaseRecovery({
|
|
|
731
739
|
?? sessionStore.getSession(sessionId)?.persistentBackendTarget;
|
|
732
740
|
},
|
|
733
741
|
backendAvailable: vcMeetingPersistentBackendAvailable,
|
|
734
|
-
killPersistent: (backendType, sessionName, target) => target
|
|
735
|
-
? killPersistentBackendTarget(target)
|
|
736
|
-
: killPersistentSession(backendType, sessionName),
|
|
742
|
+
killPersistent: (backendType, sessionName, sessionId, target) => target
|
|
743
|
+
? killPersistentBackendTarget(target, sessionId)
|
|
744
|
+
: killPersistentSession(backendType, sessionName, sessionId),
|
|
737
745
|
probePersistent: (backendType, sessionName, target) => target
|
|
738
746
|
? probePersistentBackendTarget(target)
|
|
739
747
|
: probePersistentSession(backendType, sessionName),
|
|
740
748
|
warn: message => logger.warn(message),
|
|
741
749
|
error: message => logger.error(message),
|
|
742
750
|
});
|
|
751
|
+
function captureRoutingGeneration(ds) {
|
|
752
|
+
return {
|
|
753
|
+
key: sessionKey(sessionAnchorId(ds), ds.larkAppId),
|
|
754
|
+
ds,
|
|
755
|
+
session: ds.session,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function isCurrentRoutingGeneration(generation) {
|
|
759
|
+
const { key, ds, session } = generation;
|
|
760
|
+
return session.status === 'active'
|
|
761
|
+
&& ds.session === session
|
|
762
|
+
&& sessionKey(sessionAnchorId(ds), ds.larkAppId) === key
|
|
763
|
+
&& activeSessions.get(key) === ds;
|
|
764
|
+
}
|
|
765
|
+
function ensureCurrentRoutingGeneration(generation, phase) {
|
|
766
|
+
if (isCurrentRoutingGeneration(generation))
|
|
767
|
+
return;
|
|
768
|
+
throw new Error(`session routing generation changed during ${phase}`);
|
|
769
|
+
}
|
|
770
|
+
/** Register one freshly-created doc-native session without overwriting a
|
|
771
|
+
* concurrent winner. The rejected row is closed before the winner is returned,
|
|
772
|
+
* so its persisted record cannot survive as a ghost-active session. */
|
|
773
|
+
async function registerDocSessionCandidate(key, candidate, rollback = rollbackRejectedSessionAndGetWinner) {
|
|
774
|
+
if (setActiveSessionIfActive(activeSessions, key, candidate))
|
|
775
|
+
return candidate;
|
|
776
|
+
return await rollback(activeSessions, key, candidate) ?? null;
|
|
777
|
+
}
|
|
778
|
+
function sameDocBindingRoute(a, b) {
|
|
779
|
+
return a.sessionAnchor === b.sessionAnchor
|
|
780
|
+
&& a.sessionId === b.sessionId
|
|
781
|
+
&& a.scope === b.scope
|
|
782
|
+
&& a.chatId === b.chatId;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Move one subscription onto the authoritative route owned by `selected`.
|
|
786
|
+
*
|
|
787
|
+
* The store is re-read immediately before the synchronous write so cursor and
|
|
788
|
+
* metadata updates made from another event are retained. A newer explicit
|
|
789
|
+
* rebind is never overwritten: the only route change accepted since `sub` was
|
|
790
|
+
* read is the same selected route (the normal concurrent auto-create winner
|
|
791
|
+
* case). Mutating the caller's snapshot keeps the current comment turn on the
|
|
792
|
+
* same route as the persisted subscription.
|
|
793
|
+
*/
|
|
794
|
+
function persistDocBindingToSession(sub, larkAppId, selected, deps = {}) {
|
|
795
|
+
const dataDir = deps.dataDir ?? config.session.dataDir;
|
|
796
|
+
const read = deps.read ?? getDocSubscription;
|
|
797
|
+
const write = deps.write ?? putDocSubscription;
|
|
798
|
+
const current = read(dataDir, larkAppId, sub.fileToken);
|
|
799
|
+
if (!current) {
|
|
800
|
+
throw new Error(`document subscription was removed during routing: ${sub.fileToken.slice(0, 12)}`);
|
|
801
|
+
}
|
|
802
|
+
const selectedRoute = {
|
|
803
|
+
sessionAnchor: sessionAnchorId(selected),
|
|
804
|
+
sessionId: selected.session.sessionId,
|
|
805
|
+
scope: selected.scope,
|
|
806
|
+
chatId: selected.chatId,
|
|
807
|
+
};
|
|
808
|
+
const currentTargetsSelected = current.sessionAnchor === selectedRoute.sessionAnchor
|
|
809
|
+
&& current.sessionId === selectedRoute.sessionId
|
|
810
|
+
&& current.scope === selectedRoute.scope
|
|
811
|
+
&& current.chatId === selectedRoute.chatId;
|
|
812
|
+
if (!sameDocBindingRoute(current, sub) && !currentTargetsSelected) {
|
|
813
|
+
throw new Error(`document subscription was rebound during routing: ${sub.fileToken.slice(0, 12)}`);
|
|
814
|
+
}
|
|
815
|
+
const rebound = { ...current, ...selectedRoute };
|
|
816
|
+
write(dataDir, larkAppId, rebound);
|
|
817
|
+
Object.assign(sub, rebound);
|
|
818
|
+
}
|
|
819
|
+
/** Persist the route selected by registration. A candidate that won the CAS
|
|
820
|
+
* is now daemon-owned, so a failed subscription write must close only that
|
|
821
|
+
* exact occupant. A rejected candidate must never close the concurrent
|
|
822
|
+
* winner whose route it received from registerDocSessionCandidate(). */
|
|
823
|
+
async function persistSelectedDocBinding(routingKey, sub, larkAppId, selected, candidate, persist = persistDocBindingToSession, rollback = closeSessionHelper) {
|
|
824
|
+
try {
|
|
825
|
+
persist(sub, larkAppId, selected);
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
if (selected === candidate && activeSessions.get(routingKey) === candidate) {
|
|
829
|
+
try {
|
|
830
|
+
await rollback(candidate.session.sessionId);
|
|
831
|
+
}
|
|
832
|
+
catch (rollbackErr) {
|
|
833
|
+
logger.error(`[doc-comment] failed to roll back auto-created session ` +
|
|
834
|
+
`${candidate.session.sessionId.slice(0, 8)} after binding error: ` +
|
|
835
|
+
`${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
|
|
836
|
+
}
|
|
837
|
+
finally {
|
|
838
|
+
// closeSessionHelper removes this synchronously in production. Keep a
|
|
839
|
+
// final identity guard for injected/test rollback failures so no newer
|
|
840
|
+
// route owner can be evicted.
|
|
841
|
+
if (activeSessions.get(routingKey) === candidate)
|
|
842
|
+
activeSessions.delete(routingKey);
|
|
843
|
+
candidate.session.status = 'closed';
|
|
844
|
+
candidate.session.closedAt ??= new Date().toISOString();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function ownsCurrentRoute(ds, larkAppId) {
|
|
851
|
+
return ds.larkAppId === larkAppId
|
|
852
|
+
&& ds.session.status === 'active'
|
|
853
|
+
&& activeSessions.get(sessionKey(sessionAnchorId(ds), larkAppId)) === ds;
|
|
854
|
+
}
|
|
855
|
+
/** Resolve a binding without ever handing a stale anchor's replacement the
|
|
856
|
+
* comment. If the subscribed session was relayed, find it by stable sessionId
|
|
857
|
+
* and lazily migrate the subscription to its new authoritative route. */
|
|
858
|
+
function resolveBoundDocSession(sub, larkAppId, persist = persistDocBindingToSession) {
|
|
859
|
+
const direct = activeSessions.get(sessionKey(sub.sessionAnchor, larkAppId));
|
|
860
|
+
if (direct
|
|
861
|
+
&& ownsCurrentRoute(direct, larkAppId)
|
|
862
|
+
&& (!sub.sessionId || direct.session.sessionId === sub.sessionId)) {
|
|
863
|
+
return direct;
|
|
864
|
+
}
|
|
865
|
+
if (!sub.sessionId)
|
|
866
|
+
return undefined;
|
|
867
|
+
const relocated = [...new Set(activeSessions.values())].find(candidate => candidate.session.sessionId === sub.sessionId
|
|
868
|
+
&& ownsCurrentRoute(candidate, larkAppId));
|
|
869
|
+
if (!relocated)
|
|
870
|
+
return undefined;
|
|
871
|
+
persist(sub, larkAppId, relocated);
|
|
872
|
+
return relocated;
|
|
873
|
+
}
|
|
874
|
+
export const __testOnly_captureRoutingGeneration = captureRoutingGeneration;
|
|
875
|
+
export const __testOnly_isCurrentRoutingGeneration = isCurrentRoutingGeneration;
|
|
876
|
+
export const __testOnly_registerDocSessionCandidate = registerDocSessionCandidate;
|
|
877
|
+
export const __testOnly_persistDocBindingToSession = persistDocBindingToSession;
|
|
878
|
+
export const __testOnly_persistSelectedDocBinding = persistSelectedDocBinding;
|
|
879
|
+
export const __testOnly_resolveBoundDocSession = resolveBoundDocSession;
|
|
743
880
|
let vcMeetingOutputTextSenderForTest;
|
|
744
881
|
let vcMeetingTextOutputAvailableForTest;
|
|
745
882
|
const vcMeetingSessions = new Map();
|
|
@@ -1900,6 +2037,24 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId, turnId
|
|
|
1900
2037
|
const appId = larkAppId ?? ds?.larkAppId ?? getAllBots()[0]?.config.larkAppId;
|
|
1901
2038
|
if (!appId)
|
|
1902
2039
|
throw new Error('No bot configured');
|
|
2040
|
+
// Central Lark-transport gate (fail-closed): a core-only (apiOnly) bot or an
|
|
2041
|
+
// HTTP virtual session (http_async_* / http_wait_*) has no real Feishu chat.
|
|
2042
|
+
// Auxiliary worker UI is already suppressed at its source (worker-pool
|
|
2043
|
+
// managedAuxUiSuppressed) and HTTP final_output is intercepted upstream in
|
|
2044
|
+
// deliverFinalOutput, so this path is normally unreachable for such sessions;
|
|
2045
|
+
// this is a defense-in-depth backstop. Return '' (NOT the synthetic anchor) —
|
|
2046
|
+
// an empty message id reads as "nothing posted" to every caller that stores
|
|
2047
|
+
// the result as a card id, so the falsy-guarded scheduleCardPatch/updateMessage
|
|
2048
|
+
// never fire on a synthetic id. Returning `anchor` would be a latent bug: it
|
|
2049
|
+
// would be stored as streamCardId and a later PATCH would dial Feishu.
|
|
2050
|
+
const transportAllowed = larkTransportEnabled({
|
|
2051
|
+
chatId: ds?.chatId ?? anchor,
|
|
2052
|
+
apiOnly: getBot(appId).config.apiOnly,
|
|
2053
|
+
});
|
|
2054
|
+
if (!transportAllowed) {
|
|
2055
|
+
logger.debug(`[lark-transport] suppressed reply for no-transport session (app=${appId} anchor=${anchor.substring(0, 16)})`);
|
|
2056
|
+
return '';
|
|
2057
|
+
}
|
|
1903
2058
|
const hookContext = ds ? {
|
|
1904
2059
|
sessionId: ds.session.sessionId,
|
|
1905
2060
|
scope: ds.scope,
|
|
@@ -2016,12 +2171,22 @@ async function notifyQuotaExhausted(larkAppId, anchor, senderOpenId, limit) {
|
|
|
2016
2171
|
logger.warn(`[quota:${larkAppId}] quota exhausted notify failed: ${err}`);
|
|
2017
2172
|
}
|
|
2018
2173
|
}
|
|
2019
|
-
export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, senderUnionId, memberUnionId, chatType) {
|
|
2174
|
+
export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, senderUnionId, memberUnionId, chatType, botSender, opts) {
|
|
2175
|
+
if (opts?.listenerAuthorized)
|
|
2176
|
+
return true;
|
|
2020
2177
|
// senderUnionId(bot-locked)让 evaluateTalk 认出跨部署团队 peer bot(teamBot 腿);
|
|
2021
2178
|
// memberUnionId(可为真人 union)走 teamMember 腿——否则外部闸门/群闸门放进来的
|
|
2022
2179
|
// 团队 bot 或团队成员消息会在这里复查处被静默丢弃(#332 端到端断点,人腿同理)。
|
|
2023
2180
|
// chatType 同理:不传的话,p2pOpen 放行的私聊消息会在这道复查里被丢掉(同一个断点)。
|
|
2024
|
-
|
|
2181
|
+
//
|
|
2182
|
+
// botSender:这条消息的发送方是飞书盖章的 bot(sender_type ∈ app|bot,或 cross-ref
|
|
2183
|
+
// 认识的自家 peer)。命中时必须与 dispatcher 的外层闸用**同一个谓词** evaluateBotTalk,
|
|
2184
|
+
// 否则「团队拉群 + sender 没带 union_id」这条路径会外层放行、这里静默丢弃——比弹卡
|
|
2185
|
+
// 更糟:owner 以为授权生效了,消息凭空消失(#332 同款断点的最后一截)。
|
|
2186
|
+
// 不传(人的路径)→ 原样走 evaluateTalk,语义不变。
|
|
2187
|
+
const ev = botSender
|
|
2188
|
+
? evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId)
|
|
2189
|
+
: evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, memberUnionId, chatType);
|
|
2025
2190
|
if (!ev.allowed) {
|
|
2026
2191
|
logger.debug(`[quota:${larkAppId}] dropping message ${messageId.substring(0, 12)} from non-allowed sender ${senderOpenId?.substring(0, 12) ?? '?'}`);
|
|
2027
2192
|
return false;
|
|
@@ -2039,8 +2204,18 @@ export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOp
|
|
|
2039
2204
|
return false;
|
|
2040
2205
|
let quota;
|
|
2041
2206
|
try {
|
|
2042
|
-
|
|
2043
|
-
|
|
2207
|
+
// oncall ∩ chatGrant 交集的额度决策**全部收口进 consumeQuota 的同一把锁**,杜绝跨 await
|
|
2208
|
+
// 用陈旧 ev 决策:
|
|
2209
|
+
// • explicitGrantOverride(live 显式授权)→ 不兜 default(undefined)。
|
|
2210
|
+
// • expiredGrantCleanup(观察到过期 chatGrant)→ 透传给 consumeQuota,锁内以**当前 expiry**
|
|
2211
|
+
// 为权威:当前 expiry<=now → 原子清「成员+quota+expiry」并回落 default;当前无/未来 expiry
|
|
2212
|
+
// → grant live(成员在→不兜 default 按现有/不限;成员已清→回落 default)。
|
|
2213
|
+
// • 其余 oncall(非成员)→ 兜 default。
|
|
2214
|
+
// 传入的 def 是「拟回落值」;是否真用由 consumeQuota 锁内定夺(有 expiredGrant 时以 CAS 为准)。
|
|
2215
|
+
const def = ev.reason === 'oncall' && !ev.explicitGrantOverride
|
|
2216
|
+
? getBot(larkAppId).config.messageQuota?.defaultLimit
|
|
2217
|
+
: undefined;
|
|
2218
|
+
quota = await consumeQuota(larkAppId, ev.quotaKey, def, ev.expiredGrantCleanup);
|
|
2044
2219
|
}
|
|
2045
2220
|
catch (err) {
|
|
2046
2221
|
logger.warn(`[quota:${larkAppId}] consume failed; dropping message ${messageId.substring(0, 12)}: ${err}`);
|
|
@@ -2099,21 +2274,32 @@ function getPidFile() {
|
|
|
2099
2274
|
return join(config.session.dataDir, name);
|
|
2100
2275
|
}
|
|
2101
2276
|
/** Path to the wrapper bin directory — injected into worker PATH so CLIs
|
|
2102
|
-
* can call `botmux send` / `botmux schedule` without a global npm install.
|
|
2103
|
-
|
|
2277
|
+
* can call `botmux send` / `botmux schedule` without a global npm install.
|
|
2278
|
+
* Single source of truth: resolveBotmuxWrapperBinDir (core-only → dedicated
|
|
2279
|
+
* `<SESSION_DATA_DIR>/bin`, never the shared `~/.botmux/bin`, so a same-HOME
|
|
2280
|
+
* fleet's wrapper/PATH is not clobbered and left unrestored on exit — codex P1).
|
|
2281
|
+
* Every consumer (worker-pool fork PATH, worker.ts child-env, tmux pane scripts)
|
|
2282
|
+
* MUST resolve the SAME dir. */
|
|
2283
|
+
const BOTMUX_BIN_DIR = resolveBotmuxWrapperBinDir(process.env);
|
|
2104
2284
|
function writePidFile() {
|
|
2105
2285
|
const dir = config.session.dataDir;
|
|
2106
2286
|
if (!existsSync(dir)) {
|
|
2107
2287
|
mkdirSync(dir, { recursive: true });
|
|
2108
2288
|
}
|
|
2109
2289
|
atomicWriteFileSync(getPidFile(), String(process.pid));
|
|
2110
|
-
// Write breadcrumb so CLI tools (botmux list/delete) can find the active data dir
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2290
|
+
// Write breadcrumb so CLI tools (botmux list/delete) can find the active data dir.
|
|
2291
|
+
// SKIP in core-only (codex P1): the global `~/.botmux/.data-dir` is a shared-HOME
|
|
2292
|
+
// signpost for bare-shell CLIs; a core-only service must not rewrite it to point
|
|
2293
|
+
// a same-HOME host operator at the core-only store. Core-only's own workers get
|
|
2294
|
+
// the data dir via the frozen SESSION_DATA_DIR env, not this breadcrumb.
|
|
2295
|
+
if (process.env.BOTMUX_CORE_ONLY !== '1') {
|
|
2296
|
+
const breadcrumb = join(homedir(), '.botmux', '.data-dir');
|
|
2297
|
+
try {
|
|
2298
|
+
mkdirSync(join(homedir(), '.botmux'), { recursive: true });
|
|
2299
|
+
atomicWriteFileSync(breadcrumb, config.session.dataDir);
|
|
2300
|
+
}
|
|
2301
|
+
catch { /* best effort */ }
|
|
2115
2302
|
}
|
|
2116
|
-
catch { /* best effort */ }
|
|
2117
2303
|
// Write a thin wrapper script so `botmux` is always in PATH for CLI sessions,
|
|
2118
2304
|
// regardless of whether the package was installed globally. The wrapper
|
|
2119
2305
|
// points at THIS daemon's dist/cli.js, so it's always the same version.
|
|
@@ -2548,7 +2734,7 @@ async function recoverV3DistillationProposalsForBot(larkAppId) {
|
|
|
2548
2734
|
}
|
|
2549
2735
|
}
|
|
2550
2736
|
async function handleV3SavedWorkflowCommandIfAny(invocation) {
|
|
2551
|
-
const { content, anchor, replyRootId, messageId, chatId, chatType, larkAppId, initiatorOpenId, teamTrustUnionId, memberUnionId, } = invocation;
|
|
2737
|
+
const { content, anchor, replyRootId, messageId, chatId, chatType, larkAppId, initiatorOpenId, teamTrustUnionId, memberUnionId, botSender, } = invocation;
|
|
2552
2738
|
const command = parseV3SavedWorkflowCommand(content);
|
|
2553
2739
|
if (!command)
|
|
2554
2740
|
return false;
|
|
@@ -2585,7 +2771,7 @@ async function handleV3SavedWorkflowCommandIfAny(invocation) {
|
|
|
2585
2771
|
const operatorCanOperate = canOperate(larkAppId, chatId, initiatorOpenId, teamTrustUnionId);
|
|
2586
2772
|
const policy = await authorizeV3SavedWorkflowInvocation(command, {
|
|
2587
2773
|
canPublishGlobal: () => operatorCanOperate,
|
|
2588
|
-
consumeMessageQuotaOnce: () => enforceMessageQuotaForCliInput(larkAppId, chatId, initiatorOpenId, targets.quotaMessageId, targets.replyAnchor, teamTrustUnionId, memberUnionId, chatType),
|
|
2774
|
+
consumeMessageQuotaOnce: () => enforceMessageQuotaForCliInput(larkAppId, chatId, initiatorOpenId, targets.quotaMessageId, targets.replyAnchor, teamTrustUnionId, memberUnionId, chatType, botSender),
|
|
2589
2775
|
});
|
|
2590
2776
|
if (!policy.ok) {
|
|
2591
2777
|
if (policy.reason === 'global_requires_operate') {
|
|
@@ -2693,12 +2879,12 @@ function beginNewTurn(ds, title) {
|
|
|
2693
2879
|
ds.streamingCardForced = undefined;
|
|
2694
2880
|
const previousUsageLimit = ds.usageLimit;
|
|
2695
2881
|
const previousStatus = ds.lastScreenStatus === 'limited' && previousUsageLimit ? 'limited' : 'idle';
|
|
2696
|
-
if (ds.streamCardId && ds
|
|
2697
|
-
const readUrl =
|
|
2882
|
+
if (ds.streamCardId && workerHasInitialized(ds)) {
|
|
2883
|
+
const readUrl = readableTerminalUrlFor(ds);
|
|
2698
2884
|
const dsBotCfg = getBot(ds.larkAppId).config;
|
|
2699
2885
|
const prevTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(dsBotCfg.cliId);
|
|
2700
2886
|
const prevMode = ds.displayMode ?? 'hidden';
|
|
2701
|
-
const frozenCard = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, prevTitle, ds.lastScreenContent ?? '', previousStatus, dsBotCfg.cliId, prevMode, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), previousUsageLimit, writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: dsBotCfg.cliId }));
|
|
2887
|
+
const frozenCard = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, prevTitle, ds.lastScreenContent ?? '', previousStatus, dsBotCfg.cliId, prevMode, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), previousUsageLimit, writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: dsBotCfg.cliId }), getDaemonStreamingCardUsageSnapshot(ds, dsBotCfg.cliId, { fresh: true }));
|
|
2702
2888
|
scheduleCardPatch(ds, frozenCard);
|
|
2703
2889
|
if (ds.streamCardNonce && ds.streamCardId !== CARD_POSTING_SENTINEL) {
|
|
2704
2890
|
if (!ds.frozenCards)
|
|
@@ -2735,6 +2921,8 @@ function isSessionlessCommandInvocation(cmd, content) {
|
|
|
2735
2921
|
return !docWatchCommandNeedsSession(content);
|
|
2736
2922
|
}
|
|
2737
2923
|
async function prewarmDocCommentSession(ds, sub) {
|
|
2924
|
+
const generation = captureRoutingGeneration(ds);
|
|
2925
|
+
ensureCurrentRoutingGeneration(generation, 'prewarm:start');
|
|
2738
2926
|
const bot = getBot(ds.larkAppId);
|
|
2739
2927
|
const botCfg = bot.config;
|
|
2740
2928
|
const loc = localeForBot(ds.larkAppId);
|
|
@@ -2748,6 +2936,7 @@ async function prewarmDocCommentSession(ds, sub) {
|
|
|
2748
2936
|
const title = `[Doc Watch] ${sub.fileToken.slice(0, 12)}`;
|
|
2749
2937
|
const turnId = `doc-watch-${Date.now()}-${sub.fileToken.slice(0, 8)}`;
|
|
2750
2938
|
const sender = sub.ownerOpenId ? await resolveSender(ds.larkAppId, sub.ownerOpenId, 'user') : undefined;
|
|
2939
|
+
ensureCurrentRoutingGeneration(generation, 'prewarm:sender');
|
|
2751
2940
|
beginNewTurn(ds, title);
|
|
2752
2941
|
ds.lastMessageAt = Date.now();
|
|
2753
2942
|
ds.session.lastMessageAt = new Date(ds.lastMessageAt).toISOString();
|
|
@@ -3059,6 +3248,92 @@ async function deliverCodexNotifierEvent(larkAppId, event) {
|
|
|
3059
3248
|
codexNotifierDeliveries.delete(deliveryKey);
|
|
3060
3249
|
}
|
|
3061
3250
|
}
|
|
3251
|
+
/**
|
|
3252
|
+
* P1 revalidation for the Codex-notifier「继续处理」takeover. After the dynamic
|
|
3253
|
+
* import + AbortSignal await inside adoptCodexNotifierEvent, a concurrent path may
|
|
3254
|
+
* have moved the ground: a `/relay` transfer opened its input gate (the starter
|
|
3255
|
+
* would then fail-closed with a bare `return` our no-op sessionReply swallows,
|
|
3256
|
+
* and the outer handler would render a bogus green「已接管」over a half-rewritten
|
|
3257
|
+
* session), or the session was /close'd / swapped / re-created under this key.
|
|
3258
|
+
* Returns true when the takeover MUST abort before mutating any state — so the
|
|
3259
|
+
* caller can throw with zero side effects and no buffered input dropped.
|
|
3260
|
+
*/
|
|
3261
|
+
function notifierAdoptStaleOrTransferring(ds, sessions, activeKey, genSessionId) {
|
|
3262
|
+
return (sessions.get(activeKey) !== ds
|
|
3263
|
+
|| ds.session.sessionId !== genSessionId
|
|
3264
|
+
|| ds.session.status !== 'active'
|
|
3265
|
+
|| isSessionTransferring(ds));
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* P2 predicate: would clearPendingRepoStateForNotifierAdopt drop user input that
|
|
3269
|
+
* was accepted but not yet delivered to the CLI? Two DISTINCT windows with
|
|
3270
|
+
* different gating — do NOT collapse them:
|
|
3271
|
+
*
|
|
3272
|
+
* (1) repo-select pending buffer — ONLY meaningful while pendingRepo===true.
|
|
3273
|
+
* pendingPrompt / pendingCodexAppText / pendingAttachments / pendingFollowUps
|
|
3274
|
+
* are the message stashed for the deferred post-repo-selection fork. But the
|
|
3275
|
+
* pinned/defaultWorkingDir immediate-launch path (daemon.ts ~16031/17325)
|
|
3276
|
+
* ALSO seeds these same mirror fields, then forks and clears only
|
|
3277
|
+
* pendingTurnId (~16097/17387) — leaving pendingPrompt/etc behind on an
|
|
3278
|
+
* already-delivered session with pendingRepo=false. Gating on pendingRepo
|
|
3279
|
+
* here is what stops us from warning "message not delivered, resend" about
|
|
3280
|
+
* a prompt the worker already ran (which would make the user re-run a
|
|
3281
|
+
* non-idempotent command).
|
|
3282
|
+
*
|
|
3283
|
+
* (2) just-committed launch window — INDEPENDENT of pendingRepo. commitRepoSelection
|
|
3284
|
+
* set pendingRepo=false and forked, but pendingRawInput / pendingFollowUpInput
|
|
3285
|
+
* still wait on the new worker's prompt_ready to actually send. These are the
|
|
3286
|
+
* only fields that legitimately represent undelivered input while
|
|
3287
|
+
* pendingRepo=false, so they are checked unconditionally. (The immediate-launch
|
|
3288
|
+
* path never populates them — it forks with the prompt as a direct argument.)
|
|
3289
|
+
*/
|
|
3290
|
+
function notifierAdoptWouldDropInput(ds) {
|
|
3291
|
+
const repoSelectBuffered = ds.pendingRepo === true && ((ds.pendingPrompt?.trim().length ?? 0) > 0
|
|
3292
|
+
|| (ds.pendingFollowUps?.length ?? 0) > 0
|
|
3293
|
+
|| (ds.pendingAttachments?.length ?? 0) > 0
|
|
3294
|
+
|| (ds.pendingCodexAppText?.trim().length ?? 0) > 0);
|
|
3295
|
+
// Undelivered regardless of pendingRepo (awaiting prompt_ready on a forked worker).
|
|
3296
|
+
const undeliveredLaunchInput = (ds.pendingRawInput?.trim().length ?? 0) > 0
|
|
3297
|
+
|| (ds.pendingFollowUpInput?.cliInput?.trim().length ?? 0) > 0;
|
|
3298
|
+
return repoSelectBuffered || undeliveredLaunchInput;
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Fully retire an in-memory repo-select placeholder when the Codex-notifier
|
|
3302
|
+
* 「继续处理」callback takes over the DM session. The legacy inline clear only
|
|
3303
|
+
* reset six fields and left `repoCardMessageId`, `pendingFollowUps`,
|
|
3304
|
+
* `pendingRepoCommitInFlight`, `worktreeCreating`, attachments, mentions, raw
|
|
3305
|
+
* input, etc. behind — a stale repo-card click or a late auto-worktree
|
|
3306
|
+
* completion could then act on the just-adopted session. Clear the whole
|
|
3307
|
+
* pending-repo surface so no residue survives the takeover. Buffered/undelivered
|
|
3308
|
+
* user input (repo-select buffer AND the pendingRawInput/pendingFollowUpInput a
|
|
3309
|
+
* just-committed session is still waiting to send on prompt_ready) is dropped
|
|
3310
|
+
* here — the drop is terminal (never rolled back, since concurrent background
|
|
3311
|
+
* tasks may already have observed pendingRepo=false); the caller tells the user
|
|
3312
|
+
* their input was cancelled on both the success and failure card.
|
|
3313
|
+
*/
|
|
3314
|
+
function clearPendingRepoStateForNotifierAdopt(ds) {
|
|
3315
|
+
ds.pendingRepo = false;
|
|
3316
|
+
ds.pendingRepoCommitInFlight = false;
|
|
3317
|
+
ds.worktreeCreating = false;
|
|
3318
|
+
ds.repoCardMessageId = undefined;
|
|
3319
|
+
ds.pendingPrompt = undefined;
|
|
3320
|
+
ds.pendingTurnId = undefined;
|
|
3321
|
+
ds.pendingRawInput = undefined;
|
|
3322
|
+
ds.pendingRawTurnId = undefined;
|
|
3323
|
+
ds.pendingFollowUpInput = undefined;
|
|
3324
|
+
ds.pendingAttachments = undefined;
|
|
3325
|
+
ds.pendingMentions = undefined;
|
|
3326
|
+
ds.pendingSubstituteTrigger = undefined;
|
|
3327
|
+
ds.pendingSubstituteControlCard = undefined;
|
|
3328
|
+
ds.pendingSender = undefined;
|
|
3329
|
+
ds.pendingFollowUps = undefined;
|
|
3330
|
+
ds.pendingFollowUpTurnId = undefined;
|
|
3331
|
+
ds.pendingCodexAppText = undefined;
|
|
3332
|
+
ds.pendingCodexAppApplicationContext = undefined;
|
|
3333
|
+
ds.pendingCodexAppMessageContext = undefined;
|
|
3334
|
+
ds.pendingCodexAppFollowUps = undefined;
|
|
3335
|
+
ds.pendingCodexAppFollowUpContexts = undefined;
|
|
3336
|
+
}
|
|
3062
3337
|
async function adoptCodexNotifierEvent(larkAppId, event, cardMessageId, ownerOpenId, signal, deadlineAt) {
|
|
3063
3338
|
signal.throwIfAborted();
|
|
3064
3339
|
const chatId = await getMessageChatId(larkAppId, cardMessageId, {
|
|
@@ -3103,9 +3378,40 @@ async function adoptCodexNotifierEvent(larkAppId, event, cardMessageId, ownerOpe
|
|
|
3103
3378
|
workingDir: event.cwd,
|
|
3104
3379
|
ownerOpenId,
|
|
3105
3380
|
};
|
|
3106
|
-
activeSessions
|
|
3107
|
-
|
|
3381
|
+
if (!setActiveSessionIfActive(activeSessions, activeKey, ds)) {
|
|
3382
|
+
await closeSessionHelper(session.sessionId);
|
|
3383
|
+
throw new Error('通知所在会话路由被一条待处理的持久会话占用');
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
// Transfer guard OUTSIDE the launch branch. If the session is already on the
|
|
3387
|
+
// target thread with a live worker, the branch below is skipped entirely —
|
|
3388
|
+
// but a concurrent /relay could still be moving this session's routing away.
|
|
3389
|
+
// Returning a green「已接管,可继续」while the route is migrating is a lie.
|
|
3390
|
+
// Fail-closed here first (a freshly-created ds has no transfer gate, so this
|
|
3391
|
+
// is a no-op for the new-session path); the launch branch keeps its own
|
|
3392
|
+
// post-await revalidation for the drift that can open during its awaits.
|
|
3393
|
+
if (isSessionTransferring(ds)) {
|
|
3394
|
+
throw new Error('该会话正在转移,暂时无法接管;请转移完成后在完成通知卡上重试');
|
|
3395
|
+
}
|
|
3396
|
+
let bufferedInputDropped = false;
|
|
3397
|
+
// Snapshot the session identity BEFORE the throwable/deadline-bound awaits
|
|
3398
|
+
// below. If the session is closed, swapped, or re-created under this active
|
|
3399
|
+
// key while we await, the post-await revalidation must detect the drift.
|
|
3400
|
+
const adoptGenSessionId = ds.session.sessionId;
|
|
3108
3401
|
if (ds.session.cliSessionId !== event.threadId || !ds.worker || ds.worker.killed) {
|
|
3402
|
+
// Do the two throwable, deadline-sensitive steps FIRST, before mutating any
|
|
3403
|
+
// session/pending state: the dynamic import and the 2.2s AbortSignal check.
|
|
3404
|
+
// If either fails here nothing has been touched, so no rollback is needed
|
|
3405
|
+
// and the buffered repo-select input is left intact for a retry.
|
|
3406
|
+
const { startCodexAppThreadSession } = await import('./core/command-handler.js');
|
|
3407
|
+
signal.throwIfAborted();
|
|
3408
|
+
// Re-validate AFTER the awaits and BEFORE mutating anything (see
|
|
3409
|
+
// notifierAdoptStaleOrTransferring): a concurrent /relay transfer or a
|
|
3410
|
+
// /close/swap/re-create under this key must fail the takeover explicitly
|
|
3411
|
+
// rather than half-rewrite the session and report a bogus green success.
|
|
3412
|
+
if (notifierAdoptStaleOrTransferring(ds, activeSessions, activeKey, adoptGenSessionId)) {
|
|
3413
|
+
throw new Error('该会话正在转移或已变更,无法接管;请稍后在完成通知卡上重试');
|
|
3414
|
+
}
|
|
3109
3415
|
// 接管原生 Codex App 线程时固定为纯 codex-app 启动,不能继承通知 Bot 的
|
|
3110
3416
|
// wrapper/model;这些配置针对普通 CLI,会错误包装 app-server runner。
|
|
3111
3417
|
ds.session.cliId = 'codex-app';
|
|
@@ -3114,12 +3420,27 @@ async function adoptCodexNotifierEvent(larkAppId, event, cardMessageId, ownerOpe
|
|
|
3114
3420
|
delete ds.session.model;
|
|
3115
3421
|
ds.session.agentFrozen = true;
|
|
3116
3422
|
sessionStore.updateSession(ds.session);
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3423
|
+
// 默认 flat DM(p2pMode=chat)下这条按钮回调复用同一私聊的现有会话。若该会话
|
|
3424
|
+
// 恰好停在「待选仓库」挂起态且已缓冲一条尚未提交的输入,接管会消费掉
|
|
3425
|
+
// pendingRepo(#669 的守卫要求接管前 pendingRepo 必须为 false)。旧逻辑只清了
|
|
3426
|
+
// 部分字段并静默丢弃缓冲输入。这里完整清空全部 pending 状态(否则残留的
|
|
3427
|
+
// repoCardMessageId/pendingFollowUps/worktreeCreating 等会让旧选仓卡二次点击、
|
|
3428
|
+
// 迟到 auto-worktree 完成误操作已接管会话——见 runAutoWorktreeCommit 的
|
|
3429
|
+
// pendingRepo 护栏),并记录「缓冲输入已丢弃」以便如实告知用户。
|
|
3430
|
+
//
|
|
3431
|
+
// 不做 pending 快照回滚:clear 之后 startCodexAppThreadSession 的 async guard
|
|
3432
|
+
// 会让出微任务,迟到 auto-worktree / 正在 prepare 的 commit 可能已观察到
|
|
3433
|
+
// pendingRepo=false 而正常结束并在 finally 清掉 in-flight 标志;此时若把
|
|
3434
|
+
// pendingRepo=true/worktreeCreating=true 快照复活,后台任务已不在,会话会永久
|
|
3435
|
+
// 卡在「正在创建/提交」。因此丢弃一旦发布即为终态——用「取消并请重发」的显式
|
|
3436
|
+
// 语义告知用户,而非假装能恢复。
|
|
3437
|
+
// 判断是否有「已收下但尚未真正送达 CLI」的用户输入会被下面的 clear 抹掉
|
|
3438
|
+
// (见 notifierAdoptWouldDropInput:repo-select 镜像字段只在 pendingRepo===true
|
|
3439
|
+
// 计入——立即启动路径也播种这些字段但已 fork 送达,不能误判;pendingRawInput/
|
|
3440
|
+
// pendingFollowUpInput 独立于 pendingRepo,覆盖选仓刚提交等 prompt_ready 的启动窗)。
|
|
3441
|
+
// 有则记录已丢弃,如实告知用户。
|
|
3442
|
+
bufferedInputDropped = notifierAdoptWouldDropInput(ds);
|
|
3443
|
+
clearPendingRepoStateForNotifierAdopt(ds);
|
|
3123
3444
|
// 完成事件已经携带恢复所需的原生 threadId/cwd。按钮回调不再额外启动一次
|
|
3124
3445
|
// app-server 做非必要的元数据刷新,避免子进程把飞书回调拖过截止时间。
|
|
3125
3446
|
const target = {
|
|
@@ -3129,19 +3450,36 @@ async function adoptCodexNotifierEvent(larkAppId, event, cardMessageId, ownerOpe
|
|
|
3129
3450
|
cwd: event.cwd,
|
|
3130
3451
|
status: event.status,
|
|
3131
3452
|
};
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
}
|
|
3453
|
+
try {
|
|
3454
|
+
await startCodexNotifierAdoptionSession(startCodexAppThreadSession, target, ds, {
|
|
3455
|
+
activeSessions,
|
|
3456
|
+
sessionReply,
|
|
3457
|
+
getActiveCount,
|
|
3458
|
+
lastRepoScan,
|
|
3459
|
+
}, larkAppId, cardMessageId);
|
|
3460
|
+
}
|
|
3461
|
+
catch (err) {
|
|
3462
|
+
// 接管失败/超时:pending 已清空且不回滚(见上)。若曾丢弃过缓冲/未送达输入,把「已取消、
|
|
3463
|
+
// 请重发」前置到抛给外层的错误里——外层用它渲染失败卡,用户就不会以为原消息还
|
|
3464
|
+
// 在队列里等着(否则失败卡只说"接管失败请重试",缓冲却已没了=静默丢失)。
|
|
3465
|
+
if (bufferedInputDropped) {
|
|
3466
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3467
|
+
throw new Error(`你此前发送但尚未送达的消息已取消,请在接管成功后重新发送。(接管未完成:${detail})`);
|
|
3468
|
+
}
|
|
3469
|
+
throw err;
|
|
3470
|
+
}
|
|
3140
3471
|
}
|
|
3141
|
-
|
|
3472
|
+
const baseAdoptNotice = scope === 'chat'
|
|
3142
3473
|
? '现在可以直接在当前私聊继续发送指令;需要操作终端时,请点击会话卡内的「获取操作链接」。'
|
|
3143
|
-
: '请在本卡片的话题中继续发送指令;需要操作终端时,请点击话题会话卡内的「获取操作链接」。'
|
|
3144
|
-
|
|
3474
|
+
: '请在本卡片的话题中继续发送指令;需要操作终端时,请点击话题会话卡内的「获取操作链接」。';
|
|
3475
|
+
return buildCodexNotifierResultCard('已接管 Codex App 任务', bufferedInputDropped
|
|
3476
|
+
? `⚠️ 你此前发送但尚未送达的消息未随本次接管发送,请重新发送一次。\n\n${baseAdoptNotice}`
|
|
3477
|
+
: baseAdoptNotice, 'green');
|
|
3478
|
+
}
|
|
3479
|
+
export const __testOnly_notifierAdoptStaleOrTransferring = notifierAdoptStaleOrTransferring;
|
|
3480
|
+
export const __testOnly_notifierAdoptWouldDropInput = notifierAdoptWouldDropInput;
|
|
3481
|
+
export const __testOnly_clearPendingRepoStateForNotifierAdopt = clearPendingRepoStateForNotifierAdopt;
|
|
3482
|
+
export const __testOnly_adoptCodexNotifierEvent = adoptCodexNotifierEvent;
|
|
3145
3483
|
const handleCodexNotifierCardAction = createCodexNotifierCardActionHandler({
|
|
3146
3484
|
getExpectedOwnerOpenId: larkAppId => getOwnerOpenId(larkAppId) ?? resolvePrimaryOwnerOpenId(larkAppId),
|
|
3147
3485
|
getEventRecord: (larkAppId, eventId) => codexNotifierStore(larkAppId).get(eventId),
|
|
@@ -3599,6 +3937,18 @@ ipcRoute('POST', '/api/asks', async (req, res) => {
|
|
|
3599
3937
|
detail: 'meeting receiver asks are not an idempotent managed action',
|
|
3600
3938
|
});
|
|
3601
3939
|
}
|
|
3940
|
+
// No-transport session (apiOnly bot or HTTP virtual chat): `botmux ask` posts
|
|
3941
|
+
// an interactive Lark card and blocks the turn on a human click — impossible
|
|
3942
|
+
// without a real Feishu chat. Fail fast with `unsupported` instead of letting
|
|
3943
|
+
// it fall into the Lark dispatcher and hang the agent on a card that can never
|
|
3944
|
+
// render. (Structured HTTP awaiting_input is a separate, future capability.)
|
|
3945
|
+
if (askSession && !larkTransportEnabled({ chatId: askSession.chatId, apiOnly: getBot(askSession.larkAppId).config.apiOnly })) {
|
|
3946
|
+
return jsonRes(res, 400, {
|
|
3947
|
+
ok: false,
|
|
3948
|
+
error: 'unsupported',
|
|
3949
|
+
detail: 'botmux ask requires a Feishu chat; this session has no Lark transport (apiOnly / HTTP virtual session)',
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3602
3952
|
// 谁能答复 = 谁能在该 chat 跟 bot 说话(canTalk)。鉴权在 broker 点击时按注入的
|
|
3603
3953
|
// canTalkChecker 判定(见下方 setAskCanTalkChecker),daemon 这里不再预解析 approver。
|
|
3604
3954
|
// chatType 随 ask 一起记下:p2pOpen 的 bot 在私聊里提问时,答复权靠这一腿才判得出
|
|
@@ -3645,7 +3995,12 @@ ipcRoute('POST', '/api/asks', async (req, res) => {
|
|
|
3645
3995
|
else {
|
|
3646
3996
|
navKeys = computeCocoPickerKeys(parsed.questions, result.answers).navKeys;
|
|
3647
3997
|
}
|
|
3648
|
-
cocoDs
|
|
3998
|
+
sendWorkerSessionInput(cocoDs, {
|
|
3999
|
+
type: 'coco_drive_picker',
|
|
4000
|
+
navKeys,
|
|
4001
|
+
needsReviewSubmit,
|
|
4002
|
+
comment,
|
|
4003
|
+
});
|
|
3649
4004
|
logger.info(`[${cocoDs.session.sessionId.slice(0, 8)}] CoCo picker drive: ${navKeys.length} keys, review=${needsReviewSubmit}, comment=${comment ? 'yes' : 'no'}`);
|
|
3650
4005
|
}
|
|
3651
4006
|
catch (err) {
|
|
@@ -4365,8 +4720,15 @@ ipcRoute('POST', '/api/vc-meetings/consumer-catch-up', async (req, res) => {
|
|
|
4365
4720
|
}
|
|
4366
4721
|
});
|
|
4367
4722
|
function effectiveVcMeetingAgentConfig(larkAppId) {
|
|
4368
|
-
|
|
4369
|
-
|
|
4723
|
+
// Delegate to the pure predicate (bot-registry) so the apiOnly fail-close and the
|
|
4724
|
+
// enabled check live in ONE tested place. apiOnly (core-only) bots have no Feishu
|
|
4725
|
+
// connection — a VC listener drives `lark-cli vc +meeting-events --as bot`, which
|
|
4726
|
+
// breaks the zero-Feishu-network contract. This accessor gates every VC entry
|
|
4727
|
+
// (config read / event fetch / sync) INCLUDING the boot-time
|
|
4728
|
+
// `restoreVcMeetingRuntimeSessionsForBot`, whose call site sits OUTSIDE the
|
|
4729
|
+
// `!cfg.apiOnly` boot block — so a migrated bots.json (normal VC bot flipped to
|
|
4730
|
+
// apiOnly, stale runtime record on disk) can no longer re-spawn lark-cli at boot.
|
|
4731
|
+
return vcMeetingAgentConfigActive(getBot(larkAppId)?.config);
|
|
4370
4732
|
}
|
|
4371
4733
|
function configuredVcMeetingListenerChatId(cfg) {
|
|
4372
4734
|
return cfg.listenerChatId ?? cfg.notificationChatId;
|
|
@@ -12365,6 +12727,21 @@ function resolveBotDefaultWorkingDir(larkAppId) {
|
|
|
12365
12727
|
* both honor the defaultOncall auto-bind the same way.
|
|
12366
12728
|
*/
|
|
12367
12729
|
async function resolvePinnedWorkingDir(ctx) {
|
|
12730
|
+
if (ctx.listenerWorkingDir) {
|
|
12731
|
+
const resolved = expandHome(ctx.listenerWorkingDir);
|
|
12732
|
+
try {
|
|
12733
|
+
if (statSync(resolved).isDirectory()) {
|
|
12734
|
+
return {
|
|
12735
|
+
pinnedWorkingDir: resolved,
|
|
12736
|
+
oncallEntry: undefined,
|
|
12737
|
+
inheritedFrom: null,
|
|
12738
|
+
pinnedFromBotDefault: false,
|
|
12739
|
+
};
|
|
12740
|
+
}
|
|
12741
|
+
}
|
|
12742
|
+
catch { /* fall through to normal resolution */ }
|
|
12743
|
+
logger.warn(`[message-listener:${ctx.larkAppId}] listener workingDir invalid (${resolved}); falling back to normal resolution`);
|
|
12744
|
+
}
|
|
12368
12745
|
let oncallEntry = findOncallChat(ctx.larkAppId, ctx.chatId);
|
|
12369
12746
|
if (!oncallEntry) {
|
|
12370
12747
|
oncallEntry = await maybeAutoBindDefaultOncall(ctx.larkAppId, ctx.chatId, ctx.chatType);
|
|
@@ -12438,6 +12815,7 @@ async function replyInvalidWorkingDirs(anchor, larkAppId, ds) {
|
|
|
12438
12815
|
ds.pendingRepo = false;
|
|
12439
12816
|
activeSessions.delete(sessionKey(anchor, larkAppId));
|
|
12440
12817
|
sessionStore.closeSession(ds.session.sessionId);
|
|
12818
|
+
publishClosedSessionPatch(ds.session.sessionId, ds.session.closedAt ? Date.parse(ds.session.closedAt) : undefined);
|
|
12441
12819
|
const msg = tr('cmd.repo.working_dir_not_exist', {
|
|
12442
12820
|
dirs: invalid.map(d => `\`${d}\``).join(', '),
|
|
12443
12821
|
}, localeForBot(larkAppId));
|
|
@@ -12448,9 +12826,51 @@ async function replyInvalidWorkingDirs(anchor, larkAppId, ds) {
|
|
|
12448
12826
|
function isInitialSessionPassthrough(larkAppId, cmd) {
|
|
12449
12827
|
return resolveAdapterDefaultPassthroughCommands(larkAppId).includes(cmd);
|
|
12450
12828
|
}
|
|
12829
|
+
/** Preserve the established mid-session passthrough semantics when a cold-start
|
|
12830
|
+
* scratch loses its registration race to a concurrently-created real session. */
|
|
12831
|
+
function deliverPassthroughToExistingSession(ds, cmd, commandContent, anchor, larkAppId, turn) {
|
|
12832
|
+
if ((ds.worker && !ds.worker.killed) || isSessionTransferring(ds)) {
|
|
12833
|
+
// Passthrough commands bypass the normal message-forwarding block, so bind
|
|
12834
|
+
// the accepted Lark turn before the worker rotates its marker at the PTY
|
|
12835
|
+
// write boundary. This helper also covers a cold-start registration race.
|
|
12836
|
+
ds.session.quoteTargetId = turn.messageId;
|
|
12837
|
+
ds.session.quoteTargetSenderOpenId = turn.senderOpenId;
|
|
12838
|
+
ds.session.quoteTargetSenderIsBot = turn.senderIsBot;
|
|
12839
|
+
const substituteReplyMode = turn.substitute
|
|
12840
|
+
? (getBot(larkAppId).config.substituteMode?.replyMode ?? 'thread')
|
|
12841
|
+
: 'thread';
|
|
12842
|
+
beginReplyTargetTurn(ds, turn.replyRootId, turn.messageId, new Date().toISOString(), {
|
|
12843
|
+
quoteOnly: substituteReplyMode === 'quote',
|
|
12844
|
+
substitute: turn.substitute,
|
|
12845
|
+
});
|
|
12846
|
+
if (turn.senderOpenId && ds.session.lastCallerOpenId !== turn.senderOpenId) {
|
|
12847
|
+
ds.session.lastCallerOpenId = turn.senderOpenId;
|
|
12848
|
+
}
|
|
12849
|
+
sessionStore.updateSession(ds.session);
|
|
12850
|
+
// Compatibility note (empty-start opening): a raw passthrough is a
|
|
12851
|
+
// LITERAL CLI command — its contract is that the CLI sees exactly the
|
|
12852
|
+
// bytes the user typed, never a botmux XML envelope. So it deliberately
|
|
12853
|
+
// does NOT consume `Session.initialUserTurnPending`: wrapping `/model`
|
|
12854
|
+
// in `<user_message>` would break the literal contract, and silently
|
|
12855
|
+
// clearing the marker would lose the opening for the next real turn.
|
|
12856
|
+
// `/model` on an empty-started session therefore stays literal and the
|
|
12857
|
+
// FOLLOWING business message still opens as a new topic.
|
|
12858
|
+
beginNewTurn(ds, commandContent);
|
|
12859
|
+
sendWorkerSessionInput(ds, {
|
|
12860
|
+
type: 'raw_input',
|
|
12861
|
+
content: commandContent,
|
|
12862
|
+
turnId: turn.messageId,
|
|
12863
|
+
});
|
|
12864
|
+
markSessionActivity(ds);
|
|
12865
|
+
logger.info(`[${anchor.substring(0, 12)}] Passthrough ${cmd} → worker`);
|
|
12866
|
+
return;
|
|
12867
|
+
}
|
|
12868
|
+
void sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
12869
|
+
}
|
|
12870
|
+
export const __testOnly_deliverPassthroughToExistingSession = deliverPassthroughToExistingSession;
|
|
12451
12871
|
async function startInitialPassthroughSession(args) {
|
|
12452
|
-
const { larkAppId, chatId, chatType, scope, anchor, messageId, replyRootId, parsed, commandContent, senderOpenId, senderUnionId, memberUnionId, ownerOpenId, ownerUnionId, creatorOpenId, } = args;
|
|
12453
|
-
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, senderUnionId, memberUnionId, chatType)) {
|
|
12872
|
+
const { larkAppId, chatId, chatType, scope, anchor, messageId, replyRootId, parsed, cmd, commandContent, senderOpenId, substitute, senderUnionId, memberUnionId, ownerOpenId, ownerUnionId, creatorOpenId, botSender, } = args;
|
|
12873
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, senderUnionId, memberUnionId, chatType, botSender)) {
|
|
12454
12874
|
return;
|
|
12455
12875
|
}
|
|
12456
12876
|
const botCfg = getBot(larkAppId).config;
|
|
@@ -12516,7 +12936,25 @@ async function startInitialPassthroughSession(args) {
|
|
|
12516
12936
|
}
|
|
12517
12937
|
beginReplyTargetTurn(ds, replyRootId, messageId);
|
|
12518
12938
|
sessionStore.updateSession(ds.session);
|
|
12519
|
-
|
|
12939
|
+
const creationKey = sessionKey(anchor, larkAppId);
|
|
12940
|
+
if (!setActiveSessionIfActive(activeSessions, creationKey, ds)) {
|
|
12941
|
+
const winner = await rollbackRejectedSessionAndGetWinner(activeSessions, creationKey, ds);
|
|
12942
|
+
if (winner) {
|
|
12943
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Initial passthrough registration lost to ` +
|
|
12944
|
+
`${winner.session.sessionId.substring(0, 8)}; handing ${cmd} to routing winner`);
|
|
12945
|
+
deliverPassthroughToExistingSession(winner, cmd, commandContent, anchor, larkAppId, {
|
|
12946
|
+
messageId,
|
|
12947
|
+
replyRootId,
|
|
12948
|
+
senderOpenId,
|
|
12949
|
+
senderIsBot: parsed.senderType === 'app' || parsed.senderType === 'bot',
|
|
12950
|
+
substitute,
|
|
12951
|
+
});
|
|
12952
|
+
}
|
|
12953
|
+
else {
|
|
12954
|
+
void sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
12955
|
+
}
|
|
12956
|
+
return;
|
|
12957
|
+
}
|
|
12520
12958
|
if (pinnedWorkingDir && autoWt) {
|
|
12521
12959
|
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
12522
12960
|
return;
|
|
@@ -12574,7 +13012,7 @@ function mergeVcMeetingApplicationContext(existing, incoming) {
|
|
|
12574
13012
|
return `${[...new Set(lines)].join('\n')}\n`;
|
|
12575
13013
|
}
|
|
12576
13014
|
async function handleNewTopic(data, ctx) {
|
|
12577
|
-
const { chatId, messageId, chatType, larkAppId, replyRootId, substituteTrigger } = ctx;
|
|
13015
|
+
const { chatId, messageId, chatType, larkAppId, replyRootId, substituteTrigger, messageListener } = ctx;
|
|
12578
13016
|
// scope/anchor are mutable here: `/t` / `/topic` may flip a 普通群 chat-scope
|
|
12579
13017
|
// routing into thread-scope so the bot's first reply seeds a Lark thread.
|
|
12580
13018
|
let scope = ctx.scope;
|
|
@@ -12645,8 +13083,18 @@ async function handleNewTopic(data, ctx) {
|
|
|
12645
13083
|
// operate),破坏「人绝不继承 bot 信任」的边界。旧联邦 team-bots 表结构上只收
|
|
12646
13084
|
// bot(学习入口限 bot sender),这里对齐同一不变量。senderUnionId 本身保持原义
|
|
12647
13085
|
//(人类会话的 ownerUnionId 还靠它)。
|
|
12648
|
-
|
|
13086
|
+
// 飞书盖章的 bot 发送方。既锁 union 信任腿(下面的 teamTrustUnionId),也决定
|
|
13087
|
+
// talk 复查走哪个谓词(evaluateBotTalk vs evaluateTalk)——两者必须同源,见
|
|
13088
|
+
// enforceMessageQuotaForCliInput 的 botSender 说明。
|
|
13089
|
+
const isBotSenderType = data.sender?.sender_type === 'app' || data.sender?.sender_type === 'bot';
|
|
13090
|
+
const teamTrustUnionId = isBotSenderType ? senderUnionId : undefined;
|
|
12649
13091
|
const botCfg = getBot(larkAppId).config;
|
|
13092
|
+
const listenerPrompt = messageListener ? renderMessageListenerPrompt(messageListener) : undefined;
|
|
13093
|
+
if (listenerPrompt) {
|
|
13094
|
+
content = listenerPrompt;
|
|
13095
|
+
parsed.content = listenerPrompt;
|
|
13096
|
+
cmdContent = listenerPrompt;
|
|
13097
|
+
}
|
|
12650
13098
|
logger.info(`New session: "${content.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)}, resources: ${resources.length}, active: ${getActiveCount()}, messageId: ${messageId}, chatId: ${chatId})`);
|
|
12651
13099
|
emitHookEvent('topic.new', {
|
|
12652
13100
|
larkAppId,
|
|
@@ -12673,6 +13121,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
12673
13121
|
initiatorOpenId: senderOpenId,
|
|
12674
13122
|
teamTrustUnionId,
|
|
12675
13123
|
memberUnionId: senderUnionId,
|
|
13124
|
+
botSender: isBotSenderType,
|
|
12676
13125
|
})) {
|
|
12677
13126
|
return;
|
|
12678
13127
|
}
|
|
@@ -12754,10 +13203,13 @@ async function handleNewTopic(data, ctx) {
|
|
|
12754
13203
|
messageId,
|
|
12755
13204
|
replyRootId,
|
|
12756
13205
|
parsed,
|
|
13206
|
+
cmd,
|
|
12757
13207
|
commandContent,
|
|
12758
13208
|
senderOpenId,
|
|
13209
|
+
substitute: !!substituteTrigger,
|
|
12759
13210
|
senderUnionId: teamTrustUnionId,
|
|
12760
13211
|
memberUnionId: senderUnionId, // 原始 union(人腿),不锁 bot
|
|
13212
|
+
botSender: isBotSenderType,
|
|
12761
13213
|
// New-topic senders are humans here (mirrors the normal new-topic
|
|
12762
13214
|
// spawn path, which assigns ownership unconditionally too).
|
|
12763
13215
|
ownerOpenId: senderOpenId,
|
|
@@ -12777,7 +13229,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
12777
13229
|
// which left a hole once per-chat grants flow through canTalk.
|
|
12778
13230
|
// canRunDaemonCommand = canOperate ∪(cmd ∈ canTalkDaemonCommands && canTalk):
|
|
12779
13231
|
// bot 可通过名单把选定命令(如 /status)降到 canTalk;未配置时与 canOperate 全等。
|
|
12780
|
-
if (!canRunDaemonCommand(larkAppId, chatId, senderOpenId, teamTrustUnionId, cmd, senderUnionId, chatType)) {
|
|
13232
|
+
if (!canRunDaemonCommand(larkAppId, chatId, senderOpenId, teamTrustUnionId, cmd, senderUnionId, chatType, isBotSenderType)) {
|
|
12781
13233
|
await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
12782
13234
|
return;
|
|
12783
13235
|
}
|
|
@@ -12836,7 +13288,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
12836
13288
|
cmdPending = { pendingRepo: true, pendingPrompt: '', workingDir: pinnedWorkingDir };
|
|
12837
13289
|
}
|
|
12838
13290
|
sessionStore.updateSession(session);
|
|
12839
|
-
|
|
13291
|
+
const commandDs = {
|
|
12840
13292
|
session,
|
|
12841
13293
|
worker: null,
|
|
12842
13294
|
workerPort: null,
|
|
@@ -12851,13 +13303,23 @@ async function handleNewTopic(data, ctx) {
|
|
|
12851
13303
|
hasHistory: false,
|
|
12852
13304
|
ownerOpenId: senderOpenId,
|
|
12853
13305
|
...cmdPending,
|
|
12854
|
-
}
|
|
13306
|
+
};
|
|
13307
|
+
const commandKey = sessionKey(anchor, larkAppId);
|
|
13308
|
+
if (!setActiveSessionIfActive(activeSessions, commandKey, commandDs)) {
|
|
13309
|
+
const winner = await rollbackRejectedSessionAndGetWinner(activeSessions, commandKey, commandDs);
|
|
13310
|
+
if (!winner)
|
|
13311
|
+
return;
|
|
13312
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Daemon-command registration lost to ` +
|
|
13313
|
+
`${winner.session.sessionId.substring(0, 8)}; handing ${cmd} to routing winner`);
|
|
13314
|
+
}
|
|
12855
13315
|
// Pass mention-stripped content so /command argument parsing works.
|
|
12856
13316
|
await handleCommand(cmd, anchor, { ...parsed, content: commandContent }, commandDeps, larkAppId);
|
|
12857
13317
|
return;
|
|
12858
13318
|
}
|
|
12859
13319
|
}
|
|
12860
|
-
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, teamTrustUnionId, senderUnionId, chatType
|
|
13320
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor, teamTrustUnionId, senderUnionId, chatType, isBotSenderType, {
|
|
13321
|
+
listenerAuthorized: !!messageListener,
|
|
13322
|
+
})) {
|
|
12861
13323
|
return;
|
|
12862
13324
|
}
|
|
12863
13325
|
// Download attachments
|
|
@@ -12879,9 +13341,21 @@ async function handleNewTopic(data, ctx) {
|
|
|
12879
13341
|
const codexAppQuoteContext = ctx.forwardSeedData
|
|
12880
13342
|
? ''
|
|
12881
13343
|
: buildQuoteHint(parsed, scope, anchor, localeForBot(larkAppId));
|
|
12882
|
-
const codexAppMessageContext = codexAppQuoteContext + (workflowGrillPrompt ?? '');
|
|
12883
13344
|
const codexAppApplicationContext = vcMeetingApplicationContext(ctx);
|
|
12884
|
-
|
|
13345
|
+
// 普通群「对已有消息发起话题再 @」:入站是话题内回复(root_id 指向另一条更早
|
|
13346
|
+
// 的话题根消息),bot 从未留存该话题历史,且正文里没有任何信号表明存在话题根+
|
|
13347
|
+
// 前情。首轮注入一行 hint(非全文、零网络请求),提示 CLI 可用 `botmux history`
|
|
13348
|
+
// 按需拉取话题历史(对齐 quote hint 的「提示+按需」模式,避免无条件全量预取+
|
|
13349
|
+
// 下载所有历史附件+长话题只取最旧 50 条且把 50 当精确总数的问题)。gate 已证明
|
|
13350
|
+
// 这是话题回复,无需再发网络探测。coalesced forward 已自带上下文不注入。仅首轮
|
|
13351
|
+
// — 后续 handleThreadReply 回合 CLI 已在首轮拿到该 hint,不重复注入。
|
|
13352
|
+
const topicThreadContext = (parsed.rootId && parsed.rootId !== parsed.messageId && !ctx.forwardSeedData)
|
|
13353
|
+
? buildTopicThreadContext(localeForBot(larkAppId))
|
|
13354
|
+
: '';
|
|
13355
|
+
// 话题 hint 同样前置到 codex-app 结构化 sidecar lane(与 quote hint 一致双 lane
|
|
13356
|
+
// 下发),否则 codex-app(clean input)bot 走 sidecar 时会静默丢掉该 hint。
|
|
13357
|
+
const codexAppMessageContext = topicThreadContext + codexAppQuoteContext + (workflowGrillPrompt ?? '');
|
|
13358
|
+
const promptContent = topicThreadContext + codexAppQuoteContext + codexAppApplicationContext + content;
|
|
12885
13359
|
// Resolve sender identity for <sender> tag injection. The first call to
|
|
12886
13360
|
// resolveSender for an unseen open_id may await contact.v3.user.get with a
|
|
12887
13361
|
// short budget; subsequent calls hit the cache and are sync-fast.
|
|
@@ -12891,7 +13365,14 @@ async function handleNewTopic(data, ctx) {
|
|
|
12891
13365
|
// Pin the working dir via the layered oncall / inherit / default lookup
|
|
12892
13366
|
// (auto-binds a defaultOncall chat as a side effect). Shared with the
|
|
12893
13367
|
// first-message `/repo` command branch so both paths stay consistent.
|
|
12894
|
-
const { pinnedWorkingDir, oncallEntry, inheritedFrom, pinnedFromBotDefault } = await resolvePinnedWorkingDir({
|
|
13368
|
+
const { pinnedWorkingDir, oncallEntry, inheritedFrom, pinnedFromBotDefault } = await resolvePinnedWorkingDir({
|
|
13369
|
+
scope,
|
|
13370
|
+
anchor,
|
|
13371
|
+
chatId,
|
|
13372
|
+
chatType,
|
|
13373
|
+
larkAppId,
|
|
13374
|
+
listenerWorkingDir: messageListener?.workingDir,
|
|
13375
|
+
});
|
|
12895
13376
|
// Auto-worktree: register PENDING (router buffers concurrent msgs, no force-fork)
|
|
12896
13377
|
// and build the worktree off the critical path (willAutoWorktree / runAutoWorktreeCommit).
|
|
12897
13378
|
const autoWt = willAutoWorktree(larkAppId, pinnedWorkingDir, pinnedFromBotDefault);
|
|
@@ -12905,7 +13386,8 @@ async function handleNewTopic(data, ctx) {
|
|
|
12905
13386
|
// For chat-scope, rootMessageId stores the seed message_id (audit only);
|
|
12906
13387
|
// routing keys off chatId via sessionAnchorId(), so any value works.
|
|
12907
13388
|
const rootIdForStore = scope === 'thread' ? anchor : messageId;
|
|
12908
|
-
const
|
|
13389
|
+
const initialTurnTitle = (messageListener?.replyCardTitle ?? (ctx.forwardSeedData ? followupContent : content)).substring(0, 50);
|
|
13390
|
+
const session = sessionStore.createSession(chatId, rootIdForStore, initialTurnTitle, chatType);
|
|
12909
13391
|
const now = Date.now();
|
|
12910
13392
|
setDirectChatDisplayNameFromSender(session, chatType, newTopicSender);
|
|
12911
13393
|
const groupChatName = await groupChatNamePromise;
|
|
@@ -12960,7 +13442,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
12960
13442
|
pendingSubstituteControlCard: shouldSendSubstituteControlCard,
|
|
12961
13443
|
pendingSender: newTopicSender,
|
|
12962
13444
|
ownerOpenId: senderOpenId,
|
|
12963
|
-
currentTurnTitle:
|
|
13445
|
+
currentTurnTitle: initialTurnTitle,
|
|
12964
13446
|
workingDir: pinnedWorkingDir,
|
|
12965
13447
|
};
|
|
12966
13448
|
if (pinnedWorkingDir) {
|
|
@@ -12972,7 +13454,24 @@ async function handleNewTopic(data, ctx) {
|
|
|
12972
13454
|
: 'thread';
|
|
12973
13455
|
beginReplyTargetTurn(ds, replyRootId, messageId, new Date().toISOString(), { quoteOnly: substituteReplyMode === 'quote', substitute: !!substituteTrigger });
|
|
12974
13456
|
sessionStore.updateSession(ds.session);
|
|
12975
|
-
|
|
13457
|
+
const creationKey = sessionKey(anchor, larkAppId);
|
|
13458
|
+
if (!setActiveSessionIfActive(activeSessions, creationKey, ds)) {
|
|
13459
|
+
const winner = await rollbackRejectedSessionAndGetWinner(activeSessions, creationKey, ds);
|
|
13460
|
+
if (winner) {
|
|
13461
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] New-topic registration lost to ` +
|
|
13462
|
+
`${winner.session.sessionId.substring(0, 8)}; handing message to routing winner`);
|
|
13463
|
+
await handleThreadReply(data, { ...ctx, scope, anchor }, {
|
|
13464
|
+
parsed,
|
|
13465
|
+
resources,
|
|
13466
|
+
attachments,
|
|
13467
|
+
quotaChecked: true,
|
|
13468
|
+
queueAlreadyAppended: true,
|
|
13469
|
+
senderResolved: true,
|
|
13470
|
+
sender: newTopicSender,
|
|
13471
|
+
});
|
|
13472
|
+
}
|
|
13473
|
+
return;
|
|
13474
|
+
}
|
|
12976
13475
|
// Auto-worktree: session is registered PENDING; build the worktree off the
|
|
12977
13476
|
// critical path, then commitRepoSelection pins it + forks (folding in any
|
|
12978
13477
|
// messages buffered during creation). detach → return immediately.
|
|
@@ -13034,6 +13533,74 @@ async function handleNewTopic(data, ctx) {
|
|
|
13034
13533
|
// for the same chat (reconnect replay / double-delivery) can't both spawn —
|
|
13035
13534
|
// claimed synchronously before the first await, released in `finally` (FR-13).
|
|
13036
13535
|
const autoStartJoinInFlight = new Set();
|
|
13536
|
+
// A join candidate becomes visible in activeSessions before its seed/reply
|
|
13537
|
+
// target and synchronous bootstrap are ready. Inbound turns for that exact
|
|
13538
|
+
// routing key must wait; otherwise they can re-fork the worker-null session and
|
|
13539
|
+
// then be replaced by the join handler's own fork. Deferred pendingRepo /
|
|
13540
|
+
// auto-worktree paths release after publishing their stable buffered state.
|
|
13541
|
+
const AUTO_START_JOIN_READY_MAX_WAIT_MS = 5_000;
|
|
13542
|
+
let testOnlyAutoStartJoinReadyMaxWaitMs;
|
|
13543
|
+
const autoStartJoinReadyBySessionKey = new Map();
|
|
13544
|
+
function beginAutoStartJoinReadyBarrier(routingKey, onCancel) {
|
|
13545
|
+
let resolveReady;
|
|
13546
|
+
const ready = new Promise((resolve) => {
|
|
13547
|
+
resolveReady = resolve;
|
|
13548
|
+
});
|
|
13549
|
+
let settled = false;
|
|
13550
|
+
let barrier;
|
|
13551
|
+
const settle = () => {
|
|
13552
|
+
if (settled)
|
|
13553
|
+
return;
|
|
13554
|
+
settled = true;
|
|
13555
|
+
resolveReady();
|
|
13556
|
+
if (autoStartJoinReadyBySessionKey.get(routingKey) === barrier) {
|
|
13557
|
+
autoStartJoinReadyBySessionKey.delete(routingKey);
|
|
13558
|
+
}
|
|
13559
|
+
};
|
|
13560
|
+
const cancel = () => {
|
|
13561
|
+
if (settled)
|
|
13562
|
+
return;
|
|
13563
|
+
barrier.cancelled = true;
|
|
13564
|
+
try {
|
|
13565
|
+
onCancel();
|
|
13566
|
+
}
|
|
13567
|
+
catch (err) {
|
|
13568
|
+
logger.warn(`[auto-start:入群] bootstrap timeout cleanup failed for ${routingKey}: ${err}`);
|
|
13569
|
+
}
|
|
13570
|
+
finally {
|
|
13571
|
+
settle();
|
|
13572
|
+
}
|
|
13573
|
+
};
|
|
13574
|
+
barrier = { ready, cancelled: false, settle, cancel };
|
|
13575
|
+
autoStartJoinReadyBySessionKey.set(routingKey, barrier);
|
|
13576
|
+
return barrier;
|
|
13577
|
+
}
|
|
13578
|
+
async function waitForAutoStartJoinReady(larkAppId, anchor) {
|
|
13579
|
+
const routingKey = sessionKey(anchor, larkAppId);
|
|
13580
|
+
while (true) {
|
|
13581
|
+
const barrier = autoStartJoinReadyBySessionKey.get(routingKey);
|
|
13582
|
+
if (!barrier)
|
|
13583
|
+
return;
|
|
13584
|
+
const maxWaitMs = testOnlyAutoStartJoinReadyMaxWaitMs ?? AUTO_START_JOIN_READY_MAX_WAIT_MS;
|
|
13585
|
+
let timer;
|
|
13586
|
+
const outcome = await Promise.race([
|
|
13587
|
+
barrier.ready.then(() => 'ready'),
|
|
13588
|
+
new Promise((resolve) => {
|
|
13589
|
+
timer = setTimeout(() => resolve('timeout'), maxWaitMs);
|
|
13590
|
+
}),
|
|
13591
|
+
]);
|
|
13592
|
+
if (timer)
|
|
13593
|
+
clearTimeout(timer);
|
|
13594
|
+
if (outcome === 'timeout' && autoStartJoinReadyBySessionKey.get(routingKey) === barrier) {
|
|
13595
|
+
logger.warn(`[auto-start:入群] bootstrap exceeded ${maxWaitMs}ms for ${anchor.substring(0, 12)}; ` +
|
|
13596
|
+
`yielding registration to the waiting turn`);
|
|
13597
|
+
barrier.cancel();
|
|
13598
|
+
}
|
|
13599
|
+
}
|
|
13600
|
+
}
|
|
13601
|
+
export function __testOnly_setAutoStartJoinReadyMaxWaitMs(value) {
|
|
13602
|
+
testOnlyAutoStartJoinReadyMaxWaitMs = value;
|
|
13603
|
+
}
|
|
13037
13604
|
// 主动开工 — 场景①: `${appId}:${chatId}` → the activeSessions key of the
|
|
13038
13605
|
// auto-started join session. Needed because in a 话题群 the session is keyed at
|
|
13039
13606
|
// the seed message id (not chatId), so a plain `activeSessions.has(chatId)`
|
|
@@ -13102,6 +13669,7 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13102
13669
|
if (priorAnchorKey)
|
|
13103
13670
|
groupJoinAnchorByChat.delete(chatLiveKey); // stale entry, will re-register below
|
|
13104
13671
|
autoStartJoinInFlight.add(lockKey);
|
|
13672
|
+
let joinReady;
|
|
13105
13673
|
try {
|
|
13106
13674
|
// D7 gate: an allowedUser must be a member of the chat. Alarm/oncall
|
|
13107
13675
|
// platforms (e.g. Nexus) create the chat, add bots first and the human
|
|
@@ -13156,9 +13724,8 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13156
13724
|
logger.info(`[auto-start:入群] ${chatId.substring(0, 12)} 锚点已有会话,跳过`);
|
|
13157
13725
|
return;
|
|
13158
13726
|
}
|
|
13159
|
-
const
|
|
13160
|
-
|
|
13161
|
-
: undefined;
|
|
13727
|
+
const needsSharedReply = mode === 'group'
|
|
13728
|
+
&& resolveRegularGroupMode(larkAppId, chatId) === 'shared';
|
|
13162
13729
|
const { pinnedWorkingDir, pinnedFromBotDefault } = await resolvePinnedWorkingDir({ scope, anchor, chatId, chatType, larkAppId });
|
|
13163
13730
|
const autoWt = willAutoWorktree(larkAppId, pinnedWorkingDir, pinnedFromBotDefault);
|
|
13164
13731
|
refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
|
|
@@ -13192,13 +13759,103 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13192
13759
|
ownerOpenId: operatorOpenId,
|
|
13193
13760
|
currentTurnTitle: title,
|
|
13194
13761
|
workingDir: pinnedWorkingDir,
|
|
13195
|
-
pendingTurnId:
|
|
13762
|
+
pendingTurnId: undefined,
|
|
13196
13763
|
};
|
|
13197
|
-
if (
|
|
13764
|
+
if (!setActiveSessionIfActive(activeSessions, dsKey, ds)) {
|
|
13765
|
+
await closeSessionHelper(session.sessionId);
|
|
13766
|
+
return;
|
|
13767
|
+
}
|
|
13768
|
+
const joinBootstrapExternallyTakenOver = () => (activeSessions.get(dsKey) !== ds
|
|
13769
|
+
|| ds.session.status !== 'active'
|
|
13770
|
+
|| !!(ds.worker && !ds.worker.killed));
|
|
13771
|
+
const joinBootstrapWasTakenOver = () => {
|
|
13772
|
+
const takenOver = (joinReady?.cancelled === true
|
|
13773
|
+
|| joinBootstrapExternallyTakenOver());
|
|
13774
|
+
if (takenOver && ds.worker && !ds.worker.killed) {
|
|
13775
|
+
logger.warn(`[auto-start:入群] ${chatId.substring(0, 12)} bootstrap 期间会话已由其它入口启动,让位且不再 refork`);
|
|
13776
|
+
}
|
|
13777
|
+
return takenOver;
|
|
13778
|
+
};
|
|
13779
|
+
// Publish the barrier only after this candidate wins registration. There is
|
|
13780
|
+
// no await between the CAS and this write, so a turn can never observe the
|
|
13781
|
+
// worker-null session without also observing the barrier. Publishing earlier
|
|
13782
|
+
// would unnecessarily stall real messages during membership retry and
|
|
13783
|
+
// mode/working-dir discovery, where their own session may safely win.
|
|
13784
|
+
joinReady = beginAutoStartJoinReadyBarrier(dsKey, () => {
|
|
13785
|
+
// Scheduler/dashboard/trigger paths do not wait on the inbound-message
|
|
13786
|
+
// barrier and may legitimately start this exact ds while join bootstrap
|
|
13787
|
+
// is awaiting Lark/network I/O. Once that happens, timeout cancellation
|
|
13788
|
+
// must yield to the new worker instead of closing its active turn.
|
|
13789
|
+
if (joinBootstrapExternallyTakenOver()) {
|
|
13790
|
+
logger.warn(`[auto-start:入群] ${chatId.substring(0, 12)} bootstrap timeout 时会话已被接管,跳过候选回收`);
|
|
13791
|
+
return;
|
|
13792
|
+
}
|
|
13793
|
+
// A user turn waited longer than the serializer's normal safety cap.
|
|
13794
|
+
// Relinquish this half-bootstrapped registration instead of letting the
|
|
13795
|
+
// turn fall back to the old "refork now, then get killed by late join"
|
|
13796
|
+
// behavior. closeSessionHelper completes map/status/store mutation before
|
|
13797
|
+
// its first await; the late best-effort cleanup must not hold the turn.
|
|
13798
|
+
const closing = closeSessionHelper(session.sessionId);
|
|
13799
|
+
if (activeSessions.get(dsKey) === ds)
|
|
13800
|
+
activeSessions.delete(dsKey);
|
|
13801
|
+
void closing.catch((err) => {
|
|
13802
|
+
logger.warn(`[auto-start:入群] timeout close failed for ${session.sessionId.substring(0, 8)}: ${err}`);
|
|
13803
|
+
});
|
|
13804
|
+
});
|
|
13805
|
+
const rollbackRegisteredJoinSession = async () => {
|
|
13806
|
+
// closeSession transitions the row before its first await, but this
|
|
13807
|
+
// handler owns a caller-supplied registry too. Drop only our identity so
|
|
13808
|
+
// slow best-effort cleanup cannot block a retry or erase a replacement.
|
|
13809
|
+
const closing = closeSessionHelper(session.sessionId);
|
|
13810
|
+
if (activeSessions.get(dsKey) === ds)
|
|
13811
|
+
activeSessions.delete(dsKey);
|
|
13812
|
+
await closing;
|
|
13813
|
+
};
|
|
13814
|
+
// Shared-mode seed/reply authority is externally visible. Publish it only
|
|
13815
|
+
// after this candidate owns the routing key; a CAS loser must leave no
|
|
13816
|
+
// orphaned "joined" topic and must not silently consume the first prompt.
|
|
13817
|
+
let sharedReplyRootId;
|
|
13818
|
+
const withdrawSharedReplySeed = () => {
|
|
13819
|
+
if (!sharedReplyRootId)
|
|
13820
|
+
return;
|
|
13821
|
+
const messageId = sharedReplyRootId;
|
|
13822
|
+
sharedReplyRootId = undefined;
|
|
13823
|
+
void deleteMessage(larkAppId, messageId);
|
|
13824
|
+
};
|
|
13825
|
+
const armSharedReplyTarget = () => {
|
|
13826
|
+
if (!sharedReplyRootId)
|
|
13827
|
+
return;
|
|
13828
|
+
ds.pendingTurnId = sharedReplyRootId;
|
|
13198
13829
|
beginReplyTargetTurn(ds, sharedReplyRootId, sharedReplyRootId, new Date(now).toISOString());
|
|
13199
13830
|
sessionStore.updateSession(ds.session);
|
|
13831
|
+
};
|
|
13832
|
+
if (needsSharedReply) {
|
|
13833
|
+
try {
|
|
13834
|
+
sharedReplyRootId = await sendMessage(larkAppId, chatId, tr('daemon.auto_start_join_seed', undefined, localeForBot(larkAppId)), 'text');
|
|
13835
|
+
}
|
|
13836
|
+
catch (err) {
|
|
13837
|
+
logger.warn(`[auto-start:入群] ${chatId.substring(0, 12)} shared seed 发送失败:${err?.message ?? err}`);
|
|
13838
|
+
await rollbackRegisteredJoinSession();
|
|
13839
|
+
return;
|
|
13840
|
+
}
|
|
13841
|
+
if (joinReady.cancelled) {
|
|
13842
|
+
// The send may have reached Lark even though its response exceeded the
|
|
13843
|
+
// local wait cap. Best-effort withdraw the now-ownerless shared seed.
|
|
13844
|
+
withdrawSharedReplySeed();
|
|
13845
|
+
return;
|
|
13846
|
+
}
|
|
13847
|
+
if (activeSessions.get(dsKey) !== ds || ds.session.status !== 'active') {
|
|
13848
|
+
logger.warn(`[auto-start:入群] ${chatId.substring(0, 12)} shared seed 返回时会话已被替换,停止首轮`);
|
|
13849
|
+
withdrawSharedReplySeed();
|
|
13850
|
+
await rollbackRegisteredJoinSession();
|
|
13851
|
+
return;
|
|
13852
|
+
}
|
|
13853
|
+
if (ds.worker && !ds.worker.killed) {
|
|
13854
|
+
withdrawSharedReplySeed();
|
|
13855
|
+
logger.warn(`[auto-start:入群] ${chatId.substring(0, 12)} shared seed 返回时会话已由其它入口启动,让位`);
|
|
13856
|
+
return;
|
|
13857
|
+
}
|
|
13200
13858
|
}
|
|
13201
|
-
activeSessions.set(dsKey, ds);
|
|
13202
13859
|
// Register the anchor so a later duplicate bot.added for this chat is deduped
|
|
13203
13860
|
// even in 话题群 (where dsKey is the seed id, not chatId).
|
|
13204
13861
|
groupJoinAnchorByChat.set(chatLiveKey, dsKey);
|
|
@@ -13206,18 +13863,32 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13206
13863
|
const buildPrompt = async () => buildNewTopicCliInput(promptBody, session.sessionId, botCfg.cliId, botCfg.cliPathOverride, undefined, undefined, await getAvailableBots(larkAppId, chatId), undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), undefined, { larkAppId, chatId, whiteboardId: ds.session.whiteboardId, codexAppText });
|
|
13207
13864
|
// Auto-worktree: register PENDING, build worktree off-path, commit+fork later.
|
|
13208
13865
|
if (pinnedWorkingDir && autoWt) {
|
|
13209
|
-
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
13866
|
+
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds) || joinBootstrapWasTakenOver()) {
|
|
13867
|
+
withdrawSharedReplySeed();
|
|
13210
13868
|
return;
|
|
13869
|
+
}
|
|
13870
|
+
armSharedReplyTarget();
|
|
13211
13871
|
startAutoWorktreePending(ds, { anchor, baseDir: pinnedWorkingDir, title, prompt: promptBody, operatorOpenId });
|
|
13212
13872
|
return;
|
|
13213
13873
|
}
|
|
13214
13874
|
// Pinned working dir → spawn immediately.
|
|
13215
13875
|
if (pinnedWorkingDir) {
|
|
13216
|
-
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
13876
|
+
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds) || joinBootstrapWasTakenOver()) {
|
|
13877
|
+
withdrawSharedReplySeed();
|
|
13217
13878
|
return;
|
|
13879
|
+
}
|
|
13218
13880
|
ensureSessionWhiteboard(ds);
|
|
13219
13881
|
const prompt = await buildPrompt();
|
|
13882
|
+
if (joinBootstrapWasTakenOver()) {
|
|
13883
|
+
withdrawSharedReplySeed();
|
|
13884
|
+
return;
|
|
13885
|
+
}
|
|
13220
13886
|
await noteTurnReceived(ds, anchor, promptBody);
|
|
13887
|
+
if (joinBootstrapWasTakenOver()) {
|
|
13888
|
+
withdrawSharedReplySeed();
|
|
13889
|
+
return;
|
|
13890
|
+
}
|
|
13891
|
+
armSharedReplyTarget();
|
|
13221
13892
|
rememberLastCliInput(ds, promptBody, prompt);
|
|
13222
13893
|
forkWorker(ds, prompt, sharedReplyRootId ? { turnId: sharedReplyRootId } : false);
|
|
13223
13894
|
ds.pendingTurnId = undefined;
|
|
@@ -13225,14 +13896,30 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13225
13896
|
return;
|
|
13226
13897
|
}
|
|
13227
13898
|
// No default dir → degrade to repo-selection card (D6 / FR-4).
|
|
13228
|
-
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds))
|
|
13899
|
+
if (await replyInvalidWorkingDirs(anchor, larkAppId, ds) || joinBootstrapWasTakenOver()) {
|
|
13900
|
+
withdrawSharedReplySeed();
|
|
13229
13901
|
return;
|
|
13902
|
+
}
|
|
13230
13903
|
const scanDirs = getProjectScanDirs(ds).filter(d => existsSync(d));
|
|
13231
13904
|
const projects = scanDirs.length > 0 ? scanMultipleProjects(scanDirs, 3, repoPickerScanOptions()) : [];
|
|
13232
13905
|
if (projects.length > 0) {
|
|
13233
13906
|
lastRepoScan.set(chatId, projects);
|
|
13234
13907
|
const cardJson = buildRepoSelectCard(projects, getSessionWorkingDir(ds), anchor, localeForBot(larkAppId), getBot(larkAppId).config.worktreeMultiPicker);
|
|
13235
|
-
|
|
13908
|
+
// The repo card itself belongs under the shared join seed. Arm the target
|
|
13909
|
+
// immediately before its single network await; pendingRepo already makes
|
|
13910
|
+
// this a stable buffered state for normal inbound/scheduler paths.
|
|
13911
|
+
armSharedReplyTarget();
|
|
13912
|
+
const repoCardMessageId = await sessionReply(anchor, cardJson, 'interactive', larkAppId);
|
|
13913
|
+
if (joinBootstrapWasTakenOver()) {
|
|
13914
|
+
void deleteMessage(larkAppId, repoCardMessageId);
|
|
13915
|
+
// If another entry actually started this same ds, its output still owns
|
|
13916
|
+
// the shared seed we just armed. Preserve that root; closed/replaced
|
|
13917
|
+
// candidates may withdraw it.
|
|
13918
|
+
if (!ds.worker || ds.worker.killed)
|
|
13919
|
+
withdrawSharedReplySeed();
|
|
13920
|
+
return;
|
|
13921
|
+
}
|
|
13922
|
+
ds.repoCardMessageId = repoCardMessageId;
|
|
13236
13923
|
announcePendingRepoSession(ds);
|
|
13237
13924
|
logger.info(`[auto-start:入群] ${chatId.substring(0, 12)} 无默认目录,弹 repo 选择卡(${projects.length} 个项目)`);
|
|
13238
13925
|
}
|
|
@@ -13240,7 +13927,16 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13240
13927
|
ds.pendingRepo = false;
|
|
13241
13928
|
ensureSessionWhiteboard(ds);
|
|
13242
13929
|
const prompt = await buildPrompt();
|
|
13930
|
+
if (joinBootstrapWasTakenOver()) {
|
|
13931
|
+
withdrawSharedReplySeed();
|
|
13932
|
+
return;
|
|
13933
|
+
}
|
|
13243
13934
|
await noteTurnReceived(ds, anchor, promptBody);
|
|
13935
|
+
if (joinBootstrapWasTakenOver()) {
|
|
13936
|
+
withdrawSharedReplySeed();
|
|
13937
|
+
return;
|
|
13938
|
+
}
|
|
13939
|
+
armSharedReplyTarget();
|
|
13244
13940
|
rememberLastCliInput(ds, promptBody, prompt);
|
|
13245
13941
|
forkWorker(ds, prompt, sharedReplyRootId ? { turnId: sharedReplyRootId } : false);
|
|
13246
13942
|
ds.pendingTurnId = undefined;
|
|
@@ -13248,6 +13944,7 @@ async function handleBotAdded(chatId, operatorOpenId, larkAppId) {
|
|
|
13248
13944
|
}
|
|
13249
13945
|
}
|
|
13250
13946
|
finally {
|
|
13947
|
+
joinReady?.settle();
|
|
13251
13948
|
autoStartJoinInFlight.delete(lockKey);
|
|
13252
13949
|
}
|
|
13253
13950
|
}
|
|
@@ -13287,16 +13984,21 @@ function lookupForeignBotName(senderOpenId, larkAppId) {
|
|
|
13287
13984
|
catch { /* */ }
|
|
13288
13985
|
return 'Bot';
|
|
13289
13986
|
}
|
|
13290
|
-
async function handleThreadReply(data, ctx) {
|
|
13987
|
+
async function handleThreadReply(data, ctx, prepared) {
|
|
13291
13988
|
const { chatId: ctxChatId, chatType: ctxChatType, scope, anchor, larkAppId, replyRootId, substituteTrigger } = ctx;
|
|
13292
|
-
await
|
|
13293
|
-
|
|
13989
|
+
await waitForAutoStartJoinReady(larkAppId, anchor);
|
|
13990
|
+
if (!prepared)
|
|
13991
|
+
await resolveNonsupportMessage(data, larkAppId);
|
|
13992
|
+
const parsedResult = prepared ?? parseEventMessage(data);
|
|
13993
|
+
const parsed = parsedResult.parsed;
|
|
13994
|
+
const resources = parsedResult.resources;
|
|
13294
13995
|
// Expand merge_forward: fetch sub-messages and collect their resources
|
|
13295
|
-
if (parsed.msgType === 'merge_forward') {
|
|
13996
|
+
if (!prepared && parsed.msgType === 'merge_forward') {
|
|
13296
13997
|
const { extraResources } = await expandMergeForward(larkAppId, parsed.messageId, parsed);
|
|
13297
13998
|
resources.push(...extraResources);
|
|
13298
13999
|
}
|
|
13299
|
-
|
|
14000
|
+
if (!prepared)
|
|
14001
|
+
learnFromMentions(larkAppId, parsed.mentions);
|
|
13300
14002
|
// Foreign bot @mention prefix: when sender is another botmux bot,把内容包成
|
|
13301
14003
|
// [来自 X 的 @mention]\n<原文> 喂给 worker,让 CLI 知道这是另一个 bot 发的——
|
|
13302
14004
|
// 不是用户直接发的——后续不需要按"对话用户"的方式处理。signal-file 路径
|
|
@@ -13330,33 +14032,35 @@ async function handleThreadReply(data, ctx) {
|
|
|
13330
14032
|
+ parsed.content;
|
|
13331
14033
|
let promptContent = initialPromptContent;
|
|
13332
14034
|
let rewrittenCodexAppMessageContext;
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
`
|
|
14035
|
+
if (!prepared) {
|
|
14036
|
+
const existingHookSession = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
14037
|
+
emitHookEvent('thread.reply', {
|
|
14038
|
+
larkAppId,
|
|
14039
|
+
chatId: ctxChatId,
|
|
14040
|
+
chatType: ctxChatType,
|
|
14041
|
+
scope,
|
|
14042
|
+
anchor,
|
|
14043
|
+
messageId: parsed.messageId,
|
|
14044
|
+
rootId: parsed.rootId,
|
|
14045
|
+
parentId: parsed.parentId,
|
|
14046
|
+
senderOpenId: senderOpenIdForPrefix,
|
|
14047
|
+
senderType: parsed.senderType,
|
|
14048
|
+
msgType: parsed.msgType,
|
|
14049
|
+
sessionId: existingHookSession?.session.sessionId,
|
|
14050
|
+
content: parsed.content,
|
|
14051
|
+
});
|
|
14052
|
+
if (isForeignBot) {
|
|
14053
|
+
logger.info(`[${larkAppId}] foreign-bot @mention prefix attached: sender=${senderOpenIdForPrefix?.substring(0, 12)} ` +
|
|
14054
|
+
`senderType=${parsed.senderType} via=${isBotSenderType ? 'sender_type' : 'cross-ref'}`);
|
|
14055
|
+
}
|
|
13352
14056
|
}
|
|
13353
14057
|
// resolveSender is deferred until we know the message actually needs prompt
|
|
13354
14058
|
// injection. callback URLs, daemon commands, and "other bot owns this
|
|
13355
14059
|
// anchor" all return early; routing them through resolveSender first would
|
|
13356
14060
|
// tack the 800ms budget onto paths that never see the sender tag. Use the
|
|
13357
14061
|
// helper below at every actual injection point.
|
|
13358
|
-
let threadSenderCached;
|
|
13359
|
-
let threadSenderResolved = false;
|
|
14062
|
+
let threadSenderCached = prepared?.sender;
|
|
14063
|
+
let threadSenderResolved = prepared?.senderResolved ?? false;
|
|
13360
14064
|
const getThreadSender = async () => {
|
|
13361
14065
|
if (threadSenderResolved)
|
|
13362
14066
|
return threadSenderCached;
|
|
@@ -13428,6 +14132,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
13428
14132
|
initiatorOpenId: threadSenderOpenId,
|
|
13429
14133
|
teamTrustUnionId: threadTeamTrustUnionId,
|
|
13430
14134
|
memberUnionId: threadSenderUnionId,
|
|
14135
|
+
botSender: isBotSenderType || isForeignBot,
|
|
13431
14136
|
})) {
|
|
13432
14137
|
return;
|
|
13433
14138
|
}
|
|
@@ -13497,10 +14202,13 @@ async function handleThreadReply(data, ctx) {
|
|
|
13497
14202
|
messageId: parsed.messageId,
|
|
13498
14203
|
replyRootId,
|
|
13499
14204
|
parsed,
|
|
14205
|
+
cmd,
|
|
13500
14206
|
commandContent,
|
|
13501
14207
|
senderOpenId: threadSenderOpenId,
|
|
14208
|
+
substitute: !!substituteTrigger,
|
|
13502
14209
|
senderUnionId: threadTeamTrustUnionId,
|
|
13503
14210
|
memberUnionId: threadSenderUnionId, // 原始 union(人腿),不锁 bot
|
|
14211
|
+
botSender: isBotSenderType || isForeignBot,
|
|
13504
14212
|
// Bot-started cold starts get no human owner (mirrors the auto-create
|
|
13505
14213
|
// path) — see the ownership note on startInitialPassthroughSession.
|
|
13506
14214
|
ownerOpenId: isForeignBot ? undefined : threadSenderOpenId,
|
|
@@ -13517,48 +14225,17 @@ async function handleThreadReply(data, ctx) {
|
|
|
13517
14225
|
// 拉起)。TODO(后续产品决策):是否把 CLI passthrough 也纳入 canOperate,
|
|
13518
14226
|
// 收紧到与 daemon 命令同档;这会同时改变真人 oncall 成员的现有行为,应单独评估。
|
|
13519
14227
|
const ds = existingDs;
|
|
13520
|
-
if (ds
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
ds.session.quoteTargetSenderIsBot = isForeignBot;
|
|
13527
|
-
const substituteReplyMode = substituteTrigger
|
|
13528
|
-
? (getBot(larkAppId).config.substituteMode?.replyMode ?? 'thread')
|
|
13529
|
-
: 'thread';
|
|
13530
|
-
beginReplyTargetTurn(ds, replyRootId, parsed.messageId, new Date().toISOString(), {
|
|
13531
|
-
quoteOnly: substituteReplyMode === 'quote',
|
|
14228
|
+
if (ds) {
|
|
14229
|
+
deliverPassthroughToExistingSession(ds, cmd, commandContent, anchor, larkAppId, {
|
|
14230
|
+
messageId: parsed.messageId,
|
|
14231
|
+
replyRootId,
|
|
14232
|
+
senderOpenId: threadSenderOpenId,
|
|
14233
|
+
senderIsBot: isForeignBot,
|
|
13532
14234
|
substitute: !!substituteTrigger,
|
|
13533
14235
|
});
|
|
13534
|
-
if (threadSenderOpenId && ds.session.lastCallerOpenId !== threadSenderOpenId) {
|
|
13535
|
-
ds.session.lastCallerOpenId = threadSenderOpenId;
|
|
13536
|
-
}
|
|
13537
|
-
sessionStore.updateSession(ds.session);
|
|
13538
|
-
// Mark a new turn so the CLI's response to /model, /clear, /compact, etc.
|
|
13539
|
-
// shows up as a fresh streaming card instead of silently PATCH-ing the
|
|
13540
|
-
// previous turn's card.
|
|
13541
|
-
//
|
|
13542
|
-
// Compatibility note (empty-start opening): a raw passthrough is a
|
|
13543
|
-
// LITERAL CLI command — its contract is that the CLI sees exactly the
|
|
13544
|
-
// bytes the user typed, never a botmux XML envelope. So it deliberately
|
|
13545
|
-
// does NOT consume `Session.initialUserTurnPending`: wrapping `/model`
|
|
13546
|
-
// in `<user_message>` would break the literal contract, and silently
|
|
13547
|
-
// clearing the marker would lose the opening for the next real turn.
|
|
13548
|
-
// `/model` on an empty-started session therefore stays literal and the
|
|
13549
|
-
// FOLLOWING business message still opens as a new topic.
|
|
13550
|
-
beginNewTurn(ds, commandContent);
|
|
13551
|
-
ds.worker.send({
|
|
13552
|
-
type: 'raw_input',
|
|
13553
|
-
content: commandContent,
|
|
13554
|
-
turnId: parsed.messageId,
|
|
13555
|
-
});
|
|
13556
|
-
markSessionActivity(ds);
|
|
13557
|
-
logger.info(`[${anchor.substring(0, 12)}] Passthrough ${cmd} → worker`);
|
|
13558
|
-
}
|
|
13559
|
-
else {
|
|
13560
|
-
sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13561
14236
|
}
|
|
14237
|
+
else
|
|
14238
|
+
void sessionReply(anchor, tr('daemon.cmd_needs_active_cli', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13562
14239
|
return;
|
|
13563
14240
|
}
|
|
13564
14241
|
if (DAEMON_COMMANDS.has(cmd)) {
|
|
@@ -13576,7 +14253,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
13576
14253
|
// (see spawn-path gate above). Denies chat-granted users management commands.
|
|
13577
14254
|
// canRunDaemonCommand:canTalkDaemonCommands 名单内的命令降到 canTalk,
|
|
13578
14255
|
// 与 new-topic 路径的统一闸同款(未配置时与 canOperate 全等)。
|
|
13579
|
-
if (!canRunDaemonCommand(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId, cmd, threadSenderUnionId, ctxChatType)) {
|
|
14256
|
+
if (!canRunDaemonCommand(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId, cmd, threadSenderUnionId, ctxChatType, isBotSenderType || isForeignBot)) {
|
|
13580
14257
|
sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
13581
14258
|
return;
|
|
13582
14259
|
}
|
|
@@ -13611,7 +14288,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
13611
14288
|
cmdPending = { pendingRepo: true, pendingPrompt: '', workingDir: pinnedWorkingDir };
|
|
13612
14289
|
}
|
|
13613
14290
|
sessionStore.updateSession(session);
|
|
13614
|
-
|
|
14291
|
+
const threadCommandDs = {
|
|
13615
14292
|
session,
|
|
13616
14293
|
worker: null,
|
|
13617
14294
|
workerPort: null,
|
|
@@ -13626,7 +14303,15 @@ async function handleThreadReply(data, ctx) {
|
|
|
13626
14303
|
hasHistory: false,
|
|
13627
14304
|
ownerOpenId: threadSenderOpenId,
|
|
13628
14305
|
...cmdPending,
|
|
13629
|
-
}
|
|
14306
|
+
};
|
|
14307
|
+
const commandKey = sessionKey(anchor, larkAppId);
|
|
14308
|
+
if (!setActiveSessionIfActive(activeSessions, commandKey, threadCommandDs)) {
|
|
14309
|
+
const winner = await rollbackRejectedSessionAndGetWinner(activeSessions, commandKey, threadCommandDs);
|
|
14310
|
+
if (!winner)
|
|
14311
|
+
return;
|
|
14312
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Thread daemon-command registration lost to ` +
|
|
14313
|
+
`${winner.session.sessionId.substring(0, 8)}; handing ${cmd} to routing winner`);
|
|
14314
|
+
}
|
|
13630
14315
|
}
|
|
13631
14316
|
// Pass mention-stripped content so /command argument parsing works.
|
|
13632
14317
|
// chatId lets session-less handlers (e.g. /group) reach the chat roster.
|
|
@@ -13658,6 +14343,16 @@ async function handleThreadReply(data, ctx) {
|
|
|
13658
14343
|
askId: pendingAsk.askId,
|
|
13659
14344
|
by: threadSenderOpenId,
|
|
13660
14345
|
text: askReplyText,
|
|
14346
|
+
// Actor context so the broker's talk check uses the same predicate as
|
|
14347
|
+
// the dispatcher gate / quota recheck: a bot text-reply → evaluateBotTalk
|
|
14348
|
+
// (covers team-拉群 with no union_id), a platform teamMember human →
|
|
14349
|
+
// evaluateTalk's teamMember union leg. Omitting it (card clicks) degrades
|
|
14350
|
+
// to the plain evaluateTalk(openId, chatType).
|
|
14351
|
+
actor: {
|
|
14352
|
+
botSender: isBotSenderType || isForeignBot,
|
|
14353
|
+
senderUnionId: threadTeamTrustUnionId,
|
|
14354
|
+
memberUnionId: threadSenderUnionId,
|
|
14355
|
+
},
|
|
13661
14356
|
});
|
|
13662
14357
|
if (outcome === 'accepted') {
|
|
13663
14358
|
logger.info(`[${anchor.substring(0, 12)}] ask custom reply accepted from ${threadSenderOpenId.substring(0, 12)}`);
|
|
@@ -13688,7 +14383,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
13688
14383
|
}
|
|
13689
14384
|
}
|
|
13690
14385
|
const quotaSenderOpenId = threadSenderOpenId;
|
|
13691
|
-
if (!await enforceMessageQuotaForCliInput(larkAppId, ctxChatId ?? data?.message?.chat_id, quotaSenderOpenId, parsed.messageId, anchor, threadTeamTrustUnionId, threadSenderUnionId, ctxChatType)) {
|
|
14386
|
+
if (!prepared?.quotaChecked && !await enforceMessageQuotaForCliInput(larkAppId, ctxChatId ?? data?.message?.chat_id, quotaSenderOpenId, parsed.messageId, anchor, threadTeamTrustUnionId, threadSenderUnionId, ctxChatType, isBotSenderType || isForeignBot)) {
|
|
13692
14387
|
return;
|
|
13693
14388
|
}
|
|
13694
14389
|
// When a command path rewrites the model prompt (for example /workflow),
|
|
@@ -13700,12 +14395,19 @@ async function handleThreadReply(data, ctx) {
|
|
|
13700
14395
|
const codexAppApplicationContext = initialCodexAppApplicationContext;
|
|
13701
14396
|
// Download attachments
|
|
13702
14397
|
const effectiveAppId = ds?.larkAppId ?? larkAppId;
|
|
13703
|
-
|
|
13704
|
-
if (
|
|
13705
|
-
|
|
14398
|
+
let attachments;
|
|
14399
|
+
if (prepared) {
|
|
14400
|
+
attachments = prepared.attachments;
|
|
13706
14401
|
}
|
|
13707
|
-
|
|
13708
|
-
|
|
14402
|
+
else {
|
|
14403
|
+
const downloaded = await downloadResources(effectiveAppId, parsed.messageId, resources);
|
|
14404
|
+
attachments = downloaded.attachments;
|
|
14405
|
+
if (attachments.length > 0) {
|
|
14406
|
+
parsed.attachments = attachments;
|
|
14407
|
+
}
|
|
14408
|
+
if (downloaded.needLogin) {
|
|
14409
|
+
sessionReply(anchor, tr('daemon.download_failed_need_login', undefined, localeForBot(effectiveAppId)), 'text', effectiveAppId);
|
|
14410
|
+
}
|
|
13709
14411
|
}
|
|
13710
14412
|
// Update last message time + last caller (used by `botmux send` to address
|
|
13711
14413
|
// reply cards to whoever triggered this turn — matters in oncall groups
|
|
@@ -13837,7 +14539,10 @@ async function handleThreadReply(data, ctx) {
|
|
|
13837
14539
|
}
|
|
13838
14540
|
// Route to file queue (keyed by anchor: rootMessageId for thread, chatId for chat)
|
|
13839
14541
|
messageQueue.ensureQueue(anchor);
|
|
13840
|
-
|
|
14542
|
+
if (!prepared?.queueAlreadyAppended)
|
|
14543
|
+
messageQueue.appendMessage(anchor, parsed);
|
|
14544
|
+
if (ds)
|
|
14545
|
+
publishSessionMessagePreviewPatch(ds);
|
|
13841
14546
|
if (!ds) {
|
|
13842
14547
|
// No active session at this anchor — auto-create. This branch is mostly a
|
|
13843
14548
|
// safety net; the dispatcher routes here only when isSessionOwner() returns
|
|
@@ -13942,7 +14647,24 @@ async function handleThreadReply(data, ctx) {
|
|
|
13942
14647
|
: 'thread';
|
|
13943
14648
|
beginReplyTargetTurn(newDs, replyRootId, parsed.messageId, new Date().toISOString(), { quoteOnly: substituteReplyMode === 'quote', substitute: !!substituteTrigger });
|
|
13944
14649
|
sessionStore.updateSession(newDs.session);
|
|
13945
|
-
|
|
14650
|
+
const creationKey = sessionKey(anchor, larkAppId);
|
|
14651
|
+
if (!setActiveSessionIfActive(activeSessions, creationKey, newDs)) {
|
|
14652
|
+
const winner = await rollbackRejectedSessionAndGetWinner(activeSessions, creationKey, newDs);
|
|
14653
|
+
if (winner) {
|
|
14654
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Reply auto-create registration lost to ` +
|
|
14655
|
+
`${winner.session.sessionId.substring(0, 8)}; handing message to routing winner`);
|
|
14656
|
+
await handleThreadReply(data, ctx, {
|
|
14657
|
+
parsed,
|
|
14658
|
+
resources,
|
|
14659
|
+
attachments,
|
|
14660
|
+
quotaChecked: true,
|
|
14661
|
+
queueAlreadyAppended: true,
|
|
14662
|
+
senderResolved: true,
|
|
14663
|
+
sender: autoCreateSender,
|
|
14664
|
+
});
|
|
14665
|
+
}
|
|
14666
|
+
return;
|
|
14667
|
+
}
|
|
13946
14668
|
// Auto-worktree: register PENDING, build worktree off-path, commit+fork later.
|
|
13947
14669
|
if (pinnedWorkingDir && autoWt) {
|
|
13948
14670
|
if (await replyInvalidWorkingDirs(anchor, larkAppId, newDs))
|
|
@@ -14241,20 +14963,27 @@ async function handleThreadReply(data, ctx) {
|
|
|
14241
14963
|
* 2) bot 的 defaultWorkingDir / workingDir 配置
|
|
14242
14964
|
* 3) fallback 到 ~
|
|
14243
14965
|
*
|
|
14244
|
-
*
|
|
14245
|
-
*
|
|
14966
|
+
* 本函数只同步建立 session skeleton 并竞争 routing ownership;不解析评论
|
|
14967
|
+
* 作者、不写 per-turn target、也不 fork。胜出的 authoritative
|
|
14968
|
+
* handleDocComment delivery owner 统一完成这些动作。
|
|
14969
|
+
* 返回胜出的 DaemonSession(已加入 activeSessions),失败返回 null。
|
|
14246
14970
|
*/
|
|
14247
14971
|
async function autoCreateDocSession(sub, larkAppId, ctx) {
|
|
14248
14972
|
const botCfg = getBot(larkAppId).config;
|
|
14973
|
+
const virtualChatId = `doc:${sub.fileToken}`;
|
|
14974
|
+
const virtualAnchor = virtualChatId;
|
|
14975
|
+
const routingKey = sessionKey(virtualAnchor, larkAppId);
|
|
14976
|
+
const existing = activeSessions.get(routingKey);
|
|
14977
|
+
if (existing?.session.status === 'active' && ownsCurrentRoute(existing, larkAppId)) {
|
|
14978
|
+
persistDocBindingToSession(sub, larkAppId, existing);
|
|
14979
|
+
return existing;
|
|
14980
|
+
}
|
|
14249
14981
|
// 解析 workingDir:订阅时指定的 > bot 配置 defaultWorkingDir > bot 配置 workingDir > ~
|
|
14250
14982
|
const workingDir = sub.workingDir
|
|
14251
14983
|
?? effectiveDefaultWorkingDir(botCfg)
|
|
14252
14984
|
?? botCfg.workingDir
|
|
14253
14985
|
?? '~';
|
|
14254
|
-
const sender = ctx.authorOpenId ? await resolveSender(larkAppId, ctx.authorOpenId, 'user') : undefined;
|
|
14255
14986
|
const title = `[Doc] ${sub.fileToken.slice(0, 8)}: ${ctx.text.slice(0, 40)}`;
|
|
14256
|
-
const virtualChatId = `doc:${sub.fileToken}`;
|
|
14257
|
-
const virtualAnchor = sub.sessionAnchor;
|
|
14258
14987
|
const now = Date.now();
|
|
14259
14988
|
const session = sessionStore.createSession(virtualChatId, virtualAnchor, title, 'group');
|
|
14260
14989
|
session.larkAppId = larkAppId;
|
|
@@ -14281,36 +15010,57 @@ async function autoCreateDocSession(sub, larkAppId, ctx) {
|
|
|
14281
15010
|
ownerOpenId: ctx.authorOpenId || sub.ownerOpenId,
|
|
14282
15011
|
currentTurnTitle: ctx.text.substring(0, 50),
|
|
14283
15012
|
};
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
});
|
|
14295
|
-
const docTarget = {
|
|
14296
|
-
fileToken: sub.fileToken,
|
|
14297
|
-
fileType: sub.fileType,
|
|
14298
|
-
commentId: ctx.commentId,
|
|
14299
|
-
replyToName: sender?.name,
|
|
14300
|
-
replyToOpenId: ctx.authorOpenId,
|
|
14301
|
-
turnId,
|
|
14302
|
-
replyId: ctx.replyId,
|
|
14303
|
-
};
|
|
14304
|
-
(ds.session.docCommentTargets ??= {})[turnId] = docTarget;
|
|
14305
|
-
try {
|
|
14306
|
-
sessionStore.updateSession(ds.session);
|
|
15013
|
+
const selected = await registerDocSessionCandidate(routingKey, ds);
|
|
15014
|
+
if (!selected) {
|
|
15015
|
+
logger.warn(`[doc-comment] auto-create registration lost without an active winner file=${sub.fileToken.slice(0, 12)}`);
|
|
15016
|
+
return null;
|
|
15017
|
+
}
|
|
15018
|
+
await persistSelectedDocBinding(routingKey, sub, larkAppId, selected, ds);
|
|
15019
|
+
if (selected !== ds) {
|
|
15020
|
+
logger.info(`[doc-comment] auto-create ${session.sessionId.slice(0, 8)} lost to ` +
|
|
15021
|
+
`${selected.session.sessionId.slice(0, 8)}; handing comment to routing winner`);
|
|
15022
|
+
return selected;
|
|
14307
15023
|
}
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
// 不在这里 forkWorker —— handleDocComment 会统一处理(它会检查 worker 状态、
|
|
14311
|
-
// 加 reaction、设 docCommentTurns、然后 fork 或 send)。这里只建好 session 骨架。
|
|
15024
|
+
// 不在这里 forkWorker —— handleDocComment 会统一处理 reaction、per-turn
|
|
15025
|
+
// 回复落点及 fork/send。这里只建好 session skeleton。
|
|
14312
15026
|
logger.info(`[doc-comment] auto-created session for file=${sub.fileToken.slice(0, 12)} (wd=${workingDir}, cli=${botCfg.cliId})`);
|
|
14313
|
-
return
|
|
15027
|
+
return selected;
|
|
15028
|
+
}
|
|
15029
|
+
/**
|
|
15030
|
+
* One authoritative delivery result per document-comment turn. Event delivery
|
|
15031
|
+
* and the --all poller can observe the same reply concurrently; followers must
|
|
15032
|
+
* await the owner's real result rather than treating an in-flight claim as a
|
|
15033
|
+
* success and advancing the poll cursor past a later failure.
|
|
15034
|
+
*/
|
|
15035
|
+
const handledDocCommentTurns = new BoundedMap(5_000);
|
|
15036
|
+
async function runClaimedDocCommentTurn(claimKey, work, onOwnerFailure) {
|
|
15037
|
+
const existing = handledDocCommentTurns.get(claimKey);
|
|
15038
|
+
if (existing)
|
|
15039
|
+
return await existing;
|
|
15040
|
+
let resolveClaim;
|
|
15041
|
+
const claim = new Promise((resolve) => { resolveClaim = resolve; });
|
|
15042
|
+
handledDocCommentTurns.set(claimKey, claim);
|
|
15043
|
+
try {
|
|
15044
|
+
await work();
|
|
15045
|
+
resolveClaim(true);
|
|
15046
|
+
return true;
|
|
15047
|
+
}
|
|
15048
|
+
catch (err) {
|
|
15049
|
+
try {
|
|
15050
|
+
await onOwnerFailure?.(err);
|
|
15051
|
+
}
|
|
15052
|
+
finally {
|
|
15053
|
+
if (handledDocCommentTurns.get(claimKey) === claim) {
|
|
15054
|
+
handledDocCommentTurns.delete(claimKey);
|
|
15055
|
+
}
|
|
15056
|
+
resolveClaim(false);
|
|
15057
|
+
}
|
|
15058
|
+
throw err;
|
|
15059
|
+
}
|
|
15060
|
+
}
|
|
15061
|
+
export const __testOnly_runClaimedDocCommentTurn = runClaimedDocCommentTurn;
|
|
15062
|
+
export function __testOnly_resetDocCommentClaims() {
|
|
15063
|
+
handledDocCommentTurns.clear();
|
|
14314
15064
|
}
|
|
14315
15065
|
/**
|
|
14316
15066
|
* 文档评论入口(/watch-comment / /subscribe-lark-doc):一条命中绑定的文档评论喂进会话。
|
|
@@ -14327,85 +15077,143 @@ async function handleDocComment(ctx) {
|
|
|
14327
15077
|
const { larkAppId, sub, commentId, text } = ctx;
|
|
14328
15078
|
const turnId = ctx.replyId || commentId;
|
|
14329
15079
|
const claimKey = `${larkAppId}:${sub.fileToken}:${turnId}`;
|
|
14330
|
-
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
const
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
if (
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
15080
|
+
const userReplyId = ctx.replyId;
|
|
15081
|
+
let reactionId;
|
|
15082
|
+
let deliveryDs;
|
|
15083
|
+
let deliverySession;
|
|
15084
|
+
const targetMatchesThisTurn = (target) => !!target
|
|
15085
|
+
&& target.fileToken === sub.fileToken
|
|
15086
|
+
&& target.fileType === sub.fileType
|
|
15087
|
+
&& target.commentId === commentId
|
|
15088
|
+
&& target.replyId === userReplyId;
|
|
15089
|
+
const cleanupFailedDelivery = async () => {
|
|
15090
|
+
if (deliveryDs) {
|
|
15091
|
+
const runtimeTarget = deliveryDs.docCommentTurns?.get(turnId);
|
|
15092
|
+
if (targetMatchesThisTurn(runtimeTarget)) {
|
|
15093
|
+
deliveryDs.docCommentTurns?.delete(turnId);
|
|
15094
|
+
}
|
|
14344
15095
|
}
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
15096
|
+
let removedPersistedTarget = false;
|
|
15097
|
+
if (deliverySession) {
|
|
15098
|
+
const persistedTarget = deliverySession.docCommentTargets?.[turnId];
|
|
15099
|
+
if (targetMatchesThisTurn(persistedTarget)) {
|
|
15100
|
+
delete deliverySession.docCommentTargets?.[turnId];
|
|
15101
|
+
removedPersistedTarget = true;
|
|
15102
|
+
}
|
|
15103
|
+
}
|
|
15104
|
+
if (removedPersistedTarget && deliverySession) {
|
|
15105
|
+
try {
|
|
15106
|
+
// Persist only the exact object still owned by this bot store. A stale
|
|
15107
|
+
// continuation must not reinsert a displaced/cross-file Session row.
|
|
15108
|
+
if (sessionStore.getOwnedSession(deliverySession.sessionId) === deliverySession) {
|
|
15109
|
+
sessionStore.updateSession(deliverySession);
|
|
15110
|
+
}
|
|
15111
|
+
}
|
|
15112
|
+
catch (persistErr) {
|
|
15113
|
+
logger.debug(`[doc-comment] failed to persist aborted target cleanup ` +
|
|
15114
|
+
`file=${sub.fileToken.slice(0, 12)} turn=${turnId.slice(0, 12)}: ` +
|
|
15115
|
+
`${persistErr instanceof Error ? persistErr.message : String(persistErr)}`);
|
|
15116
|
+
}
|
|
15117
|
+
}
|
|
15118
|
+
if (reactionId && userReplyId) {
|
|
15119
|
+
try {
|
|
15120
|
+
await removeCommentReaction(larkAppId, { fileToken: sub.fileToken, fileType: sub.fileType }, commentId, userReplyId, reactionId);
|
|
15121
|
+
}
|
|
15122
|
+
catch (reactionErr) {
|
|
15123
|
+
logger.debug(`[doc-comment] failed to remove Typing after aborted delivery ` +
|
|
15124
|
+
`file=${sub.fileToken.slice(0, 12)} turn=${turnId.slice(0, 12)}: ` +
|
|
15125
|
+
`${reactionErr instanceof Error ? reactionErr.message : String(reactionErr)}`);
|
|
15126
|
+
}
|
|
15127
|
+
}
|
|
15128
|
+
};
|
|
14348
15129
|
try {
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
15130
|
+
const duplicate = handledDocCommentTurns.has(claimKey);
|
|
15131
|
+
if (duplicate) {
|
|
15132
|
+
logger.info(`[doc-comment] duplicate turn awaiting owner file=${sub.fileToken.slice(0, 12)} turn=${turnId.slice(0, 12)}`);
|
|
15133
|
+
}
|
|
15134
|
+
return await runClaimedDocCommentTurn(claimKey, async () => {
|
|
15135
|
+
const loc = localeForBot(larkAppId);
|
|
15136
|
+
let ds = resolveBoundDocSession(sub, larkAppId);
|
|
15137
|
+
if (!ds) {
|
|
15138
|
+
// 无活跃 session → 自动为该文档创建一个(用虚拟 anchor = doc:{fileToken})
|
|
15139
|
+
logger.info(`[doc-comment] no active session for anchor=${sub.sessionAnchor.slice(0, 12)}; auto-creating for file=${sub.fileToken.slice(0, 12)}`);
|
|
15140
|
+
ds = await autoCreateDocSession(sub, larkAppId, ctx);
|
|
15141
|
+
if (!ds) {
|
|
15142
|
+
throw new Error(`auto-create session failed for ${sub.fileToken.slice(0, 12)}`);
|
|
15143
|
+
}
|
|
15144
|
+
}
|
|
15145
|
+
deliveryDs = ds;
|
|
15146
|
+
const generation = captureRoutingGeneration(ds);
|
|
15147
|
+
deliverySession = generation.session;
|
|
15148
|
+
ensureCurrentRoutingGeneration(generation, 'comment:start');
|
|
15149
|
+
// 给用户的回复加 "Typing" reaction,让评论者知道 bot 正在处理。
|
|
15150
|
+
if (userReplyId) {
|
|
15151
|
+
reactionId = await addCommentReaction(larkAppId, { fileToken: sub.fileToken, fileType: sub.fileType }, commentId, userReplyId, 'Typing');
|
|
15152
|
+
}
|
|
15153
|
+
ensureCurrentRoutingGeneration(generation, 'comment:reaction');
|
|
15154
|
+
const sender = ctx.authorOpenId ? await resolveSender(larkAppId, ctx.authorOpenId, 'user') : undefined;
|
|
15155
|
+
ensureCurrentRoutingGeneration(generation, 'comment:sender');
|
|
15156
|
+
const authorName = sender?.name || ctx.authorOpenId?.slice(0, 8) || '?';
|
|
15157
|
+
const dsBotCfg = getBot(ds.larkAppId).config;
|
|
15158
|
+
const promptInput = {
|
|
15159
|
+
fileToken: sub.fileToken,
|
|
15160
|
+
fileType: sub.fileType,
|
|
15161
|
+
question: text,
|
|
15162
|
+
author: authorName,
|
|
15163
|
+
selectedText: ctx.selectedText,
|
|
15164
|
+
priorReplies: ctx.priorReplies?.map(reply => ({
|
|
15165
|
+
author: reply.authorOpenId?.slice(0, 12),
|
|
15166
|
+
text: reply.text,
|
|
15167
|
+
})),
|
|
15168
|
+
projectDir: ds.workingDir ?? sub.workingDir,
|
|
15169
|
+
brand: normalizeBrand(dsBotCfg.brand),
|
|
15170
|
+
locale: loc,
|
|
15171
|
+
};
|
|
15172
|
+
// 记录本轮回评论的落点。两条路都要覆盖:
|
|
15173
|
+
// • ds.docCommentTurns(内存,按 turnId)→ deliverFinalOutput「兜底」分流用
|
|
15174
|
+
// • session.docCommentTargets(磁盘,per-turn map)→ `botmux send`「主回复」分流用
|
|
15175
|
+
// (botmux send 跑在独立子进程,只能从磁盘读会话态;per-turn 避免并发评论串线)
|
|
15176
|
+
(ds.docCommentTurns ??= new Map()).set(turnId, {
|
|
15177
|
+
fileToken: sub.fileToken,
|
|
15178
|
+
fileType: sub.fileType,
|
|
15179
|
+
commentId,
|
|
15180
|
+
replyToOpenId: ctx.authorOpenId,
|
|
15181
|
+
replyToName: sender?.name,
|
|
15182
|
+
replyId: userReplyId,
|
|
15183
|
+
reactionId,
|
|
14399
15184
|
});
|
|
14400
|
-
|
|
14401
|
-
(ds.
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
15185
|
+
const docTarget = { fileToken: sub.fileToken, fileType: sub.fileType, commentId, replyToName: sender?.name, replyToOpenId: ctx.authorOpenId, turnId, replyId: userReplyId, reactionId };
|
|
15186
|
+
const selfBot = getBot(ds.larkAppId);
|
|
15187
|
+
if (ds.worker && !ds.worker.killed) {
|
|
15188
|
+
const targetWorker = ds.worker;
|
|
15189
|
+
const isBridge = !!ds.adoptedFrom;
|
|
15190
|
+
if (!isBridge)
|
|
15191
|
+
ensureSessionWhiteboard(ds);
|
|
15192
|
+
const { promptContent, cliInput } = buildDocCommentTurnInput({
|
|
15193
|
+
ds,
|
|
15194
|
+
promptInput,
|
|
15195
|
+
botCliId: dsBotCfg.cliId,
|
|
15196
|
+
botCliPathOverride: dsBotCfg.cliPathOverride,
|
|
15197
|
+
botIdentity: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
15198
|
+
sender,
|
|
15199
|
+
mode: 'live',
|
|
15200
|
+
});
|
|
15201
|
+
beginNewTurn(ds, text);
|
|
15202
|
+
(ds.session.docCommentTargets ??= {})[turnId] = docTarget; // per-turn map,不覆盖其他并发轮
|
|
15203
|
+
// rememberLastCliInput persists both the exact comment target and the
|
|
15204
|
+
// structured sidecar before any worker-visible delivery can occur.
|
|
15205
|
+
rememberLastCliInput(ds, promptContent, cliInput);
|
|
15206
|
+
await noteTurnReceived(ds, commentId, text, sender, turnId);
|
|
15207
|
+
ensureCurrentRoutingGeneration(generation, 'comment:live-note');
|
|
15208
|
+
if (ds.worker !== targetWorker || targetWorker.killed) {
|
|
15209
|
+
throw new Error('worker generation changed during comment:live-note');
|
|
15210
|
+
}
|
|
15211
|
+
if (!sendWorkerInput(ds, cliInput, turnId)) {
|
|
15212
|
+
throw new Error('worker became unavailable during comment:live-send');
|
|
15213
|
+
}
|
|
15214
|
+
logger.info(`[${tag(ds)}] doc-comment turn injected (turn ${turnId.slice(0, 8)})`);
|
|
15215
|
+
return;
|
|
15216
|
+
}
|
|
14409
15217
|
// Worker 挂起 / 已退出 —— resume 重 fork(与 handleThreadReply 同路)。
|
|
14410
15218
|
logger.info(`[${tag(ds)}] Worker not running for doc-comment, re-forking...`);
|
|
14411
15219
|
if (ds.usageLimitRetryTimer) {
|
|
@@ -14434,33 +15242,26 @@ async function handleDocComment(ctx) {
|
|
|
14434
15242
|
mode: 'refork',
|
|
14435
15243
|
});
|
|
14436
15244
|
(ds.session.docCommentTargets ??= {})[turnId] = docTarget; // per-turn map,不覆盖其他并发轮
|
|
14437
|
-
await noteTurnReceived(ds, commentId, text, sender, turnId);
|
|
14438
15245
|
rememberLastCliInput(ds, promptContent, wrappedInput);
|
|
15246
|
+
await noteTurnReceived(ds, commentId, text, sender, turnId);
|
|
15247
|
+
ensureCurrentRoutingGeneration(generation, 'comment:refork-note');
|
|
15248
|
+
if (ds.worker && !ds.worker.killed) {
|
|
15249
|
+
throw new Error('worker became active during comment:refork-note');
|
|
15250
|
+
}
|
|
14439
15251
|
sessionStore.updateSession(ds.session);
|
|
14440
|
-
// Same adopt re-fork routing as handleThreadReply: an adopt session whose
|
|
14441
|
-
// bridge worker exited must come back through forkAdoptWorker (observe +
|
|
14442
|
-
// bridge), never forkWorker. buildDocCommentTurnInput(mode:'refork')
|
|
14443
|
-
// delegates to buildReforkCliInput, which already bridge-formats the content
|
|
14444
|
-
// when ds.adoptedFrom is set, so the doc-comment turn reaches the observed
|
|
14445
|
-
// pane without a <user_message> wrapper.
|
|
14446
15252
|
if (ds.adoptedFrom) {
|
|
14447
15253
|
forkAdoptWorker(ds, { prompt: wrappedInput.content, turnId });
|
|
14448
15254
|
}
|
|
14449
15255
|
else {
|
|
14450
15256
|
forkWorker(ds, wrappedInput, { resume: ds.hasHistory, turnId });
|
|
14451
15257
|
}
|
|
14452
|
-
}
|
|
14453
|
-
return true;
|
|
15258
|
+
}, cleanupFailedDelivery);
|
|
14454
15259
|
}
|
|
14455
15260
|
catch (err) {
|
|
14456
|
-
// 投递失败:清理 claim 允许重试,返回 false 让 poller 不推进游标。
|
|
14457
|
-
handledDocCommentTurns.delete(claimKey);
|
|
14458
15261
|
logger.warn(`[doc-comment] delivery failed, claim released for retry file=${sub.fileToken.slice(0, 12)} turn=${turnId.slice(0, 12)} err=${err instanceof Error ? err.message : String(err)}`);
|
|
14459
15262
|
return false;
|
|
14460
15263
|
}
|
|
14461
15264
|
}
|
|
14462
|
-
/** 同一条评论可能同时被长连接通知和 --all 轮询看到;daemon 内统一去重。 */
|
|
14463
|
-
const handledDocCommentTurns = new BoundedMap(5_000);
|
|
14464
15265
|
let docCommentPollRunning = false;
|
|
14465
15266
|
/**
|
|
14466
15267
|
* `/watch-comment --all` 的应用身份增量轮询。
|
|
@@ -14561,7 +15362,7 @@ async function restoreDocSubscriptions(_sessions) {
|
|
|
14561
15362
|
// • 有 sessionId 且其会话 status==='closed' → 真的关了 → 退订 + 删表
|
|
14562
15363
|
// • 会话不存在(被清理)→ 同上
|
|
14563
15364
|
// • 会话 active / 缺 sessionId(老订阅,无从判定)→ 保留 + 重订阅(保守,不误删)
|
|
14564
|
-
const stored = sub.sessionId ? sessionStore.
|
|
15365
|
+
const stored = sub.sessionId ? sessionStore.getOwnedSession(sub.sessionId) : undefined;
|
|
14565
15366
|
const definitelyClosed = sub.sessionId && (!stored || stored.status === 'closed');
|
|
14566
15367
|
if (definitelyClosed) {
|
|
14567
15368
|
if (sub.managedBy !== 'watch-comment')
|
|
@@ -14599,14 +15400,6 @@ function resolvePrimaryOwnerOpenId(larkAppId) {
|
|
|
14599
15400
|
return undefined;
|
|
14600
15401
|
}
|
|
14601
15402
|
}
|
|
14602
|
-
/** Parse a positive float env override, falling back to `dflt` when unset/invalid. */
|
|
14603
|
-
function envFloat(name, dflt) {
|
|
14604
|
-
const raw = process.env[name];
|
|
14605
|
-
if (raw === undefined || raw.trim() === '')
|
|
14606
|
-
return dflt;
|
|
14607
|
-
const n = Number(raw);
|
|
14608
|
-
return Number.isFinite(n) && n >= 0 ? n : dflt;
|
|
14609
|
-
}
|
|
14610
15403
|
/**
|
|
14611
15404
|
* Host-overload thresholds, seeded from module defaults and overridable via env
|
|
14612
15405
|
* so an operator can tune sensitivity without a code change:
|
|
@@ -14618,37 +15411,24 @@ function envFloat(name, dflt) {
|
|
|
14618
15411
|
* but are inert (reading passes swap=undefined).
|
|
14619
15412
|
*/
|
|
14620
15413
|
function resolveOverloadThresholds() {
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
logger.warn(`[overload] BOTMUX_OVERLOAD_EXIT_MEM_FRAC (${exitMemUsedFrac}) >= ENTER (${enterMemUsedFrac}); clamping exit to ${clamped}`);
|
|
14640
|
-
exitMemUsedFrac = clamped;
|
|
14641
|
-
}
|
|
14642
|
-
return {
|
|
14643
|
-
cpuCount,
|
|
14644
|
-
enterLoadRatio,
|
|
14645
|
-
exitLoadRatio,
|
|
14646
|
-
enterMemUsedFrac,
|
|
14647
|
-
exitMemUsedFrac,
|
|
14648
|
-
enterSwapUsedFrac: DEFAULT_OVERLOAD_THRESHOLDS.enterSwapUsedFrac,
|
|
14649
|
-
exitSwapUsedFrac: DEFAULT_OVERLOAD_THRESHOLDS.exitSwapUsedFrac,
|
|
14650
|
-
minReAlertMs: envFloat('BOTMUX_OVERLOAD_MIN_REALERT_MS', DEFAULT_OVERLOAD_THRESHOLDS.minReAlertMs),
|
|
14651
|
-
};
|
|
15414
|
+
// Enter thresholds priority (env > global config > default) + hysteresis-safe
|
|
15415
|
+
// exit derivation live in the pure computeOverloadThresholds() so they can be
|
|
15416
|
+
// unit-tested without importing this side-effectful module. We only gather the
|
|
15417
|
+
// impure inputs (CPU count, persisted config, env strings) here.
|
|
15418
|
+
const cfg = readGlobalConfig().hostOverloadAlert ?? {};
|
|
15419
|
+
return computeOverloadThresholds({
|
|
15420
|
+
cpuCount: cpus().length || 1,
|
|
15421
|
+
configEnterLoadRatio: cfg.enterLoadRatio,
|
|
15422
|
+
configEnterMemUsedFrac: cfg.enterMemUsedFrac,
|
|
15423
|
+
env: {
|
|
15424
|
+
enterLoadRatio: process.env.BOTMUX_OVERLOAD_ENTER_LOAD_RATIO,
|
|
15425
|
+
exitLoadRatio: process.env.BOTMUX_OVERLOAD_EXIT_LOAD_RATIO,
|
|
15426
|
+
enterMemUsedFrac: process.env.BOTMUX_OVERLOAD_ENTER_MEM_FRAC,
|
|
15427
|
+
exitMemUsedFrac: process.env.BOTMUX_OVERLOAD_EXIT_MEM_FRAC,
|
|
15428
|
+
minReAlertMs: process.env.BOTMUX_OVERLOAD_MIN_REALERT_MS,
|
|
15429
|
+
},
|
|
15430
|
+
warn: (message) => logger.warn(message),
|
|
15431
|
+
});
|
|
14652
15432
|
}
|
|
14653
15433
|
/**
|
|
14654
15434
|
* Machine-wide de-dup for the host-overload alert. load/mem are host-wide, so if
|
|
@@ -14763,7 +15543,12 @@ export async function startDaemon(botIndex) {
|
|
|
14763
15543
|
// sandboxReadonlyPaths / readDenyExtraPaths → sandbox + sandboxPaths) BEFORE
|
|
14764
15544
|
// loading, so the parsed configs below already carry the new model. Writes
|
|
14765
15545
|
// new fields, keeps old ones (downgrade = zero-op), backs up once. Idempotent.
|
|
14766
|
-
|
|
15546
|
+
// SKIP in core-only (codex P1-2): this reads + backs-up + rewrites the on-disk
|
|
15547
|
+
// fleet bots.json. A headless core-only service must never touch an ambient
|
|
15548
|
+
// host fleet config — its identity is a synthesized in-memory apiOnly bot.
|
|
15549
|
+
if (process.env.BOTMUX_CORE_ONLY !== '1') {
|
|
15550
|
+
await migrateSandboxConfigAtStartup();
|
|
15551
|
+
}
|
|
14767
15552
|
// Load the assigned bot (one daemon per bot)
|
|
14768
15553
|
let botConfigs = loadBotConfigs();
|
|
14769
15554
|
const idx = botIndex ?? 0;
|
|
@@ -14910,18 +15695,34 @@ export async function startDaemon(botIndex) {
|
|
|
14910
15695
|
// running in a separate node process) so dashboard event bus stays in sync.
|
|
14911
15696
|
scheduleStore.setScheduleScope(cfg.larkAppId);
|
|
14912
15697
|
migrateSharedSchedulesAtStartup(botConfigs.map(b => b.larkAppId), botConfigs[0]?.larkAppId ?? cfg.larkAppId);
|
|
15698
|
+
void migrateOverloadAlertAtStartup(botConfigs.map(b => ({ larkAppId: b.larkAppId, apiOnly: b.apiOnly })));
|
|
14913
15699
|
scheduleStore.startExternalWriteWatcher();
|
|
14914
15700
|
logger.info(`Bot ${idx}/${botConfigs.length}: ${cfg.larkAppId} (cli: ${cfg.cliId})`);
|
|
14915
15701
|
setAskCardDispatcher(createLarkAskCardDispatcher());
|
|
14916
15702
|
// Honour the bot's canTalk gate for `botmux ask` answers: a clicker who may
|
|
14917
15703
|
// address the bot in this chat may answer an implicit-approver ask.
|
|
14918
|
-
|
|
15704
|
+
//
|
|
15705
|
+
// 分派逻辑在 evaluateAskAnswerTalk(生产共用真源):actor 透传时(文字作答路径)
|
|
15706
|
+
// bot → evaluateBotTalk、人 → evaluateTalk 的完整模型(teamMember 走 memberUnionId
|
|
15707
|
+
// 腿);不传 actor(卡片点击路径,飞书 card-action 回调无 sender union/bot 标记)→
|
|
15708
|
+
// 退化为纯 evaluateTalk(openId, chatType),与改动前语义一致。三条分派由
|
|
15709
|
+
// test/ask-answer-talk-dispatch.test.ts 直接咬住。
|
|
15710
|
+
setAskCanTalkChecker((appId, chatId, openId, chatType, actor) => evaluateAskAnswerTalk(appId, chatId, openId, chatType, actor));
|
|
14919
15711
|
writePidFile();
|
|
14920
15712
|
const memoryDiagnostics = startMemoryDiagnostics();
|
|
14921
15713
|
// Publish self-descriptor for the dashboard registry. The dashboard sibling
|
|
14922
15714
|
// process discovers running daemons by scanning <resolvedDataDir>/dashboard-daemons/
|
|
14923
15715
|
// and watching for mtime updates (heartbeat) / file removal (shutdown).
|
|
14924
|
-
|
|
15716
|
+
//
|
|
15717
|
+
// Core-only (in-sandbox, single service): riff's task-runner is handed ONE
|
|
15718
|
+
// fixed port and dials 127.0.0.1:<port> directly, so the port must be exactly
|
|
15719
|
+
// BOTMUX_API_PORT and must NOT drift via the fleet's upward EADDRINUSE probe —
|
|
15720
|
+
// a silent drift would leave the client dialing a dead port. Fleet daemons
|
|
15721
|
+
// keep the ipcBasePort+idx scheme + probe (a port race must not crash boot).
|
|
15722
|
+
const coreOnlyApiPort = process.env.BOTMUX_CORE_ONLY === '1'
|
|
15723
|
+
? Number(process.env.BOTMUX_API_PORT) || 0
|
|
15724
|
+
: 0;
|
|
15725
|
+
const ipcPort = coreOnlyApiPort || config.dashboard.ipcBasePort + idx;
|
|
14925
15726
|
// Worker/CLI descendants use this only to reach the current daemon's
|
|
14926
15727
|
// agent-facing, live-origin-gated endpoints. Internal control endpoints use
|
|
14927
15728
|
// a separate daemon-to-daemon credential and never trust this port marker.
|
|
@@ -14968,55 +15769,61 @@ export async function startDaemon(botIndex) {
|
|
|
14968
15769
|
}
|
|
14969
15770
|
};
|
|
14970
15771
|
setDisplayNameRefresher(refreshBotNameState);
|
|
14971
|
-
//
|
|
14972
|
-
//
|
|
14973
|
-
//
|
|
14974
|
-
//
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
// 是飞书 CDN 上的持久资源,与 /bot/v3/info 的 canonical avatar_url 指向同一张图,
|
|
14999
|
-
// 会一直沿用到下次 daemon 重启时 probeBotOpenId 重新拉取(probe 在 botOpenId
|
|
15000
|
-
// 已知时不会重复请求,进程内没有周期性刷新)。
|
|
15001
|
-
setBotAvatarChanger(async (image) => {
|
|
15002
|
-
const r = await changeBotAvatarOnOpenPlatform(cfg.larkAppId, image, cfg.brand);
|
|
15003
|
-
if (!r.ok)
|
|
15772
|
+
// apiOnly (core-only) bots have no Feishu app to rename / re-avatar. These
|
|
15773
|
+
// handlers drive the open-platform console (browser web-session, NOT
|
|
15774
|
+
// getBotClient — so the bot-level gate can't catch them); skip registering
|
|
15775
|
+
// them entirely so the dashboard profile actions are inert for a core-only bot.
|
|
15776
|
+
if (!cfg.apiOnly) {
|
|
15777
|
+
// 机器人真·改名(dashboard 档案头 ✎):开放平台自动化改飞书应用名并发布新版本
|
|
15778
|
+
// (群内显示名跟随已发布版本,见 services/open-platform-rename.ts)。成功后同步
|
|
15779
|
+
// 内存 botName / bots-info 名册 / descriptor,并清掉冗余的 displayName 别名——
|
|
15780
|
+
// 飞书名已经是新名,保持单一事实来源。
|
|
15781
|
+
setBotRenamer(async (newName) => {
|
|
15782
|
+
const r = await renameBotOnOpenPlatform(cfg.larkAppId, newName, cfg.brand);
|
|
15783
|
+
if (!r.ok)
|
|
15784
|
+
return r;
|
|
15785
|
+
const bot = getBot(cfg.larkAppId);
|
|
15786
|
+
bot.botName = newName;
|
|
15787
|
+
const spec = findConfigField('displayName');
|
|
15788
|
+
if (spec && bot.config.displayName) {
|
|
15789
|
+
// applyConfigField 的 displayName 钩子会顺带触发 refreshBotNameState。
|
|
15790
|
+
await applyConfigField(cfg.larkAppId, spec, null);
|
|
15791
|
+
}
|
|
15792
|
+
else {
|
|
15793
|
+
refreshBotNameState();
|
|
15794
|
+
}
|
|
15795
|
+
try {
|
|
15796
|
+
writeBotInfoFile(config.session.dataDir);
|
|
15797
|
+
}
|
|
15798
|
+
catch { /* best effort */ }
|
|
15004
15799
|
return r;
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15800
|
+
});
|
|
15801
|
+
// 机器人真·改头像(dashboard 档案头头像入口):开放平台自动化换飞书应用头像并
|
|
15802
|
+
// 发布新版本(群内头像跟随已发布版本,同 rename)。成功后用上传返回的 url
|
|
15803
|
+
// 同步内存 botAvatarUrl / descriptor / bots-info 让 dashboard 立即换图——该 url
|
|
15804
|
+
// 是飞书 CDN 上的持久资源,与 /bot/v3/info 的 canonical avatar_url 指向同一张图,
|
|
15805
|
+
// 会一直沿用到下次 daemon 重启时 probeBotOpenId 重新拉取(probe 在 botOpenId
|
|
15806
|
+
// 已知时不会重复请求,进程内没有周期性刷新)。
|
|
15807
|
+
setBotAvatarChanger(async (image) => {
|
|
15808
|
+
const r = await changeBotAvatarOnOpenPlatform(cfg.larkAppId, image, cfg.brand);
|
|
15809
|
+
if (!r.ok)
|
|
15810
|
+
return r;
|
|
15811
|
+
const bot = getBot(cfg.larkAppId);
|
|
15812
|
+
bot.botAvatarUrl = r.avatarUrl;
|
|
15813
|
+
if (desc.botAvatarUrl !== r.avatarUrl) {
|
|
15814
|
+
desc.botAvatarUrl = r.avatarUrl;
|
|
15815
|
+
try {
|
|
15816
|
+
writeDaemonDescriptor(desc);
|
|
15817
|
+
}
|
|
15818
|
+
catch { /* best effort */ }
|
|
15819
|
+
}
|
|
15009
15820
|
try {
|
|
15010
|
-
|
|
15821
|
+
writeBotInfoFile(config.session.dataDir);
|
|
15011
15822
|
}
|
|
15012
15823
|
catch { /* best effort */ }
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
}
|
|
15017
|
-
catch { /* best effort */ }
|
|
15018
|
-
return r;
|
|
15019
|
-
});
|
|
15824
|
+
return r;
|
|
15825
|
+
});
|
|
15826
|
+
} // end !cfg.apiOnly (open-platform rename/avatar handlers)
|
|
15020
15827
|
// One cap implementation shared by event-driven checks (process start / idle
|
|
15021
15828
|
// edge) and the 60s safety-net timer below. Each daemon owns exactly one
|
|
15022
15829
|
// bot's activeSessions map, so the configured limit is per bot.
|
|
@@ -15112,10 +15919,27 @@ export async function startDaemon(botIndex) {
|
|
|
15112
15919
|
// host-only shared secret on every daemon IPC route except the tiny
|
|
15113
15920
|
// capability-gated receiver/readiness apertures in dashboard-ipc-server.
|
|
15114
15921
|
loadOrCreateDashboardSecret(join(homedir(), '.botmux', '.dashboard-secret'));
|
|
15922
|
+
const coreOnly = process.env.BOTMUX_CORE_ONLY === '1';
|
|
15923
|
+
// Core-only keeps the trusted-host HMAC ON (codex P1: authRequired:false opened
|
|
15924
|
+
// ALL 96 IPC routes — a co-resident model turn could read/perturb sessions,
|
|
15925
|
+
// scheduler, mutations). Instead we allowlist ONLY the tight riff-facing routes
|
|
15926
|
+
// (routeIsCoreOnlyPublic: /api/trigger + /api/sessions/:id/{trigger-result,insight}
|
|
15927
|
+
// + the always-public /healthz) as no-HMAC; every other route still requires it.
|
|
15928
|
+
// Arm the readiness gate BEFORE the bind (codex P1): between listen() and a
|
|
15929
|
+
// later arm there'd be a window where the port answers unarmed and a trigger
|
|
15930
|
+
// could slip past the 503 barrier. Armed-first means the very first accepted
|
|
15931
|
+
// connection already sees the not-ready gate.
|
|
15932
|
+
if (coreOnly)
|
|
15933
|
+
armCoreOnlyReadinessGate();
|
|
15115
15934
|
const ipcHandle = await startIpcServer({
|
|
15116
15935
|
port: ipcPort,
|
|
15117
15936
|
host: '127.0.0.1',
|
|
15118
15937
|
authRequired: true,
|
|
15938
|
+
coreOnlyPublicRoutes: coreOnly,
|
|
15939
|
+
// Fleet: probe upward so a port race can't crash boot. Core-only: BIND-OR-FAIL
|
|
15940
|
+
// on the exact BOTMUX_API_PORT — riff was handed that port and the service must
|
|
15941
|
+
// never silently drift to another (client would dial a dead port).
|
|
15942
|
+
...(coreOnly ? { maxProbe: 0 } : {}),
|
|
15119
15943
|
});
|
|
15120
15944
|
// startIpcServer probes upward on EADDRINUSE (e.g. a second botmux instance on
|
|
15121
15945
|
// this host already holds ipcBasePort+idx), so the bound port may differ from
|
|
@@ -15124,19 +15948,30 @@ export async function startDaemon(botIndex) {
|
|
|
15124
15948
|
desc.ipcPort = ipcHandle.port;
|
|
15125
15949
|
process.env.BOTMUX_DAEMON_IPC_PORT = String(ipcHandle.port);
|
|
15126
15950
|
logger.info(`[dashboard-ipc] listening on 127.0.0.1:${ipcHandle.port} (bot ${idx})`);
|
|
15951
|
+
// NOTE: the core-only ready line + readiness release (setCoreOnlyReady) are
|
|
15952
|
+
// emitted LATER, AFTER restoreActiveSessions completes — until then BOTH
|
|
15953
|
+
// /healthz AND the public control routes (trigger/result/insight) return 503,
|
|
15954
|
+
// so riff never triggers into a racing durable restore (codex P1).
|
|
15127
15955
|
// Single reverse-proxy port that fronts every session's web terminal under
|
|
15128
15956
|
// /s/{sessionId}, so dev-machine users forward one port (proxyBasePort+idx)
|
|
15129
15957
|
// instead of one per topic. Bound on the public host so `ssh -L` can reach it.
|
|
15130
15958
|
const proxyPort = config.web.proxyBasePort + idx;
|
|
15959
|
+
// Core-only (codex P1-4): the web terminal proxy + worker web ports default to
|
|
15960
|
+
// config.web.host (0.0.0.0). In riff's sandbox the whole surface must stay
|
|
15961
|
+
// loopback — an in-sandbox headless service has no reason to expose terminals
|
|
15962
|
+
// on all interfaces. Force 127.0.0.1 so nothing but the local task-runner can
|
|
15963
|
+
// reach it (the IPC server is already 127.0.0.1-bound above).
|
|
15964
|
+
const terminalProxyHost = coreOnly ? '127.0.0.1' : config.web.host;
|
|
15131
15965
|
let terminalProxy = null;
|
|
15132
15966
|
try {
|
|
15133
15967
|
terminalProxy = await startTerminalProxy({
|
|
15134
15968
|
port: proxyPort,
|
|
15135
|
-
host:
|
|
15969
|
+
host: terminalProxyHost,
|
|
15136
15970
|
resolvePort: (sessionId) => {
|
|
15137
15971
|
for (const ds of activeSessions.values()) {
|
|
15138
|
-
if (ds.session.sessionId === sessionId && ds.workerPort)
|
|
15972
|
+
if (ds.session.sessionId === sessionId && sessionSupportsWebTerminal(ds) && ds.workerPort) {
|
|
15139
15973
|
return ds.workerPort;
|
|
15974
|
+
}
|
|
15140
15975
|
}
|
|
15141
15976
|
return undefined;
|
|
15142
15977
|
},
|
|
@@ -15154,7 +15989,7 @@ export async function startDaemon(botIndex) {
|
|
|
15154
15989
|
// falls back to the worker's own port so links stay reachable if the port
|
|
15155
15990
|
// was taken (e.g. EADDRINUSE).
|
|
15156
15991
|
setTerminalProxyPort(terminalProxy.port);
|
|
15157
|
-
logger.info(`[terminal-proxy] listening on ${
|
|
15992
|
+
logger.info(`[terminal-proxy] listening on ${terminalProxyHost}:${terminalProxy.port} (bot ${idx}) — session terminals at /s/{sessionId}`);
|
|
15158
15993
|
}
|
|
15159
15994
|
catch (err) {
|
|
15160
15995
|
logger.error(`[terminal-proxy] failed to bind port ${proxyPort} — falling back to direct worker ports for terminal links: ${err.message}`);
|
|
@@ -15201,8 +16036,11 @@ export async function startDaemon(botIndex) {
|
|
|
15201
16036
|
const cfg = bot.config;
|
|
15202
16037
|
// Refresh CLI version per bot's cliId
|
|
15203
16038
|
refreshCliVersion(cfg.cliId, cfg.cliPathOverride);
|
|
15204
|
-
// Resolve allowed users per bot
|
|
15205
|
-
|
|
16039
|
+
// Resolve allowed users per bot. Skipped for apiOnly (core-only) bots:
|
|
16040
|
+
// their HTTP control-API triggers authenticate via the dashboard token, not
|
|
16041
|
+
// allowedUsers, and resolving email/union_id entries would call the Feishu
|
|
16042
|
+
// contact API — a network round-trip a no-Feishu bot must never make.
|
|
16043
|
+
if (!cfg.apiOnly && ((bot.config.allowedUsers?.length ?? 0) > 0 || bot.resolvedAllowedUsers.length > 0)) {
|
|
15206
16044
|
// 含邮箱或 union_id(on_) 都要重解析成本 app 的 open_id —— 否则 canTalk/canOperate
|
|
15207
16045
|
// 拿 sender 的 ou_ 对不上 on_,owner 会被自己的 bot 锁死(PR#72)。
|
|
15208
16046
|
// literal ou_ 也走 best-effort 校验,用于诊断把其他 app 视角 open_id
|
|
@@ -15277,45 +16115,61 @@ export async function startDaemon(botIndex) {
|
|
|
15277
16115
|
catch { /* best effort */ }
|
|
15278
16116
|
}
|
|
15279
16117
|
checkAllowedChatGroupsConfig(bot);
|
|
15280
|
-
//
|
|
15281
|
-
//
|
|
15282
|
-
//
|
|
15283
|
-
//
|
|
15284
|
-
//
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
16118
|
+
// apiOnly (core-only) bots never connect to Feishu: skip the open_id probe,
|
|
16119
|
+
// the required-scope check, and the WSClient event subscription. They are
|
|
16120
|
+
// driven purely over the HTTP control API (trigger → spawn → trigger-result),
|
|
16121
|
+
// whose async path early-returns in deliverFinalOutput before any Feishu send.
|
|
16122
|
+
// Seed a synthetic identity so downstream reads (worker botOpenId, dashboard
|
|
16123
|
+
// roster) get a stable non-undefined value instead of the never-probed one.
|
|
16124
|
+
if (cfg.apiOnly) {
|
|
16125
|
+
bot.botOpenId ||= `bot_${cfg.larkAppId}`;
|
|
16126
|
+
bot.botName ||= cfg.displayName ?? cfg.larkAppId;
|
|
16127
|
+
logger.info(`[api-only] ${cfg.larkAppId} 以 core-only 模式启动:跳过飞书 open_id 探测 / scope 校验 / WSClient 订阅,仅 HTTP 控制 API 驱动`);
|
|
16128
|
+
}
|
|
16129
|
+
else {
|
|
16130
|
+
// Probe bot open_id and persist to bots-info.json. When the friendly
|
|
16131
|
+
// botName comes back from /bot/v3/info, refresh the dashboard descriptor
|
|
16132
|
+
// so the registry shows "Claude" / "Codex" instead of the raw app id.
|
|
16133
|
+
// A custom displayName (bots.json) beats the probed Lark name — the probe
|
|
16134
|
+
// must not overwrite a rename seeded at startup.
|
|
16135
|
+
probeBotOpenId(cfg.larkAppId).then(() => {
|
|
16136
|
+
writeBotInfoFile(config.session.dataDir);
|
|
16137
|
+
const probedName = cfg.displayName ?? bot.botName;
|
|
16138
|
+
const probedAvatar = bot.botAvatarUrl;
|
|
16139
|
+
let descChanged = false;
|
|
16140
|
+
if (probedName && probedName !== desc.botName) {
|
|
16141
|
+
desc.botName = probedName;
|
|
16142
|
+
descChanged = true;
|
|
15301
16143
|
}
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
16144
|
+
if (probedAvatar && probedAvatar !== desc.botAvatarUrl) {
|
|
16145
|
+
desc.botAvatarUrl = probedAvatar;
|
|
16146
|
+
descChanged = true;
|
|
16147
|
+
}
|
|
16148
|
+
if (descChanged) {
|
|
16149
|
+
try {
|
|
16150
|
+
writeDaemonDescriptor(desc);
|
|
16151
|
+
}
|
|
16152
|
+
catch { /* best effort */ }
|
|
16153
|
+
}
|
|
16154
|
+
// SessionRow.botName 同步换成友好名——否则 dashboard 会话行一直显示
|
|
16155
|
+
// 启动时 seed 的 larkAppId(web 端有注册表映射兜底,这里是根因修复)。
|
|
16156
|
+
if (probedName)
|
|
16157
|
+
setBotName(probedName);
|
|
16158
|
+
}).catch(err => {
|
|
16159
|
+
// Probe runs in background and is retried by the periodic heartbeat;
|
|
16160
|
+
// a single failure here is not actionable. Surface as debug only.
|
|
16161
|
+
logger.debug(`[${cfg.larkAppId}] Bot open_id probe failed (will retry): ${err.message}`);
|
|
16162
|
+
});
|
|
16163
|
+
} // end !cfg.apiOnly (open_id probe)
|
|
15313
16164
|
// Required-scope check: 启动后 best-effort 校验
|
|
15314
16165
|
// im:message.group_at_msg.include_bot:readonly。缺失会 logger.error +
|
|
15315
16166
|
// 私信 allowedUsers[0]。校验异步,跑失败不影响 daemon。
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
16167
|
+
// apiOnly 无飞书连接 → 无 scope 概念,跳过。
|
|
16168
|
+
if (!cfg.apiOnly) {
|
|
16169
|
+
checkRequiredScopes(cfg.larkAppId).catch(err => {
|
|
16170
|
+
logger.debug(`[${cfg.larkAppId}] required-scope check failed: ${err?.message ?? err}`);
|
|
16171
|
+
});
|
|
16172
|
+
}
|
|
15319
16173
|
// 主动开工 — 场景①: the bot.added event can't be self-verified via API, and
|
|
15320
16174
|
// if it isn't subscribed the handler simply never fires (no runtime signal).
|
|
15321
16175
|
// Surface a startup breadcrumb whenever the toggle is on so a misconfigured
|
|
@@ -15351,7 +16205,11 @@ export async function startDaemon(botIndex) {
|
|
|
15351
16205
|
};
|
|
15352
16206
|
// 存起来供授权成功后重放消息用(replayGrantedMessage → replayMessageEvent)。
|
|
15353
16207
|
botHandlers.set(cfg.larkAppId, botEventHandlers);
|
|
15354
|
-
|
|
16208
|
+
// apiOnly bots never subscribe to Feishu events → no WSClient. This is the
|
|
16209
|
+
// core decoupling: the daemon serves the HTTP control API only.
|
|
16210
|
+
if (!cfg.apiOnly) {
|
|
16211
|
+
startLarkEventDispatcher(cfg.larkAppId, cfg.larkAppSecret, botEventHandlers, normalizeBrand(cfg.brand));
|
|
16212
|
+
}
|
|
15355
16213
|
// A distillation command is durably prepared before its model run/card
|
|
15356
16214
|
// delivery. Resume active prepared/proposed allocations after a daemon
|
|
15357
16215
|
// crash; deterministic Lark UUIDs suppress duplicate cards in the
|
|
@@ -15453,16 +16311,22 @@ export async function startDaemon(botIndex) {
|
|
|
15453
16311
|
catch (err) {
|
|
15454
16312
|
logger.warn(`[skills] post-restore global sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
15455
16313
|
}
|
|
15456
|
-
//
|
|
15457
|
-
//
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
|
|
16314
|
+
// 文档订阅恢复 + 评论轮询:都是主动调飞书文档 API 的路径。apiOnly(core-only)
|
|
16315
|
+
// bot 从不连飞书,且其合成身份不会有真实文档订阅——整块跳过,否则非 pristine
|
|
16316
|
+
// dataDir 上的遗留订阅会让「无飞书连接」的 bot 每 5 秒主动打飞书。
|
|
16317
|
+
let docCommentPollTimer;
|
|
16318
|
+
if (!cfg.apiOnly) {
|
|
16319
|
+
// 文档订阅恢复:重启后订阅可能已失效,给仍活跃的会话重订阅;会话没恢复
|
|
16320
|
+
// (已关/丢失)的订阅则退订 + 清表,避免「命中订阅但无会话」的孤儿。
|
|
16321
|
+
await restoreDocSubscriptions(activeSessions);
|
|
16322
|
+
// `drive.notice.comment_add_v1` 只可靠推送 @Bot 通知;--all 通过应用身份轮询
|
|
16323
|
+
// 评论列表补齐普通评论。先立即建/续基线,之后每 5 秒增量检查。
|
|
16324
|
+
docCommentPollTimer = setInterval(() => {
|
|
16325
|
+
void pollWatchedDocComments(cfg.larkAppId);
|
|
16326
|
+
}, 5_000);
|
|
16327
|
+
docCommentPollTimer.unref?.();
|
|
15462
16328
|
void pollWatchedDocComments(cfg.larkAppId);
|
|
15463
|
-
}
|
|
15464
|
-
docCommentPollTimer.unref?.();
|
|
15465
|
-
void pollWatchedDocComments(cfg.larkAppId);
|
|
16329
|
+
}
|
|
15466
16330
|
// Sweep orphan sandbox trees left by a previous run's crash/kill: any
|
|
15467
16331
|
// <dataDir>/sandboxes/<sid> whose session is no longer active gets its
|
|
15468
16332
|
// deny-mask mountpoints reclaimed and its dirs removed.
|
|
@@ -15529,8 +16393,12 @@ export async function startDaemon(botIndex) {
|
|
|
15529
16393
|
const maintenanceHeartbeat = setInterval(writeBusyHeartbeat, 15_000);
|
|
15530
16394
|
maintenanceHeartbeat.unref?.();
|
|
15531
16395
|
// Auto-update / auto-restart and the restart-report DM run only on the
|
|
15532
|
-
// primary daemon (bot-0) — a restart is host-wide.
|
|
15533
|
-
|
|
16396
|
+
// primary daemon (bot-0) — a restart is host-wide. NEVER in core-only (codex
|
|
16397
|
+
// P1): a headless in-sandbox service synthesizes its single bot at idx=0, but
|
|
16398
|
+
// it must NOT own host-wide fleet maintenance — auto-update could rewrite the
|
|
16399
|
+
// global botmux install and a detached `botmux restart` would tear down the
|
|
16400
|
+
// real fleet. Core-only manages only its own single process.
|
|
16401
|
+
if (idx === 0 && !coreOnly) {
|
|
15534
16402
|
startMaintenance();
|
|
15535
16403
|
startCliRuntimeUpdateMonitor({
|
|
15536
16404
|
dataDir: config.session.dataDir,
|
|
@@ -15555,34 +16423,33 @@ export async function startDaemon(botIndex) {
|
|
|
15555
16423
|
});
|
|
15556
16424
|
}, 5_000).unref?.();
|
|
15557
16425
|
}
|
|
15558
|
-
// Host-overload watcher.
|
|
15559
|
-
// (dashboard →
|
|
15560
|
-
//
|
|
15561
|
-
//
|
|
15562
|
-
//
|
|
15563
|
-
//
|
|
15564
|
-
//
|
|
15565
|
-
//
|
|
15566
|
-
//
|
|
15567
|
-
//
|
|
16426
|
+
// Host-overload watcher. Machine-level: enabled via the GLOBAL config
|
|
16427
|
+
// `hostOverloadAlert` (dashboard → Global Settings), which names one "notifier
|
|
16428
|
+
// bot". ONLY that bot's own daemon samples + advances the state machine + DMs
|
|
16429
|
+
// (it runs on this host, so no cross-daemon delivery queue is needed) — other
|
|
16430
|
+
// daemons no-op and keep their state cleared. load/mem are host-wide; the
|
|
16431
|
+
// cross-process episode lock is kept purely as a belt-and-suspenders guard
|
|
16432
|
+
// (e.g. a stale duplicate daemon for the same bot). Reads os.loadavg()[2] +
|
|
16433
|
+
// mem every 30s; on a healthy→overloaded or overloaded→healthy edge it DMs the
|
|
16434
|
+
// notifier bot's admin. Hysteresis + a min re-alert window (see
|
|
16435
|
+
// host-overload-alert.ts) keep a load near the line from spamming. Set
|
|
16436
|
+
// BOTMUX_OVERLOAD_ALERT=0 to force the whole feature off regardless of config.
|
|
15568
16437
|
if (process.env.BOTMUX_OVERLOAD_ALERT !== '0') {
|
|
15569
|
-
const overloadThresholds = resolveOverloadThresholds();
|
|
15570
16438
|
let overloadState = INITIAL_OVERLOAD_STATE;
|
|
15571
16439
|
const overloadTimer = setInterval(() => {
|
|
15572
16440
|
void (async () => {
|
|
15573
16441
|
try {
|
|
15574
|
-
//
|
|
15575
|
-
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
}
|
|
15579
|
-
catch {
|
|
15580
|
-
enabled = false;
|
|
15581
|
-
}
|
|
15582
|
-
if (!enabled) {
|
|
16442
|
+
// Global hot switch + target gating: only the selected notifier bot's
|
|
16443
|
+
// daemon samples. When disabled, or the target is another bot / unset,
|
|
16444
|
+
// reset local state so a later re-enable starts clean (no stale edge).
|
|
16445
|
+
const alertCfg = readGlobalConfig().hostOverloadAlert ?? {};
|
|
16446
|
+
if (!isOverloadAlertTarget(alertCfg, { larkAppId: cfg.larkAppId, apiOnly: cfg.apiOnly })) {
|
|
15583
16447
|
overloadState = INITIAL_OVERLOAD_STATE;
|
|
15584
16448
|
return;
|
|
15585
16449
|
}
|
|
16450
|
+
// Re-read thresholds each tick so live config edits (enter load/mem)
|
|
16451
|
+
// take effect on the next sample without a restart.
|
|
16452
|
+
const overloadThresholds = resolveOverloadThresholds();
|
|
15586
16453
|
const reading = {
|
|
15587
16454
|
load15: loadavg()[2] ?? 0,
|
|
15588
16455
|
memTotalBytes: totalmem(),
|
|
@@ -15592,11 +16459,11 @@ export async function startDaemon(botIndex) {
|
|
|
15592
16459
|
overloadState = nextState;
|
|
15593
16460
|
if (!action)
|
|
15594
16461
|
return;
|
|
15595
|
-
//
|
|
15596
|
-
//
|
|
15597
|
-
//
|
|
16462
|
+
// Belt-and-suspenders de-dup: normally only the target daemon reaches
|
|
16463
|
+
// here, but a stale duplicate daemon for the same bot could double-fire.
|
|
16464
|
+
// Claim a short-lived episode lock so the host DMs once per edge.
|
|
15598
16465
|
if (!claimOverloadEpisode(action.kind)) {
|
|
15599
|
-
logger.info(`[overload] ${action.kind} edge already claimed
|
|
16466
|
+
logger.info(`[overload] ${action.kind} edge already claimed; skipping DM (${cfg.larkAppId})`);
|
|
15600
16467
|
return;
|
|
15601
16468
|
}
|
|
15602
16469
|
const ownerOpenId = resolvePrimaryOwnerOpenId(cfg.larkAppId);
|
|
@@ -15713,7 +16580,7 @@ export async function startDaemon(botIndex) {
|
|
|
15713
16580
|
// detach-preserve a "herdr" session whose real pane is tmux (freeze-once).
|
|
15714
16581
|
// undefined (frozen pty, or unresolvable legacy) → non-persistent → killWorker.
|
|
15715
16582
|
if (shutdownBackendDisposition(ds) === 'detach') {
|
|
15716
|
-
// Persistent backends (tmux / herdr / zellij): just kill the worker process —
|
|
16583
|
+
// Persistent backends (tmux / herdr / zellij / zmx): just kill the worker process —
|
|
15717
16584
|
// the multiplexer session survives for re-attach. The worker's SIGTERM
|
|
15718
16585
|
// handler calls backend.kill(), which only DETACHES. Going through
|
|
15719
16586
|
// killWorker() instead would send {type:'close'} → destroySession() →
|
|
@@ -15776,5 +16643,16 @@ export async function startDaemon(botIndex) {
|
|
|
15776
16643
|
flushIdentityCacheSync();
|
|
15777
16644
|
});
|
|
15778
16645
|
logger.info('Daemon is running. Press Ctrl+C to stop.');
|
|
16646
|
+
if (coreOnly) {
|
|
16647
|
+
// Readiness barrier release (codex P1-3): restoreActiveSessions + v3 attach +
|
|
16648
|
+
// scheduler + signal handlers are all wired now, so the HTTP surface is safe
|
|
16649
|
+
// to drive. Flip /healthz → 200 THEN print the machine-parseable ready line.
|
|
16650
|
+
// riff's launcher waits for either signal before pointing its client here, so
|
|
16651
|
+
// a trigger can't race a durable restore (transient not_found / re-fire).
|
|
16652
|
+
// Exact ready-line text is a locked contract (regex ^\[core-only\] listening on ).
|
|
16653
|
+
setCoreOnlyReady();
|
|
16654
|
+
// eslint-disable-next-line no-console
|
|
16655
|
+
console.log(`[core-only] listening on 127.0.0.1:${ipcHandle.port} (bot ${cfg.larkAppId}, cli ${cfg.cliId})`);
|
|
16656
|
+
}
|
|
15779
16657
|
}
|
|
15780
16658
|
//# sourceMappingURL=daemon.js.map
|