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,812 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentMaestroWorkflow = agentMaestroWorkflow;
|
|
4
|
+
exports.agentGlobalMaestroWorkflow = agentGlobalMaestroWorkflow;
|
|
5
|
+
const workflow_1 = require("@temporalio/workflow");
|
|
6
|
+
/**
|
|
7
|
+
* Workflow-deterministic clock — mirrors the helper in `src/workflows/session.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The Temporal TS SDK intercepts `new Date()` at the sandbox level so it
|
|
10
|
+
* returns replay-consistent time. `Date.now()`, however, is NOT intercepted
|
|
11
|
+
* by default — calling it from workflow code reads the host clock and breaks
|
|
12
|
+
* replay determinism. The architect's #318 review flagged the per-ensemble
|
|
13
|
+
* maestro's idle-timeout check (`Date.now() - lastActiveSessionTime > IDLE_TIMEOUT_MS`)
|
|
14
|
+
* as a pre-existing instance of this bug; this commit replaces every
|
|
15
|
+
* `Date.now()` site in `agentMaestroWorkflow` with `workflowNow().getTime()`
|
|
16
|
+
* so the determinism guarantee holds end-to-end. See CLAUDE.md
|
|
17
|
+
* ("no `Date.now()` in workflow code, use `workflow.now()` instead") and
|
|
18
|
+
* `src/workflows/session.ts:21-23` for the convention.
|
|
19
|
+
*
|
|
20
|
+
* Naming matches the session-workflow helper so a grep for `workflowNow`
|
|
21
|
+
* finds both implementations.
|
|
22
|
+
*/
|
|
23
|
+
function workflowNow() {
|
|
24
|
+
return new Date();
|
|
25
|
+
}
|
|
26
|
+
const maestro_signals_1 = require("./maestro-signals");
|
|
27
|
+
const workflow_2 = require("@temporalio/workflow");
|
|
28
|
+
const validation_1 = require("../utils/validation");
|
|
29
|
+
// ── Activity Proxies ──
|
|
30
|
+
// Only proxy activities actually used in the workflow.
|
|
31
|
+
// fetchConductorHistory is available in the activities but reserved for Phase 2 (TUI).
|
|
32
|
+
const { refreshEnsembleState, relayCommandToConductor, fetchEnsembleChat } = (0, workflow_1.proxyActivities)({
|
|
33
|
+
startToCloseTimeout: '30 seconds',
|
|
34
|
+
retry: { maximumAttempts: 3 },
|
|
35
|
+
});
|
|
36
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 5_000; // 5 seconds
|
|
37
|
+
const MAX_EVENTS = 200;
|
|
38
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes with no running sessions
|
|
39
|
+
// #399 W1 (Q5.6 Flavor B) — tempo bucket sizing.
|
|
40
|
+
const TEMPO_BUCKET_MS = 30_000; // 30s windows
|
|
41
|
+
const TEMPO_HISTORY_MAX = 60; // 60 buckets × 30s = 30-minute sparkline
|
|
42
|
+
// BPM is "messages per minute" — derived from the most recent window
|
|
43
|
+
// so the dashboard reads a stable number even between bucket rollovers.
|
|
44
|
+
const TEMPO_BPM_WINDOW_MS = 60_000;
|
|
45
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// Per-Ensemble Maestro (existing — unchanged)
|
|
47
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
async function agentMaestroWorkflow(input) {
|
|
49
|
+
(0, workflow_1.patched)('v0.17-initial');
|
|
50
|
+
const refreshIntervalMs = input.pollIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
|
|
51
|
+
let players = input.players ?? [];
|
|
52
|
+
const events = input.events ?? [];
|
|
53
|
+
const pendingCommands = input.pendingCommands ?? [];
|
|
54
|
+
let cachedChat = input.cachedChat ?? [];
|
|
55
|
+
let cachedChatMeta = input.cachedChatMeta ?? { hasConductor: false };
|
|
56
|
+
let chatHighWater = input.chatHighWater ?? maestro_signals_1.ZERO_CHAT_HIGH_WATER;
|
|
57
|
+
let shutdownRequested = false;
|
|
58
|
+
let commandQueued = false;
|
|
59
|
+
let lastActiveSessionTime = workflowNow().getTime();
|
|
60
|
+
let ensemblePaused = input.paused ?? false;
|
|
61
|
+
// #399 W1 (Q5.1) — ensemble description, restored across CAN.
|
|
62
|
+
let description = input.description ?? '';
|
|
63
|
+
// #399 W1 (Q5.3a) — first-ever start time, frozen across CAN.
|
|
64
|
+
// Use the input value if we're a continueAsNew successor; otherwise
|
|
65
|
+
// adopt the current execution's startTime as the canonical first start.
|
|
66
|
+
const startTimeIso = input.startTimeIso ?? (0, workflow_1.workflowInfo)().startTime.toISOString();
|
|
67
|
+
// #399 W1 (Q5.6 Flavor B) — activity-bucket state machine.
|
|
68
|
+
// `historyBuckets` is the sparkline (oldest first, max 60). The
|
|
69
|
+
// `currentBucket` accumulates until 30 s elapse, then rolls.
|
|
70
|
+
const tempoHistoryBuckets = input.tempoHistoryBuckets
|
|
71
|
+
? input.tempoHistoryBuckets.slice(-TEMPO_HISTORY_MAX)
|
|
72
|
+
: [];
|
|
73
|
+
let tempoCurrentBucket = input.tempoCurrentBucket ?? { startMs: workflowNow().getTime(), count: 0 };
|
|
74
|
+
// ── Coat-check (#318, ADR 0008) ─────────────────────────────────────
|
|
75
|
+
//
|
|
76
|
+
// Per-ensemble ticket-keyed stash. Restored across CAN; mutated by the
|
|
77
|
+
// four `coatCheck*` handlers below. Inline-sweep eviction runs at the
|
|
78
|
+
// start of every handler entry so an idle ensemble doesn't need a
|
|
79
|
+
// dedicated timer to GC expired entries (the 5s refresh tick below
|
|
80
|
+
// also touches it opportunistically).
|
|
81
|
+
//
|
|
82
|
+
// `putByIndex` is a reverse index of `putBy → Set<ticket>` rebuilt from
|
|
83
|
+
// the stored entries on CAN restore. It's cheap to maintain at admission
|
|
84
|
+
// time and supports the future per-host quota (researcher's flagged v1
|
|
85
|
+
// mitigation, intentionally not enforced in v1).
|
|
86
|
+
const coatCheck = { ...(input.coatCheck ?? {}) };
|
|
87
|
+
const putByIndex = new Map();
|
|
88
|
+
for (const [ticket, entry] of Object.entries(coatCheck)) {
|
|
89
|
+
const bucket = putByIndex.get(entry.putBy) ?? new Set();
|
|
90
|
+
bucket.add(ticket);
|
|
91
|
+
putByIndex.set(entry.putBy, bucket);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Drop entries whose `expiresAt` is in the past. Pure, deterministic —
|
|
95
|
+
* uses `workflowNow()`. Called at the head of every coat-check handler
|
|
96
|
+
* and opportunistically inside the main 5s refresh tick so idle ensembles
|
|
97
|
+
* still trim. Mutates `coatCheck` + `putByIndex` in place.
|
|
98
|
+
*/
|
|
99
|
+
function sweepExpiredCoatCheck() {
|
|
100
|
+
const nowMs = workflowNow().getTime();
|
|
101
|
+
for (const [ticket, entry] of Object.entries(coatCheck)) {
|
|
102
|
+
if (Date.parse(entry.expiresAt) <= nowMs) {
|
|
103
|
+
delete coatCheck[ticket];
|
|
104
|
+
const bucket = putByIndex.get(entry.putBy);
|
|
105
|
+
if (bucket) {
|
|
106
|
+
bucket.delete(ticket);
|
|
107
|
+
if (bucket.size === 0)
|
|
108
|
+
putByIndex.delete(entry.putBy);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Signal Handlers ──
|
|
114
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroShutdownSignal, () => {
|
|
115
|
+
shutdownRequested = true;
|
|
116
|
+
});
|
|
117
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroSetPausedSignal, (value) => {
|
|
118
|
+
ensemblePaused = value;
|
|
119
|
+
});
|
|
120
|
+
// #399 W1 (Q5.1) — store the description as-supplied. Length is
|
|
121
|
+
// enforced at the MCP tool boundary (`set_ensemble_description` Zod
|
|
122
|
+
// schema clamps to ENSEMBLE_DESCRIPTION_MAX from utils/validation).
|
|
123
|
+
(0, workflow_1.setHandler)(maestro_signals_1.setEnsembleDescriptionSignal, (next) => {
|
|
124
|
+
description = next;
|
|
125
|
+
});
|
|
126
|
+
// ── Query Handlers ──
|
|
127
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroPlayersQuery, () => players);
|
|
128
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroEventsQuery, () => events);
|
|
129
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroPendingCommandsQuery, () => pendingCommands);
|
|
130
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroPausedQuery, () => ensemblePaused);
|
|
131
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroEnsembleChatQuery, ({ offset = 0, limit = 50 } = {}) => {
|
|
132
|
+
const clampedLimit = Math.min(limit, 200);
|
|
133
|
+
const total = cachedChat.length;
|
|
134
|
+
const end = Math.max(0, total - offset);
|
|
135
|
+
const start = Math.max(0, end - clampedLimit);
|
|
136
|
+
return {
|
|
137
|
+
messages: cachedChat.slice(start, end),
|
|
138
|
+
total,
|
|
139
|
+
hasMore: start > 0,
|
|
140
|
+
hasConductor: cachedChatMeta.hasConductor,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
// #399 W1 (Q5.1 / Q5.3a / Q5.6) — new W1 query handlers.
|
|
144
|
+
(0, workflow_1.setHandler)(maestro_signals_1.getEnsembleDescriptionQuery, () => description);
|
|
145
|
+
(0, workflow_1.setHandler)(maestro_signals_1.getEnsembleStartTimeQuery, () => startTimeIso);
|
|
146
|
+
(0, workflow_1.setHandler)(maestro_signals_1.getCurrentBpmQuery, () => computeCurrentBpm(tempoHistoryBuckets, tempoCurrentBucket));
|
|
147
|
+
(0, workflow_1.setHandler)(maestro_signals_1.getTempoSeriesQuery, () => [...tempoHistoryBuckets]);
|
|
148
|
+
// ── Update Handler ──
|
|
149
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroSendCommandUpdate, (cmd) => {
|
|
150
|
+
const entry = {
|
|
151
|
+
id: (0, workflow_1.uuid4)(),
|
|
152
|
+
text: cmd.text,
|
|
153
|
+
source: cmd.source,
|
|
154
|
+
replyTo: cmd.replyTo,
|
|
155
|
+
createdAt: new Date().toISOString(),
|
|
156
|
+
status: 'pending',
|
|
157
|
+
};
|
|
158
|
+
pendingCommands.push(entry);
|
|
159
|
+
commandQueued = true;
|
|
160
|
+
return entry.id;
|
|
161
|
+
}, {
|
|
162
|
+
validator: (cmd) => {
|
|
163
|
+
if (!cmd.text || cmd.text.trim().length === 0) {
|
|
164
|
+
throw new Error('Command text must not be empty');
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
// ── Coat-check Handlers (#318, ADR 0008) ─────────────────────────────
|
|
169
|
+
(0, workflow_1.setHandler)(maestro_signals_1.coatCheckPutUpdate, (input) => {
|
|
170
|
+
// Sweep first so a saturation rejection reflects fresh state — a
|
|
171
|
+
// caller who fills the cap but waits past TTL of older entries should
|
|
172
|
+
// succeed on retry.
|
|
173
|
+
sweepExpiredCoatCheck();
|
|
174
|
+
const ticket = (0, workflow_1.uuid4)();
|
|
175
|
+
const nowDate = workflowNow();
|
|
176
|
+
const ttlMs = input.ttlMs ?? validation_1.COAT_CHECK_TTL_DEFAULT_MS;
|
|
177
|
+
const expiresAtMs = nowDate.getTime() + ttlMs;
|
|
178
|
+
const size = new TextEncoder().encode(input.content).length;
|
|
179
|
+
const entry = {
|
|
180
|
+
summary: input.summary,
|
|
181
|
+
content: input.content,
|
|
182
|
+
...(input.contentType !== undefined ? { contentType: input.contentType } : {}),
|
|
183
|
+
putBy: input.putBy,
|
|
184
|
+
putAt: nowDate.toISOString(),
|
|
185
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
186
|
+
size,
|
|
187
|
+
fetchCount: 0,
|
|
188
|
+
};
|
|
189
|
+
coatCheck[ticket] = entry;
|
|
190
|
+
const bucket = putByIndex.get(input.putBy) ?? new Set();
|
|
191
|
+
bucket.add(ticket);
|
|
192
|
+
putByIndex.set(input.putBy, bucket);
|
|
193
|
+
const slotsUsed = Object.keys(coatCheck).length;
|
|
194
|
+
return {
|
|
195
|
+
ticket,
|
|
196
|
+
expiresAt: entry.expiresAt,
|
|
197
|
+
slotsUsed,
|
|
198
|
+
slotsTotal: validation_1.COAT_CHECK_SLOTS_MAX,
|
|
199
|
+
};
|
|
200
|
+
}, {
|
|
201
|
+
validator: (input) => {
|
|
202
|
+
if (typeof input.summary !== 'string' || input.summary.length === 0) {
|
|
203
|
+
throw workflow_2.ApplicationFailure.nonRetryable('coat-check summary must be a non-empty string', 'CoatCheckInvalidSummary');
|
|
204
|
+
}
|
|
205
|
+
if (input.summary.length > validation_1.COAT_CHECK_SUMMARY_MAX) {
|
|
206
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check summary exceeds ${validation_1.COAT_CHECK_SUMMARY_MAX} chars`, 'CoatCheckSummaryTooLarge');
|
|
207
|
+
}
|
|
208
|
+
if (typeof input.content !== 'string') {
|
|
209
|
+
throw workflow_2.ApplicationFailure.nonRetryable('coat-check content must be a string', 'CoatCheckInvalidContent');
|
|
210
|
+
}
|
|
211
|
+
// `TextEncoder` is replay-safe (pure string→bytes); `Buffer` is Node-
|
|
212
|
+
// only and not available in the workflow sandbox. Same idiom as #334.
|
|
213
|
+
if (new TextEncoder().encode(input.content).length > validation_1.COAT_CHECK_CONTENT_MAX) {
|
|
214
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check content exceeds ${validation_1.COAT_CHECK_CONTENT_MAX} bytes`, 'CoatCheckEntryTooLarge');
|
|
215
|
+
}
|
|
216
|
+
if (input.contentType !== undefined) {
|
|
217
|
+
if (typeof input.contentType !== 'string' || input.contentType.length > validation_1.COAT_CHECK_CONTENT_TYPE_MAX) {
|
|
218
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check contentType must be a string ≤ ${validation_1.COAT_CHECK_CONTENT_TYPE_MAX} chars`, 'CoatCheckInvalidContentType');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (input.ttlMs !== undefined) {
|
|
222
|
+
if (typeof input.ttlMs !== 'number' || !Number.isFinite(input.ttlMs)
|
|
223
|
+
|| input.ttlMs < validation_1.COAT_CHECK_TTL_MIN_MS || input.ttlMs > validation_1.COAT_CHECK_TTL_MAX_MS) {
|
|
224
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check ttlMs must be a number in [${validation_1.COAT_CHECK_TTL_MIN_MS}, ${validation_1.COAT_CHECK_TTL_MAX_MS}]`, 'CoatCheckInvalidTtl');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (typeof input.putBy !== 'string' || input.putBy.length === 0) {
|
|
228
|
+
throw workflow_2.ApplicationFailure.nonRetryable('coat-check putBy must be a non-empty string', 'CoatCheckInvalidPutBy');
|
|
229
|
+
}
|
|
230
|
+
// Saturation check runs LAST + on a post-sweep view so a caller who
|
|
231
|
+
// hit saturation at T=0 but whose entries have since TTL-expired can
|
|
232
|
+
// succeed on retry. Sweep is idempotent inside the validator scope —
|
|
233
|
+
// it runs again in the handler body before admitting.
|
|
234
|
+
const probe = { ...coatCheck };
|
|
235
|
+
const nowMs = workflowNow().getTime();
|
|
236
|
+
for (const [t, e] of Object.entries(probe)) {
|
|
237
|
+
if (Date.parse(e.expiresAt) <= nowMs)
|
|
238
|
+
delete probe[t];
|
|
239
|
+
}
|
|
240
|
+
if (Object.keys(probe).length >= validation_1.COAT_CHECK_SLOTS_MAX) {
|
|
241
|
+
const oldest = Object.entries(probe)
|
|
242
|
+
.sort((a, b) => Date.parse(a[1].putAt) - Date.parse(b[1].putAt))
|
|
243
|
+
.slice(0, 3)
|
|
244
|
+
.map(([t, e]) => `${t} (putBy=${e.putBy}, putAt=${e.putAt})`)
|
|
245
|
+
.join('; ');
|
|
246
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check slots full (${validation_1.COAT_CHECK_SLOTS_MAX}). Evict one via \`coat_check_evict\` (owner-or-conductor) or wait for TTL. Oldest 3: ${oldest}`, 'CoatCheckSlotsFull');
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
(0, workflow_1.setHandler)(maestro_signals_1.coatCheckGetUpdate, ({ ticket, fetchedBy }) => {
|
|
251
|
+
sweepExpiredCoatCheck();
|
|
252
|
+
const entry = coatCheck[ticket];
|
|
253
|
+
if (!entry)
|
|
254
|
+
return null;
|
|
255
|
+
// Successful fetch — bump audit. The bump only fires on the happy path
|
|
256
|
+
// so failed lookups don't pollute the counter.
|
|
257
|
+
entry.lastFetchedAt = workflowNow().toISOString();
|
|
258
|
+
entry.lastFetchedBy = fetchedBy;
|
|
259
|
+
entry.fetchCount += 1;
|
|
260
|
+
return entry;
|
|
261
|
+
});
|
|
262
|
+
(0, workflow_1.setHandler)(maestro_signals_1.coatCheckListQuery, (input) => {
|
|
263
|
+
// Queries cannot mutate state, but inline filtering against a post-
|
|
264
|
+
// sweep VIEW gives consistent results — expired entries shouldn't
|
|
265
|
+
// appear in the listing even if they haven't been physically swept.
|
|
266
|
+
const nowMs = workflowNow().getTime();
|
|
267
|
+
const headers = [];
|
|
268
|
+
for (const [ticket, entry] of Object.entries(coatCheck)) {
|
|
269
|
+
if (Date.parse(entry.expiresAt) <= nowMs)
|
|
270
|
+
continue;
|
|
271
|
+
if (input?.putBy && entry.putBy !== input.putBy)
|
|
272
|
+
continue;
|
|
273
|
+
if (input?.prefix && !entry.summary.startsWith(input.prefix))
|
|
274
|
+
continue;
|
|
275
|
+
if (input?.unfetchedOnly && entry.fetchCount > 0)
|
|
276
|
+
continue;
|
|
277
|
+
headers.push({
|
|
278
|
+
ticket,
|
|
279
|
+
summary: entry.summary,
|
|
280
|
+
...(entry.contentType !== undefined ? { contentType: entry.contentType } : {}),
|
|
281
|
+
putBy: entry.putBy,
|
|
282
|
+
putAt: entry.putAt,
|
|
283
|
+
expiresAt: entry.expiresAt,
|
|
284
|
+
size: entry.size,
|
|
285
|
+
...(entry.lastFetchedAt !== undefined ? { lastFetchedAt: entry.lastFetchedAt } : {}),
|
|
286
|
+
...(entry.lastFetchedBy !== undefined ? { lastFetchedBy: entry.lastFetchedBy } : {}),
|
|
287
|
+
fetchCount: entry.fetchCount,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// Newest-first so the operator sees recent activity at the top.
|
|
291
|
+
headers.sort((a, b) => Date.parse(b.putAt) - Date.parse(a.putAt));
|
|
292
|
+
return headers;
|
|
293
|
+
});
|
|
294
|
+
(0, workflow_1.setHandler)(maestro_signals_1.coatCheckEvictUpdate, ({ ticket, evictedBy }) => {
|
|
295
|
+
sweepExpiredCoatCheck();
|
|
296
|
+
const entry = coatCheck[ticket];
|
|
297
|
+
if (!entry)
|
|
298
|
+
return { evicted: false };
|
|
299
|
+
// Owner-or-conductor permission gate. `players` is the closure-captured
|
|
300
|
+
// snapshot, refreshed every 5s by the main loop — good enough for an
|
|
301
|
+
// identity check (conductor identity is stable across the ensemble's
|
|
302
|
+
// lifetime). A non-owner / non-conductor evict throws structurally so
|
|
303
|
+
// the caller's MCP tool surfaces it as a permission error.
|
|
304
|
+
const isOwner = entry.putBy === evictedBy;
|
|
305
|
+
const isConductor = players.some((p) => p.isConductor && p.playerId === evictedBy);
|
|
306
|
+
if (!isOwner && !isConductor) {
|
|
307
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check evict denied: "${evictedBy}" is neither the owner ("${entry.putBy}") nor the ensemble conductor`, 'CoatCheckEvictPermissionDenied');
|
|
308
|
+
}
|
|
309
|
+
delete coatCheck[ticket];
|
|
310
|
+
const bucket = putByIndex.get(entry.putBy);
|
|
311
|
+
if (bucket) {
|
|
312
|
+
bucket.delete(ticket);
|
|
313
|
+
if (bucket.size === 0)
|
|
314
|
+
putByIndex.delete(entry.putBy);
|
|
315
|
+
}
|
|
316
|
+
return { evicted: true };
|
|
317
|
+
}, {
|
|
318
|
+
validator: ({ ticket, evictedBy }) => {
|
|
319
|
+
if (typeof ticket !== 'string' || ticket.length === 0 || ticket.length > validation_1.COAT_CHECK_TICKET_MAX
|
|
320
|
+
|| !validation_1.COAT_CHECK_TICKET_REGEX.test(ticket)) {
|
|
321
|
+
throw workflow_2.ApplicationFailure.nonRetryable(`coat-check ticket must match ${validation_1.COAT_CHECK_TICKET_REGEX} and be ≤ ${validation_1.COAT_CHECK_TICKET_MAX} chars`, 'CoatCheckInvalidTicket');
|
|
322
|
+
}
|
|
323
|
+
if (typeof evictedBy !== 'string' || evictedBy.length === 0) {
|
|
324
|
+
throw workflow_2.ApplicationFailure.nonRetryable('coat-check evictedBy must be a non-empty string', 'CoatCheckInvalidEvictedBy');
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
// ── Main Loop ──
|
|
329
|
+
while (!shutdownRequested) {
|
|
330
|
+
// Wait for either the refresh interval or a queued command
|
|
331
|
+
commandQueued = false;
|
|
332
|
+
await (0, workflow_1.condition)(() => shutdownRequested || commandQueued, `${refreshIntervalMs} milliseconds`);
|
|
333
|
+
if (shutdownRequested)
|
|
334
|
+
break;
|
|
335
|
+
// ── Refresh Ensemble State ──
|
|
336
|
+
try {
|
|
337
|
+
const newPlayers = await refreshEnsembleState(input.ensemble);
|
|
338
|
+
const nowDate = workflowNow();
|
|
339
|
+
const now = nowDate.toISOString();
|
|
340
|
+
const nowMs = nowDate.getTime();
|
|
341
|
+
// #399 W1 (Q5.6 Flavor B) — accumulate per-player activity deltas
|
|
342
|
+
// into the current bucket BEFORE replacing `players`. We diff the
|
|
343
|
+
// monotonic counter from each session's `getActivityState` query
|
|
344
|
+
// (forwarded via `MaestroPlayerInfo.activityCount`); a positive
|
|
345
|
+
// delta means the player emitted N messages since the last refresh.
|
|
346
|
+
const oldActivity = new Map(players
|
|
347
|
+
.filter((p) => p.activityCount !== undefined)
|
|
348
|
+
.map((p) => [p.playerId, p.activityCount]));
|
|
349
|
+
let bucketDelta = 0;
|
|
350
|
+
for (const np of newPlayers) {
|
|
351
|
+
if (np.activityCount === undefined)
|
|
352
|
+
continue;
|
|
353
|
+
const prev = oldActivity.get(np.playerId);
|
|
354
|
+
if (prev === undefined) {
|
|
355
|
+
// First sighting — adopt the counter as the baseline; don't
|
|
356
|
+
// back-fill the bucket with a "delta" against zero, otherwise
|
|
357
|
+
// a long-running session that just joined would spike the
|
|
358
|
+
// sparkline by its lifetime activity.
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const d = np.activityCount - prev;
|
|
362
|
+
if (d > 0)
|
|
363
|
+
bucketDelta += d;
|
|
364
|
+
}
|
|
365
|
+
if (bucketDelta > 0 || nowMs - tempoCurrentBucket.startMs >= TEMPO_BUCKET_MS) {
|
|
366
|
+
tempoCurrentBucket = rollTempoBucket(tempoCurrentBucket, bucketDelta, tempoHistoryBuckets, nowMs);
|
|
367
|
+
}
|
|
368
|
+
// Diff snapshots to generate events
|
|
369
|
+
const oldMap = new Map(players.map((p) => [p.playerId, p]));
|
|
370
|
+
const newMap = new Map(newPlayers.map((p) => [p.playerId, p]));
|
|
371
|
+
// Player joined
|
|
372
|
+
for (const [id, player] of newMap) {
|
|
373
|
+
if (!oldMap.has(id)) {
|
|
374
|
+
events.push({ type: 'player_joined', playerId: id, timestamp: now });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const old = oldMap.get(id);
|
|
378
|
+
// `status_changed` events fire on attachment-phase transitions; the
|
|
379
|
+
// event name is kept for dashboard stability (MaestroEvent wire shape).
|
|
380
|
+
if (old.phase !== player.phase) {
|
|
381
|
+
events.push({
|
|
382
|
+
type: 'status_changed',
|
|
383
|
+
playerId: id,
|
|
384
|
+
timestamp: now,
|
|
385
|
+
oldValue: old.phase,
|
|
386
|
+
newValue: player.phase,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// Part changed
|
|
390
|
+
if (old.part !== player.part) {
|
|
391
|
+
events.push({
|
|
392
|
+
type: 'part_changed',
|
|
393
|
+
playerId: id,
|
|
394
|
+
timestamp: now,
|
|
395
|
+
oldValue: old.part,
|
|
396
|
+
newValue: player.part,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Player left
|
|
402
|
+
for (const [id] of oldMap) {
|
|
403
|
+
if (!newMap.has(id)) {
|
|
404
|
+
events.push({ type: 'player_left', playerId: id, timestamp: now });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const eventsExcess = events.length - MAX_EVENTS;
|
|
408
|
+
if (eventsExcess > 0)
|
|
409
|
+
events.splice(0, eventsExcess);
|
|
410
|
+
players = newPlayers;
|
|
411
|
+
// Track last time we saw running sessions — phases the ensemble can
|
|
412
|
+
// meaningfully coordinate with (attached/processing/awaiting/booting).
|
|
413
|
+
const COORDINATABLE_PHASES = ['attached', 'processing', 'awaiting', 'booting'];
|
|
414
|
+
const hasRunningSessions = players.some((p) => p.phase !== undefined && COORDINATABLE_PHASES.includes(p.phase));
|
|
415
|
+
if (hasRunningSessions) {
|
|
416
|
+
lastActiveSessionTime = workflowNow().getTime();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Activity failure after retries — skip this cycle, try again next loop
|
|
421
|
+
}
|
|
422
|
+
// ── Refresh Ensemble Chat ──
|
|
423
|
+
if ((0, workflow_1.patched)('v0.19-ensemble-chat')) {
|
|
424
|
+
try {
|
|
425
|
+
const chatResult = await fetchEnsembleChat({
|
|
426
|
+
ensemble: input.ensemble,
|
|
427
|
+
knownCounts: chatHighWater,
|
|
428
|
+
});
|
|
429
|
+
if (chatResult.success) {
|
|
430
|
+
cachedChat.push(...chatResult.newMessages);
|
|
431
|
+
const MAX_CACHED_CHAT = 500;
|
|
432
|
+
const chatExcess = cachedChat.length - MAX_CACHED_CHAT;
|
|
433
|
+
if (chatExcess > 0)
|
|
434
|
+
cachedChat.splice(0, chatExcess);
|
|
435
|
+
chatHighWater = chatResult.currentCounts;
|
|
436
|
+
cachedChatMeta = { hasConductor: chatResult.hasConductor };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Chat refresh failed — keep stale cache, retry next cycle
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ── Dispatch Pending Commands ──
|
|
444
|
+
const pending = pendingCommands.filter((c) => c.status === 'pending');
|
|
445
|
+
for (const cmd of pending) {
|
|
446
|
+
cmd.status = 'delivered'; // optimistic — revert on failure
|
|
447
|
+
try {
|
|
448
|
+
const result = await relayCommandToConductor({
|
|
449
|
+
ensemble: input.ensemble,
|
|
450
|
+
text: cmd.text,
|
|
451
|
+
source: cmd.source,
|
|
452
|
+
replyTo: cmd.replyTo,
|
|
453
|
+
});
|
|
454
|
+
if (!result.success) {
|
|
455
|
+
cmd.status = 'failed';
|
|
456
|
+
cmd.error = result.error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
cmd.status = 'failed';
|
|
461
|
+
cmd.error = String(err);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// ── Coat-check opportunistic TTL sweep (#318) ──
|
|
465
|
+
// Idle ensembles still trim expired entries even when no caller is
|
|
466
|
+
// exercising the handlers. The sweep is also cheap (Object.entries scan)
|
|
467
|
+
// and idempotent, so it's safe to call on every refresh tick.
|
|
468
|
+
sweepExpiredCoatCheck();
|
|
469
|
+
// ── Auto-terminate if idle ──
|
|
470
|
+
// #318: was `Date.now() - lastActiveSessionTime` — non-deterministic on
|
|
471
|
+
// replay. `workflowNow()` reads the SDK-intercepted clock; idle decision
|
|
472
|
+
// is now replay-stable.
|
|
473
|
+
if (workflowNow().getTime() - lastActiveSessionTime > IDLE_TIMEOUT_MS) {
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
// ── ContinueAsNew if suggested ──
|
|
477
|
+
const info = (0, workflow_1.workflowInfo)();
|
|
478
|
+
if (info.continueAsNewSuggested) {
|
|
479
|
+
await (0, workflow_1.condition)(workflow_1.allHandlersFinished);
|
|
480
|
+
// #318: only carry coat-check across CAN when non-empty. Matches the
|
|
481
|
+
// `playerState` carry idiom in `src/workflows/session.ts:1617-1638`
|
|
482
|
+
// — keeps the wire small for the common no-coat-check case.
|
|
483
|
+
const carryCoatCheck = Object.keys(coatCheck).length > 0;
|
|
484
|
+
await (0, workflow_1.continueAsNew)({
|
|
485
|
+
ensemble: input.ensemble,
|
|
486
|
+
players,
|
|
487
|
+
events,
|
|
488
|
+
// Only carry pending commands; delivered/failed are historical
|
|
489
|
+
pendingCommands: pendingCommands.filter((c) => c.status === 'pending'),
|
|
490
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
491
|
+
cachedChat,
|
|
492
|
+
cachedChatMeta,
|
|
493
|
+
chatHighWater,
|
|
494
|
+
paused: ensemblePaused,
|
|
495
|
+
// #399 W1 — forward description, original start time, and tempo
|
|
496
|
+
// state across CAN so dashboard signals stay continuous.
|
|
497
|
+
description,
|
|
498
|
+
startTimeIso,
|
|
499
|
+
tempoHistoryBuckets,
|
|
500
|
+
tempoCurrentBucket,
|
|
501
|
+
// #318 — carry the coat-check map only when populated.
|
|
502
|
+
...(carryCoatCheck ? { coatCheck } : {}),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Graceful shutdown — wait for in-flight handlers
|
|
507
|
+
await (0, workflow_1.condition)(workflow_1.allHandlersFinished);
|
|
508
|
+
}
|
|
509
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
510
|
+
// #399 W1 (Q5.6 Flavor B) — tempo bucket helpers.
|
|
511
|
+
//
|
|
512
|
+
// `rollTempoBucket` runs in workflow context and is therefore
|
|
513
|
+
// deterministic — `nowMs` is supplied by the caller via `Date.now()`
|
|
514
|
+
// during the (workflow-time) refresh tick, never sampled inside the
|
|
515
|
+
// helper. The current bucket finalises (gets pushed onto the history
|
|
516
|
+
// ring) once it covers a full `TEMPO_BUCKET_MS` window OR when the next
|
|
517
|
+
// refresh sees activity that should land in a fresh bucket — either way
|
|
518
|
+
// we never lose a window.
|
|
519
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
520
|
+
function rollTempoBucket(current, pendingDelta, history, nowMs) {
|
|
521
|
+
const elapsed = nowMs - current.startMs;
|
|
522
|
+
if (elapsed >= TEMPO_BUCKET_MS) {
|
|
523
|
+
history.push(current.count);
|
|
524
|
+
while (history.length > TEMPO_HISTORY_MAX)
|
|
525
|
+
history.shift();
|
|
526
|
+
return { startMs: nowMs, count: pendingDelta };
|
|
527
|
+
}
|
|
528
|
+
return { startMs: current.startMs, count: current.count + pendingDelta };
|
|
529
|
+
}
|
|
530
|
+
function computeCurrentBpm(history, current) {
|
|
531
|
+
// Sum activity in the last `TEMPO_BPM_WINDOW_MS` (1 minute):
|
|
532
|
+
// - the in-progress bucket always counts
|
|
533
|
+
// - prior history buckets count proportionally to how recent they are
|
|
534
|
+
// We approximate by taking the last two finished buckets (60s of
|
|
535
|
+
// history at 30s/bucket) plus the current. That over-counts slightly
|
|
536
|
+
// when the current bucket is fresh — acceptable for a heads-up display.
|
|
537
|
+
const tail = history.slice(-2);
|
|
538
|
+
const sum = tail.reduce((a, b) => a + b, 0) + current.count;
|
|
539
|
+
return sum;
|
|
540
|
+
}
|
|
541
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
542
|
+
// Global Maestro — single instance handling ALL ensembles
|
|
543
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
544
|
+
const GLOBAL_MAX_MESSAGES = 500;
|
|
545
|
+
const GLOBAL_MAX_EVENTS = 500;
|
|
546
|
+
const globalActivities = (0, workflow_1.proxyActivities)({
|
|
547
|
+
startToCloseTimeout: '30 seconds',
|
|
548
|
+
retry: { maximumAttempts: 3 },
|
|
549
|
+
});
|
|
550
|
+
async function agentGlobalMaestroWorkflow(input) {
|
|
551
|
+
(0, workflow_1.patched)('v0.18-global-maestro');
|
|
552
|
+
const refreshIntervalMs = input.pollIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
|
|
553
|
+
let knownEnsembles = input.knownEnsembles ?? [];
|
|
554
|
+
let playersByEnsemble = input.playersByEnsemble ?? {};
|
|
555
|
+
const recentMessages = input.recentMessages ?? [];
|
|
556
|
+
const events = input.events ?? [];
|
|
557
|
+
const pendingCommands = input.pendingCommands ?? [];
|
|
558
|
+
// #274 — host capability ledger. Plain `Record<hostname, HostProfile>`
|
|
559
|
+
// so CAN serialization is the default-converter happy path (Maps require
|
|
560
|
+
// a codec tweak). Lazy GC of stale hosts happens at the `listHosts` join
|
|
561
|
+
// site; the workflow itself just stores forever (or until CAN).
|
|
562
|
+
const hostProfiles = { ...(input.hostProfiles ?? {}) };
|
|
563
|
+
let shutdownRequested = false;
|
|
564
|
+
let actionQueued = false;
|
|
565
|
+
// ── Signal Handlers ──
|
|
566
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroShutdownSignal, () => {
|
|
567
|
+
shutdownRequested = true;
|
|
568
|
+
});
|
|
569
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroNotifyMessageSignal, (msg) => {
|
|
570
|
+
recentMessages.push(msg);
|
|
571
|
+
const msgExcess = recentMessages.length - GLOBAL_MAX_MESSAGES;
|
|
572
|
+
if (msgExcess > 0)
|
|
573
|
+
recentMessages.splice(0, msgExcess);
|
|
574
|
+
});
|
|
575
|
+
/**
|
|
576
|
+
* #274 — daemon boot signal carrying the host's capability profile.
|
|
577
|
+
*
|
|
578
|
+
* Validation policy per architect delta AC3c (M9): ONLY `hostname` is
|
|
579
|
+
* validated here (required, ≤64 chars, alphanumeric + `_-`). All other
|
|
580
|
+
* fields are stored opaquely — the per-field Zod guard lives at the
|
|
581
|
+
* `listHosts` join site in `src/utils/hosts.ts`, never at this
|
|
582
|
+
* handler. This keeps the workflow additive-compatible across daemon
|
|
583
|
+
* versions: a newer daemon can signal new fields and older maestros
|
|
584
|
+
* will still accept the payload; an older daemon's payload is accepted
|
|
585
|
+
* by a newer maestro without special-casing.
|
|
586
|
+
*
|
|
587
|
+
* The hostname regex is inlined (rather than importing from
|
|
588
|
+
* `src/utils/validation.ts`) to keep the workflow bundle's import
|
|
589
|
+
* surface narrow — it's pure constants either way.
|
|
590
|
+
*/
|
|
591
|
+
(0, workflow_1.setHandler)(maestro_signals_1.hostProfileSignal, (payload) => {
|
|
592
|
+
if (!payload || typeof payload !== 'object')
|
|
593
|
+
return;
|
|
594
|
+
const hostname = payload.hostname;
|
|
595
|
+
if (typeof hostname !== 'string')
|
|
596
|
+
return;
|
|
597
|
+
if (hostname.length === 0 || hostname.length > 64)
|
|
598
|
+
return;
|
|
599
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(hostname))
|
|
600
|
+
return;
|
|
601
|
+
// Cast is deliberate: `HostProfile` has an open `[extraField]: unknown`
|
|
602
|
+
// index signature, so the spread is semantically safe. TypeScript can't
|
|
603
|
+
// prove narrower optional fields from the spread alone.
|
|
604
|
+
hostProfiles[hostname] = { ...payload, hostname };
|
|
605
|
+
});
|
|
606
|
+
// ── Query Handlers ──
|
|
607
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroEnsemblesQuery, () => knownEnsembles);
|
|
608
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroPlayersByEnsembleQuery, () => ({ ...playersByEnsemble }));
|
|
609
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroRecentMessagesQuery, () => recentMessages);
|
|
610
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroEventsQuery, () => events);
|
|
611
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroPendingCommandsQuery, () => pendingCommands);
|
|
612
|
+
// Return a defensive copy so callers can't mutate workflow state.
|
|
613
|
+
(0, workflow_1.setHandler)(maestro_signals_1.hostProfilesQuery, () => ({ ...hostProfiles }));
|
|
614
|
+
// #280 — combined query: reaching this handler proves the workflow is
|
|
615
|
+
// running, so `exists` is always `true` here. Callers infer "missing"
|
|
616
|
+
// by catching transport-level errors (workflow not found, etc.).
|
|
617
|
+
(0, workflow_1.setHandler)(maestro_signals_1.hostProfilesWithExistenceQuery, () => ({
|
|
618
|
+
exists: true,
|
|
619
|
+
profiles: { ...hostProfiles },
|
|
620
|
+
}));
|
|
621
|
+
// ── Update Handlers (can await activities) ──
|
|
622
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroSendMessageUpdate, async (req) => {
|
|
623
|
+
const msgId = (0, workflow_1.uuid4)();
|
|
624
|
+
const result = await globalActivities.deliverMaestroMessage({
|
|
625
|
+
ensemble: req.ensemble,
|
|
626
|
+
to: req.to,
|
|
627
|
+
text: req.text,
|
|
628
|
+
source: req.source,
|
|
629
|
+
});
|
|
630
|
+
if (!result.success) {
|
|
631
|
+
throw new Error(result.error ?? 'Failed to deliver message');
|
|
632
|
+
}
|
|
633
|
+
// Record in ring buffer
|
|
634
|
+
const relayMsg = {
|
|
635
|
+
id: msgId,
|
|
636
|
+
ensemble: req.ensemble,
|
|
637
|
+
from: req.source,
|
|
638
|
+
to: req.to,
|
|
639
|
+
text: req.text,
|
|
640
|
+
timestamp: new Date().toISOString(),
|
|
641
|
+
direction: 'outbound',
|
|
642
|
+
};
|
|
643
|
+
recentMessages.push(relayMsg);
|
|
644
|
+
const relayExcess = recentMessages.length - GLOBAL_MAX_MESSAGES;
|
|
645
|
+
if (relayExcess > 0)
|
|
646
|
+
recentMessages.splice(0, relayExcess);
|
|
647
|
+
return msgId;
|
|
648
|
+
}, {
|
|
649
|
+
validator: (req) => {
|
|
650
|
+
if (!req.ensemble || !req.ensemble.trim())
|
|
651
|
+
throw new Error('Ensemble must not be empty');
|
|
652
|
+
if (!req.to || !req.to.trim())
|
|
653
|
+
throw new Error('Target player must not be empty');
|
|
654
|
+
if (!req.text || !req.text.trim())
|
|
655
|
+
throw new Error('Message text must not be empty');
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroFetchPlayerMessagesUpdate, async (req) => {
|
|
659
|
+
const result = await globalActivities.fetchPlayerMessages({
|
|
660
|
+
ensemble: req.ensemble,
|
|
661
|
+
playerId: req.playerId,
|
|
662
|
+
});
|
|
663
|
+
if (!result.success) {
|
|
664
|
+
throw new Error(result.error ?? 'Failed to fetch player messages');
|
|
665
|
+
}
|
|
666
|
+
return result.messages;
|
|
667
|
+
}, {
|
|
668
|
+
validator: (req) => {
|
|
669
|
+
if (!req.ensemble || !req.ensemble.trim())
|
|
670
|
+
throw new Error('Ensemble must not be empty');
|
|
671
|
+
if (!req.playerId || !req.playerId.trim())
|
|
672
|
+
throw new Error('Player ID must not be empty');
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroFetchConductorHistoryUpdate, async (req) => {
|
|
676
|
+
const result = await globalActivities.fetchConductorHistory({
|
|
677
|
+
ensemble: req.ensemble,
|
|
678
|
+
});
|
|
679
|
+
return result;
|
|
680
|
+
}, {
|
|
681
|
+
validator: (req) => {
|
|
682
|
+
if (!req.ensemble || !req.ensemble.trim())
|
|
683
|
+
throw new Error('Ensemble must not be empty');
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
(0, workflow_1.setHandler)(maestro_signals_1.maestroGlobalSendCommandUpdate, (cmd) => {
|
|
687
|
+
const entry = {
|
|
688
|
+
id: (0, workflow_1.uuid4)(),
|
|
689
|
+
text: cmd.text,
|
|
690
|
+
source: cmd.source,
|
|
691
|
+
replyTo: cmd.replyTo,
|
|
692
|
+
createdAt: new Date().toISOString(),
|
|
693
|
+
status: 'pending',
|
|
694
|
+
};
|
|
695
|
+
entry.ensemble = cmd.ensemble;
|
|
696
|
+
pendingCommands.push(entry);
|
|
697
|
+
actionQueued = true;
|
|
698
|
+
return entry.id;
|
|
699
|
+
}, {
|
|
700
|
+
validator: (cmd) => {
|
|
701
|
+
if (!cmd.ensemble || !cmd.ensemble.trim())
|
|
702
|
+
throw new Error('Ensemble must not be empty');
|
|
703
|
+
if (!cmd.text || !cmd.text.trim())
|
|
704
|
+
throw new Error('Command text must not be empty');
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
// ── Main Loop ──
|
|
708
|
+
while (!shutdownRequested) {
|
|
709
|
+
actionQueued = false;
|
|
710
|
+
await (0, workflow_1.condition)(() => shutdownRequested || actionQueued, `${refreshIntervalMs} milliseconds`);
|
|
711
|
+
if (shutdownRequested)
|
|
712
|
+
break;
|
|
713
|
+
// ── Discover ensembles ──
|
|
714
|
+
try {
|
|
715
|
+
knownEnsembles = await globalActivities.discoverEnsembles();
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// Discovery failed — use last known ensembles
|
|
719
|
+
}
|
|
720
|
+
// ── Refresh players per ensemble ──
|
|
721
|
+
for (const ensemble of knownEnsembles) {
|
|
722
|
+
try {
|
|
723
|
+
const newPlayers = await globalActivities.refreshEnsembleState(ensemble);
|
|
724
|
+
const oldPlayers = playersByEnsemble[ensemble] ?? [];
|
|
725
|
+
const now = new Date().toISOString();
|
|
726
|
+
// Diff snapshots to generate events
|
|
727
|
+
const oldMap = new Map(oldPlayers.map((p) => [p.playerId, p]));
|
|
728
|
+
const newMap = new Map(newPlayers.map((p) => [p.playerId, p]));
|
|
729
|
+
for (const [id, player] of newMap) {
|
|
730
|
+
if (!oldMap.has(id)) {
|
|
731
|
+
events.push({ type: 'player_joined', playerId: id, timestamp: now });
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
const old = oldMap.get(id);
|
|
735
|
+
// Diffs attachment-phase values (see per-ensemble Maestro comment).
|
|
736
|
+
if (old.phase !== player.phase) {
|
|
737
|
+
events.push({ type: 'status_changed', playerId: id, timestamp: now, oldValue: old.phase, newValue: player.phase });
|
|
738
|
+
}
|
|
739
|
+
if (old.part !== player.part) {
|
|
740
|
+
events.push({ type: 'part_changed', playerId: id, timestamp: now, oldValue: old.part, newValue: player.part });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
for (const [id] of oldMap) {
|
|
745
|
+
if (!newMap.has(id)) {
|
|
746
|
+
events.push({ type: 'player_left', playerId: id, timestamp: now });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
playersByEnsemble[ensemble] = newPlayers;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// Activity failure — keep last known state for this ensemble
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Remove stale ensembles (no longer discovered)
|
|
756
|
+
for (const key of Object.keys(playersByEnsemble)) {
|
|
757
|
+
if (!knownEnsembles.includes(key)) {
|
|
758
|
+
delete playersByEnsemble[key];
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const globalEventsExcess = events.length - GLOBAL_MAX_EVENTS;
|
|
762
|
+
if (globalEventsExcess > 0)
|
|
763
|
+
events.splice(0, globalEventsExcess);
|
|
764
|
+
// ── Dispatch Pending Commands ──
|
|
765
|
+
const pending = pendingCommands.filter((c) => c.status === 'pending');
|
|
766
|
+
for (const cmd of pending) {
|
|
767
|
+
const ensemble = cmd.ensemble;
|
|
768
|
+
if (!ensemble) {
|
|
769
|
+
cmd.status = 'failed';
|
|
770
|
+
cmd.error = 'Missing ensemble';
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
cmd.status = 'delivered';
|
|
774
|
+
try {
|
|
775
|
+
const result = await globalActivities.relayCommandToConductor({
|
|
776
|
+
ensemble,
|
|
777
|
+
text: cmd.text,
|
|
778
|
+
source: cmd.source,
|
|
779
|
+
replyTo: cmd.replyTo,
|
|
780
|
+
});
|
|
781
|
+
if (!result.success) {
|
|
782
|
+
cmd.status = 'failed';
|
|
783
|
+
cmd.error = result.error;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
cmd.status = 'failed';
|
|
788
|
+
cmd.error = String(err);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// ── ContinueAsNew if suggested ──
|
|
792
|
+
const info = (0, workflow_1.workflowInfo)();
|
|
793
|
+
if (info.continueAsNewSuggested) {
|
|
794
|
+
await (0, workflow_1.condition)(workflow_1.allHandlersFinished);
|
|
795
|
+
await (0, workflow_1.continueAsNew)({
|
|
796
|
+
knownEnsembles,
|
|
797
|
+
playersByEnsemble,
|
|
798
|
+
recentMessages,
|
|
799
|
+
events,
|
|
800
|
+
pendingCommands: pendingCommands.filter((c) => c.status === 'pending'),
|
|
801
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
802
|
+
// #274 — carry the capability ledger across the CAN boundary so
|
|
803
|
+
// hosts that signaled their profile in a prior execution don't
|
|
804
|
+
// disappear on the next one (they won't re-signal until the next
|
|
805
|
+
// daemon boot).
|
|
806
|
+
hostProfiles,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// Graceful shutdown
|
|
811
|
+
await (0, workflow_1.condition)(workflow_1.allHandlersFinished);
|
|
812
|
+
}
|