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,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnsembleNotFoundError = exports.SNAPSHOT_CHAT_LIMIT = void 0;
|
|
4
|
+
exports.toPlayerSummaryV1 = toPlayerSummaryV1;
|
|
5
|
+
exports.buildEnsembleSnapshot = buildEnsembleSnapshot;
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
const event_types_1 = require("./event-types");
|
|
8
|
+
/**
|
|
9
|
+
* Runtime type guard — `MaestroPlayerInfo.agentType` is intentionally typed
|
|
10
|
+
* as open `string` so a forked daemon advertising a never-shipped adapter
|
|
11
|
+
* (e.g. `'gemini'`) doesn't crash the projection at the type level. The
|
|
12
|
+
* guard narrows the open-string input to the closed wire union before it
|
|
13
|
+
* reaches `PlayerSummaryV1`. Anything not in `AGENT_TYPES` (the canonical
|
|
14
|
+
* source-of-truth list at `src/types.ts`) falls back to `'claude'` in the
|
|
15
|
+
* caller — same defensive default as pre-#535, just over a wider whitelist.
|
|
16
|
+
*/
|
|
17
|
+
function isWireAgentType(s) {
|
|
18
|
+
return types_1.AGENT_TYPES.includes(s);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Maximum chat messages embedded in the snapshot. Larger paging is left
|
|
22
|
+
* to the explicit `getEnsembleChat` query / `recall` tool — the snapshot
|
|
23
|
+
* is a steady-state view, not a paginator.
|
|
24
|
+
*/
|
|
25
|
+
exports.SNAPSHOT_CHAT_LIMIT = 50;
|
|
26
|
+
/** Thrown when the ensemble doesn't exist; route handler maps to 404. */
|
|
27
|
+
class EnsembleNotFoundError extends Error {
|
|
28
|
+
ensemble;
|
|
29
|
+
constructor(ensemble) {
|
|
30
|
+
super(`ensemble not found: ${ensemble}`);
|
|
31
|
+
this.ensemble = ensemble;
|
|
32
|
+
this.name = 'EnsembleNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.EnsembleNotFoundError = EnsembleNotFoundError;
|
|
36
|
+
/**
|
|
37
|
+
* Fan-out helper — run all client queries in parallel and tolerate
|
|
38
|
+
* per-query failures (an empty array / `false` is the right fallback for
|
|
39
|
+
* every soft-failure case the existing TempoClient already returns).
|
|
40
|
+
*/
|
|
41
|
+
async function fanOut(queries) {
|
|
42
|
+
const keys = Object.keys(queries);
|
|
43
|
+
const results = await Promise.all(keys.map((key) => queries[key]().catch(() => undefined)));
|
|
44
|
+
const out = {};
|
|
45
|
+
keys.forEach((key, i) => {
|
|
46
|
+
out[key] = results[i];
|
|
47
|
+
});
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Project a `MaestroPlayerInfo` into the wire-stable `PlayerSummaryV1`.
|
|
52
|
+
* Drops fields that aren't part of the v1 contract; passes `agentType`
|
|
53
|
+
* through verbatim — the wire union mirrors {@link AgentType} from
|
|
54
|
+
* `src/types.ts`, so every shipped adapter projects to its own label
|
|
55
|
+
* (#535). Pre-#535 the wire union was closed at `'claude' | 'copilot' |
|
|
56
|
+
* 'mock'` and headless adapters were coerced to `'claude'`, which made
|
|
57
|
+
* them indistinguishable from interactive Claude Code players in the
|
|
58
|
+
* dashboard. The union expansion is additive per the §6 stability rule
|
|
59
|
+
* in `event-types.ts` — no `/v1/` → `/v2/` bump.
|
|
60
|
+
*
|
|
61
|
+
* `wireMeta` is the session-level projection from
|
|
62
|
+
* `TempoClient.getPlayerWireMeta` (Issue #399 W2). Pass `null` (or omit)
|
|
63
|
+
* when the session workflow couldn't be queried — the projection then
|
|
64
|
+
* carries no `runId` / `messaging` / `lease` fields and the dashboard
|
|
65
|
+
* renders `—` placeholders.
|
|
66
|
+
*
|
|
67
|
+
* `activityCount` passes through from `MaestroPlayerInfo` (Q5.6).
|
|
68
|
+
* `MaestroPlayerInfo.lastActivityAt` maps to the wire field
|
|
69
|
+
* `PlayerSummaryV1.lastHeartbeatAt` — same data, rename happens at the
|
|
70
|
+
* wire boundary so consumers read one canonical name.
|
|
71
|
+
*/
|
|
72
|
+
function toPlayerSummaryV1(p, wireMeta = null) {
|
|
73
|
+
// `MaestroPlayerInfo.agentType` is intentionally open (`string`) so a
|
|
74
|
+
// forked daemon advertising a never-shipped adapter doesn't crash the
|
|
75
|
+
// type system here. `isWireAgentType` narrows it against `AGENT_TYPES`
|
|
76
|
+
// (the canonical source-of-truth list); anything outside falls back to
|
|
77
|
+
// `'claude'` — same defensive default as pre-#535, just enforced over
|
|
78
|
+
// a wider whitelist. The drift detector in `test/snapshot.test.ts`
|
|
79
|
+
// asserts the wire union mirrors `AgentType`, so a future shipped
|
|
80
|
+
// adapter missing from one of the two surfaces fails CI.
|
|
81
|
+
const agentType = isWireAgentType(p.agentType) ? p.agentType : 'claude';
|
|
82
|
+
return {
|
|
83
|
+
playerId: p.playerId,
|
|
84
|
+
ensemble: p.ensemble,
|
|
85
|
+
hostname: p.hostname,
|
|
86
|
+
isConductor: p.isConductor,
|
|
87
|
+
agentType,
|
|
88
|
+
...(p.playerType !== undefined ? { playerType: p.playerType } : {}),
|
|
89
|
+
...(p.phase !== undefined ? { phase: p.phase } : {}),
|
|
90
|
+
part: p.part ?? '',
|
|
91
|
+
workDir: p.workDir ?? '',
|
|
92
|
+
...(p.gitBranch !== undefined ? { gitBranch: p.gitBranch } : {}),
|
|
93
|
+
// Issue #399 Q5.6 — pass-through from MaestroPlayerInfo.
|
|
94
|
+
...(p.activityCount !== undefined ? { activityCount: p.activityCount } : {}),
|
|
95
|
+
// Wire-name rename: source `lastActivityAt` → contract `lastHeartbeatAt`
|
|
96
|
+
// (#389 R3.P1.4). Aggregate's phase-change diff reads the wire name.
|
|
97
|
+
...(p.lastActivityAt !== undefined ? { lastHeartbeatAt: p.lastActivityAt } : {}),
|
|
98
|
+
// Issue #399 W2 — session-query fan-out merged in when reachable.
|
|
99
|
+
...(wireMeta?.runId !== undefined ? { runId: wireMeta.runId } : {}),
|
|
100
|
+
...(wireMeta?.messaging !== undefined ? { messaging: wireMeta.messaging } : {}),
|
|
101
|
+
...(wireMeta?.lease !== undefined ? { lease: wireMeta.lease } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the `/v1/state/:ensemble` payload. Throws
|
|
106
|
+
* {@link EnsembleNotFoundError} when the requested ensemble has no live
|
|
107
|
+
* workflows; route handler turns that into a 404.
|
|
108
|
+
*/
|
|
109
|
+
async function buildEnsembleSnapshot(client, ensemble, opts = {}) {
|
|
110
|
+
// Existence gate — `listEnsembles` is fast (single workflow.list scan)
|
|
111
|
+
// and returns the canonical state classification (online/paused/offline).
|
|
112
|
+
const ensembles = await client.listEnsembles().catch(() => []);
|
|
113
|
+
const summary = ensembles.find((e) => e.name === ensemble);
|
|
114
|
+
if (!summary)
|
|
115
|
+
throw new EnsembleNotFoundError(ensemble);
|
|
116
|
+
// Fan-out the rest in parallel. Each soft-fails to a sane default — the
|
|
117
|
+
// PR-1 snapshot must NEVER 500 just because one downstream query glitched.
|
|
118
|
+
// Issue #399 DB1a adds `meta` (4 maestro queries) to the ensemble-level
|
|
119
|
+
// fan-out; per-player wire-meta is fanned out below once `players`
|
|
120
|
+
// resolves so we know which session ids to query.
|
|
121
|
+
const fanned = await fanOut({
|
|
122
|
+
players: () => client.getPlayers(ensemble),
|
|
123
|
+
chat: () => client.getEnsembleChat(ensemble, 0, exports.SNAPSHOT_CHAT_LIMIT),
|
|
124
|
+
schedules: () => client.getSchedules(ensemble),
|
|
125
|
+
paused: () => client.isMaestroPaused(ensemble),
|
|
126
|
+
held: () => client.isAnySessionHeld(ensemble),
|
|
127
|
+
hosts: () => client.listHosts(),
|
|
128
|
+
meta: () => client.getEnsembleMeta(ensemble),
|
|
129
|
+
});
|
|
130
|
+
// Issue #399 W2 — fan-out 3 session queries per player in parallel.
|
|
131
|
+
// For a 12-player ensemble that's 36 queries; running them concurrently
|
|
132
|
+
// keeps total snapshot latency bounded by the slowest single session
|
|
133
|
+
// query, not the sum. `getPlayerWireMeta` returns `null` when the
|
|
134
|
+
// session workflow can't be reached — `toPlayerSummaryV1` gracefully
|
|
135
|
+
// omits the wire-meta fields in that case.
|
|
136
|
+
const playerInfos = fanned.players ?? [];
|
|
137
|
+
const wireMetas = await Promise.all(playerInfos.map((p) => client.getPlayerWireMeta(ensemble, p.playerId).catch(() => null)));
|
|
138
|
+
const players = playerInfos.map((p, i) => toPlayerSummaryV1(p, wireMetas[i]));
|
|
139
|
+
const chat = fanned.chat ?? { messages: [], total: 0, hasMore: false, hasConductor: false };
|
|
140
|
+
const schedules = fanned.schedules ?? [];
|
|
141
|
+
const paused = fanned.paused === true;
|
|
142
|
+
const held = fanned.held === true;
|
|
143
|
+
const hostProfiles = {};
|
|
144
|
+
for (const h of fanned.hosts ?? []) {
|
|
145
|
+
if (h.profile)
|
|
146
|
+
hostProfiles[h.hostname] = h.profile;
|
|
147
|
+
}
|
|
148
|
+
// Issue #399 W1 — sentinel defaults when the maestro fan-out soft-failed
|
|
149
|
+
// entirely. `getEnsembleMeta` already soft-fails individual queries to
|
|
150
|
+
// their sentinels; this branch covers the outer `fanOut` swallow when
|
|
151
|
+
// the whole call rejected.
|
|
152
|
+
const meta = fanned.meta ?? {
|
|
153
|
+
description: '',
|
|
154
|
+
startedAt: '',
|
|
155
|
+
currentBpm: 0,
|
|
156
|
+
tempoSeries: [],
|
|
157
|
+
};
|
|
158
|
+
const capturedAt = (opts.now?.() ?? new Date()).toISOString();
|
|
159
|
+
return {
|
|
160
|
+
v: 1,
|
|
161
|
+
ensemble,
|
|
162
|
+
capturedAt,
|
|
163
|
+
lastEventId: event_types_1.PR1_SENTINEL_EVENT_ID,
|
|
164
|
+
state: summary.state ?? 'online',
|
|
165
|
+
hasConductor: summary.hasConductor,
|
|
166
|
+
flags: { paused, held },
|
|
167
|
+
players,
|
|
168
|
+
schedules,
|
|
169
|
+
chat: {
|
|
170
|
+
messages: chat.messages,
|
|
171
|
+
total: chat.total,
|
|
172
|
+
hasMore: chat.hasMore,
|
|
173
|
+
},
|
|
174
|
+
hostProfiles,
|
|
175
|
+
description: meta.description,
|
|
176
|
+
startedAt: meta.startedAt,
|
|
177
|
+
currentBpm: meta.currentBpm,
|
|
178
|
+
tempoSeries: meta.tempoSeries,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE handler — wires `EnsembleEventBus` to the HTTP socket.
|
|
3
|
+
*
|
|
4
|
+
* **Responsibilities**:
|
|
5
|
+
* - Frame `BusEvent` into SSE `id:` / `event:` / `data:` lines (§5).
|
|
6
|
+
* - Decide the §7.2 connection-flow branch:
|
|
7
|
+
* - no `Last-Event-ID` → emit `snapshot` (per-ensemble only), then live tail
|
|
8
|
+
* - epoch mismatch → emit `gap` with `reason: 'epoch-mismatch'`, live tail
|
|
9
|
+
* - epoch match, `seq < ringStart` → emit `gap` with `reason: 'overflow'`, live tail
|
|
10
|
+
* - epoch match, `seq >= ringStart` → replay `[seq+1 … newest]`, live tail
|
|
11
|
+
* - Per-connection 1 MiB write-buffer cap (§7.3) with destroy-on-overflow.
|
|
12
|
+
* - Process-wide connection cap (`AGENT_TEMPO_SSE_MAX_CONNECTIONS`,
|
|
13
|
+
* default 100); over-cap → `503 Service Unavailable`, `Retry-After: 5`.
|
|
14
|
+
* - Cancellation: socket close drops the subscription within one
|
|
15
|
+
* event-loop tick.
|
|
16
|
+
*/
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
18
|
+
import type { TempoClient } from '../client/interface';
|
|
19
|
+
import { EnsembleEventBus, type BusEvent, type EventBus, type Subscription, type TopicCategory } from './event-bus';
|
|
20
|
+
/** Default per-process SSE connection cap. */
|
|
21
|
+
export declare const DEFAULT_MAX_CONNECTIONS = 100;
|
|
22
|
+
/** Per-connection write-buffer ceiling (§7.3). */
|
|
23
|
+
export declare const PER_CONNECTION_BUFFER_LIMIT: number;
|
|
24
|
+
/** Counts open SSE connections. Exported for `/v1/health.subscriberCount`. */
|
|
25
|
+
export declare class ConnectionCap {
|
|
26
|
+
readonly limit: number;
|
|
27
|
+
private current;
|
|
28
|
+
constructor(limit?: number);
|
|
29
|
+
size(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Try to take a slot. Returns `true` when the connection is
|
|
32
|
+
* accepted; `false` when over the cap.
|
|
33
|
+
*/
|
|
34
|
+
acquire(): boolean;
|
|
35
|
+
release(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* SSE frame format (§5). Returns the byte buffer that goes on the wire.
|
|
39
|
+
*
|
|
40
|
+
* Multi-line payloads are split on `\n` so each becomes its own
|
|
41
|
+
* `data:` line per the WHATWG spec — but in practice the daemon
|
|
42
|
+
* always emits single-line JSON, so this branch is defensive.
|
|
43
|
+
*/
|
|
44
|
+
export declare function frameSseEvent(event: BusEvent): Buffer;
|
|
45
|
+
/**
|
|
46
|
+
* Same on-the-wire shape as {@link frameSseEvent} but takes the wire
|
|
47
|
+
* fields directly — for callers (e.g. fixture mode) that don't have a
|
|
48
|
+
* full `BusEvent` to project from.
|
|
49
|
+
*/
|
|
50
|
+
export declare function frameSseEventData(eventId: string, type: string, payload: unknown): Buffer;
|
|
51
|
+
/**
|
|
52
|
+
* Frame a synthetic event-id-less prelude — `: <comment>\n\n`. Used for
|
|
53
|
+
* the initial keepalive nudge some intermediaries need before the
|
|
54
|
+
* first real event.
|
|
55
|
+
*/
|
|
56
|
+
export declare function frameSseComment(comment: string): Buffer;
|
|
57
|
+
/**
|
|
58
|
+
* Write the §1 SSE response headers and the initial keepalive comment.
|
|
59
|
+
* Shared by {@link handleSseRequest} and the fixture-mode handler so
|
|
60
|
+
* the wire-level handshake stays in one place.
|
|
61
|
+
*/
|
|
62
|
+
export declare function openSseResponse(res: ServerResponse, comment?: string): void;
|
|
63
|
+
/** Parse `?topics=phase,chat,...` query string into a Set, or `undefined`. */
|
|
64
|
+
export declare function parseTopicQuery(raw: string | string[] | undefined): Set<TopicCategory> | undefined;
|
|
65
|
+
export interface HandleSseOptions {
|
|
66
|
+
client: TempoClient;
|
|
67
|
+
bus: EventBus;
|
|
68
|
+
/**
|
|
69
|
+
* `true` for `/v1/events/:ensemble` — emits `snapshot` on fresh
|
|
70
|
+
* connect. `false` for `/v1/events` global stream which has no
|
|
71
|
+
* snapshot semantics (§6 table).
|
|
72
|
+
*/
|
|
73
|
+
emitSnapshot: boolean;
|
|
74
|
+
/** Ensemble name when `emitSnapshot` is true; ignored otherwise. */
|
|
75
|
+
ensemble?: string;
|
|
76
|
+
/** Process-wide connection cap. */
|
|
77
|
+
cap: ConnectionCap;
|
|
78
|
+
/** Test seam for the per-connection write buffer ceiling. */
|
|
79
|
+
bufferLimit?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Handle an SSE request. Long-lived — resolves only when the client
|
|
83
|
+
* disconnects, the bus closes, or the per-connection buffer overflows.
|
|
84
|
+
*/
|
|
85
|
+
export declare function handleSseRequest(req: IncomingMessage, res: ServerResponse, opts: HandleSseOptions): Promise<void>;
|
|
86
|
+
/** Type guard re-export to satisfy TS without circular refs. */
|
|
87
|
+
export type { EnsembleEventBus, Subscription };
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectionCap = exports.PER_CONNECTION_BUFFER_LIMIT = exports.DEFAULT_MAX_CONNECTIONS = void 0;
|
|
4
|
+
exports.frameSseEvent = frameSseEvent;
|
|
5
|
+
exports.frameSseEventData = frameSseEventData;
|
|
6
|
+
exports.frameSseComment = frameSseComment;
|
|
7
|
+
exports.openSseResponse = openSseResponse;
|
|
8
|
+
exports.parseTopicQuery = parseTopicQuery;
|
|
9
|
+
exports.handleSseRequest = handleSseRequest;
|
|
10
|
+
const snapshot_1 = require("./snapshot");
|
|
11
|
+
const event_id_1 = require("./event-id");
|
|
12
|
+
/** Default per-process SSE connection cap. */
|
|
13
|
+
exports.DEFAULT_MAX_CONNECTIONS = 100;
|
|
14
|
+
/** Per-connection write-buffer ceiling (§7.3). */
|
|
15
|
+
exports.PER_CONNECTION_BUFFER_LIMIT = 1 * 1024 * 1024;
|
|
16
|
+
const log = (...args) => console.error('[agent-tempo:sse]', ...args);
|
|
17
|
+
/** Counts open SSE connections. Exported for `/v1/health.subscriberCount`. */
|
|
18
|
+
class ConnectionCap {
|
|
19
|
+
limit;
|
|
20
|
+
current = 0;
|
|
21
|
+
constructor(limit = exports.DEFAULT_MAX_CONNECTIONS) {
|
|
22
|
+
this.limit = limit;
|
|
23
|
+
}
|
|
24
|
+
size() { return this.current; }
|
|
25
|
+
/**
|
|
26
|
+
* Try to take a slot. Returns `true` when the connection is
|
|
27
|
+
* accepted; `false` when over the cap.
|
|
28
|
+
*/
|
|
29
|
+
acquire() {
|
|
30
|
+
if (this.current >= this.limit)
|
|
31
|
+
return false;
|
|
32
|
+
this.current++;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
release() {
|
|
36
|
+
if (this.current > 0)
|
|
37
|
+
this.current--;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.ConnectionCap = ConnectionCap;
|
|
41
|
+
/**
|
|
42
|
+
* SSE frame format (§5). Returns the byte buffer that goes on the wire.
|
|
43
|
+
*
|
|
44
|
+
* Multi-line payloads are split on `\n` so each becomes its own
|
|
45
|
+
* `data:` line per the WHATWG spec — but in practice the daemon
|
|
46
|
+
* always emits single-line JSON, so this branch is defensive.
|
|
47
|
+
*/
|
|
48
|
+
function frameSseEvent(event) {
|
|
49
|
+
return frameSseEventData(event.eventId, event.type, event.payload);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Same on-the-wire shape as {@link frameSseEvent} but takes the wire
|
|
53
|
+
* fields directly — for callers (e.g. fixture mode) that don't have a
|
|
54
|
+
* full `BusEvent` to project from.
|
|
55
|
+
*/
|
|
56
|
+
function frameSseEventData(eventId, type, payload) {
|
|
57
|
+
const lines = [
|
|
58
|
+
`id: ${eventId}`,
|
|
59
|
+
`event: ${type}`,
|
|
60
|
+
];
|
|
61
|
+
const data = JSON.stringify({ v: 1, eventId, payload });
|
|
62
|
+
// Single-line JSON — but defend against line breaks in payloads.
|
|
63
|
+
for (const piece of data.split('\n'))
|
|
64
|
+
lines.push(`data: ${piece}`);
|
|
65
|
+
return Buffer.from(lines.join('\n') + '\n\n', 'utf8');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Frame a synthetic event-id-less prelude — `: <comment>\n\n`. Used for
|
|
69
|
+
* the initial keepalive nudge some intermediaries need before the
|
|
70
|
+
* first real event.
|
|
71
|
+
*/
|
|
72
|
+
function frameSseComment(comment) {
|
|
73
|
+
return Buffer.from(`: ${comment}\n\n`, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Write the §1 SSE response headers and the initial keepalive comment.
|
|
77
|
+
* Shared by {@link handleSseRequest} and the fixture-mode handler so
|
|
78
|
+
* the wire-level handshake stays in one place.
|
|
79
|
+
*/
|
|
80
|
+
function openSseResponse(res, comment = 'agent-tempo SSE') {
|
|
81
|
+
res.writeHead(200, {
|
|
82
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
83
|
+
'Cache-Control': 'no-store',
|
|
84
|
+
Connection: 'keep-alive',
|
|
85
|
+
'X-Accel-Buffering': 'no', // disable buffering on nginx + friends
|
|
86
|
+
});
|
|
87
|
+
res.flushHeaders?.();
|
|
88
|
+
res.write(frameSseComment(comment));
|
|
89
|
+
}
|
|
90
|
+
/** Parse `?topics=phase,chat,...` query string into a Set, or `undefined`. */
|
|
91
|
+
function parseTopicQuery(raw) {
|
|
92
|
+
if (!raw)
|
|
93
|
+
return undefined;
|
|
94
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
95
|
+
const known = new Set(['phase', 'chat', 'flags', 'schedules', 'heartbeat']);
|
|
96
|
+
const out = new Set();
|
|
97
|
+
for (const tok of value.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
98
|
+
if (known.has(tok))
|
|
99
|
+
out.add(tok);
|
|
100
|
+
}
|
|
101
|
+
return out.size > 0 ? out : undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Pull a single string from a possibly-array header. */
|
|
104
|
+
function headerString(v) {
|
|
105
|
+
if (Array.isArray(v))
|
|
106
|
+
return v[0];
|
|
107
|
+
return v;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Handle an SSE request. Long-lived — resolves only when the client
|
|
111
|
+
* disconnects, the bus closes, or the per-connection buffer overflows.
|
|
112
|
+
*/
|
|
113
|
+
async function handleSseRequest(req, res, opts) {
|
|
114
|
+
// Connection cap.
|
|
115
|
+
if (!opts.cap.acquire()) {
|
|
116
|
+
res.writeHead(503, {
|
|
117
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
118
|
+
'Retry-After': '5',
|
|
119
|
+
});
|
|
120
|
+
res.end(JSON.stringify({ error: 'connection-cap-exceeded' }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Always release the cap on exit.
|
|
124
|
+
let capReleased = false;
|
|
125
|
+
const releaseCap = () => {
|
|
126
|
+
if (!capReleased) {
|
|
127
|
+
capReleased = true;
|
|
128
|
+
opts.cap.release();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// Open SSE response (headers per §1 + initial keepalive comment).
|
|
132
|
+
openSseResponse(res);
|
|
133
|
+
// Determine §7.2 entry branch.
|
|
134
|
+
const lastEventIdRaw = headerString(req.headers['last-event-id']);
|
|
135
|
+
const requested = (0, event_id_1.parseEventId)(lastEventIdRaw);
|
|
136
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
137
|
+
const topics = parseTopicQuery(url.searchParams.get('topics') ?? undefined);
|
|
138
|
+
// Subscribe — afterSeq is determined by branch decisions below. The
|
|
139
|
+
// bus's subscribe call replays `seq > afterSeq` events from the ring.
|
|
140
|
+
let afterSeq;
|
|
141
|
+
let preludeEvents = [];
|
|
142
|
+
if (requested) {
|
|
143
|
+
if (requested.epoch !== opts.bus.bootEpoch) {
|
|
144
|
+
// Epoch mismatch → gap.
|
|
145
|
+
preludeEvents.push(synthEvent(opts.bus, 'gap', {
|
|
146
|
+
from: lastEventIdRaw ?? '',
|
|
147
|
+
to: makeIdToken(opts.bus, opts.bus.nextSeq()),
|
|
148
|
+
reason: 'epoch-mismatch',
|
|
149
|
+
}));
|
|
150
|
+
// Don't replay — client will re-fetch /v1/state and reconnect.
|
|
151
|
+
afterSeq = undefined;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const ringStart = opts.bus.oldestSeq();
|
|
155
|
+
if (ringStart === null || requested.seq + 1 < ringStart) {
|
|
156
|
+
// Overflow → gap. `ringStart === null` happens when the bus
|
|
157
|
+
// has emitted nothing yet — semantically "I have no events
|
|
158
|
+
// to replay you" rather than the strict overflow case
|
|
159
|
+
// (client predates the ring's oldest entry). The wire spec
|
|
160
|
+
// §6 only defines two `gap` reasons (`epoch-mismatch` and
|
|
161
|
+
// `overflow`), so we use `overflow` for both flavors. The
|
|
162
|
+
// recovery path is identical: re-fetch `/v1/state` and
|
|
163
|
+
// reconnect with the snapshot's `lastEventId`. PR #324
|
|
164
|
+
// review nit — flagged for a possible §6 spec extension
|
|
165
|
+
// (`reason: 'empty-ring'`) if a future PR-3 consumer can use
|
|
166
|
+
// the distinction; for now collapsing keeps the wire stable.
|
|
167
|
+
preludeEvents.push(synthEvent(opts.bus, 'gap', {
|
|
168
|
+
from: lastEventIdRaw ?? '',
|
|
169
|
+
to: makeIdToken(opts.bus, opts.bus.nextSeq()),
|
|
170
|
+
reason: 'overflow',
|
|
171
|
+
}));
|
|
172
|
+
afterSeq = undefined;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Replay range — bus.subscribe will pull from the ring.
|
|
176
|
+
afterSeq = requested.seq;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (opts.emitSnapshot) {
|
|
181
|
+
// No Last-Event-ID + per-ensemble stream → emit snapshot.
|
|
182
|
+
if (!opts.ensemble) {
|
|
183
|
+
res.end();
|
|
184
|
+
releaseCap();
|
|
185
|
+
throw new Error('handleSseRequest: ensemble required when emitSnapshot=true');
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const snap = await (0, snapshot_1.buildEnsembleSnapshot)(opts.client, opts.ensemble);
|
|
189
|
+
preludeEvents.push(synthEvent(opts.bus, 'snapshot', snap));
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err instanceof snapshot_1.EnsembleNotFoundError) {
|
|
193
|
+
// The route handler validates ensemble existence before calling
|
|
194
|
+
// this, but the ensemble could go away between resolution and
|
|
195
|
+
// snapshot fetch — return a 404-equivalent SSE close with a
|
|
196
|
+
// helpful body comment.
|
|
197
|
+
res.write(frameSseComment(`ensemble-not-found ${opts.ensemble}`));
|
|
198
|
+
res.end();
|
|
199
|
+
releaseCap();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
log('snapshot fetch failed:', err instanceof Error ? err.message : err);
|
|
203
|
+
// Non-fatal — let the bus do the live tail; consumer is no worse off.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const sub = opts.bus.subscribe({ ...(afterSeq !== undefined ? { afterSeq } : {}), ...(topics ? { topics } : {}) });
|
|
207
|
+
let closed = false;
|
|
208
|
+
const close = () => {
|
|
209
|
+
if (closed)
|
|
210
|
+
return;
|
|
211
|
+
closed = true;
|
|
212
|
+
try {
|
|
213
|
+
sub.close();
|
|
214
|
+
}
|
|
215
|
+
catch { /* ignore */ }
|
|
216
|
+
try {
|
|
217
|
+
res.end();
|
|
218
|
+
}
|
|
219
|
+
catch { /* ignore */ }
|
|
220
|
+
releaseCap();
|
|
221
|
+
};
|
|
222
|
+
// Bind socket lifecycle. `req.on('close')` covers TCP drop + abort.
|
|
223
|
+
req.on('close', close);
|
|
224
|
+
res.on('close', close);
|
|
225
|
+
// Stream loop.
|
|
226
|
+
try {
|
|
227
|
+
// Prelude (snapshot / gap) goes first.
|
|
228
|
+
for (const ev of preludeEvents) {
|
|
229
|
+
if (closed)
|
|
230
|
+
return;
|
|
231
|
+
if (!writeOrDrop(res, ev, opts.bufferLimit ?? exports.PER_CONNECTION_BUFFER_LIMIT, close))
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for await (const ev of sub) {
|
|
235
|
+
if (closed)
|
|
236
|
+
return;
|
|
237
|
+
if (!writeOrDrop(res, ev, opts.bufferLimit ?? exports.PER_CONNECTION_BUFFER_LIMIT, close))
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Per-event write with §7.3 backpressure. Returns `false` when the connection was destroyed. */
|
|
246
|
+
function writeOrDrop(res, event, bufferLimit, close) {
|
|
247
|
+
const payload = frameSseEvent(event);
|
|
248
|
+
// Track per-socket buffered bytes via Node's `writableLength`.
|
|
249
|
+
const socket = res.socket;
|
|
250
|
+
if (socket && socket.writableLength + payload.length > bufferLimit) {
|
|
251
|
+
log('connection write buffer exceeded — dropping subscriber');
|
|
252
|
+
try {
|
|
253
|
+
socket.destroy();
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
close();
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
res.write(payload);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Build a synthetic `BusEvent` not allocated by the bus's `emit`
|
|
264
|
+
* pipeline — used only for the connection prelude (snapshot / gap)
|
|
265
|
+
* which is per-connection rather than per-bus. Allocates a sentinel
|
|
266
|
+
* `eventId` based on the bus's current `nextSeq` minus one so the
|
|
267
|
+
* client's local `Last-Event-ID` tracking stays internally
|
|
268
|
+
* consistent without polluting the ring.
|
|
269
|
+
*
|
|
270
|
+
* NOTE: the eventId on a synthetic event is for client-side ordering
|
|
271
|
+
* only — the daemon will never reuse this id from its allocator.
|
|
272
|
+
* Per §7.2 the snapshot/gap re-bridge means the client always
|
|
273
|
+
* follows up with a fresh `/v1/state/:ensemble` fetch on `gap` and
|
|
274
|
+
* normalizes its Last-Event-ID against the snapshot's `lastEventId`.
|
|
275
|
+
*/
|
|
276
|
+
function synthEvent(bus, type, payload) {
|
|
277
|
+
// Pick an id strictly less than the bus's next allocation so it
|
|
278
|
+
// sorts before any real event. Synthetic prelude events are an
|
|
279
|
+
// implementation detail of the per-connection wire and are never
|
|
280
|
+
// expected to be addressed via Last-Event-ID resume.
|
|
281
|
+
const seq = Math.max(0, bus.nextSeq() - 1);
|
|
282
|
+
const tuple = { epoch: bus.bootEpoch, seq };
|
|
283
|
+
return {
|
|
284
|
+
eventId: `${tuple.epoch}:${tuple.seq}`,
|
|
285
|
+
tuple,
|
|
286
|
+
type,
|
|
287
|
+
payload,
|
|
288
|
+
emittedAt: Date.now(),
|
|
289
|
+
bufferable: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function makeIdToken(bus, seq) {
|
|
293
|
+
return `${bus.bootEpoch}:${seq}`;
|
|
294
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon HTTP write surface — PR-7a of #340.
|
|
3
|
+
*
|
|
4
|
+
* Adds five POST routes under `/v1/ensembles/:ensemble/{cue, pause,
|
|
5
|
+
* play, release, recruit}`. Each handler is a thin shim over the
|
|
6
|
+
* existing {@link TempoClient} method the daemon already has in scope
|
|
7
|
+
* (the same `ctx.client` used for snapshots).
|
|
8
|
+
*
|
|
9
|
+
* **Why now**: PR-1 → PR-6 of the dashboard shipped a read-only
|
|
10
|
+
* surface; PR-7b wires the dashboard's disabled CTAs to real submit
|
|
11
|
+
* handlers. Those handlers need an HTTP write API — until now the
|
|
12
|
+
* daemon was GET-only by design. See the full background in
|
|
13
|
+
* `docs/SSE-PROTOCOL.md` § 11b.
|
|
14
|
+
*
|
|
15
|
+
* **Auth posture** matches the read side:
|
|
16
|
+
* - Loopback bind + no `Origin` header → no auth (TUI/CLI parity; the
|
|
17
|
+
* TUI already writes via Temporal directly anyway, so loopback-no-auth
|
|
18
|
+
* is equivalent risk).
|
|
19
|
+
* - Non-loopback bind OR cross-origin browser → bearer required.
|
|
20
|
+
*
|
|
21
|
+
* **Body validation** is strict — every field shape is checked before
|
|
22
|
+
* the handler reaches the Temporal layer. Bad bodies fast-fail with
|
|
23
|
+
* 400; the underlying Temporal calls aren't asked to validate things
|
|
24
|
+
* the HTTP layer can catch.
|
|
25
|
+
*
|
|
26
|
+
* **Error mapping**: `Error('No session found …')` → 404 not-found;
|
|
27
|
+
* any other thrown `Error` → 500 (logged at the dispatcher).
|
|
28
|
+
*/
|
|
29
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
30
|
+
import type { TempoClient } from '../client/interface';
|
|
31
|
+
import { WRITE_BODY_MAX } from './body';
|
|
32
|
+
export { WRITE_BODY_MAX };
|
|
33
|
+
/**
|
|
34
|
+
* Names of the write actions exposed under `/v1/ensembles/:ensemble/<action>`.
|
|
35
|
+
*
|
|
36
|
+
* Two semantic groups, kept in this order so the table reads top-to-bottom
|
|
37
|
+
* by surface intent:
|
|
38
|
+
* - **Ensemble-scoped** (cue / pause / play / release / recruit) — the
|
|
39
|
+
* original PR-7a #340 surface; bodies don't carry `playerId`.
|
|
40
|
+
* - **Per-player destructive** (restart / destroy / detach / recall) —
|
|
41
|
+
* added so the dashboard's PlayerDetail action row can wire to live
|
|
42
|
+
* mutations. Bodies are uniform `{ playerId, reason? }` (plus per-action
|
|
43
|
+
* extras); the ensemble lives in the URL.
|
|
44
|
+
*/
|
|
45
|
+
export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "destroy", "detach", "recall"];
|
|
46
|
+
export type WriteAction = (typeof WRITE_ACTIONS)[number];
|
|
47
|
+
/** Type guard — narrows an arbitrary string to a known `WriteAction`. */
|
|
48
|
+
export declare function isWriteAction(s: string): s is WriteAction;
|
|
49
|
+
/**
|
|
50
|
+
* Top-level dispatch — called from `server.ts` when the URL matches
|
|
51
|
+
* `/v1/ensembles/:ensemble/<action>` with a known `<action>`. The
|
|
52
|
+
* caller has already bearer/CORS-gated the request; this function
|
|
53
|
+
* trusts the gates and focuses on body parsing + per-action routing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function handleWriteRoute(req: IncomingMessage, res: ServerResponse, client: TempoClient, ensemble: string, action: WriteAction): Promise<void>;
|