agent-tempo 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +213 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/icon-dark-32.png +0 -0
- package/assets/icon-dark-64.png +0 -0
- package/assets/icon-dark.svg +9 -0
- package/assets/icon.svg +9 -0
- package/assets/logo-dark.svg +11 -0
- package/assets/logo-light.svg +11 -0
- package/dashboard/README.md +91 -0
- package/dashboard/dist/assets/index-CB78ToNE.css +2 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js +62 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js.map +1 -0
- package/dashboard/dist/index.html +21 -0
- package/dashboard/package.json +47 -0
- package/dist/activities/hard-terminate.d.ts +32 -0
- package/dist/activities/hard-terminate.js +460 -0
- package/dist/activities/maestro.d.ts +72 -0
- package/dist/activities/maestro.js +254 -0
- package/dist/activities/outbox.d.ts +188 -0
- package/dist/activities/outbox.js +849 -0
- package/dist/activities/resolve.d.ts +64 -0
- package/dist/activities/resolve.js +129 -0
- package/dist/activities/schedule-fire.d.ts +36 -0
- package/dist/activities/schedule-fire.js +147 -0
- package/dist/adapters/base.d.ts +426 -0
- package/dist/adapters/base.js +1270 -0
- package/dist/adapters/claude-api/adapter.d.ts +168 -0
- package/dist/adapters/claude-api/adapter.js +797 -0
- package/dist/adapters/claude-api/api-error.d.ts +96 -0
- package/dist/adapters/claude-api/api-error.js +191 -0
- package/dist/adapters/claude-api/index.d.ts +16 -0
- package/dist/adapters/claude-api/index.js +21 -0
- package/dist/adapters/claude-api/mcp-bridge.d.ts +50 -0
- package/dist/adapters/claude-api/mcp-bridge.js +157 -0
- package/dist/adapters/claude-code/adapter.d.ts +133 -0
- package/dist/adapters/claude-code/adapter.js +274 -0
- package/dist/adapters/claude-code/index.d.ts +15 -0
- package/dist/adapters/claude-code/index.js +20 -0
- package/dist/adapters/claude-code-headless/adapter.d.ts +131 -0
- package/dist/adapters/claude-code-headless/adapter.js +710 -0
- package/dist/adapters/claude-code-headless/error-mapper.d.ts +107 -0
- package/dist/adapters/claude-code-headless/error-mapper.js +281 -0
- package/dist/adapters/claude-code-headless/index.d.ts +17 -0
- package/dist/adapters/claude-code-headless/index.js +26 -0
- package/dist/adapters/claude-code-headless/pre-flight.d.ts +51 -0
- package/dist/adapters/claude-code-headless/pre-flight.js +207 -0
- package/dist/adapters/claude-code-headless/prompt.d.ts +93 -0
- package/dist/adapters/claude-code-headless/prompt.js +79 -0
- package/dist/adapters/claude-code-headless/stream-json.d.ts +242 -0
- package/dist/adapters/claude-code-headless/stream-json.js +208 -0
- package/dist/adapters/claude-code-headless/types.d.ts +28 -0
- package/dist/adapters/claude-code-headless/types.js +36 -0
- package/dist/adapters/copilot/adapter.d.ts +100 -0
- package/dist/adapters/copilot/adapter.js +730 -0
- package/dist/adapters/copilot/index.d.ts +15 -0
- package/dist/adapters/copilot/index.js +20 -0
- package/dist/adapters/index.d.ts +42 -0
- package/dist/adapters/index.js +115 -0
- package/dist/adapters/opencode/adapter.d.ts +82 -0
- package/dist/adapters/opencode/adapter.js +710 -0
- package/dist/adapters/opencode/config.d.ts +90 -0
- package/dist/adapters/opencode/config.js +137 -0
- package/dist/adapters/opencode/helpers.d.ts +40 -0
- package/dist/adapters/opencode/helpers.js +144 -0
- package/dist/adapters/opencode/index.d.ts +12 -0
- package/dist/adapters/opencode/index.js +17 -0
- package/dist/adapters/opencode/server-bridge.d.ts +124 -0
- package/dist/adapters/opencode/server-bridge.js +216 -0
- package/dist/adapters/sdk/base.d.ts +95 -0
- package/dist/adapters/sdk/base.js +134 -0
- package/dist/adapters/sdk/system-prompt.d.ts +64 -0
- package/dist/adapters/sdk/system-prompt.js +78 -0
- package/dist/adapters/terminal-error.d.ts +27 -0
- package/dist/adapters/terminal-error.js +39 -0
- package/dist/channel.d.ts +3 -0
- package/dist/channel.js +48 -0
- package/dist/cli/commands.d.ts +245 -0
- package/dist/cli/commands.js +2438 -0
- package/dist/cli/config-command.d.ts +8 -0
- package/dist/cli/config-command.js +254 -0
- package/dist/cli/daemon-command.d.ts +57 -0
- package/dist/cli/daemon-command.js +493 -0
- package/dist/cli/daemon.d.ts +217 -0
- package/dist/cli/daemon.js +632 -0
- package/dist/cli/dashboard-command.d.ts +20 -0
- package/dist/cli/dashboard-command.js +241 -0
- package/dist/cli/dev-banner.d.ts +107 -0
- package/dist/cli/dev-banner.js +190 -0
- package/dist/cli/dev-mode-bootstrap.d.ts +29 -0
- package/dist/cli/dev-mode-bootstrap.js +36 -0
- package/dist/cli/dev-verbs.d.ts +43 -0
- package/dist/cli/dev-verbs.js +254 -0
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +158 -0
- package/dist/cli/legacy-migration.d.ts +35 -0
- package/dist/cli/legacy-migration.js +335 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +63 -0
- package/dist/cli/output.d.ts +12 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/preflight.d.ts +9 -0
- package/dist/cli/preflight.js +96 -0
- package/dist/cli/removed-verbs.d.ts +9 -0
- package/dist/cli/removed-verbs.js +78 -0
- package/dist/cli/sa-preflight.d.ts +99 -0
- package/dist/cli/sa-preflight.js +183 -0
- package/dist/cli/scenarios-command.d.ts +6 -0
- package/dist/cli/scenarios-command.js +167 -0
- package/dist/cli/startup.d.ts +112 -0
- package/dist/cli/startup.js +641 -0
- package/dist/cli/upgrade-command.d.ts +5 -0
- package/dist/cli/upgrade-command.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +680 -0
- package/dist/client/core.d.ts +33 -0
- package/dist/client/core.js +1260 -0
- package/dist/client/ensure-conductor-spawned.d.ts +35 -0
- package/dist/client/ensure-conductor-spawned.js +48 -0
- package/dist/client/index.d.ts +32 -0
- package/dist/client/index.js +22 -0
- package/dist/client/interface.d.ts +461 -0
- package/dist/client/interface.js +2 -0
- package/dist/client/subscribe.d.ts +108 -0
- package/dist/client/subscribe.js +598 -0
- package/dist/client/with-spawn.d.ts +27 -0
- package/dist/client/with-spawn.js +87 -0
- package/dist/config.d.ts +323 -0
- package/dist/config.js +593 -0
- package/dist/connection.d.ts +7 -0
- package/dist/connection.js +46 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.js +74 -0
- package/dist/copilot-bridge.d.ts +22 -0
- package/dist/copilot-bridge.js +565 -0
- package/dist/daemon-adapter-versions.d.ts +52 -0
- package/dist/daemon-adapter-versions.js +170 -0
- package/dist/daemon.d.ts +275 -0
- package/dist/daemon.js +989 -0
- package/dist/ensemble/agent-types.d.ts +23 -0
- package/dist/ensemble/agent-types.js +132 -0
- package/dist/ensemble/loader.d.ts +14 -0
- package/dist/ensemble/loader.js +140 -0
- package/dist/ensemble/saver.d.ts +49 -0
- package/dist/ensemble/saver.js +201 -0
- package/dist/ensemble/schema.d.ts +71 -0
- package/dist/ensemble/schema.js +3 -0
- package/dist/git-info.d.ts +4 -0
- package/dist/git-info.js +29 -0
- package/dist/http/aggregate.d.ts +319 -0
- package/dist/http/aggregate.js +684 -0
- package/dist/http/auth.d.ts +67 -0
- package/dist/http/auth.js +177 -0
- package/dist/http/body.d.ts +71 -0
- package/dist/http/body.js +121 -0
- package/dist/http/catalog.d.ts +67 -0
- package/dist/http/catalog.js +209 -0
- package/dist/http/cors.d.ts +42 -0
- package/dist/http/cors.js +111 -0
- package/dist/http/dashboard-pair.d.ts +94 -0
- package/dist/http/dashboard-pair.js +148 -0
- package/dist/http/dashboard.d.ts +20 -0
- package/dist/http/dashboard.js +160 -0
- package/dist/http/event-bus.d.ts +217 -0
- package/dist/http/event-bus.js +365 -0
- package/dist/http/event-id.d.ts +77 -0
- package/dist/http/event-id.js +117 -0
- package/dist/http/event-types.d.ts +348 -0
- package/dist/http/event-types.js +36 -0
- package/dist/http/fixtures/chat-stress.d.ts +8 -0
- package/dist/http/fixtures/chat-stress.js +63 -0
- package/dist/http/fixtures/conductor-leaving.d.ts +8 -0
- package/dist/http/fixtures/conductor-leaving.js +80 -0
- package/dist/http/fixtures/constants.d.ts +10 -0
- package/dist/http/fixtures/constants.js +13 -0
- package/dist/http/fixtures/eight-player-broadcast.d.ts +10 -0
- package/dist/http/fixtures/eight-player-broadcast.js +81 -0
- package/dist/http/fixtures/empty-ensemble.d.ts +6 -0
- package/dist/http/fixtures/empty-ensemble.js +26 -0
- package/dist/http/fixtures/index.d.ts +55 -0
- package/dist/http/fixtures/index.js +110 -0
- package/dist/http/fixtures/single-conductor.d.ts +7 -0
- package/dist/http/fixtures/single-conductor.js +46 -0
- package/dist/http/fixtures/sse-reconnect.d.ts +8 -0
- package/dist/http/fixtures/sse-reconnect.js +77 -0
- package/dist/http/index.d.ts +21 -0
- package/dist/http/index.js +61 -0
- package/dist/http/port-file.d.ts +22 -0
- package/dist/http/port-file.js +132 -0
- package/dist/http/responses.d.ts +27 -0
- package/dist/http/responses.js +40 -0
- package/dist/http/ring-buffer.d.ts +41 -0
- package/dist/http/ring-buffer.js +80 -0
- package/dist/http/server.d.ts +122 -0
- package/dist/http/server.js +459 -0
- package/dist/http/snapshot.d.ts +85 -0
- package/dist/http/snapshot.js +180 -0
- package/dist/http/sse-handler.d.ts +87 -0
- package/dist/http/sse-handler.js +294 -0
- package/dist/http/writes.d.ts +55 -0
- package/dist/http/writes.js +240 -0
- package/dist/palette/index.d.ts +138 -0
- package/dist/palette/index.js +221 -0
- package/dist/reconcile/orphans.d.ts +255 -0
- package/dist/reconcile/orphans.js +340 -0
- package/dist/scripts/258-spotcheck.js +303 -0
- package/dist/scripts/check-components-css-sync.js +199 -0
- package/dist/scripts/run-shard.js +121 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +128 -0
- package/dist/server-tools.d.ts +87 -0
- package/dist/server-tools.js +146 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +366 -0
- package/dist/spawn.d.ts +296 -0
- package/dist/spawn.js +747 -0
- package/dist/tools/agent-types.d.ts +2 -0
- package/dist/tools/agent-types.js +21 -0
- package/dist/tools/attachment-info.d.ts +4 -0
- package/dist/tools/attachment-info.js +48 -0
- package/dist/tools/broadcast.d.ts +4 -0
- package/dist/tools/broadcast.js +76 -0
- package/dist/tools/cancel-stage.d.ts +3 -0
- package/dist/tools/cancel-stage.js +20 -0
- package/dist/tools/clear-state.d.ts +3 -0
- package/dist/tools/clear-state.js +37 -0
- package/dist/tools/coat-check-evict.d.ts +4 -0
- package/dist/tools/coat-check-evict.js +43 -0
- package/dist/tools/coat-check-get.d.ts +4 -0
- package/dist/tools/coat-check-get.js +56 -0
- package/dist/tools/coat-check-list.d.ts +4 -0
- package/dist/tools/coat-check-list.js +60 -0
- package/dist/tools/coat-check-put.d.ts +4 -0
- package/dist/tools/coat-check-put.js +53 -0
- package/dist/tools/cue.d.ts +44 -0
- package/dist/tools/cue.js +201 -0
- package/dist/tools/destroy.d.ts +4 -0
- package/dist/tools/destroy.js +188 -0
- package/dist/tools/detach.d.ts +4 -0
- package/dist/tools/detach.js +45 -0
- package/dist/tools/encore.d.ts +4 -0
- package/dist/tools/encore.js +31 -0
- package/dist/tools/ensemble.d.ts +32 -0
- package/dist/tools/ensemble.js +198 -0
- package/dist/tools/evaluate-gate.d.ts +3 -0
- package/dist/tools/evaluate-gate.js +32 -0
- package/dist/tools/fetch-state.d.ts +13 -0
- package/dist/tools/fetch-state.js +78 -0
- package/dist/tools/gates.d.ts +3 -0
- package/dist/tools/gates.js +41 -0
- package/dist/tools/helpers.d.ts +21 -0
- package/dist/tools/helpers.js +25 -0
- package/dist/tools/hosts.d.ts +4 -0
- package/dist/tools/hosts.js +40 -0
- package/dist/tools/listen.d.ts +3 -0
- package/dist/tools/listen.js +22 -0
- package/dist/tools/load-lineup.d.ts +5 -0
- package/dist/tools/load-lineup.js +381 -0
- package/dist/tools/migrate.d.ts +4 -0
- package/dist/tools/migrate.js +60 -0
- package/dist/tools/pause-ensemble.d.ts +4 -0
- package/dist/tools/pause-ensemble.js +58 -0
- package/dist/tools/pause.d.ts +4 -0
- package/dist/tools/pause.js +36 -0
- package/dist/tools/play.d.ts +4 -0
- package/dist/tools/play.js +57 -0
- package/dist/tools/quality-gate.d.ts +3 -0
- package/dist/tools/quality-gate.js +26 -0
- package/dist/tools/recall.d.ts +3 -0
- package/dist/tools/recall.js +32 -0
- package/dist/tools/recruit.d.ts +38 -0
- package/dist/tools/recruit.js +447 -0
- package/dist/tools/release.d.ts +4 -0
- package/dist/tools/release.js +98 -0
- package/dist/tools/report.d.ts +3 -0
- package/dist/tools/report.js +29 -0
- package/dist/tools/resolve.d.ts +1 -0
- package/dist/tools/resolve.js +7 -0
- package/dist/tools/restart.d.ts +35 -0
- package/dist/tools/restart.js +131 -0
- package/dist/tools/restore.d.ts +4 -0
- package/dist/tools/restore.js +107 -0
- package/dist/tools/resume-ensemble.d.ts +4 -0
- package/dist/tools/resume-ensemble.js +79 -0
- package/dist/tools/save-lineup.d.ts +4 -0
- package/dist/tools/save-lineup.js +36 -0
- package/dist/tools/save-state.d.ts +3 -0
- package/dist/tools/save-state.js +57 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.js +152 -0
- package/dist/tools/schedules.d.ts +4 -0
- package/dist/tools/schedules.js +54 -0
- package/dist/tools/set-ensemble-description.d.ts +4 -0
- package/dist/tools/set-ensemble-description.js +37 -0
- package/dist/tools/set-name.d.ts +4 -0
- package/dist/tools/set-name.js +45 -0
- package/dist/tools/set-part.d.ts +3 -0
- package/dist/tools/set-part.js +20 -0
- package/dist/tools/shutdown.d.ts +4 -0
- package/dist/tools/shutdown.js +54 -0
- package/dist/tools/stage.d.ts +3 -0
- package/dist/tools/stage.js +28 -0
- package/dist/tools/stages.d.ts +3 -0
- package/dist/tools/stages.js +35 -0
- package/dist/tools/stop.d.ts +4 -0
- package/dist/tools/stop.js +29 -0
- package/dist/tools/unschedule.d.ts +4 -0
- package/dist/tools/unschedule.js +35 -0
- package/dist/tools/who-am-i.d.ts +3 -0
- package/dist/tools/who-am-i.js +34 -0
- package/dist/tools/worktree.d.ts +4 -0
- package/dist/tools/worktree.js +181 -0
- package/dist/tui/App.d.ts +85 -0
- package/dist/tui/App.js +1791 -0
- package/dist/tui/bootstrap-types.d.ts +46 -0
- package/dist/tui/bootstrap-types.js +7 -0
- package/dist/tui/client.d.ts +6 -0
- package/dist/tui/client.js +9 -0
- package/dist/tui/commands.d.ts +71 -0
- package/dist/tui/commands.js +1375 -0
- package/dist/tui/components/ActivityLog.d.ts +16 -0
- package/dist/tui/components/ActivityLog.js +36 -0
- package/dist/tui/components/ChatView.d.ts +35 -0
- package/dist/tui/components/ChatView.js +54 -0
- package/dist/tui/components/CommandOverlay.d.ts +15 -0
- package/dist/tui/components/CommandOverlay.js +34 -0
- package/dist/tui/components/CommandPalette.d.ts +21 -0
- package/dist/tui/components/CommandPalette.js +67 -0
- package/dist/tui/components/ConductorChat.d.ts +16 -0
- package/dist/tui/components/ConductorChat.js +32 -0
- package/dist/tui/components/ConversationStream.d.ts +114 -0
- package/dist/tui/components/ConversationStream.js +307 -0
- package/dist/tui/components/CreateEnsembleWizard.d.ts +19 -0
- package/dist/tui/components/CreateEnsembleWizard.js +223 -0
- package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
- package/dist/tui/components/DestroyConfirmModal.js +62 -0
- package/dist/tui/components/EnsembleListView.d.ts +14 -0
- package/dist/tui/components/EnsembleListView.js +32 -0
- package/dist/tui/components/EnsemblePanel.d.ts +12 -0
- package/dist/tui/components/EnsemblePanel.js +40 -0
- package/dist/tui/components/ErrorView.d.ts +31 -0
- package/dist/tui/components/ErrorView.js +129 -0
- package/dist/tui/components/HomeView.d.ts +54 -0
- package/dist/tui/components/HomeView.js +306 -0
- package/dist/tui/components/InputBar.d.ts +13 -0
- package/dist/tui/components/InputBar.js +58 -0
- package/dist/tui/components/LoadLineupModal.d.ts +18 -0
- package/dist/tui/components/LoadLineupModal.js +79 -0
- package/dist/tui/components/MainView.d.ts +21 -0
- package/dist/tui/components/MainView.js +107 -0
- package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
- package/dist/tui/components/NewEnsembleModal.js +73 -0
- package/dist/tui/components/Picker.d.ts +23 -0
- package/dist/tui/components/Picker.js +70 -0
- package/dist/tui/components/PlayerDetailView.d.ts +26 -0
- package/dist/tui/components/PlayerDetailView.js +118 -0
- package/dist/tui/components/PromptArea.d.ts +50 -0
- package/dist/tui/components/PromptArea.js +303 -0
- package/dist/tui/components/RecruitWizard.d.ts +17 -0
- package/dist/tui/components/RecruitWizard.js +221 -0
- package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
- package/dist/tui/components/RestoreConfirmModal.js +71 -0
- package/dist/tui/components/ScheduleOverlay.d.ts +13 -0
- package/dist/tui/components/ScheduleOverlay.js +113 -0
- package/dist/tui/components/ScheduleWizard.d.ts +19 -0
- package/dist/tui/components/ScheduleWizard.js +259 -0
- package/dist/tui/components/Splash.d.ts +23 -0
- package/dist/tui/components/Splash.js +221 -0
- package/dist/tui/components/StatusBar.d.ts +48 -0
- package/dist/tui/components/StatusBar.js +128 -0
- package/dist/tui/components/StatusOverlay.d.ts +15 -0
- package/dist/tui/components/StatusOverlay.js +76 -0
- package/dist/tui/components/TitleBar.d.ts +10 -0
- package/dist/tui/components/TitleBar.js +21 -0
- package/dist/tui/components/TopBar.d.ts +12 -0
- package/dist/tui/components/TopBar.js +15 -0
- package/dist/tui/core-api.d.ts +26 -0
- package/dist/tui/core-api.js +67 -0
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +3 -0
- package/dist/tui/hooks/useEnsembleDiscovery.js +30 -0
- package/dist/tui/hooks/useMaestroPoller.d.ts +3 -0
- package/dist/tui/hooks/useMaestroPoller.js +36 -0
- package/dist/tui/hooks/useSendCommand.d.ts +7 -0
- package/dist/tui/hooks/useSendCommand.js +29 -0
- package/dist/tui/index.d.ts +15 -0
- package/dist/tui/index.js +156 -0
- package/dist/tui/ink-context.d.ts +18 -0
- package/dist/tui/ink-context.js +59 -0
- package/dist/tui/ink-loader.d.ts +26 -0
- package/dist/tui/ink-loader.js +42 -0
- package/dist/tui/removed-commands.d.ts +9 -0
- package/dist/tui/removed-commands.js +22 -0
- package/dist/tui/sse-handler.d.ts +52 -0
- package/dist/tui/sse-handler.js +157 -0
- package/dist/tui/store.d.ts +598 -0
- package/dist/tui/store.js +753 -0
- package/dist/tui/utils/format.d.ts +56 -0
- package/dist/tui/utils/format.js +155 -0
- package/dist/tui/utils/fullscreen.d.ts +23 -0
- package/dist/tui/utils/fullscreen.js +71 -0
- package/dist/tui/utils/history.d.ts +10 -0
- package/dist/tui/utils/history.js +85 -0
- package/dist/tui/utils/platform.d.ts +45 -0
- package/dist/tui/utils/platform.js +258 -0
- package/dist/tui/utils/theme.d.ts +21 -0
- package/dist/tui/utils/theme.js +24 -0
- package/dist/types.d.ts +1020 -0
- package/dist/types.js +39 -0
- package/dist/utils/attachment-format.d.ts +22 -0
- package/dist/utils/attachment-format.js +32 -0
- package/dist/utils/default-part.d.ts +43 -0
- package/dist/utils/default-part.js +104 -0
- package/dist/utils/duration.d.ts +30 -0
- package/dist/utils/duration.js +69 -0
- package/dist/utils/ensemble-ops.d.ts +61 -0
- package/dist/utils/ensemble-ops.js +77 -0
- package/dist/utils/format-hosts.d.ts +21 -0
- package/dist/utils/format-hosts.js +73 -0
- package/dist/utils/hosts.d.ts +113 -0
- package/dist/utils/hosts.js +265 -0
- package/dist/utils/parent-death-watchdog.d.ts +1 -0
- package/dist/utils/parent-death-watchdog.js +47 -0
- package/dist/utils/query-timeout.d.ts +103 -0
- package/dist/utils/query-timeout.js +113 -0
- package/dist/utils/recall-format.d.ts +78 -0
- package/dist/utils/recall-format.js +105 -0
- package/dist/utils/restore-format.d.ts +49 -0
- package/dist/utils/restore-format.js +91 -0
- package/dist/utils/safe-path.d.ts +10 -0
- package/dist/utils/safe-path.js +43 -0
- package/dist/utils/sdk-probe.d.ts +9 -0
- package/dist/utils/sdk-probe.js +45 -0
- package/dist/utils/search-attributes.d.ts +76 -0
- package/dist/utils/search-attributes.js +86 -0
- package/dist/utils/validation.d.ts +113 -0
- package/dist/utils/validation.js +163 -0
- package/dist/utils/visibility-deadline.d.ts +186 -0
- package/dist/utils/visibility-deadline.js +158 -0
- package/dist/utils/worktree.d.ts +103 -0
- package/dist/utils/worktree.js +327 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +146 -0
- package/dist/workflows/attachment-math.d.ts +56 -0
- package/dist/workflows/attachment-math.js +47 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +11 -0
- package/dist/workflows/maestro-signals.d.ts +217 -0
- package/dist/workflows/maestro-signals.js +155 -0
- package/dist/workflows/maestro.d.ts +3 -0
- package/dist/workflows/maestro.js +812 -0
- package/dist/workflows/scheduler-signals.d.ts +10 -0
- package/dist/workflows/scheduler-signals.js +14 -0
- package/dist/workflows/scheduler.d.ts +17 -0
- package/dist/workflows/scheduler.js +143 -0
- package/dist/workflows/session.d.ts +2 -0
- package/dist/workflows/session.js +1638 -0
- package/dist/workflows/signals.d.ts +297 -0
- package/dist/workflows/signals.js +239 -0
- package/examples/agents/tempo-composer.md +56 -0
- package/examples/agents/tempo-conductor.md +117 -0
- package/examples/agents/tempo-critic.md +73 -0
- package/examples/agents/tempo-improv.md +74 -0
- package/examples/agents/tempo-liner.md +75 -0
- package/examples/agents/tempo-roadie.md +61 -0
- package/examples/agents/tempo-soloist.md +71 -0
- package/examples/agents/tempo-tuner.md +94 -0
- package/examples/ensembles/tempo-big-band.yaml +146 -0
- package/examples/ensembles/tempo-dev-team.yaml +58 -0
- package/examples/ensembles/tempo-headless-jam.yaml +77 -0
- package/examples/ensembles/tempo-jam-session.yaml +41 -0
- package/examples/ensembles/tempo-mock-jam.yaml +79 -0
- package/examples/ensembles/tempo-review-squad.yaml +32 -0
- package/package.json +172 -0
- package/packaging/launchd/com.agent.tempo.plist +46 -0
- package/packaging/systemd/agent-tempo.service +32 -0
- package/packaging/windows/install-task.ps1 +71 -0
- package/scenarios/conductor-recruit-mock.yaml +33 -0
- package/scenarios/echo-roundtrip.yaml +15 -0
- package/scenarios/multi-player-handoff.yaml +38 -0
- package/scenarios/recruit-cascade.yaml +38 -0
- package/scenarios/two-player-conversation.yaml +33 -0
- package/workflow-bundle.js +14146 -0
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createTempoClientCore = createTempoClientCore;
|
|
37
|
+
/**
|
|
38
|
+
* `TempoClientCore` — pure-RPC factory implementation.
|
|
39
|
+
*
|
|
40
|
+
* Every method here goes through the Temporal `Client`; none shell out to
|
|
41
|
+
* a local terminal. Safe to instantiate from the daemon, MCP server,
|
|
42
|
+
* future SSE event source, and any external SDK consumer that wants a
|
|
43
|
+
* headless surface (no `child_process` dependency).
|
|
44
|
+
*
|
|
45
|
+
* The two spawn methods (`createEnsemble`, `spawnConductor`) and the
|
|
46
|
+
* `runTempoCli` helper live in `./with-spawn.ts`, which composes this
|
|
47
|
+
* factory and adds the TTY-bound surface.
|
|
48
|
+
*
|
|
49
|
+
* See `docs/adr/0007-tempoclient-core-withspawn-split.md` and
|
|
50
|
+
* `docs/design/tempoclient-core-spawn-split.md`.
|
|
51
|
+
*/
|
|
52
|
+
const os_1 = require("os");
|
|
53
|
+
const client_1 = require("@temporalio/client");
|
|
54
|
+
const config_1 = require("../config");
|
|
55
|
+
const signals_1 = require("../workflows/signals");
|
|
56
|
+
const maestro_signals_1 = require("../workflows/maestro-signals");
|
|
57
|
+
const resolve_1 = require("../activities/resolve");
|
|
58
|
+
const orphans_1 = require("../reconcile/orphans");
|
|
59
|
+
const query_timeout_1 = require("../utils/query-timeout");
|
|
60
|
+
const visibility_deadline_1 = require("../utils/visibility-deadline");
|
|
61
|
+
const ensemble_ops_1 = require("../utils/ensemble-ops");
|
|
62
|
+
const search_attributes_1 = require("../utils/search-attributes");
|
|
63
|
+
const subscribe_1 = require("./subscribe");
|
|
64
|
+
// ── Helpers (module-private; shared with `with-spawn.ts` if needed via re-export) ──
|
|
65
|
+
/** Escape a value for use in Temporal visibility query strings.
|
|
66
|
+
* Strips characters that could break or inject into the query. */
|
|
67
|
+
function sanitizeQueryValue(value) {
|
|
68
|
+
return value.replace(/["\\\n\r]/g, '');
|
|
69
|
+
}
|
|
70
|
+
/** Shared unknown-error → string helper for summary `error` fields. */
|
|
71
|
+
function errMsg(err) {
|
|
72
|
+
return err instanceof Error ? err.message : String(err);
|
|
73
|
+
}
|
|
74
|
+
// ── Factory ──
|
|
75
|
+
/**
|
|
76
|
+
* Build a `TempoClientCore` over a configured Temporal `Client`. Headless
|
|
77
|
+
* callers (daemon, MCP tools, SSE event source) use this directly; TTY
|
|
78
|
+
* callers go through {@link createTempoClientWithSpawn} from
|
|
79
|
+
* `./with-spawn.ts`.
|
|
80
|
+
*
|
|
81
|
+
* `opts.subscribeDeps` is forwarded to the SSE subscribe wrapper so
|
|
82
|
+
* tests/non-default environments can override `baseUrl`, `token`,
|
|
83
|
+
* `fetchImpl`, or `sleep` without monkey-patching globals.
|
|
84
|
+
*/
|
|
85
|
+
function createTempoClientCore(client, opts = {}) {
|
|
86
|
+
const globalMaestroId = config_1.GLOBAL_MAESTRO_WORKFLOW_ID;
|
|
87
|
+
const subscribe = (0, subscribe_1.createSubscribe)(opts.subscribeDeps);
|
|
88
|
+
// Closed over by `listHosts` below — see #437. Daemon HTTP/MCP/aggregate
|
|
89
|
+
// construction sites must pass `taskQueue: config.taskQueue` for dev-mode
|
|
90
|
+
// host discovery to find the right pollers.
|
|
91
|
+
const taskQueue = opts.taskQueue;
|
|
92
|
+
/** Helper: get a workflow handle by ID. */
|
|
93
|
+
function handle(workflowId) {
|
|
94
|
+
return client.workflow.getHandle(workflowId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Shared between `listEnsembles()` and `listEnsemblesBounded()` (#336/#529).
|
|
98
|
+
* Given the per-ensemble aggregation map produced by the visibility-list
|
|
99
|
+
* loop, fans out the per-ensemble maestro `maestroPaused` query and
|
|
100
|
+
* builds the final `EnsembleSummary[]`.
|
|
101
|
+
*
|
|
102
|
+
* Kept as a closure so it inherits `handle` from the factory scope (and
|
|
103
|
+
* the `queryHandleWithTimeout` + `maestroPausedQuery` bindings from
|
|
104
|
+
* module scope) without threading them through as args.
|
|
105
|
+
*/
|
|
106
|
+
async function finishEnsembleSummaries(byEnsemble) {
|
|
107
|
+
// Per-ensemble paused lookup: `/pause` and `/shutdown` both flip
|
|
108
|
+
// `maestroSetPausedSignal` on the maestro hub workflow. The hub's
|
|
109
|
+
// `maestroPaused` query is the authoritative "ensemble is paused"
|
|
110
|
+
// signal — fall back to the phase heuristic when the hub doesn't
|
|
111
|
+
// exist (bare ensemble before any conductor / TUI was attached).
|
|
112
|
+
const pausedByEnsemble = new Map();
|
|
113
|
+
await Promise.all([...byEnsemble.keys()].map(async (name) => {
|
|
114
|
+
try {
|
|
115
|
+
// Issue #433 — bound per-ensemble maestro query so a wedged
|
|
116
|
+
// maestro can't hang `listEnsembles` (the snapshot existence
|
|
117
|
+
// gate at snapshot.ts:144). Existing catch maps any failure
|
|
118
|
+
// to "leave paused undefined" and the downstream phase
|
|
119
|
+
// heuristic classifies the ensemble.
|
|
120
|
+
const paused = await (0, query_timeout_1.queryHandleWithTimeout)(handle((0, config_1.maestroWorkflowId)(name)), maestro_signals_1.maestroPausedQuery);
|
|
121
|
+
pausedByEnsemble.set(name, !!paused);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Hub workflow not running, or worker wedged (#433) — leave
|
|
125
|
+
// undefined so the phase heuristic below decides
|
|
126
|
+
// classification.
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const [name, info] of byEnsemble) {
|
|
131
|
+
// Skip ensembles with no non-gone sessions — they're either
|
|
132
|
+
// terminating or fully destroyed.
|
|
133
|
+
if (info.liveAdapterCount === 0 && !info.hasDetached)
|
|
134
|
+
continue;
|
|
135
|
+
const paused = pausedByEnsemble.get(name);
|
|
136
|
+
// Three-state classification:
|
|
137
|
+
// online — hub unpaused (or no hub + at least one live adapter).
|
|
138
|
+
// paused — hub paused AND at least one live adapter remains
|
|
139
|
+
// (`/pause` semantics: resume in place via `/play`).
|
|
140
|
+
// offline — hub paused AND zero live adapters
|
|
141
|
+
// (`/shutdown` semantics: requires `/restore`).
|
|
142
|
+
// When the hub didn't answer (no maestro yet), fall back to the
|
|
143
|
+
// phase heuristic — a live adapter implies online.
|
|
144
|
+
let state;
|
|
145
|
+
if (paused === true) {
|
|
146
|
+
state = info.liveAdapterCount > 0 ? 'paused' : 'offline';
|
|
147
|
+
}
|
|
148
|
+
else if (paused === false) {
|
|
149
|
+
state = 'online';
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
state = info.liveAdapterCount > 0 ? 'online' : 'offline';
|
|
153
|
+
}
|
|
154
|
+
out.push({
|
|
155
|
+
name,
|
|
156
|
+
playerCount: info.count,
|
|
157
|
+
hasConductor: info.hasConductor,
|
|
158
|
+
conductorStatus: info.conductorStatus,
|
|
159
|
+
state,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
subscribe,
|
|
166
|
+
async discoverEnsembles() {
|
|
167
|
+
// Strategy 1: Global Maestro playersByEnsemble query
|
|
168
|
+
try {
|
|
169
|
+
const h = handle(globalMaestroId);
|
|
170
|
+
// #433: unbounded — justified because `discoverEnsembles` is not
|
|
171
|
+
// reachable from `buildEnsembleSnapshot` (it's a CLI / TUI
|
|
172
|
+
// discovery surface, separate from the snapshot existence gate
|
|
173
|
+
// which uses `listEnsembles`). A hung global maestro here only
|
|
174
|
+
// affects the CLI lister, which has its own user-facing
|
|
175
|
+
// cancellability via Ctrl-C.
|
|
176
|
+
const byEnsemble = await h.query('maestroPlayersByEnsemble');
|
|
177
|
+
const results = Object.entries(byEnsemble).map(([name, players]) => {
|
|
178
|
+
const conductor = players.find(p => p.isConductor);
|
|
179
|
+
return {
|
|
180
|
+
name,
|
|
181
|
+
playerCount: players.length,
|
|
182
|
+
hasConductor: !!conductor,
|
|
183
|
+
// `conductorStatus` is a public TempoClient API field (EnsembleInfo);
|
|
184
|
+
// its value now carries the attachment-phase string (post-#176 drift).
|
|
185
|
+
conductorStatus: conductor?.phase,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
// Only trust Maestro if it has discovered ensembles; fall through to
|
|
189
|
+
// Strategy 2 when empty — the Maestro may not have refreshed yet.
|
|
190
|
+
if (results.length > 0)
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Global Maestro not available — fall through
|
|
195
|
+
}
|
|
196
|
+
// Strategy 2: Direct workflow list scan
|
|
197
|
+
try {
|
|
198
|
+
const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
|
|
199
|
+
const ensembleMap = new Map();
|
|
200
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
201
|
+
const name = (0, search_attributes_1.getEnsembleName)(wf);
|
|
202
|
+
if (!name)
|
|
203
|
+
continue;
|
|
204
|
+
const entry = ensembleMap.get(name) || { count: 0, hasConductor: false };
|
|
205
|
+
entry.count++;
|
|
206
|
+
// Preferred: AgentTempoIsConductor search attribute (canonical, queryable).
|
|
207
|
+
// Fallback: workflow ID convention — covers the brief window after a
|
|
208
|
+
// conductor spawn before the search attribute is indexed.
|
|
209
|
+
const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
|
|
210
|
+
const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
|
|
211
|
+
if (isConductorFromSA || isConductorFromId) {
|
|
212
|
+
entry.hasConductor = true;
|
|
213
|
+
// Post-#175 the workflow writes `AgentTempoAttachmentState` (phase) in
|
|
214
|
+
// place of the removed `AgentTempoStatus` search attribute.
|
|
215
|
+
entry.conductorStatus = (0, search_attributes_1.getAttachmentPhase)(wf);
|
|
216
|
+
}
|
|
217
|
+
ensembleMap.set(name, entry);
|
|
218
|
+
}
|
|
219
|
+
return [...ensembleMap.entries()].map(([name, info]) => ({
|
|
220
|
+
name,
|
|
221
|
+
playerCount: info.count,
|
|
222
|
+
hasConductor: info.hasConductor,
|
|
223
|
+
conductorStatus: info.conductorStatus,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async listEnsembles() {
|
|
231
|
+
// Direct workflow-list scan — the Global Maestro index only tracks
|
|
232
|
+
// live ensembles, so classifying paused/offline ensembles requires
|
|
233
|
+
// reading the attachment-state search attribute per workflow.
|
|
234
|
+
//
|
|
235
|
+
// `liveAdapterCount` distinguishes `paused` (≥1 live adapter, can
|
|
236
|
+
// resume in place via `/play`) from `offline` (zero live adapters,
|
|
237
|
+
// requires `/restore`). The maestro session is excluded from this
|
|
238
|
+
// count — it's the TUI's own dashboard attachment, never a peer
|
|
239
|
+
// agent that user-facing `/play` should target.
|
|
240
|
+
const LIVE_PHASES = new Set([
|
|
241
|
+
'attached', 'processing', 'awaiting', 'booting', 'draining',
|
|
242
|
+
]);
|
|
243
|
+
const byEnsemble = new Map();
|
|
244
|
+
try {
|
|
245
|
+
const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
|
|
246
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
247
|
+
const name = (0, search_attributes_1.getEnsembleName)(wf);
|
|
248
|
+
if (!name)
|
|
249
|
+
continue;
|
|
250
|
+
// Exclude the maestro session from the headline player count.
|
|
251
|
+
// The maestro is the TUI's own dashboard attachment, not a peer
|
|
252
|
+
// agent — counting it produced confusing "(2 players)" rows on
|
|
253
|
+
// a fresh ensemble with one real player. Mirrors the
|
|
254
|
+
// `filterRealPlayers` rule used in StatusBar (cf6becd). Detect
|
|
255
|
+
// via the canonical `AgentTempoPlayerType` search attribute,
|
|
256
|
+
// with a workflow-id-suffix fallback for the brief post-start
|
|
257
|
+
// window before search attributes propagate.
|
|
258
|
+
const playerType = (0, search_attributes_1.getSearchAttrString)(wf, 'AgentTempoPlayerType');
|
|
259
|
+
const isMaestroSession = playerType === 'maestro'
|
|
260
|
+
|| (wf.workflowId?.endsWith('-maestro') ?? false);
|
|
261
|
+
const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
|
|
262
|
+
const entry = byEnsemble.get(name) ?? {
|
|
263
|
+
count: 0, hasConductor: false, liveAdapterCount: 0, hasDetached: false,
|
|
264
|
+
};
|
|
265
|
+
if (!isMaestroSession)
|
|
266
|
+
entry.count++;
|
|
267
|
+
if (phase === 'detached')
|
|
268
|
+
entry.hasDetached = true;
|
|
269
|
+
else if (phase && LIVE_PHASES.has(phase) && !isMaestroSession) {
|
|
270
|
+
entry.liveAdapterCount++;
|
|
271
|
+
}
|
|
272
|
+
const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
|
|
273
|
+
const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
|
|
274
|
+
if (isConductorFromSA || isConductorFromId) {
|
|
275
|
+
entry.hasConductor = true;
|
|
276
|
+
if (phase)
|
|
277
|
+
entry.conductorStatus = phase;
|
|
278
|
+
}
|
|
279
|
+
byEnsemble.set(name, entry);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
return await finishEnsembleSummaries(byEnsemble);
|
|
286
|
+
},
|
|
287
|
+
async listEnsemblesBounded(deadlineMs) {
|
|
288
|
+
// #336/#529 site 6 — bounded variant for `AggregateRunner.collect()`'s
|
|
289
|
+
// 750ms poll. On deadline, propagates `timedOut: true` so the
|
|
290
|
+
// collect tick can skip the entire diff round (preserving
|
|
291
|
+
// `knownEnsembles` and avoiding phantom `ensemble.destroyed`
|
|
292
|
+
// SSE events). Architect-approved invariant for this PR.
|
|
293
|
+
const LIVE_PHASES = new Set([
|
|
294
|
+
'attached', 'processing', 'awaiting', 'booting', 'draining',
|
|
295
|
+
]);
|
|
296
|
+
const byEnsemble = new Map();
|
|
297
|
+
let scanned = 0;
|
|
298
|
+
let timedOut = false;
|
|
299
|
+
try {
|
|
300
|
+
const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
|
|
301
|
+
for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query }), deadlineMs, 'listEnsemblesBounded')) {
|
|
302
|
+
scanned++;
|
|
303
|
+
const name = (0, search_attributes_1.getEnsembleName)(wf);
|
|
304
|
+
if (!name)
|
|
305
|
+
continue;
|
|
306
|
+
const playerType = (0, search_attributes_1.getSearchAttrString)(wf, 'AgentTempoPlayerType');
|
|
307
|
+
const isMaestroSession = playerType === 'maestro'
|
|
308
|
+
|| (wf.workflowId?.endsWith('-maestro') ?? false);
|
|
309
|
+
const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
|
|
310
|
+
const entry = byEnsemble.get(name) ?? {
|
|
311
|
+
count: 0, hasConductor: false, liveAdapterCount: 0, hasDetached: false,
|
|
312
|
+
};
|
|
313
|
+
if (!isMaestroSession)
|
|
314
|
+
entry.count++;
|
|
315
|
+
if (phase === 'detached')
|
|
316
|
+
entry.hasDetached = true;
|
|
317
|
+
else if (phase && LIVE_PHASES.has(phase) && !isMaestroSession) {
|
|
318
|
+
entry.liveAdapterCount++;
|
|
319
|
+
}
|
|
320
|
+
const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
|
|
321
|
+
const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
|
|
322
|
+
if (isConductorFromSA || isConductorFromId) {
|
|
323
|
+
entry.hasConductor = true;
|
|
324
|
+
if (phase)
|
|
325
|
+
entry.conductorStatus = phase;
|
|
326
|
+
}
|
|
327
|
+
byEnsemble.set(name, entry);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if ((0, visibility_deadline_1.isVisibilityTimeout)(err)) {
|
|
332
|
+
timedOut = true;
|
|
333
|
+
// Fall through: the caller (`AggregateRunner`) checks
|
|
334
|
+
// `timedOut` and bails before applying any diff; we still
|
|
335
|
+
// return whatever we managed to enumerate so test/diag
|
|
336
|
+
// surfaces can inspect partial state if useful.
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
throw err; // catastrophic — propagate (no unbounded swallow).
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const items = await finishEnsembleSummaries(byEnsemble);
|
|
343
|
+
return { items, timedOut, scanned };
|
|
344
|
+
},
|
|
345
|
+
async getPlayers(ensemble) {
|
|
346
|
+
// Strategy 1: Global Maestro — filter by ensemble
|
|
347
|
+
try {
|
|
348
|
+
const h = handle(globalMaestroId);
|
|
349
|
+
// Issue #433 — bound the global-maestro query so a wedged
|
|
350
|
+
// global maestro can't hang `getPlayers` (called from the
|
|
351
|
+
// snapshot fan-out and many other paths). Existing catch falls
|
|
352
|
+
// through to Strategy 2 → Strategy 3 on any failure.
|
|
353
|
+
const byEnsemble = await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroPlayersByEnsemble');
|
|
354
|
+
if (byEnsemble[ensemble])
|
|
355
|
+
return byEnsemble[ensemble];
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Fall through
|
|
359
|
+
}
|
|
360
|
+
// Strategy 2: Per-ensemble Maestro
|
|
361
|
+
try {
|
|
362
|
+
const h = handle((0, config_1.maestroWorkflowId)(ensemble));
|
|
363
|
+
// Issue #433 — same reasoning, applied to the per-ensemble maestro.
|
|
364
|
+
return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroPlayers');
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Fall through
|
|
368
|
+
}
|
|
369
|
+
// Strategy 3: Direct workflow list
|
|
370
|
+
try {
|
|
371
|
+
const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}"`;
|
|
372
|
+
const players = [];
|
|
373
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
374
|
+
const sa = wf.searchAttributes || {};
|
|
375
|
+
const playerId = Array.isArray(sa.AgentTempoPlayerId) ? String(sa.AgentTempoPlayerId[0]) : wf.workflowId;
|
|
376
|
+
// Preferred: AgentTempoIsConductor search attribute (canonical, queryable).
|
|
377
|
+
// Fallback: workflow ID convention — covers the brief window after a
|
|
378
|
+
// conductor spawn before the search attribute is indexed.
|
|
379
|
+
const isConductorFromSA = Array.isArray(sa.AgentTempoIsConductor) && sa.AgentTempoIsConductor[0] === true;
|
|
380
|
+
const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
|
|
381
|
+
players.push({
|
|
382
|
+
playerId,
|
|
383
|
+
ensemble,
|
|
384
|
+
part: '',
|
|
385
|
+
hostname: Array.isArray(sa.AgentTempoHostname) ? String(sa.AgentTempoHostname[0]) : '',
|
|
386
|
+
workDir: '',
|
|
387
|
+
isConductor: isConductorFromSA || isConductorFromId,
|
|
388
|
+
agentType: 'claude',
|
|
389
|
+
// Attachment phase from `AgentTempoAttachmentState` search attr.
|
|
390
|
+
phase: Array.isArray(sa.AgentTempoAttachmentState)
|
|
391
|
+
? String(sa.AgentTempoAttachmentState[0])
|
|
392
|
+
: undefined,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return players;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
async getEnsembleMeta(ensemble) {
|
|
402
|
+
// Issue #399 W1 — fan-out four queries against the per-ensemble
|
|
403
|
+
// maestro hub. Each query soft-fails to its sentinel default so
|
|
404
|
+
// a single transient failure can't block the snapshot endpoint.
|
|
405
|
+
const h = handle((0, config_1.maestroWorkflowId)(ensemble));
|
|
406
|
+
// Issue #433 — bound each per-maestro query so a wedged maestro
|
|
407
|
+
// worker can't hang `getEnsembleMeta` (called from snapshot fan-out
|
|
408
|
+
// on every `/v1/state/:ensemble` request and every aggregate tick).
|
|
409
|
+
// Each query already soft-fails to its sentinel; `QueryTimeoutError`
|
|
410
|
+
// falls into the same `.catch(() => sentinel)` path.
|
|
411
|
+
const [description, startedAt, currentBpm, tempoSeries] = await Promise.all([
|
|
412
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getEnsembleDescriptionQuery).catch(() => ''),
|
|
413
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getEnsembleStartTimeQuery).catch(() => ''),
|
|
414
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getCurrentBpmQuery).catch(() => 0),
|
|
415
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getTempoSeriesQuery).catch(() => []),
|
|
416
|
+
]);
|
|
417
|
+
return { description, startedAt, currentBpm, tempoSeries };
|
|
418
|
+
},
|
|
419
|
+
async getPlayerWireMeta(ensemble, playerId) {
|
|
420
|
+
// Issue #399 W2 — fan-out three queries against the session
|
|
421
|
+
// workflow. The handle is opened by workflow ID directly; if the
|
|
422
|
+
// workflow can't be resolved (just-recruited, just-destroyed,
|
|
423
|
+
// transient lookup failure) every query rejects together and
|
|
424
|
+
// we return `null` so the caller's projection drops the whole
|
|
425
|
+
// wire-meta block rather than emitting half-populated fields.
|
|
426
|
+
const h = handle((0, config_1.sessionWorkflowId)(ensemble, playerId));
|
|
427
|
+
// Issue #433 — bound each per-session query. Without a timeout,
|
|
428
|
+
// `Promise.allSettled` waits for the slowest query to settle (or
|
|
429
|
+
// never, if the session worker is wedged), so a single hung session
|
|
430
|
+
// would block the entire snapshot fan-out for `/v1/state/:ensemble`
|
|
431
|
+
// and the AggregateRunner's 750ms poll loop. With timeouts, hung
|
|
432
|
+
// queries reject as `QueryTimeoutError`, `Promise.allSettled` sees
|
|
433
|
+
// three rejections and the existing all-rejected branch returns
|
|
434
|
+
// `null` — caller treats this player's wireMeta as missing.
|
|
435
|
+
const [runIdR, messagingR, leaseR] = await Promise.allSettled([
|
|
436
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getRunIdQuery),
|
|
437
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getMessagingStateQuery),
|
|
438
|
+
(0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getLeaseStateQuery),
|
|
439
|
+
]);
|
|
440
|
+
// If every query rejected, treat this as "session unreachable" —
|
|
441
|
+
// the caller renders no wire-meta rather than partial sentinels.
|
|
442
|
+
if (runIdR.status === 'rejected' &&
|
|
443
|
+
messagingR.status === 'rejected' &&
|
|
444
|
+
leaseR.status === 'rejected') {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const out = {};
|
|
448
|
+
if (runIdR.status === 'fulfilled')
|
|
449
|
+
out.runId = runIdR.value;
|
|
450
|
+
if (messagingR.status === 'fulfilled')
|
|
451
|
+
out.messaging = messagingR.value;
|
|
452
|
+
if (leaseR.status === 'fulfilled')
|
|
453
|
+
out.lease = leaseR.value;
|
|
454
|
+
return out;
|
|
455
|
+
},
|
|
456
|
+
async getMessages(ensemble, limit) {
|
|
457
|
+
try {
|
|
458
|
+
const h = handle(globalMaestroId);
|
|
459
|
+
// #433: unbounded — justified, `getMessages` is not reachable from
|
|
460
|
+
// `buildEnsembleSnapshot` (snapshot uses `getEnsembleChat`).
|
|
461
|
+
// Called by the recall MCP tool / TUI on user demand; user can
|
|
462
|
+
// Ctrl-C if it hangs.
|
|
463
|
+
const all = await h.query('maestroRecentMessages');
|
|
464
|
+
const filtered = all.filter(m => m.ensemble === ensemble);
|
|
465
|
+
return limit ? filtered.slice(-limit) : filtered;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
async getConductorHistory(ensemble) {
|
|
472
|
+
try {
|
|
473
|
+
const h = handle(globalMaestroId);
|
|
474
|
+
const result = await h.executeUpdate('maestroFetchConductorHistory', {
|
|
475
|
+
args: [{ ensemble }],
|
|
476
|
+
});
|
|
477
|
+
if (result.success)
|
|
478
|
+
return result.history;
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
async getPlayerMessages(ensemble, playerId) {
|
|
486
|
+
try {
|
|
487
|
+
const h = handle(globalMaestroId);
|
|
488
|
+
return await h.executeUpdate('maestroFetchPlayerMessages', {
|
|
489
|
+
args: [{ ensemble, playerId }],
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
async getPlayerMetadata(ensemble, playerId) {
|
|
497
|
+
try {
|
|
498
|
+
// Query the player's workflow directly for metadata
|
|
499
|
+
const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(playerId)}"`;
|
|
500
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
501
|
+
const h = handle(wf.workflowId);
|
|
502
|
+
// #433: unbounded — justified, `getPlayerMetadata` is not
|
|
503
|
+
// reachable from `buildEnsembleSnapshot` (snapshot reads
|
|
504
|
+
// metadata via `getPlayers` → maestro fan-out, not per-player
|
|
505
|
+
// direct query). Used by ad-hoc tools / debug surfaces on
|
|
506
|
+
// user demand.
|
|
507
|
+
return await h.query('getMetadata');
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
async sendCommand(ensemble, text, source) {
|
|
516
|
+
// Route commands through Maestro hub → conductor's commandSignal
|
|
517
|
+
let result;
|
|
518
|
+
try {
|
|
519
|
+
const h = handle(globalMaestroId);
|
|
520
|
+
result = await h.executeUpdate('maestroGlobalSendCommand', {
|
|
521
|
+
args: [{ ensemble, text, source }],
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
const h = handle((0, config_1.maestroWorkflowId)(ensemble));
|
|
526
|
+
result = await h.executeUpdate('maestroSendCommand', {
|
|
527
|
+
args: [{ text, source }],
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
// Record on maestro workflow for history persistence
|
|
531
|
+
try {
|
|
532
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
533
|
+
const mh = handle(maestroId);
|
|
534
|
+
await mh.signal('recordSentMessage', { to: 'conductor', text });
|
|
535
|
+
}
|
|
536
|
+
catch { /* best effort */ }
|
|
537
|
+
return result;
|
|
538
|
+
},
|
|
539
|
+
async sendMessage(ensemble, to, text, source) {
|
|
540
|
+
// Direct signal with isMaestro flag — matches web Maestro pattern
|
|
541
|
+
const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(to)}"`;
|
|
542
|
+
let sent = false;
|
|
543
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
544
|
+
const h = handle(wf.workflowId);
|
|
545
|
+
await h.signal('receiveMessage', {
|
|
546
|
+
from: source,
|
|
547
|
+
text,
|
|
548
|
+
isMaestro: true,
|
|
549
|
+
});
|
|
550
|
+
sent = true;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
if (!sent) {
|
|
554
|
+
// Fallback: try via Maestro hub if direct resolution fails
|
|
555
|
+
try {
|
|
556
|
+
const h = handle(globalMaestroId);
|
|
557
|
+
await h.executeUpdate('maestroSendMessage', {
|
|
558
|
+
args: [{ ensemble, to, text, source }],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
throw new Error(`Player "${to}" not found in ensemble "${ensemble}"`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Record on maestro workflow for history persistence
|
|
566
|
+
try {
|
|
567
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
568
|
+
const mh = handle(maestroId);
|
|
569
|
+
await mh.signal('recordSentMessage', { to, text });
|
|
570
|
+
}
|
|
571
|
+
catch { /* best effort */ }
|
|
572
|
+
return `maestro-msg-${Date.now()}`;
|
|
573
|
+
},
|
|
574
|
+
async terminatePlayer(ensemble, playerId) {
|
|
575
|
+
const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(playerId)}"`;
|
|
576
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
577
|
+
const h = handle(wf.workflowId);
|
|
578
|
+
await h.terminate('terminated via TUI');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
throw new Error(`Player "${playerId}" not found in ensemble "${ensemble}"`);
|
|
582
|
+
},
|
|
583
|
+
// ── PR-D verbs — enqueue on the TUI-owned maestro session's outbox.
|
|
584
|
+
// The dispatch loop runs `deliverDetach` / `deliverDestroy` /
|
|
585
|
+
// `deliverRestart` activities against the target (QA B1/B2/B3).
|
|
586
|
+
async recruit(ensemble, opts) {
|
|
587
|
+
// #306: Lazy-import the agent-type resolver so the TUI/CLI bundle
|
|
588
|
+
// doesn't pull in the subagent YAML crawler at module-load time.
|
|
589
|
+
// The `held` flow on the TUI side doesn't currently exercise this;
|
|
590
|
+
// `playerType` is only resolved when supplied.
|
|
591
|
+
let agentDefinition;
|
|
592
|
+
let agentDefinitionPath;
|
|
593
|
+
let agentDefinitionDescription;
|
|
594
|
+
let nativeResolvable;
|
|
595
|
+
let allowedTools;
|
|
596
|
+
if (opts.playerType) {
|
|
597
|
+
const { resolveAgentType } = await Promise.resolve().then(() => __importStar(require('../ensemble/agent-types')));
|
|
598
|
+
const info = resolveAgentType(opts.playerType);
|
|
599
|
+
if (!info) {
|
|
600
|
+
throw new Error(`Unknown agent type "${opts.playerType}"`);
|
|
601
|
+
}
|
|
602
|
+
agentDefinition = info.name;
|
|
603
|
+
agentDefinitionPath = info.path;
|
|
604
|
+
agentDefinitionDescription = info.description;
|
|
605
|
+
nativeResolvable = info.nativeResolvable;
|
|
606
|
+
allowedTools = info.allowedTools;
|
|
607
|
+
}
|
|
608
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
609
|
+
const h = handle(maestroId);
|
|
610
|
+
// #306 fix: always set `targetHostname` on the entry. The TUI-owned
|
|
611
|
+
// maestro session stores `hostname: 'dashboard'` in its metadata
|
|
612
|
+
// (a placeholder, not a real host), so the session workflow's
|
|
613
|
+
// fallback path — `entry.targetHostname || input.metadata.hostname`
|
|
614
|
+
// — routes `spawnProcess` to task queue `agent-tempo-dashboard`,
|
|
615
|
+
// which has no worker. The MCP `recruit` tool worked because the
|
|
616
|
+
// conductor session that ran it had a real OS hostname in metadata.
|
|
617
|
+
// Mirror that behavior here by defaulting to `osHostname()` when
|
|
618
|
+
// the caller didn't pin a specific host.
|
|
619
|
+
const targetHostname = opts.host ?? (0, os_1.hostname)();
|
|
620
|
+
const entry = {
|
|
621
|
+
type: 'recruit',
|
|
622
|
+
targetName: opts.name,
|
|
623
|
+
workDir: opts.workDir,
|
|
624
|
+
isConductor: opts.isConductor === true,
|
|
625
|
+
agent: opts.agent ?? 'claude',
|
|
626
|
+
...(opts.initialMessage !== undefined ? { initialMessage: opts.initialMessage } : {}),
|
|
627
|
+
// If a player-type is provided, let the outbox activity supply the
|
|
628
|
+
// agent definition bundle; otherwise fall back to an explicit
|
|
629
|
+
// systemPrompt path (mirrors the recruit MCP tool's branching).
|
|
630
|
+
...(agentDefinition
|
|
631
|
+
? { agentDefinition, agentDefinitionPath, agentDefinitionDescription, nativeResolvable, allowedTools }
|
|
632
|
+
: opts.systemPrompt !== undefined ? { systemPrompt: opts.systemPrompt } : {}),
|
|
633
|
+
targetHostname,
|
|
634
|
+
...(opts.held === true ? { held: true } : {}),
|
|
635
|
+
};
|
|
636
|
+
const entryId = await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
637
|
+
return { playerId: opts.name, entryId };
|
|
638
|
+
},
|
|
639
|
+
async release(ensemble, playerId) {
|
|
640
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
641
|
+
const mh = handle(maestroId);
|
|
642
|
+
const submitRelease = async (target) => {
|
|
643
|
+
const entry = { type: 'release', targetPlayerId: target };
|
|
644
|
+
await mh.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
645
|
+
};
|
|
646
|
+
if (playerId) {
|
|
647
|
+
// Single-player release — match the MCP tool: only submit when the
|
|
648
|
+
// session's outbox is actually locked, so the caller sees a clean
|
|
649
|
+
// error instead of a no-op success for already-running sessions.
|
|
650
|
+
const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
|
|
651
|
+
if (!target) {
|
|
652
|
+
throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
|
|
653
|
+
}
|
|
654
|
+
let isLocked = false;
|
|
655
|
+
try {
|
|
656
|
+
// #433: unbounded — justified, the single-player `release()`
|
|
657
|
+
// path is an explicit MCP tool action triggered by the user
|
|
658
|
+
// ("release X"), not the snapshot fan-out. `isAnySessionHeld`
|
|
659
|
+
// (also called `outboxLockedQuery` per-session, but in the
|
|
660
|
+
// snapshot path) IS wrapped — see line ~1020.
|
|
661
|
+
isLocked = await target.query(signals_1.outboxLockedQuery);
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// Query may fail for old workflows — treat as "not held" to avoid
|
|
665
|
+
// false-positive release requests on pre-outboxLocked builds.
|
|
666
|
+
}
|
|
667
|
+
if (!isLocked) {
|
|
668
|
+
throw new Error(`Session "${playerId}" is not held (outbox not locked).`);
|
|
669
|
+
}
|
|
670
|
+
await submitRelease(playerId);
|
|
671
|
+
return { released: [playerId], errors: [] };
|
|
672
|
+
}
|
|
673
|
+
// Bulk release — scan + query + enqueue each held session. The scan
|
|
674
|
+
// skips the TUI's own maestro session so we don't try to release
|
|
675
|
+
// ourselves. Errors are returned as soft failures so the caller can
|
|
676
|
+
// render a partial-success summary.
|
|
677
|
+
const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
|
|
678
|
+
const held = [];
|
|
679
|
+
for (const s of sessions) {
|
|
680
|
+
if (s.playerId === 'maestro')
|
|
681
|
+
continue;
|
|
682
|
+
try {
|
|
683
|
+
const sh = handle(s.workflowId);
|
|
684
|
+
// Issue #433 — bound the per-session query so a single wedged
|
|
685
|
+
// worker doesn't block the rest of the bulk-release scan. The
|
|
686
|
+
// existing catch already maps failures to "not held".
|
|
687
|
+
const locked = await (0, query_timeout_1.queryHandleWithTimeout)(sh, signals_1.outboxLockedQuery);
|
|
688
|
+
if (locked)
|
|
689
|
+
held.push(s);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Skip sessions where the query fails (old workflows, terminated,
|
|
693
|
+
// or wedged-worker timeout per #433).
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const released = [];
|
|
697
|
+
const errors = [];
|
|
698
|
+
for (const s of held) {
|
|
699
|
+
try {
|
|
700
|
+
await submitRelease(s.playerId);
|
|
701
|
+
released.push(s.playerId);
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
errors.push({ playerId: s.playerId, error: errMsg(err) });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return { released, errors };
|
|
708
|
+
},
|
|
709
|
+
async restart(ensemble, playerId, opts = {}) {
|
|
710
|
+
const invokerPlayerId = opts.invokerPlayerId ?? 'cli';
|
|
711
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
712
|
+
const h = handle(maestroId);
|
|
713
|
+
// #580 — `confirmStealFromHost` is a caller-side intent flag (§16.5
|
|
714
|
+
// Option B). The outbox entry has no slot for it because the workflow
|
|
715
|
+
// trusts the caller; the gate is enforced pre-submit by the TUI
|
|
716
|
+
// handler and the shared MCP-tool guard. Accepting the field on
|
|
717
|
+
// `RestartClientOpts` gives external SDK consumers and the TUI a
|
|
718
|
+
// typed pipeline to forward the confirmed value.
|
|
719
|
+
const entry = {
|
|
720
|
+
type: 'restart',
|
|
721
|
+
targetPlayerId: playerId,
|
|
722
|
+
invokerPlayerId,
|
|
723
|
+
...(opts.host !== undefined ? { host: opts.host } : {}),
|
|
724
|
+
...(opts.fresh !== undefined ? { fresh: opts.fresh } : {}),
|
|
725
|
+
...(opts.force !== undefined ? { force: opts.force } : {}),
|
|
726
|
+
...(opts.contextMessages !== undefined ? { contextMessages: opts.contextMessages } : {}),
|
|
727
|
+
...(opts.loadFromState !== undefined ? { loadFromState: opts.loadFromState } : {}),
|
|
728
|
+
...(opts.transcript !== undefined ? { transcript: opts.transcript } : {}),
|
|
729
|
+
};
|
|
730
|
+
const entryId = await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
731
|
+
return {
|
|
732
|
+
playerId,
|
|
733
|
+
...(opts.host !== undefined ? { host: opts.host } : {}),
|
|
734
|
+
entryId,
|
|
735
|
+
};
|
|
736
|
+
},
|
|
737
|
+
async detach(ensemble, playerId, deadlineMs = 5_000) {
|
|
738
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
739
|
+
const h = handle(maestroId);
|
|
740
|
+
const entry = {
|
|
741
|
+
type: 'detach',
|
|
742
|
+
targetPlayerId: playerId,
|
|
743
|
+
reason: 'user-stop',
|
|
744
|
+
deadlineMs,
|
|
745
|
+
};
|
|
746
|
+
await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
747
|
+
},
|
|
748
|
+
async destroy(ensemble, playerId, reason) {
|
|
749
|
+
// #287: ensemble-scope when `playerId` is omitted. Peer sessions in
|
|
750
|
+
// parallel → scheduler + maestro terminate in parallel → conductor
|
|
751
|
+
// last so it sees every peer teardown. Matches the destroy tool.
|
|
752
|
+
if (playerId === undefined) {
|
|
753
|
+
const destroyReason = reason ?? 'ensemble destroy via TempoClient';
|
|
754
|
+
const conductorWfId = (0, config_1.conductorWorkflowId)(ensemble);
|
|
755
|
+
const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
|
|
756
|
+
const peers = [];
|
|
757
|
+
let conductorPresent = false;
|
|
758
|
+
for (const s of sessions) {
|
|
759
|
+
if (s.workflowId === conductorWfId)
|
|
760
|
+
conductorPresent = true;
|
|
761
|
+
else
|
|
762
|
+
peers.push(s);
|
|
763
|
+
}
|
|
764
|
+
const summary = {
|
|
765
|
+
destroyed: 0,
|
|
766
|
+
terminated: 0,
|
|
767
|
+
failed: 0,
|
|
768
|
+
details: [],
|
|
769
|
+
};
|
|
770
|
+
const destroyArgs = { reason: destroyReason, terminatedBy: 'tempo-client' };
|
|
771
|
+
// Peers in parallel.
|
|
772
|
+
const peerResults = await Promise.allSettled(peers.map(async (s) => {
|
|
773
|
+
try {
|
|
774
|
+
await handle(s.workflowId).executeUpdate(signals_1.destroyUpdate, { args: [destroyArgs] });
|
|
775
|
+
return { session: s, outcome: 'destroyed' };
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
return { session: s, outcome: 'failed', error: errMsg(err) };
|
|
779
|
+
}
|
|
780
|
+
}));
|
|
781
|
+
for (const r of peerResults) {
|
|
782
|
+
if (r.status !== 'fulfilled')
|
|
783
|
+
continue;
|
|
784
|
+
const v = r.value;
|
|
785
|
+
if (v.outcome === 'destroyed') {
|
|
786
|
+
summary.details.push({ target: v.session.playerId, outcome: 'destroyed' });
|
|
787
|
+
summary.destroyed++;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
summary.details.push({ target: v.session.playerId, outcome: 'failed', error: v.error });
|
|
791
|
+
summary.failed++;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Scheduler + maestro terminate in parallel. `terminate` rejects on
|
|
795
|
+
// missing workflows; treat as "not present" (don't count as failure).
|
|
796
|
+
const [schedRes, maestroRes] = await Promise.allSettled([
|
|
797
|
+
handle((0, config_1.schedulerWorkflowId)(ensemble)).terminate(destroyReason),
|
|
798
|
+
handle((0, config_1.maestroWorkflowId)(ensemble)).terminate(destroyReason),
|
|
799
|
+
]);
|
|
800
|
+
if (schedRes.status === 'fulfilled') {
|
|
801
|
+
summary.details.push({ target: 'scheduler', outcome: 'terminated' });
|
|
802
|
+
summary.terminated++;
|
|
803
|
+
}
|
|
804
|
+
if (maestroRes.status === 'fulfilled') {
|
|
805
|
+
summary.details.push({ target: 'maestro', outcome: 'terminated' });
|
|
806
|
+
summary.terminated++;
|
|
807
|
+
}
|
|
808
|
+
// Conductor last.
|
|
809
|
+
if (conductorPresent) {
|
|
810
|
+
try {
|
|
811
|
+
await handle(conductorWfId).executeUpdate(signals_1.destroyUpdate, { args: [destroyArgs] });
|
|
812
|
+
summary.details.push({ target: 'conductor', outcome: 'destroyed' });
|
|
813
|
+
summary.destroyed++;
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
summary.details.push({ target: 'conductor', outcome: 'failed', error: errMsg(err) });
|
|
817
|
+
summary.failed++;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return summary;
|
|
821
|
+
}
|
|
822
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
823
|
+
const h = handle(maestroId);
|
|
824
|
+
const entry = {
|
|
825
|
+
type: 'destroy',
|
|
826
|
+
targetPlayerId: playerId,
|
|
827
|
+
...(reason !== undefined ? { reason } : {}),
|
|
828
|
+
notifyConductor: true,
|
|
829
|
+
};
|
|
830
|
+
await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
831
|
+
},
|
|
832
|
+
async pause(ensemble) {
|
|
833
|
+
await Promise.all([
|
|
834
|
+
(0, ensemble_ops_1.pauseMaestroAndScheduler)(client, ensemble),
|
|
835
|
+
(0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, true),
|
|
836
|
+
]);
|
|
837
|
+
},
|
|
838
|
+
async play(ensemble, opts = {}) {
|
|
839
|
+
const [, unpaused] = await Promise.all([
|
|
840
|
+
(0, ensemble_ops_1.unpauseMaestroAndScheduler)(client, ensemble),
|
|
841
|
+
(0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, false),
|
|
842
|
+
]);
|
|
843
|
+
if (opts.release === true && unpaused.sent > 0) {
|
|
844
|
+
// Fan out releaseHeld AFTER everyone is unpaused so no session
|
|
845
|
+
// receives `releaseHeld` while still paused.
|
|
846
|
+
await (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.releaseHeldSignal.name, undefined);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
async shutdown(ensemble, opts = {}) {
|
|
850
|
+
const deadlineMs = opts.deadlineMs ?? 5_000;
|
|
851
|
+
const [toggle, fanout] = await Promise.all([
|
|
852
|
+
(0, ensemble_ops_1.pauseMaestroAndScheduler)(client, ensemble),
|
|
853
|
+
(0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.requestDetachSignal.name, { reason: 'user-stop', deadlineMs }),
|
|
854
|
+
]);
|
|
855
|
+
return {
|
|
856
|
+
detached: fanout.sent,
|
|
857
|
+
skipped: fanout.skipped,
|
|
858
|
+
failed: fanout.failed,
|
|
859
|
+
maestroPaused: toggle.maestro,
|
|
860
|
+
schedulerPaused: toggle.scheduler,
|
|
861
|
+
// #299 sibling: TempoClient does not pass a `skip` predicate to
|
|
862
|
+
// `signalAllSessions`, so `fanout.perSession[*].outcome` will never
|
|
863
|
+
// be `'skipped'` here. The narrowed `EnsembleShutdownDetail.outcome`
|
|
864
|
+
// reflects the actual public surface; the `'skipped'` branch is an
|
|
865
|
+
// explicit no-op that emits nothing.
|
|
866
|
+
details: fanout.perSession.flatMap((p) => {
|
|
867
|
+
if (p.outcome === 'sent') {
|
|
868
|
+
return [{ playerId: p.playerId, outcome: 'detaching' }];
|
|
869
|
+
}
|
|
870
|
+
if (p.outcome === 'failed') {
|
|
871
|
+
return [{ playerId: p.playerId, outcome: 'failed', error: p.error }];
|
|
872
|
+
}
|
|
873
|
+
return []; // 'skipped' is unreachable in the TempoClient path
|
|
874
|
+
}),
|
|
875
|
+
};
|
|
876
|
+
},
|
|
877
|
+
async restore(ensemble) {
|
|
878
|
+
// Scope the orphan scan to the requested ensemble (#298 — matches the
|
|
879
|
+
// `ensemble?` filter the CLI/TUI pass through) and unpause maestro +
|
|
880
|
+
// scheduler for the same ensemble in parallel.
|
|
881
|
+
//
|
|
882
|
+
// #306: narrow to `phases: ['detached']`. User-invoked `/restore`
|
|
883
|
+
// revives a parked ensemble — a live attached/processing session is
|
|
884
|
+
// NOT a restorable orphan and must not be flagged as one. The broad
|
|
885
|
+
// live-phase default is reserved for daemon reconcile-on-boot + CLI
|
|
886
|
+
// `up --resume`, which have no PID memory after a crash and must
|
|
887
|
+
// treat every live phase as a presumed orphan. Without this narrowing
|
|
888
|
+
// a healthy conductor gets deliverRestart → requestDetach and is
|
|
889
|
+
// hard-terminated by `drainingDeadline`.
|
|
890
|
+
//
|
|
891
|
+
// Bug A: also fan out `setPaused=false` to every session. Without
|
|
892
|
+
// this, sessions whose `paused` flag was flipped (via `/pause` or
|
|
893
|
+
// any prior pause path) stay frozen — the conductor receives
|
|
894
|
+
// messages but its outbox dispatcher is gated by `!paused`, so
|
|
895
|
+
// typed messages get no reply. Mirrors the pattern in `play()`:
|
|
896
|
+
// the maestro/scheduler hub toggle is not enough on its own.
|
|
897
|
+
const [summary] = await Promise.all([
|
|
898
|
+
(0, orphans_1.restoreOrphansOnce)(client, {
|
|
899
|
+
hostname: (0, os_1.hostname)(),
|
|
900
|
+
invokerPlayerId: 'tempo-client',
|
|
901
|
+
policy: 'auto',
|
|
902
|
+
ensemble,
|
|
903
|
+
phases: ['detached'],
|
|
904
|
+
}),
|
|
905
|
+
(0, ensemble_ops_1.unpauseMaestroAndScheduler)(client, ensemble),
|
|
906
|
+
(0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, false),
|
|
907
|
+
]);
|
|
908
|
+
return summary;
|
|
909
|
+
},
|
|
910
|
+
async migrate(ensemble, playerId, host, opts = {}) {
|
|
911
|
+
if (!host || !host.trim()) {
|
|
912
|
+
throw new Error('`host` is required for migrate. Use `restart` to revive on the current host.');
|
|
913
|
+
}
|
|
914
|
+
return this.restart(ensemble, playerId, { ...opts, host });
|
|
915
|
+
},
|
|
916
|
+
async attachmentInfo(ensemble, playerId) {
|
|
917
|
+
// Read-only query — resolve + query directly (no outbox needed).
|
|
918
|
+
const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
|
|
919
|
+
if (!target)
|
|
920
|
+
throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
|
|
921
|
+
// #433: unbounded — justified, `attachmentInfo()` is the
|
|
922
|
+
// user-facing MCP tool that returns one player's lease/phase to
|
|
923
|
+
// the operator. Not reachable from `buildEnsembleSnapshot`
|
|
924
|
+
// (snapshot reads attachment info via the `phase` search
|
|
925
|
+
// attribute and `getPlayerWireMeta`'s lease query, both bounded).
|
|
926
|
+
return target.query(signals_1.attachmentInfoQuery);
|
|
927
|
+
},
|
|
928
|
+
async listHosts(opts = {}) {
|
|
929
|
+
// Lazy import so this doesn't drag utils/hosts into every
|
|
930
|
+
// consumer of TempoClient at module-load time.
|
|
931
|
+
const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
|
|
932
|
+
// #437 — both `namespace` and `taskQueue` must match the daemon's
|
|
933
|
+
// config or poller discovery silently returns `[]` (dev mode hits
|
|
934
|
+
// `'agent-tempo-dev'`, prod hits `'agent-tempo'`). Passing
|
|
935
|
+
// `taskQueue: undefined` is harmless — `listHosts` defaults via
|
|
936
|
+
// `?? 'agent-tempo'` and unconditional pass-through avoids
|
|
937
|
+
// per-call object allocation on this hot path.
|
|
938
|
+
return listHosts(client, {
|
|
939
|
+
force: Boolean(opts.force),
|
|
940
|
+
namespace: client.options.namespace,
|
|
941
|
+
taskQueue,
|
|
942
|
+
});
|
|
943
|
+
},
|
|
944
|
+
async recall(ensemble, playerId) {
|
|
945
|
+
// #128: direct session queries, no maestro round-trip. Throws rather
|
|
946
|
+
// than returning empties so the CLI / TUI wrappers can surface a
|
|
947
|
+
// clean "session not found" error instead of rendering a silently
|
|
948
|
+
// empty timeline that looks indistinguishable from "no messages yet."
|
|
949
|
+
const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
|
|
950
|
+
if (!target)
|
|
951
|
+
throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
|
|
952
|
+
// #433: unbounded — justified, `recall()` is an explicit MCP tool
|
|
953
|
+
// action invoked on user demand ("recall messages for X"). Not
|
|
954
|
+
// reachable from `buildEnsembleSnapshot` (snapshot's per-player
|
|
955
|
+
// wire-meta uses bounded `getMessagingStateQuery` for counters
|
|
956
|
+
// only, never the full message list).
|
|
957
|
+
const [received, sent] = await Promise.all([
|
|
958
|
+
target.query('allMessages'),
|
|
959
|
+
target.query('allSentMessages'),
|
|
960
|
+
]);
|
|
961
|
+
return { received, sent };
|
|
962
|
+
},
|
|
963
|
+
async disbandEnsemble(ensemble) {
|
|
964
|
+
let terminated = 0;
|
|
965
|
+
// Terminate all session workflows in the ensemble
|
|
966
|
+
const sessionQuery = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}"`;
|
|
967
|
+
for await (const wf of client.workflow.list({ query: sessionQuery })) {
|
|
968
|
+
try {
|
|
969
|
+
const h = handle(wf.workflowId);
|
|
970
|
+
await h.terminate('disbanded via TUI');
|
|
971
|
+
terminated++;
|
|
972
|
+
}
|
|
973
|
+
catch { /* already closed */ }
|
|
974
|
+
}
|
|
975
|
+
// Terminate scheduler workflow
|
|
976
|
+
try {
|
|
977
|
+
const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
|
|
978
|
+
await h.terminate('disbanded via TUI');
|
|
979
|
+
terminated++;
|
|
980
|
+
}
|
|
981
|
+
catch { /* no scheduler or already closed */ }
|
|
982
|
+
// Terminate per-ensemble maestro workflow
|
|
983
|
+
try {
|
|
984
|
+
const h = handle((0, config_1.maestroWorkflowId)(ensemble));
|
|
985
|
+
await h.terminate('disbanded via TUI');
|
|
986
|
+
terminated++;
|
|
987
|
+
}
|
|
988
|
+
catch { /* no maestro or already closed */ }
|
|
989
|
+
return { terminated };
|
|
990
|
+
},
|
|
991
|
+
async isConnected() {
|
|
992
|
+
try {
|
|
993
|
+
// Lightweight check: list with limit 1
|
|
994
|
+
const query = 'ExecutionStatus = "Running"';
|
|
995
|
+
for await (const _ of client.workflow.list({ query })) {
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
return true; // Connected but no workflows
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
async getSchedules(ensemble) {
|
|
1005
|
+
try {
|
|
1006
|
+
const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
|
|
1007
|
+
// Issue #433 — bound the scheduler query so a wedged scheduler
|
|
1008
|
+
// worker can't hang `getSchedules` (called from snapshot fan-out
|
|
1009
|
+
// and aggregate poll). Existing catch maps any failure to `[]`.
|
|
1010
|
+
return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'getSchedules');
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
async cancelSchedule(ensemble, name) {
|
|
1017
|
+
const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
|
|
1018
|
+
await h.signal('removeSchedule', name);
|
|
1019
|
+
},
|
|
1020
|
+
async getEnsembleChat(ensemble, offset, limit) {
|
|
1021
|
+
try {
|
|
1022
|
+
const h = handle((0, config_1.maestroWorkflowId)(ensemble));
|
|
1023
|
+
// Issue #433 — bound the maestro chat query so a wedged maestro
|
|
1024
|
+
// worker can't hang `getEnsembleChat` (called from snapshot
|
|
1025
|
+
// fan-out and aggregate poll). Existing catch maps any failure
|
|
1026
|
+
// to an empty chat result. Note: dedup keys on workflowId+name
|
|
1027
|
+
// only, so concurrent snapshot+aggregate calls with different
|
|
1028
|
+
// (offset, limit) pairs share a result — the wider window is a
|
|
1029
|
+
// superset of the narrower so this is safe (see helper JSDoc).
|
|
1030
|
+
return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroEnsembleChat', { args: [{ offset, limit }] });
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return { messages: [], total: 0, hasMore: false, hasConductor: false };
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
async isMaestroPaused(ensemble) {
|
|
1037
|
+
// Reads the same `maestroPaused` query that `listEnsembles` uses for
|
|
1038
|
+
// the home-view classification. Treat hub-not-running as "not paused"
|
|
1039
|
+
// — bare ensembles without a maestro hub aren't displaying any
|
|
1040
|
+
// pause-related state in the chat view either.
|
|
1041
|
+
try {
|
|
1042
|
+
// Issue #433 — bound the maestro query so a wedged maestro worker
|
|
1043
|
+
// can't hang `isMaestroPaused` (called from `buildEnsembleSnapshot`
|
|
1044
|
+
// on every `/v1/state/:ensemble` request and aggregate tick).
|
|
1045
|
+
// Existing `catch` maps any failure to `false` (not paused).
|
|
1046
|
+
const paused = await (0, query_timeout_1.queryHandleWithTimeout)(handle((0, config_1.maestroWorkflowId)(ensemble)), maestro_signals_1.maestroPausedQuery);
|
|
1047
|
+
return !!paused;
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
async isAnySessionHeld(ensemble) {
|
|
1054
|
+
// Scan the ensemble's sessions and check the per-session
|
|
1055
|
+
// `outboxLocked` query. The maestro session is skipped — it's the
|
|
1056
|
+
// TUI's own dashboard attachment, not a peer agent that the user-
|
|
1057
|
+
// facing `/go` should target. Per-session query failures are
|
|
1058
|
+
// treated as "not held" so a single flaky workflow doesn't make
|
|
1059
|
+
// the whole ensemble appear held forever.
|
|
1060
|
+
try {
|
|
1061
|
+
const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
|
|
1062
|
+
for (const s of sessions) {
|
|
1063
|
+
if (s.playerId === 'maestro')
|
|
1064
|
+
continue;
|
|
1065
|
+
try {
|
|
1066
|
+
const sh = handle(s.workflowId);
|
|
1067
|
+
// Issue #433 — bound the per-session query so a wedged worker
|
|
1068
|
+
// can't hang `isAnySessionHeld` (called from
|
|
1069
|
+
// `buildEnsembleSnapshot` on every snapshot fan-out). Without
|
|
1070
|
+
// this, the first hung session blocks every subsequent
|
|
1071
|
+
// session and the entire `held` field of the snapshot.
|
|
1072
|
+
const locked = await (0, query_timeout_1.queryHandleWithTimeout)(sh, signals_1.outboxLockedQuery);
|
|
1073
|
+
if (locked)
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
// Old workflow without `outboxLocked` query, terminated
|
|
1078
|
+
// mid-scan, or wedged-worker timeout (#433) — skip this
|
|
1079
|
+
// session, keep checking the rest.
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
async getGates(ensemble) {
|
|
1089
|
+
// Gates are stored on the conductor's workflow
|
|
1090
|
+
try {
|
|
1091
|
+
const h = handle((0, config_1.conductorWorkflowId)(ensemble));
|
|
1092
|
+
// #433: unbounded — justified, `getGates` is not reachable from
|
|
1093
|
+
// `buildEnsembleSnapshot` (snapshot doesn't surface gates).
|
|
1094
|
+
// Called by `gates` MCP tool / dashboard quality-gate panel on
|
|
1095
|
+
// explicit fetch.
|
|
1096
|
+
return await h.query('qualityGates');
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
return [];
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
async getStages(ensemble) {
|
|
1103
|
+
try {
|
|
1104
|
+
const h = handle((0, config_1.conductorWorkflowId)(ensemble));
|
|
1105
|
+
// #433: unbounded — justified, `getStages` is not reachable from
|
|
1106
|
+
// `buildEnsembleSnapshot` (snapshot doesn't surface stages).
|
|
1107
|
+
// Called by `stages` MCP tool on explicit fetch.
|
|
1108
|
+
return await h.query('stages');
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
async getWorktrees(ensemble) {
|
|
1115
|
+
try {
|
|
1116
|
+
const h = handle((0, config_1.conductorWorkflowId)(ensemble));
|
|
1117
|
+
// #433: unbounded — justified, `getWorktrees` is not reachable
|
|
1118
|
+
// from `buildEnsembleSnapshot` (snapshot doesn't surface
|
|
1119
|
+
// worktrees). Called by `worktree` MCP tool on explicit fetch.
|
|
1120
|
+
return await h.query('worktrees');
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
return [];
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
async hasGlobalMaestro() {
|
|
1127
|
+
try {
|
|
1128
|
+
const h = handle(globalMaestroId);
|
|
1129
|
+
const desc = await h.describe();
|
|
1130
|
+
return desc.status.name === 'RUNNING';
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
// ── Maestro session (TUI-owned workflow for two-way messaging) ──
|
|
1137
|
+
async ensureMaestroSession(ensemble) {
|
|
1138
|
+
const workflowId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
1139
|
+
const sessionInput = {
|
|
1140
|
+
metadata: {
|
|
1141
|
+
playerId: 'maestro',
|
|
1142
|
+
ensemble,
|
|
1143
|
+
hostname: 'dashboard',
|
|
1144
|
+
workDir: process.cwd(),
|
|
1145
|
+
isConductor: false,
|
|
1146
|
+
agentType: 'claude',
|
|
1147
|
+
playerType: 'maestro',
|
|
1148
|
+
playerTypeDescription: 'TUI dashboard — human operator interface',
|
|
1149
|
+
},
|
|
1150
|
+
part: 'Dashboard interface (human operator)',
|
|
1151
|
+
disableStaleDetection: true,
|
|
1152
|
+
};
|
|
1153
|
+
try {
|
|
1154
|
+
const wfHandle = await client.workflow.start('agentSessionWorkflow', {
|
|
1155
|
+
workflowId,
|
|
1156
|
+
taskQueue: 'agent-tempo',
|
|
1157
|
+
args: [sessionInput],
|
|
1158
|
+
workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
|
|
1159
|
+
workflowExecutionTimeout: '24 hours',
|
|
1160
|
+
searchAttributes: {
|
|
1161
|
+
AgentTempoHostname: ['dashboard'],
|
|
1162
|
+
AgentTempoEnsemble: [ensemble],
|
|
1163
|
+
AgentTempoPlayerId: ['maestro'],
|
|
1164
|
+
AgentTempoPlayerType: ['maestro'],
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
console.error(`[tui:client] Maestro session started: ${wfHandle.workflowId}`);
|
|
1168
|
+
// Also ensure the per-ensemble Maestro hub workflow exists.
|
|
1169
|
+
// Without this, getEnsembleChat returns empty when the hub wasn't
|
|
1170
|
+
// previously created by a CLI command.
|
|
1171
|
+
const maestroHubId = (0, config_1.maestroWorkflowId)(ensemble);
|
|
1172
|
+
try {
|
|
1173
|
+
await client.workflow.start('agentMaestroWorkflow', {
|
|
1174
|
+
workflowId: maestroHubId,
|
|
1175
|
+
taskQueue: 'agent-tempo',
|
|
1176
|
+
args: [{ ensemble }],
|
|
1177
|
+
workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
|
|
1178
|
+
searchAttributes: {
|
|
1179
|
+
AgentTempoEnsemble: [ensemble],
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
console.error(`[tui:client] Maestro hub ensured: ${maestroHubId}`);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
// Maestro hub is non-critical — log but don't fail
|
|
1186
|
+
console.error(`[tui:client] Maestro hub start skipped (may already exist): ${maestroHubId}`);
|
|
1187
|
+
}
|
|
1188
|
+
return wfHandle.workflowId;
|
|
1189
|
+
}
|
|
1190
|
+
catch (err) {
|
|
1191
|
+
console.error('[tui:client] Failed to start maestro session:', err);
|
|
1192
|
+
throw err;
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
async sendAsMaestro(ensemble, targetPlayer, text) {
|
|
1196
|
+
// Resolve target player workflow via search attributes
|
|
1197
|
+
const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(targetPlayer)}"`;
|
|
1198
|
+
let targetHandle;
|
|
1199
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
1200
|
+
targetHandle = handle(wf.workflowId);
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
if (!targetHandle) {
|
|
1204
|
+
throw new Error(`Player "${targetPlayer}" not found in ensemble "${ensemble}"`);
|
|
1205
|
+
}
|
|
1206
|
+
// Signal the target with the message
|
|
1207
|
+
await targetHandle.signal('receiveMessage', { from: 'maestro', text, isMaestro: true });
|
|
1208
|
+
// Record outbound on maestro's own workflow
|
|
1209
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
1210
|
+
try {
|
|
1211
|
+
const maestroHandle = handle(maestroId);
|
|
1212
|
+
await maestroHandle.signal('recordSentMessage', { to: targetPlayer, text });
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
// Best-effort — maestro workflow may not exist yet
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
async getMaestroMessages(ensemble) {
|
|
1219
|
+
const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
|
|
1220
|
+
try {
|
|
1221
|
+
const h = handle(maestroId);
|
|
1222
|
+
// #433: unbounded (3× below) — justified, `getMaestroMessages`
|
|
1223
|
+
// is not reachable from `buildEnsembleSnapshot` (snapshot
|
|
1224
|
+
// surfaces ensemble-level chat via the bounded `getEnsembleChat`,
|
|
1225
|
+
// not the maestro session's per-message log). Called on explicit
|
|
1226
|
+
// operator fetch from the MCP `recall`/CLI inspect surfaces.
|
|
1227
|
+
// Query received messages (allMessages preferred, pendingMessages fallback)
|
|
1228
|
+
let received;
|
|
1229
|
+
try {
|
|
1230
|
+
received = await h.query('allMessages');
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
received = await h.query('pendingMessages');
|
|
1234
|
+
}
|
|
1235
|
+
// Auto-mark undelivered messages as delivered (maestro has no listener)
|
|
1236
|
+
const undeliveredIds = received.filter(m => !m.delivered).map(m => m.id);
|
|
1237
|
+
if (undeliveredIds.length > 0) {
|
|
1238
|
+
try {
|
|
1239
|
+
await h.signal('markDelivered', undeliveredIds);
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
// Best-effort
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// Query sent messages
|
|
1246
|
+
let sent;
|
|
1247
|
+
try {
|
|
1248
|
+
sent = await h.query('allSentMessages');
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
sent = [];
|
|
1252
|
+
}
|
|
1253
|
+
return { received, sent };
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
return { received: [], sent: [] };
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
};
|
|
1260
|
+
}
|