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
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.writePidFileAtomic = writePidFileAtomic;
|
|
38
|
+
exports.warnIfDevNamespaceDrift = warnIfDevNamespaceDrift;
|
|
39
|
+
exports.ensureDevNamespace = ensureDevNamespace;
|
|
40
|
+
exports.computeHostProfile = computeHostProfile;
|
|
41
|
+
exports.scrubHostProfile = scrubHostProfile;
|
|
42
|
+
exports.advertiseHostProfile = advertiseHostProfile;
|
|
43
|
+
exports.runDaemonBoot = runDaemonBoot;
|
|
44
|
+
exports.reconcileOnBoot = reconcileOnBoot;
|
|
45
|
+
exports.formatMemoryUsage = formatMemoryUsage;
|
|
46
|
+
exports.startMemoryReporter = startMemoryReporter;
|
|
47
|
+
exports.selectStaleDetachedOrphans = selectStaleDetachedOrphans;
|
|
48
|
+
exports.cleanupLoop = cleanupLoop;
|
|
49
|
+
exports.startCleanupLoop = startCleanupLoop;
|
|
50
|
+
/**
|
|
51
|
+
* Daemon entry point — runs Temporal workers in a detached background process.
|
|
52
|
+
*
|
|
53
|
+
* Started by `startDaemon()` in `src/cli/daemon.ts`.
|
|
54
|
+
* Config is passed via environment variables set by the parent.
|
|
55
|
+
*
|
|
56
|
+
* Writes its PID to ~/.agent-tempo/daemon.pid on startup and removes it
|
|
57
|
+
* on graceful shutdown (SIGTERM/SIGINT).
|
|
58
|
+
*/
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const os = __importStar(require("os"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const promises_1 = require("timers/promises");
|
|
63
|
+
const client_1 = require("@temporalio/client");
|
|
64
|
+
const client_2 = require("@temporalio/client");
|
|
65
|
+
const config_1 = require("./config");
|
|
66
|
+
const dev_banner_1 = require("./cli/dev-banner");
|
|
67
|
+
const worker_1 = require("./worker");
|
|
68
|
+
const connection_1 = require("./connection");
|
|
69
|
+
const daemon_1 = require("./cli/daemon");
|
|
70
|
+
const client_3 = require("./client");
|
|
71
|
+
const orphans_1 = require("./reconcile/orphans");
|
|
72
|
+
const agent_types_1 = require("./ensemble/agent-types");
|
|
73
|
+
const pre_flight_1 = require("./adapters/claude-code-headless/pre-flight");
|
|
74
|
+
const daemon_adapter_versions_1 = require("./daemon-adapter-versions");
|
|
75
|
+
const log = (...args) => console.error(`[agent-tempo:daemon ${new Date().toISOString()}]`, ...args);
|
|
76
|
+
/**
|
|
77
|
+
* Daemon process start time, captured at module load. Issue #399 Q5.3b
|
|
78
|
+
* advertises this on every `hostProfile` signal as
|
|
79
|
+
* {@link HostProfile.daemonStartedAt} so the dashboard's Hosts table
|
|
80
|
+
* can render `now - daemonStartedAt` as the daemon-process uptime.
|
|
81
|
+
*
|
|
82
|
+
* Captured here (top-of-module) rather than inside `computeHostProfile`
|
|
83
|
+
* so a refresh-host-profile invocation later in the daemon's lifetime
|
|
84
|
+
* still advertises the original boot time. Module load happens once
|
|
85
|
+
* per daemon process; the value is effectively the daemon's birth time.
|
|
86
|
+
*/
|
|
87
|
+
const DAEMON_STARTED_AT = Date.now();
|
|
88
|
+
/**
|
|
89
|
+
* Atomically write the daemon PID file via `writeFile(tmp) + rename(tmp, final)`.
|
|
90
|
+
*
|
|
91
|
+
* A racing reader (a CLI invocation that happens to poll during startup) will
|
|
92
|
+
* either see the previous file or the new one — never a half-written one.
|
|
93
|
+
*
|
|
94
|
+
* Windows sometimes fails the rename with EPERM/EBUSY/EACCES if an antivirus
|
|
95
|
+
* scanner or the previous handle is still active. We retry with short backoffs
|
|
96
|
+
* before giving up so a transient scanner doesn't crash startup.
|
|
97
|
+
*
|
|
98
|
+
* Exported for unit testing.
|
|
99
|
+
*/
|
|
100
|
+
async function writePidFileAtomic(pidFilePath, pid) {
|
|
101
|
+
const tmp = `${pidFilePath}.tmp.${process.pid}`;
|
|
102
|
+
fs.writeFileSync(tmp, String(pid));
|
|
103
|
+
const retryCodes = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
104
|
+
const backoffs = [50, 100, 200, 400]; // ms — total ≤ 750ms, bounded for startup
|
|
105
|
+
let lastErr;
|
|
106
|
+
for (let attempt = 0; attempt <= backoffs.length; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
fs.renameSync(tmp, pidFilePath);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
lastErr = err;
|
|
113
|
+
const code = err.code;
|
|
114
|
+
if (!code || !retryCodes.has(code) || attempt === backoffs.length) {
|
|
115
|
+
try {
|
|
116
|
+
fs.unlinkSync(tmp);
|
|
117
|
+
}
|
|
118
|
+
catch { /* ignore */ }
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
await (0, promises_1.setTimeout)(backoffs[attempt]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Unreachable — loop either returns or throws.
|
|
125
|
+
throw lastErr;
|
|
126
|
+
}
|
|
127
|
+
// ── Dev profile (ADR 0014 §6.2) ──
|
|
128
|
+
/**
|
|
129
|
+
* Runtime drift detector — #423 PR-A Fix 3. When dev mode is active but
|
|
130
|
+
* the resolved namespace is NOT the dev default, an explicit override is
|
|
131
|
+
* in play (CLI `--namespace`, `~/.agent-tempo-dev/config.json`, or — if
|
|
132
|
+
* the env-var carve-out from Fix 1 ever regressed — a leaked shell var).
|
|
133
|
+
* Either way, the operator deserves a load-bearing diagnostic so the
|
|
134
|
+
* "banner says X, daemon connects to Y" drift doesn't recur silently.
|
|
135
|
+
*
|
|
136
|
+
* Pure function over the resolved Config + an injected log sink so the
|
|
137
|
+
* unit test can capture the message without a live daemon process.
|
|
138
|
+
* Returns whether a warning fired so callers (and tests) can assert on
|
|
139
|
+
* the branch directly.
|
|
140
|
+
*
|
|
141
|
+
* The check is intentionally loose: any namespace mismatch in dev mode
|
|
142
|
+
* triggers the warning, even for intentional overrides. We can't tell
|
|
143
|
+
* a typo'd `config.json` entry from a deliberate one — the warning is
|
|
144
|
+
* cheap, and an operator who overrode the namespace on purpose can
|
|
145
|
+
* grep-skip a single line.
|
|
146
|
+
*/
|
|
147
|
+
function warnIfDevNamespaceDrift(config, logFn = log) {
|
|
148
|
+
if (!(0, config_1.isDevMode)())
|
|
149
|
+
return false;
|
|
150
|
+
if (config.temporalNamespace === config_1.DEV_TEMPORAL_NAMESPACE)
|
|
151
|
+
return false;
|
|
152
|
+
logFn(`[dev-mode] WARNING: namespace drift — connecting to "${config.temporalNamespace}" ` +
|
|
153
|
+
`instead of dev default "${config_1.DEV_TEMPORAL_NAMESPACE}". An explicit override is in ` +
|
|
154
|
+
`play (CLI flag, dev config.json). Drop the override to restore dev profile isolation ` +
|
|
155
|
+
`(ADR 0014 §5.1).`);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Auto-create the dev profile's Temporal namespace on dev daemon boot
|
|
160
|
+
* (ADR 0014 §6.2). Idempotent — calling it on every boot is correct and
|
|
161
|
+
* cheap.
|
|
162
|
+
*
|
|
163
|
+
* - `ALREADY_EXISTS`: the steady state after the first boot. Happy path.
|
|
164
|
+
* - `PERMISSION_DENIED`: e.g. managed Temporal Cloud where `RegisterNamespace`
|
|
165
|
+
* isn't granted. Log + return; the subsequent worker bootstrap fails
|
|
166
|
+
* loudly with `Namespace not found` and the operator can run
|
|
167
|
+
* `temporal operator namespace create -n agent-tempo-dev` themselves.
|
|
168
|
+
* - any other error: same fall-through; daemon stays alive without
|
|
169
|
+
* mutating state.
|
|
170
|
+
*
|
|
171
|
+
* Production daemons never call this — guarded by `isDevMode()` at the
|
|
172
|
+
* single callsite in `main()` below. Exported for direct unit testing
|
|
173
|
+
* with an injected stub workflow service.
|
|
174
|
+
*/
|
|
175
|
+
async function ensureDevNamespace(connection, namespace, logFn = log) {
|
|
176
|
+
const wfService = connection.workflowService;
|
|
177
|
+
try {
|
|
178
|
+
await wfService.registerNamespace({
|
|
179
|
+
namespace,
|
|
180
|
+
// 1-day retention is generous for dev scratch state and keeps the
|
|
181
|
+
// namespace tidy without aggressive cleanup pressure. The proto's
|
|
182
|
+
// `seconds` field is typed as `Long` (int64), but the gRPC layer
|
|
183
|
+
// accepts a plain number and coerces internally — same shape used
|
|
184
|
+
// by Temporal's own examples. Cast keeps the call site readable
|
|
185
|
+
// without dragging `long.js` into our direct dep graph.
|
|
186
|
+
workflowExecutionRetentionPeriod: { seconds: 86_400 },
|
|
187
|
+
description: 'agent-tempo dev profile — auto-created. Safe to drop.',
|
|
188
|
+
});
|
|
189
|
+
logFn(`[dev-mode] registered Temporal namespace "${namespace}"`);
|
|
190
|
+
return { ok: true, status: 'created' };
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
const code = err?.code
|
|
195
|
+
?? err?.details?.code;
|
|
196
|
+
// ALREADY_EXISTS — happy path on every boot after the first. The
|
|
197
|
+
// gRPC code is 6; the Temporal SDK also surfaces it as a string in
|
|
198
|
+
// some transports, so check both shapes plus a substring fallback.
|
|
199
|
+
if (code === 'ALREADY_EXISTS' || code === 6 || /already.?exists/i.test(message)) {
|
|
200
|
+
logFn(`[dev-mode] Temporal namespace "${namespace}" already registered`);
|
|
201
|
+
return { ok: true, status: 'already-exists' };
|
|
202
|
+
}
|
|
203
|
+
// PERMISSION_DENIED — e.g. managed Temporal Cloud without RegisterNamespace.
|
|
204
|
+
// Log a hint so operators know what to do.
|
|
205
|
+
if (code === 'PERMISSION_DENIED' || code === 7 || /permission/i.test(message)) {
|
|
206
|
+
logFn(`[dev-mode] could not register namespace "${namespace}" — permission denied. ` +
|
|
207
|
+
`Run \`temporal operator namespace create -n ${namespace}\` (or grant RegisterNamespace) once.`);
|
|
208
|
+
return { ok: false, status: 'permission-denied', message };
|
|
209
|
+
}
|
|
210
|
+
logFn(`[dev-mode] could not register namespace "${namespace}" (continuing; worker may fail with a clearer error):`, message);
|
|
211
|
+
return { ok: false, status: 'error', message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Ensure the global Maestro workflow is running.
|
|
216
|
+
* Uses USE_EXISTING conflict policy so it's safe to call on every daemon start.
|
|
217
|
+
*/
|
|
218
|
+
async function ensureGlobalMaestro(config) {
|
|
219
|
+
try {
|
|
220
|
+
const connection = await (0, connection_1.createTemporalConnection)(config);
|
|
221
|
+
const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
|
|
222
|
+
const input = {};
|
|
223
|
+
await client.workflow.start('agentGlobalMaestroWorkflow', {
|
|
224
|
+
workflowId: config_1.GLOBAL_MAESTRO_WORKFLOW_ID,
|
|
225
|
+
taskQueue: config.taskQueue,
|
|
226
|
+
args: [input],
|
|
227
|
+
workflowIdConflictPolicy: client_2.WorkflowIdConflictPolicy.USE_EXISTING,
|
|
228
|
+
});
|
|
229
|
+
log(`Global Maestro ensured (id: ${config_1.GLOBAL_MAESTRO_WORKFLOW_ID})`);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
// Non-fatal — the global maestro is optional for basic operation
|
|
233
|
+
log('Failed to ensure global Maestro (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// #274 — host capability profile: compute → scrub → signal
|
|
238
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* Daemon package version, lazily read from `package.json` so the test
|
|
241
|
+
* build (which compiles daemon.ts into `dist-test/src/` where
|
|
242
|
+
* `../package.json` doesn't resolve) can exercise daemon-boot logic
|
|
243
|
+
* without MODULE_NOT_FOUND. Tests that exercise `computeHostProfile`
|
|
244
|
+
* pass a stubbed version via `HostProfile.version` on the input.
|
|
245
|
+
*/
|
|
246
|
+
function daemonVersion() {
|
|
247
|
+
try {
|
|
248
|
+
const { version } = require('../package.json');
|
|
249
|
+
return version;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return 'unknown';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Build the daemon's capability profile from its config + runtime env.
|
|
257
|
+
* Result is NOT scrubbed — call `scrubHostProfile` before signaling.
|
|
258
|
+
*
|
|
259
|
+
* Exported for testability; production callers go through
|
|
260
|
+
* `runDaemonBoot(client, deps)` which provides this as the default
|
|
261
|
+
* `computeHostProfile` dep.
|
|
262
|
+
*/
|
|
263
|
+
function computeHostProfile(config, deps = {}) {
|
|
264
|
+
const resolveCopilotSync = deps.resolveCopilotSdkVersionSync ?? daemon_adapter_versions_1.resolveCopilotSdkVersionSync;
|
|
265
|
+
const agentTypes = (() => {
|
|
266
|
+
try {
|
|
267
|
+
return (0, agent_types_1.listAgentTypes)().map((a) => a.name);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// listAgentTypes reads the filesystem; treat any failure as "no
|
|
271
|
+
// discoverable types" rather than crashing boot.
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
// Build the available-agents array. Always includes the configured
|
|
276
|
+
// default. #520 — additionally probe whether `claude` is installed AND
|
|
277
|
+
// logged in; if both pass, advertise `'claude-code-headless'` so
|
|
278
|
+
// cross-host recruit pre-flight can reject early on hosts without the
|
|
279
|
+
// CLI configured. Bounded by the probe timeouts (3s + 5s = ≤8s worst
|
|
280
|
+
// case) — acceptable boot cost for a one-shot probe.
|
|
281
|
+
const availableAgentTypes = [config.defaultAgent];
|
|
282
|
+
try {
|
|
283
|
+
const binProbe = (0, pre_flight_1.probeClaudeBinary)(config.claudeBin ?? 'claude');
|
|
284
|
+
if (binProbe.ok) {
|
|
285
|
+
const authProbe = (0, pre_flight_1.probeClaudeAuth)(config.claudeBin ?? 'claude');
|
|
286
|
+
if (authProbe.loggedIn && !availableAgentTypes.includes('claude-code-headless')) {
|
|
287
|
+
availableAgentTypes.push('claude-code-headless');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Probe machinery should never throw, but guard anyway — host-profile
|
|
293
|
+
// computation is on the critical boot path.
|
|
294
|
+
}
|
|
295
|
+
// #532 PR-2 — copilot probe. Reuses the same sync require-source-of-
|
|
296
|
+
// truth as `defaultResolveCopilotSdkVersion` (which it delegates to);
|
|
297
|
+
// resolves to a version string when `@github/copilot-sdk` is
|
|
298
|
+
// installed, or `undefined` when missing. Closes the gap where
|
|
299
|
+
// cross-host recruit of `agent: 'copilot'` was rejected with a
|
|
300
|
+
// misleading "host cannot run copilot" message even on hosts where
|
|
301
|
+
// the SDK was installed and Copilot was logged in. Pattern mirrors
|
|
302
|
+
// the claude-code-headless block above.
|
|
303
|
+
try {
|
|
304
|
+
const copilotVersion = resolveCopilotSync();
|
|
305
|
+
if (copilotVersion && !availableAgentTypes.includes('copilot')) {
|
|
306
|
+
availableAgentTypes.push('copilot');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Defensive — `resolveCopilotSdkVersionSync` already swallows
|
|
311
|
+
// require failures, but the boot path must not crash on any
|
|
312
|
+
// surprise here.
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
hostname: os.hostname(),
|
|
316
|
+
version: daemonVersion(),
|
|
317
|
+
defaultAgent: config.defaultAgent,
|
|
318
|
+
// #520 + #532 PR-2 — was: `[config.defaultAgent]`. Now grows when
|
|
319
|
+
// the optional probes pass: `claude-code-headless` (when `claude`
|
|
320
|
+
// is on PATH AND logged in), `copilot` (when `@github/copilot-sdk`
|
|
321
|
+
// is installed). Future PRs can extend the same pattern for
|
|
322
|
+
// `claude-api` (probe `@anthropic-ai/sdk` install +
|
|
323
|
+
// ANTHROPIC_API_KEY env) and `opencode` (probe `@opencode-ai/sdk`
|
|
324
|
+
// install + `opencode` binary on PATH). Recording as an array
|
|
325
|
+
// keeps the wire shape forward-compatible.
|
|
326
|
+
availableAgentTypes,
|
|
327
|
+
availablePlayerTypes: agentTypes,
|
|
328
|
+
claudeBin: config.claudeBin,
|
|
329
|
+
platform: process.platform,
|
|
330
|
+
capabilities: [],
|
|
331
|
+
daemonStartedAt: DAEMON_STARTED_AT,
|
|
332
|
+
// adapterVersions is populated at runDaemonBoot time after the
|
|
333
|
+
// parallel probe (see runDaemonBoot below). computeHostProfile
|
|
334
|
+
// intentionally returns the immediate fields only.
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* #274 AC5c / M10 — HARD REQUIREMENT privacy scrub.
|
|
339
|
+
*
|
|
340
|
+
* Strips absolute paths and file extensions from every `HostProfile` field
|
|
341
|
+
* before the payload crosses the signal boundary. The global maestro is
|
|
342
|
+
* namespace-wide; a multi-tenant or multi-ensemble corporate setup would
|
|
343
|
+
* leak username-containing paths across ensembles if this is ever
|
|
344
|
+
* violated. Unit-tested in `test/daemon-boot.test.ts` with a dedicated
|
|
345
|
+
* "no `/` or `\\` in any string" invariant assertion against pathological
|
|
346
|
+
* inputs.
|
|
347
|
+
*
|
|
348
|
+
* Contract per architect AC5c:
|
|
349
|
+
* - `claudeBin` — basename only (e.g. `claude`), never absolute
|
|
350
|
+
* - `availableAgentTypes` — names only, never paths
|
|
351
|
+
* - `availablePlayerTypes` — names only, never paths
|
|
352
|
+
* - No env var values, no `workDir`, no user directories in any field
|
|
353
|
+
*
|
|
354
|
+
* The scrub is defense-in-depth: production callers (`computeHostProfile`)
|
|
355
|
+
* already produce clean inputs from `listAgentTypes().map(a => a.name)`.
|
|
356
|
+
* If a future code path accidentally passes a path, this function catches
|
|
357
|
+
* it before the workflow handler ever sees it.
|
|
358
|
+
*/
|
|
359
|
+
function scrubHostProfile(raw) {
|
|
360
|
+
const stripPath = (s) => {
|
|
361
|
+
// Platform-independent basename: `path.basename` is runtime-bound —
|
|
362
|
+
// on POSIX it doesn't recognise `\` as a separator, so a Windows
|
|
363
|
+
// daemon's signal leaking `'C:\Users\alice\bin\claude.exe'` into
|
|
364
|
+
// a Linux-hosted global maestro would bypass the scrub entirely
|
|
365
|
+
// (CI caught exactly this on Ubuntu shard-2). Normalize first,
|
|
366
|
+
// then use `path.posix.basename` explicitly so the scrub is
|
|
367
|
+
// deterministic regardless of where the daemon or maestro runs.
|
|
368
|
+
//
|
|
369
|
+
// Also strip a single trailing `.md` — player-type files are
|
|
370
|
+
// shipped as e.g. `tempo-soloist.md` but the name should be just
|
|
371
|
+
// `tempo-soloist` on the wire.
|
|
372
|
+
const normalized = s.replace(/\\/g, '/');
|
|
373
|
+
const base = path.posix.basename(normalized);
|
|
374
|
+
return base.endsWith('.md') ? base.slice(0, -3) : base;
|
|
375
|
+
};
|
|
376
|
+
const scrubList = (list) => list?.map(stripPath);
|
|
377
|
+
// Issue #399 — pass-through fields with no privacy concern.
|
|
378
|
+
// `daemonStartedAt` is a number; `adapterVersions` keys are adapter
|
|
379
|
+
// NAMES and values are version strings. Neither carries paths,
|
|
380
|
+
// env vars, or user-home directories, so the AC5c scrub doesn't
|
|
381
|
+
// need to touch them. We conditionally splice them in only when
|
|
382
|
+
// they're defined on the input, so the scrub output stays
|
|
383
|
+
// shape-equivalent to a clean input that omits them — the
|
|
384
|
+
// already-clean round-trip test (`scrubHostProfile(clean) === clean`)
|
|
385
|
+
// continues to hold.
|
|
386
|
+
const out = {
|
|
387
|
+
hostname: raw.hostname,
|
|
388
|
+
version: raw.version,
|
|
389
|
+
defaultAgent: raw.defaultAgent,
|
|
390
|
+
availableAgentTypes: scrubList(raw.availableAgentTypes),
|
|
391
|
+
availablePlayerTypes: scrubList(raw.availablePlayerTypes),
|
|
392
|
+
claudeBin: raw.claudeBin ? stripPath(raw.claudeBin) : undefined,
|
|
393
|
+
platform: raw.platform,
|
|
394
|
+
capabilities: raw.capabilities,
|
|
395
|
+
};
|
|
396
|
+
if (raw.daemonStartedAt !== undefined)
|
|
397
|
+
out.daemonStartedAt = raw.daemonStartedAt;
|
|
398
|
+
if (raw.adapterVersions !== undefined)
|
|
399
|
+
out.adapterVersions = raw.adapterVersions;
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
/** Production default: signal the global maestro with the profile. */
|
|
403
|
+
async function realSendHostProfileSignal(client, profile) {
|
|
404
|
+
const handle = client.workflow.getHandle(config_1.GLOBAL_MAESTRO_WORKFLOW_ID);
|
|
405
|
+
await handle.signal('hostProfile', profile);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Signal `hostProfile` with bounded retry (AC5b / M11).
|
|
409
|
+
*
|
|
410
|
+
* Default backoff: `[0, 5000, 15000]` ms → 3 attempts, ≤20 s wall-clock,
|
|
411
|
+
* well under the 30 s budget. Tests override to `[0, 0, 0]` for fast
|
|
412
|
+
* execution. On total failure, logs a warning and returns — the daemon
|
|
413
|
+
* stays alive without its profile advertised.
|
|
414
|
+
*
|
|
415
|
+
* Exported for reuse by the Phase 5 `agent-tempo refresh-host-profile`
|
|
416
|
+
* CLI subcommand, which re-signals without needing the full
|
|
417
|
+
* `runDaemonBoot` sequence (the global maestro is already up).
|
|
418
|
+
*/
|
|
419
|
+
async function advertiseHostProfile(client, profile, opts = {}) {
|
|
420
|
+
const backoffs = opts.retryBackoffsMs ?? [0, 5000, 15000];
|
|
421
|
+
const logFn = opts.log ?? log;
|
|
422
|
+
const send = opts.sendSignal ?? realSendHostProfileSignal;
|
|
423
|
+
let lastError;
|
|
424
|
+
for (let attempt = 0; attempt < backoffs.length; attempt++) {
|
|
425
|
+
const delay = backoffs[attempt];
|
|
426
|
+
if (delay > 0)
|
|
427
|
+
await (0, promises_1.setTimeout)(delay);
|
|
428
|
+
try {
|
|
429
|
+
await send(client, profile);
|
|
430
|
+
logFn(`Advertised host profile for "${profile.hostname}" (attempt ${attempt + 1}/${backoffs.length})`);
|
|
431
|
+
return { ok: true, attempts: attempt + 1 };
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
lastError = err;
|
|
435
|
+
logFn(`hostProfile signal attempt ${attempt + 1}/${backoffs.length} failed:`, err instanceof Error ? err.message : err);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
logFn(`Failed to advertise host profile after ${backoffs.length} attempts (non-fatal; daemon stays alive):`, lastError instanceof Error ? lastError.message : lastError);
|
|
439
|
+
return { ok: false, attempts: backoffs.length, lastError };
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* #274 M14 — daemon boot sequence: ensure global maestro is running,
|
|
443
|
+
* then advertise the (scrubbed) capability profile with bounded retry.
|
|
444
|
+
*
|
|
445
|
+
* Ordering is load-bearing (AC5a / M11): the `hostProfile` signal MUST
|
|
446
|
+
* NOT fire until `ensureGlobalMaestro` has resolved. Otherwise the
|
|
447
|
+
* signal races the workflow-start and gets silently dropped by Temporal
|
|
448
|
+
* (WorkflowNotFound on an unknown workflow id).
|
|
449
|
+
*
|
|
450
|
+
* Hard-failure behavior (AC5b): if `ensureGlobalMaestro` rejects, the
|
|
451
|
+
* daemon stays alive WITHOUT advertising its profile. Next opportunity
|
|
452
|
+
* is the next daemon restart OR a manual `agent-tempo refresh-host-profile`
|
|
453
|
+
* invocation (Phase 5).
|
|
454
|
+
*
|
|
455
|
+
* Tests in `test/daemon-boot.test.ts` exercise:
|
|
456
|
+
* - ensure-before-signal ordering via deferred promises
|
|
457
|
+
* - retry success on 3rd attempt
|
|
458
|
+
* - all-retries-exhausted stays alive
|
|
459
|
+
* - ensure-fails-stays-alive
|
|
460
|
+
*/
|
|
461
|
+
async function runDaemonBoot(client, deps) {
|
|
462
|
+
const logFn = deps.log ?? log;
|
|
463
|
+
const raw = deps.computeHostProfile();
|
|
464
|
+
// Issue #399 Q5.4 — probe adapter versions in parallel with the
|
|
465
|
+
// global-maestro ensure. The probe is best-effort and never throws;
|
|
466
|
+
// settled-result handling makes the boot path tolerant of either
|
|
467
|
+
// succeeding without the other. Ordering invariant (AC5a / M11) —
|
|
468
|
+
// the host-profile signal still gates on `ensureGlobalMaestro`
|
|
469
|
+
// resolving — is preserved by awaiting both before signaling.
|
|
470
|
+
const probeFn = deps.probeAdapterVersions ?? (() => Promise.resolve({}));
|
|
471
|
+
const [ensureResult, probeResult] = await Promise.allSettled([
|
|
472
|
+
deps.ensureGlobalMaestro(),
|
|
473
|
+
probeFn(),
|
|
474
|
+
]);
|
|
475
|
+
if (ensureResult.status === 'rejected') {
|
|
476
|
+
logFn('ensureGlobalMaestro failed (non-fatal); host profile not advertised this boot:', ensureResult.reason instanceof Error ? ensureResult.reason.message : ensureResult.reason);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const adapterVersions = probeResult.status === 'fulfilled' ? probeResult.value : {};
|
|
480
|
+
if (probeResult.status === 'rejected') {
|
|
481
|
+
// probeAdapterVersions is contracted to never throw, but guard the
|
|
482
|
+
// fallthrough for defense-in-depth — a thrown probe shouldn't
|
|
483
|
+
// block profile advertisement.
|
|
484
|
+
logFn('probeAdapterVersions threw (non-fatal); advertising profile without adapter versions:', probeResult.reason instanceof Error ? probeResult.reason.message : probeResult.reason);
|
|
485
|
+
}
|
|
486
|
+
// Merge probe result into the profile. We mutate `raw` rather than
|
|
487
|
+
// re-call computeHostProfile because the probe and compute are
|
|
488
|
+
// logically two halves of the same boot snapshot.
|
|
489
|
+
const profile = scrubHostProfile({
|
|
490
|
+
...raw,
|
|
491
|
+
adapterVersions: Object.keys(adapterVersions).length > 0 ? adapterVersions : undefined,
|
|
492
|
+
});
|
|
493
|
+
await advertiseHostProfile(client, profile, {
|
|
494
|
+
retryBackoffsMs: deps.retryBackoffsMs,
|
|
495
|
+
log: logFn,
|
|
496
|
+
sendSignal: deps.sendHostProfileSignal,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
// ── Reconcile-on-boot (PR-E §10.1) ──
|
|
500
|
+
/**
|
|
501
|
+
* PR-E reconcile-on-boot — design §10.1.
|
|
502
|
+
*
|
|
503
|
+
* Called once during daemon startup, after workers are running but before
|
|
504
|
+
* the main run loop blocks. Queries for orphaned sessions owned by this
|
|
505
|
+
* host and applies the effective {@link DaemonConfig.restorePolicy}:
|
|
506
|
+
*
|
|
507
|
+
* - `auto`: call `restart` on each orphan inside the allowlist + age
|
|
508
|
+
* window. `AttachmentConflict` is caught silently — another process
|
|
509
|
+
* may have restored concurrently.
|
|
510
|
+
* - `prompt`: log the orphan list and leave the restore to the CLI
|
|
511
|
+
* `agent-tempo restore` command. No automatic action.
|
|
512
|
+
* - `never`: silent no-op.
|
|
513
|
+
*
|
|
514
|
+
* All three branches exit in bounded time — never blocks worker startup.
|
|
515
|
+
* Non-fatal: any failure is logged and reconcile bails without crashing
|
|
516
|
+
* the daemon (worker loop takes over and the user can re-run the query
|
|
517
|
+
* via the CLI).
|
|
518
|
+
*/
|
|
519
|
+
async function reconcileOnBoot(client, daemonConfig, hostname = os.hostname(),
|
|
520
|
+
// Injectable clock — default to wall-clock at call time. Exposed so tests can pass a
|
|
521
|
+
// pinned reference time alongside fixtures that use ISO strings derived from that time
|
|
522
|
+
// (otherwise the 24h age filter below vs. a hardcoded test NOW drifts out of sync as
|
|
523
|
+
// calendar days roll over; matches the pattern used by the cleanup path below).
|
|
524
|
+
now = Date.now()) {
|
|
525
|
+
if (daemonConfig.restorePolicy === 'never') {
|
|
526
|
+
log(`reconcile: restorePolicy="never" — skipping orphan scan`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
log(`reconcile: scanning for orphans on host="${hostname}" (policy=${daemonConfig.restorePolicy})`);
|
|
530
|
+
// #93 / #285: the decision loop (cross-host filter, age window, allowlist,
|
|
531
|
+
// restart via outbox) was extracted to `restoreOrphansOnce` so the CLI
|
|
532
|
+
// resume flow (`up` option 2, `conduct --resume`) shares the same
|
|
533
|
+
// behavior. Pass `invokerPlayerId: 'daemon'` to preserve the previous
|
|
534
|
+
// operator identity, and inject `now` as a closure over the pinned ref
|
|
535
|
+
// time so the existing rebuild-reboot tests keep their fixture semantics.
|
|
536
|
+
const summary = await (0, orphans_1.restoreOrphansOnce)(client, {
|
|
537
|
+
hostname,
|
|
538
|
+
invokerPlayerId: 'daemon',
|
|
539
|
+
policy: daemonConfig.restorePolicy,
|
|
540
|
+
autoRestoreMaxAgeHours: daemonConfig.autoRestoreMaxAgeHours,
|
|
541
|
+
autoRestoreEnsembles: daemonConfig.autoRestoreEnsembles,
|
|
542
|
+
now: () => now,
|
|
543
|
+
}, log);
|
|
544
|
+
const total = summary.reattached + summary.skipped + summary.failed;
|
|
545
|
+
if (total === 0) {
|
|
546
|
+
log('reconcile: no orphans found');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
log(`reconcile: ${summary.reattached} reattached, ` +
|
|
550
|
+
`${summary.skipped} skipped, ${summary.failed} failed ` +
|
|
551
|
+
`(scanned ${total})`);
|
|
552
|
+
if (daemonConfig.restorePolicy === 'prompt' && summary.skipped > 0) {
|
|
553
|
+
log('reconcile: [prompt] run `agent-tempo restore` to restore interactively');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ── Memory reporter (#336) ──
|
|
557
|
+
/** Default cadence for the periodic memory log. */
|
|
558
|
+
const MEMORY_REPORT_INTERVAL_MS = 5 * 60 * 1000;
|
|
559
|
+
/**
|
|
560
|
+
* Pure formatter — turns a `process.memoryUsage()` snapshot into a single
|
|
561
|
+
* space-separated `key=NNNmb` string suitable for grepping out of the log.
|
|
562
|
+
*
|
|
563
|
+
* Exported for unit testing without a live process.
|
|
564
|
+
*/
|
|
565
|
+
function formatMemoryUsage(usage) {
|
|
566
|
+
const mb = (n) => Math.round(n / (1024 * 1024));
|
|
567
|
+
return (`rss=${mb(usage.rss)}mb ` +
|
|
568
|
+
`heapUsed=${mb(usage.heapUsed)}mb ` +
|
|
569
|
+
`heapTotal=${mb(usage.heapTotal)}mb ` +
|
|
570
|
+
`external=${mb(usage.external)}mb ` +
|
|
571
|
+
`arrayBuffers=${mb(usage.arrayBuffers)}mb`);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* #336 — schedule a periodic `[agent-tempo:daemon ...] memory: ...` log
|
|
575
|
+
* line so the next memory-leak investigation has a baseline + growth curve
|
|
576
|
+
* directly in the daemon log instead of needing a debugger attach.
|
|
577
|
+
*
|
|
578
|
+
* Returns a stop function the daemon's shutdown handler invokes.
|
|
579
|
+
*
|
|
580
|
+
* `unref()` on the timer handle so memory reporting alone never keeps the
|
|
581
|
+
* daemon alive — workers + the HTTP listener are the only legitimate
|
|
582
|
+
* long-lived references.
|
|
583
|
+
*/
|
|
584
|
+
function startMemoryReporter(intervalMs = MEMORY_REPORT_INTERVAL_MS, logFn = log, sample = () => process.memoryUsage()) {
|
|
585
|
+
const tick = () => {
|
|
586
|
+
try {
|
|
587
|
+
logFn(`memory: ${formatMemoryUsage(sample())}`);
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
// `process.memoryUsage()` can't realistically throw, but if a custom
|
|
591
|
+
// sampler does we don't want to take the daemon down.
|
|
592
|
+
logFn('memory: sample failed (non-fatal):', err instanceof Error ? err.message : err);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
// Emit immediately so the first log line is the baseline (otherwise an
|
|
596
|
+
// operator polling early sees nothing for `intervalMs`).
|
|
597
|
+
tick();
|
|
598
|
+
const timer = setInterval(tick, intervalMs);
|
|
599
|
+
timer.unref();
|
|
600
|
+
return () => clearInterval(timer);
|
|
601
|
+
}
|
|
602
|
+
// ── Cleanup loop (PR-E §13.4) ──
|
|
603
|
+
/** Hardcoded cleanup loop period per PR-E §8 answer 2. */
|
|
604
|
+
const CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
605
|
+
/**
|
|
606
|
+
* Filter a set of orphan candidates to those that exceed the
|
|
607
|
+
* `detachedMaxAgeDays` retention threshold. Exported for unit testing the
|
|
608
|
+
* retention math without a live Temporal connection.
|
|
609
|
+
*/
|
|
610
|
+
function selectStaleDetachedOrphans(orphans, detachedMaxAgeDays, now = Date.now()) {
|
|
611
|
+
const thresholdMs = detachedMaxAgeDays * 24 * 60 * 60 * 1000;
|
|
612
|
+
return orphans.filter((o) => {
|
|
613
|
+
if (o.info.phase !== 'detached')
|
|
614
|
+
return false;
|
|
615
|
+
if (!o.summary.detachedSince)
|
|
616
|
+
return false;
|
|
617
|
+
const detachedAt = Date.parse(o.summary.detachedSince);
|
|
618
|
+
if (!Number.isFinite(detachedAt))
|
|
619
|
+
return false;
|
|
620
|
+
return now - detachedAt > thresholdMs;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* PR-E cleanup loop — design §13.4 regression row 1.
|
|
625
|
+
*
|
|
626
|
+
* Runs on a 6-hour timer (hardcoded per §8 answer 2). Destroys detached
|
|
627
|
+
* orphans older than `detachedMaxAgeDays` via `TempoClient.destroy` so the
|
|
628
|
+
* workflow completes and eventually falls out of the namespace.
|
|
629
|
+
*
|
|
630
|
+
* Never touches `Running` workflows that still hold a live attachment
|
|
631
|
+
* (filter is explicit on `phase === 'detached'`).
|
|
632
|
+
*
|
|
633
|
+
* **Note (#144)**: an earlier revision included a "pass 2" that tried to
|
|
634
|
+
* `terminate()` already-Completed workflows as belt-and-suspenders retention.
|
|
635
|
+
* `terminate()` throws on Completed workflows in every Temporal namespace
|
|
636
|
+
* setting, so the pass was dead code masked by a swallowing catch. It was
|
|
637
|
+
* removed: namespace retention (Temporal Cloud default 30d, self-hosted
|
|
638
|
+
* configurable) is the authoritative reaper for Completed workflows.
|
|
639
|
+
*/
|
|
640
|
+
async function cleanupLoop(client, daemonConfig, hostname = os.hostname()) {
|
|
641
|
+
const tempo = (0, client_3.createTempoClient)(client);
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
try {
|
|
644
|
+
const orphans = await (0, orphans_1.queryOrphanedSessions)(client, { hostname }, log);
|
|
645
|
+
const stale = selectStaleDetachedOrphans(orphans, daemonConfig.cleanupPolicy.detachedMaxAgeDays, now);
|
|
646
|
+
for (const o of stale) {
|
|
647
|
+
const { ensemble, playerId } = o.summary;
|
|
648
|
+
try {
|
|
649
|
+
await tempo.destroy(ensemble, playerId, `detached >${daemonConfig.cleanupPolicy.detachedMaxAgeDays}d`);
|
|
650
|
+
log(`cleanup: [detached] destroyed ${o.workflowId} (detachedSince=${o.summary.detachedSince})`);
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
log(`cleanup: [detached] destroy failed for ${o.workflowId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
log('cleanup: failed (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Schedule {@link cleanupLoop} to run every 6 hours. Returns a clearer
|
|
663
|
+
* function that cancels the timer — called during shutdown.
|
|
664
|
+
*/
|
|
665
|
+
function startCleanupLoop(client, daemonConfig, hostname = os.hostname()) {
|
|
666
|
+
let timer = null;
|
|
667
|
+
const tick = () => {
|
|
668
|
+
cleanupLoop(client, daemonConfig, hostname).catch((err) => {
|
|
669
|
+
log('cleanup: tick failed:', err instanceof Error ? err.message : String(err));
|
|
670
|
+
});
|
|
671
|
+
timer = setTimeout(tick, CLEANUP_INTERVAL_MS);
|
|
672
|
+
timer.unref();
|
|
673
|
+
};
|
|
674
|
+
// Run first tick after the initial interval (not immediately — startup is
|
|
675
|
+
// busy enough). The retention math is idempotent so a delayed first run is
|
|
676
|
+
// always safe.
|
|
677
|
+
timer = setTimeout(tick, CLEANUP_INTERVAL_MS);
|
|
678
|
+
timer.unref();
|
|
679
|
+
return () => {
|
|
680
|
+
if (timer) {
|
|
681
|
+
clearTimeout(timer);
|
|
682
|
+
timer = null;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
async function main() {
|
|
687
|
+
// ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
|
|
688
|
+
// first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
|
|
689
|
+
// grep-friendly identification regardless of subsequent log volume.
|
|
690
|
+
(0, dev_banner_1.emitDevBannerIfActive)();
|
|
691
|
+
// Ensure daemon directory exists. AGENT_TEMPO_HOME already resolves to
|
|
692
|
+
// `~/.agent-tempo-dev/` in dev mode (ADR 0014 §5.3), so this lands in
|
|
693
|
+
// the right place without a per-callsite branch.
|
|
694
|
+
fs.mkdirSync(config_1.AGENT_TEMPO_HOME, { recursive: true });
|
|
695
|
+
// Write PID file — the parent polls for this to confirm startup.
|
|
696
|
+
// Atomic write: tmp + rename so a racing reader never sees a half-written
|
|
697
|
+
// file. Retries on Windows EPERM/EBUSY/EACCES (see #182).
|
|
698
|
+
await writePidFileAtomic(daemon_1.DAEMON_PID_PATH, process.pid);
|
|
699
|
+
log(`Daemon started (pid ${process.pid})`);
|
|
700
|
+
log(`PID file: ${daemon_1.DAEMON_PID_PATH}`);
|
|
701
|
+
log(`Log file: ${daemon_1.DAEMON_LOG_PATH}`);
|
|
702
|
+
// Create the heartbeat file synchronously so the first `daemon status`
|
|
703
|
+
// invocation after startup never races the first interval tick. The
|
|
704
|
+
// subsequent interval only has to refresh the mtime — no branching on
|
|
705
|
+
// file-existence each tick (#157 PR B).
|
|
706
|
+
try {
|
|
707
|
+
fs.writeFileSync(daemon_1.DAEMON_HEARTBEAT_PATH, '');
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
// Non-fatal — the daemon still runs, `daemon status` just reports
|
|
711
|
+
// `heartbeatAge: null`. Log loudly so operators notice.
|
|
712
|
+
log('Failed to create heartbeat file (non-fatal):', err?.message ?? err);
|
|
713
|
+
}
|
|
714
|
+
const heartbeatInterval = setInterval(() => {
|
|
715
|
+
try {
|
|
716
|
+
const now = Date.now() / 1000; // `fs.utimes` takes seconds since epoch
|
|
717
|
+
fs.utimesSync(daemon_1.DAEMON_HEARTBEAT_PATH, now, now);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
// Swallow — transient fs errors shouldn't take down the daemon.
|
|
721
|
+
}
|
|
722
|
+
}, daemon_1.HEARTBEAT_INTERVAL_MS);
|
|
723
|
+
heartbeatInterval.unref();
|
|
724
|
+
// Get config from env vars (passed by startDaemon via spawn env)
|
|
725
|
+
const config = (0, config_1.getConfig)({});
|
|
726
|
+
// #423 PR-A Fix 3 — load-bearing drift detector. The `[DEV MODE]` banner
|
|
727
|
+
// (gate 4) and the daemon's actual Temporal connection MUST agree on the
|
|
728
|
+
// namespace. Fix 1's env-var carve-out plus Fix 2's source-annotated
|
|
729
|
+
// banner make a silent disagreement impossible at the resolution layer,
|
|
730
|
+
// but a future regression — or an operator who hand-edits
|
|
731
|
+
// `~/.agent-tempo-dev/config.json` to a non-dev namespace by mistake —
|
|
732
|
+
// would still slip through. The warning fires once at boot and lands at
|
|
733
|
+
// the top of `daemon.log` so an operator chasing weird coordination bugs
|
|
734
|
+
// sees the override on first inspection.
|
|
735
|
+
warnIfDevNamespaceDrift(config);
|
|
736
|
+
// ADR 0014 §6.2 — dev daemon auto-creates its Temporal namespace before
|
|
737
|
+
// the worker bootstrap. Production daemons skip this; namespaces are
|
|
738
|
+
// operator-managed there.
|
|
739
|
+
//
|
|
740
|
+
// Idempotent on `ALREADY_EXISTS` (every boot after the first), non-fatal
|
|
741
|
+
// on `PERMISSION_DENIED`. If creation fails for an unexpected reason the
|
|
742
|
+
// worker bootstrap below fails loudly with `Namespace not found`, which
|
|
743
|
+
// is the clearer error from the operator's perspective.
|
|
744
|
+
if ((0, config_1.isDevMode)() && config.temporalNamespace === config_1.DEV_TEMPORAL_NAMESPACE) {
|
|
745
|
+
try {
|
|
746
|
+
const provisionConn = await (0, connection_1.createTemporalConnection)(config);
|
|
747
|
+
try {
|
|
748
|
+
await ensureDevNamespace(provisionConn, config.temporalNamespace);
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
await provisionConn.close();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
// Connection itself failed — log + fall through. createWorkers() will
|
|
756
|
+
// surface the same error with its own context.
|
|
757
|
+
log('[dev-mode] namespace pre-create skipped — Temporal connection failed:', err instanceof Error ? err.message : String(err));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// PR-3 of the v1.0 rebrand — fail fast if the `AgentTempo*` search
|
|
761
|
+
// attributes aren't registered on the target namespace. The actionable
|
|
762
|
+
// error message includes the exact `temporal operator search-attribute
|
|
763
|
+
// create` commands operators need to paste. Probe failure (Temporal CLI
|
|
764
|
+
// missing, namespace unreachable) is downgraded to a warning — the
|
|
765
|
+
// createWorkers() call below will surface the connection error with
|
|
766
|
+
// better context. The hard-stop is only "namespace reached, but SAs
|
|
767
|
+
// missing".
|
|
768
|
+
{
|
|
769
|
+
const { verifySearchAttributes } = await Promise.resolve().then(() => __importStar(require('./cli/sa-preflight')));
|
|
770
|
+
const result = await verifySearchAttributes({
|
|
771
|
+
temporalAddress: config.temporalAddress,
|
|
772
|
+
temporalNamespace: config.temporalNamespace,
|
|
773
|
+
});
|
|
774
|
+
if (!result.ok && !result.probeError) {
|
|
775
|
+
process.stderr.write('ERROR: ' + result.message + '\n');
|
|
776
|
+
log('Daemon refused to boot — search attributes missing on namespace ' + config.temporalNamespace);
|
|
777
|
+
try {
|
|
778
|
+
fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
|
|
779
|
+
}
|
|
780
|
+
catch { /* ignore */ }
|
|
781
|
+
try {
|
|
782
|
+
fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
|
|
783
|
+
}
|
|
784
|
+
catch { /* ignore */ }
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
else if (result.probeError) {
|
|
788
|
+
log('search-attribute preflight probe failed (non-fatal — createWorkers will surface the real error):', result.probeError);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Use mutable refs so signal handlers can be registered before workers
|
|
792
|
+
// are created — closes the narrow window where a SIGTERM during
|
|
793
|
+
// createWorkers() would be missed.
|
|
794
|
+
let sharedWorker = null;
|
|
795
|
+
let hostWorker = null;
|
|
796
|
+
// Register signal handlers first — idempotent, drain-only (no process.exit).
|
|
797
|
+
let shuttingDown = false;
|
|
798
|
+
const hardExit = () => {
|
|
799
|
+
log('Shutdown timeout — forcing exit');
|
|
800
|
+
try {
|
|
801
|
+
fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
|
|
802
|
+
}
|
|
803
|
+
catch { /* ignore */ }
|
|
804
|
+
try {
|
|
805
|
+
fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
|
|
806
|
+
}
|
|
807
|
+
catch { /* ignore */ }
|
|
808
|
+
process.exit(1);
|
|
809
|
+
};
|
|
810
|
+
// Mutable ref so the reconcile/cleanup init below can register its
|
|
811
|
+
// cancellation with shutdown (declared after signal handlers to preserve
|
|
812
|
+
// the existing signal-handler-first safety ordering).
|
|
813
|
+
let stopCleanupLoopRef = null;
|
|
814
|
+
// #336 — memory reporter. Started unconditionally below; shutdown
|
|
815
|
+
// clears the interval so the daemon can drain cleanly.
|
|
816
|
+
let stopMemoryReporterRef = null;
|
|
817
|
+
// #94/#95 PR-1 — HTTP server handle. Started after workers are up
|
|
818
|
+
// (so handlers calling into TempoClient hit a live worker), drained
|
|
819
|
+
// here on shutdown. Mutable ref because `startHttpServer` is awaited
|
|
820
|
+
// below the `shutdown` declaration.
|
|
821
|
+
let httpServerHandle = null;
|
|
822
|
+
// #94/#95 PR-2 — aggregate poll loop + per-ensemble buses. Owned by
|
|
823
|
+
// the daemon process; `close()` drains every per-ensemble bus.
|
|
824
|
+
let aggregateRunner = null;
|
|
825
|
+
const shutdown = () => {
|
|
826
|
+
if (shuttingDown)
|
|
827
|
+
return;
|
|
828
|
+
shuttingDown = true;
|
|
829
|
+
log('Shutting down (draining in-flight activities)...');
|
|
830
|
+
// Safety net: force exit if workers don't stop within 15s
|
|
831
|
+
const timer = setTimeout(hardExit, 15_000);
|
|
832
|
+
timer.unref();
|
|
833
|
+
stopCleanupLoopRef?.();
|
|
834
|
+
stopMemoryReporterRef?.();
|
|
835
|
+
clearInterval(heartbeatInterval);
|
|
836
|
+
try {
|
|
837
|
+
fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
|
|
838
|
+
}
|
|
839
|
+
catch { /* ignore */ }
|
|
840
|
+
// HTTP server closes ahead of workers. The HTTP `close()` itself
|
|
841
|
+
// returns a Promise that resolves only after live SSE sockets
|
|
842
|
+
// drain (5 s) — by which point the listener is already refusing
|
|
843
|
+
// new connections AND the port file has been unlinked. The fire-
|
|
844
|
+
// and-forget `.catch()` here means we don't await drain, so the
|
|
845
|
+
// worker shutdown below races the HTTP drain. That's intentional:
|
|
846
|
+
// the worker drain budget is 15 s (`hardExit`), which exceeds
|
|
847
|
+
// HTTP's 5 s drain window — so a polling CLI sees ECONNREFUSED
|
|
848
|
+
// either at the listener level (if it polled after `close()`
|
|
849
|
+
// returned) OR is force-disconnected (if it was inside the drain
|
|
850
|
+
// window and the worker drain pulled the rug). Both signals mean
|
|
851
|
+
// "daemon is going away," which is the contract.
|
|
852
|
+
//
|
|
853
|
+
// The aggregate runner is closed first so per-ensemble buses stop
|
|
854
|
+
// pushing events while the SSE handler is still draining its
|
|
855
|
+
// sockets — preventing wasted work in the drain window.
|
|
856
|
+
aggregateRunner?.close();
|
|
857
|
+
httpServerHandle?.close().catch((err) => log('http close error (non-fatal):', err instanceof Error ? err.message : err));
|
|
858
|
+
sharedWorker?.shutdown();
|
|
859
|
+
hostWorker?.shutdown();
|
|
860
|
+
};
|
|
861
|
+
process.on('SIGTERM', shutdown);
|
|
862
|
+
process.on('SIGINT', shutdown);
|
|
863
|
+
// Create workers (signal handlers already active via mutable refs)
|
|
864
|
+
log(`Connecting to Temporal at ${config.temporalAddress} (namespace: ${config.temporalNamespace})`);
|
|
865
|
+
const workers = await (0, worker_1.createWorkers)(config);
|
|
866
|
+
sharedWorker = workers.sharedWorker;
|
|
867
|
+
hostWorker = workers.hostWorker;
|
|
868
|
+
log('Workers created — processing tasks');
|
|
869
|
+
// #336 — start the periodic memory reporter alongside the workers. The
|
|
870
|
+
// first sample lands in the log immediately as a baseline; subsequent
|
|
871
|
+
// samples fire every MEMORY_REPORT_INTERVAL_MS and let operators spot
|
|
872
|
+
// unbounded growth without attaching a debugger.
|
|
873
|
+
stopMemoryReporterRef = startMemoryReporter();
|
|
874
|
+
// #274 — daemon boot sequence: ensure the global maestro is running,
|
|
875
|
+
// then advertise this host's capability profile with bounded retry.
|
|
876
|
+
// Fire-and-forget from main's perspective (the workers above are
|
|
877
|
+
// already polling tasks; we don't block the run loop on maestro
|
|
878
|
+
// ensure + profile signaling). Ordering INSIDE runDaemonBoot is
|
|
879
|
+
// load-bearing — see M11 / AC5a — so the outer `.catch` only
|
|
880
|
+
// handles unexpected throws (both ensure and signal paths log +
|
|
881
|
+
// return gracefully on their own).
|
|
882
|
+
(async () => {
|
|
883
|
+
try {
|
|
884
|
+
const bootConnection = await (0, connection_1.createTemporalConnection)(config);
|
|
885
|
+
const bootClient = new client_1.Client({ connection: bootConnection, namespace: config.temporalNamespace });
|
|
886
|
+
await runDaemonBoot(bootClient, {
|
|
887
|
+
ensureGlobalMaestro: () => ensureGlobalMaestro(config),
|
|
888
|
+
sendHostProfileSignal: realSendHostProfileSignal,
|
|
889
|
+
computeHostProfile: () => computeHostProfile(config),
|
|
890
|
+
// Issue #399 Q5.4 — probe upstream tool versions in parallel
|
|
891
|
+
// with the global-maestro ensure. Production uses real spawns
|
|
892
|
+
// / package.json reads; tests inject canned maps via
|
|
893
|
+
// `DaemonBootDeps.probeAdapterVersions`.
|
|
894
|
+
probeAdapterVersions: () => (0, daemon_adapter_versions_1.probeAdapterVersions)(),
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
log('runDaemonBoot background error:', err);
|
|
899
|
+
}
|
|
900
|
+
})();
|
|
901
|
+
// PR-E reconcile-on-boot + cleanup loop (design §10, §13.4). Both run
|
|
902
|
+
// against their own Temporal Client, not the worker connection — they
|
|
903
|
+
// call `workflow.list` + `workflow.getHandle().query(...)` which are
|
|
904
|
+
// client-side operations. Non-fatal: any failure is logged and the
|
|
905
|
+
// daemon continues running.
|
|
906
|
+
let reconcileClient = null;
|
|
907
|
+
try {
|
|
908
|
+
const daemonConfig = (0, config_1.loadDaemonConfig)();
|
|
909
|
+
const reconcileConnection = await (0, connection_1.createTemporalConnection)(config);
|
|
910
|
+
reconcileClient = new client_1.Client({ connection: reconcileConnection, namespace: config.temporalNamespace });
|
|
911
|
+
// Fire-and-forget reconcile; the daemon must not block on this.
|
|
912
|
+
reconcileOnBoot(reconcileClient, daemonConfig).catch((err) => {
|
|
913
|
+
log('reconcileOnBoot background error:', err);
|
|
914
|
+
});
|
|
915
|
+
// Schedule the 6-hour cleanup loop (hardcoded per §8 answer 2).
|
|
916
|
+
stopCleanupLoopRef = startCleanupLoop(reconcileClient, daemonConfig);
|
|
917
|
+
log(`cleanup loop scheduled (every ${CLEANUP_INTERVAL_MS / 3_600_000}h)`);
|
|
918
|
+
}
|
|
919
|
+
catch (err) {
|
|
920
|
+
log('reconcile/cleanup init failed (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
921
|
+
}
|
|
922
|
+
// #94/#95 PR-1 — HTTP snapshot endpoints. Reuses the reconcile client
|
|
923
|
+
// (already long-lived; the snapshot handlers fan out the same
|
|
924
|
+
// visibility queries that reconcile/cleanup do). Non-fatal: any
|
|
925
|
+
// listener error logs and the daemon stays alive — Temporal workers
|
|
926
|
+
// are the durable concern.
|
|
927
|
+
if (reconcileClient) {
|
|
928
|
+
try {
|
|
929
|
+
const { startHttpServer } = await Promise.resolve().then(() => __importStar(require('./http')));
|
|
930
|
+
const { createTempoClient } = await Promise.resolve().then(() => __importStar(require('./client')));
|
|
931
|
+
const { AggregateRunner } = await Promise.resolve().then(() => __importStar(require('./http/aggregate')));
|
|
932
|
+
// #437 — pass the daemon's polling task queue through. `listHosts`
|
|
933
|
+
// (called by `/v1/hosts`, snapshot.hostProfiles, dashboard, TUI,
|
|
934
|
+
// AggregateRunner) defaults to `'agent-tempo'` and silently
|
|
935
|
+
// returns `[]` in dev mode without this. Both `namespace` (already
|
|
936
|
+
// baked into `reconcileClient.options.namespace`) and `taskQueue`
|
|
937
|
+
// must match the daemon for poller discovery to find this host.
|
|
938
|
+
const httpClient = createTempoClient(reconcileClient, { taskQueue: config.taskQueue });
|
|
939
|
+
// Single shared bootEpoch — every bus the daemon constructs uses
|
|
940
|
+
// this same value, frozen for the process lifetime per §5.
|
|
941
|
+
const bootEpoch = Date.now();
|
|
942
|
+
aggregateRunner = new AggregateRunner({ client: httpClient, bootEpoch });
|
|
943
|
+
aggregateRunner.start();
|
|
944
|
+
httpServerHandle = await startHttpServer({
|
|
945
|
+
client: httpClient,
|
|
946
|
+
namespace: config.temporalNamespace,
|
|
947
|
+
taskQueue: config.taskQueue,
|
|
948
|
+
version: daemonVersion(),
|
|
949
|
+
aggregate: aggregateRunner,
|
|
950
|
+
});
|
|
951
|
+
log(`HTTP listening on http://${httpServerHandle.bindAddr}:${httpServerHandle.port}`);
|
|
952
|
+
log(`Aggregate poll loop running (bootEpoch=${bootEpoch})`);
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
log('http server init failed (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
log('http server skipped: no Temporal client available');
|
|
960
|
+
}
|
|
961
|
+
// Run both workers — blocks until shutdown + drain completes
|
|
962
|
+
try {
|
|
963
|
+
await Promise.all([sharedWorker.run(), hostWorker.run()]);
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
log('Worker error:', err);
|
|
967
|
+
}
|
|
968
|
+
// Workers have stopped — clean up PID file and exit
|
|
969
|
+
try {
|
|
970
|
+
fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
|
|
971
|
+
}
|
|
972
|
+
catch { /* ignore */ }
|
|
973
|
+
log('Daemon stopped');
|
|
974
|
+
process.exit(0);
|
|
975
|
+
}
|
|
976
|
+
// Only run `main()` when this file is invoked directly (e.g. via
|
|
977
|
+
// `node dist/daemon.js` or `npx ts-node src/daemon.ts`). Tests that
|
|
978
|
+
// import `reconcileOnBoot` / `cleanupLoop` / `selectStaleDetachedOrphans`
|
|
979
|
+
// must not trigger the worker-bootstrap path as a module side-effect.
|
|
980
|
+
if (require.main === module) {
|
|
981
|
+
main().catch((err) => {
|
|
982
|
+
log('Fatal error:', err);
|
|
983
|
+
try {
|
|
984
|
+
fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
|
|
985
|
+
}
|
|
986
|
+
catch { /* ignore */ }
|
|
987
|
+
process.exit(1);
|
|
988
|
+
});
|
|
989
|
+
}
|