talon-agent 1.12.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -52
- package/package.json +39 -12
- package/prompts/README.md +112 -0
- package/prompts/base.md +12 -12
- package/prompts/discord.md +10 -47
- package/prompts/heartbeat.md +23 -8
- package/prompts/identity.md +12 -22
- package/prompts/native.md +23 -0
- package/prompts/system/contract-text-or-tools.md +8 -0
- package/prompts/system/contract-text-preferred.md +5 -0
- package/prompts/system/contract-tool-only.md +10 -0
- package/prompts/system/cron.md +3 -0
- package/prompts/system/daily-memory.md +3 -0
- package/prompts/system/goals.md +7 -0
- package/prompts/system/heartbeat-agent.md +44 -0
- package/prompts/system/persistent-memory.md +8 -0
- package/prompts/system/skills.md +9 -0
- package/prompts/system/triggers.md +5 -0
- package/prompts/system/workspace.md +9 -0
- package/prompts/teams.md +4 -14
- package/prompts/telegram.md +17 -89
- package/src/app.ts +278 -0
- package/src/backend/claude-sdk/constants.ts +22 -10
- package/src/backend/claude-sdk/factory.ts +79 -22
- package/src/backend/claude-sdk/handler.ts +431 -160
- package/src/backend/claude-sdk/index.ts +6 -2
- package/src/backend/claude-sdk/mcp-ready.ts +54 -0
- package/src/backend/claude-sdk/model-drift.ts +65 -0
- package/src/backend/claude-sdk/model-provider.ts +12 -6
- package/src/backend/claude-sdk/models/convert.ts +169 -0
- package/src/backend/claude-sdk/models/discovery.ts +203 -0
- package/src/backend/claude-sdk/models/index.ts +18 -0
- package/src/backend/claude-sdk/models/parsing.ts +264 -0
- package/src/backend/claude-sdk/models/static.ts +45 -0
- package/src/backend/claude-sdk/one-shot.ts +19 -13
- package/src/backend/claude-sdk/options.ts +228 -104
- package/src/backend/claude-sdk/state.ts +1 -1
- package/src/backend/claude-sdk/stream.ts +153 -45
- package/src/backend/claude-sdk/warm.ts +12 -5
- package/src/backend/codex/auth.ts +216 -40
- package/src/backend/codex/constants.ts +63 -22
- package/src/backend/codex/discovery.ts +427 -0
- package/src/backend/codex/factory.ts +63 -33
- package/src/backend/codex/handler/events.ts +239 -0
- package/src/backend/codex/handler/index.ts +15 -0
- package/src/backend/codex/handler/message.ts +669 -0
- package/src/backend/codex/handler/state.ts +14 -0
- package/src/backend/codex/handler/usage.ts +68 -0
- package/src/backend/codex/index.ts +2 -2
- package/src/backend/codex/init.ts +92 -11
- package/src/backend/codex/mcp-config.ts +89 -62
- package/src/backend/codex/models.ts +305 -54
- package/src/backend/codex/oauth-incompat.ts +302 -0
- package/src/backend/codex/one-shot.ts +91 -8
- package/src/backend/codex/state.ts +54 -3
- package/src/backend/codex/token-usage.ts +434 -0
- package/src/backend/kilo/factory.ts +60 -35
- package/src/backend/kilo/handler/index.ts +13 -0
- package/src/backend/kilo/handler/message.ts +317 -0
- package/src/backend/kilo/handler/state.ts +14 -0
- package/src/backend/kilo/handler/turn.ts +321 -0
- package/src/backend/kilo/index.ts +5 -5
- package/src/backend/kilo/model-provider.ts +26 -168
- package/src/backend/kilo/models/index.ts +52 -0
- package/src/backend/kilo/one-shot.ts +20 -142
- package/src/backend/kilo/server.ts +14 -21
- package/src/backend/openai-agents/builtins.ts +215 -114
- package/src/backend/openai-agents/constants.ts +41 -15
- package/src/backend/openai-agents/discovery.ts +332 -0
- package/src/backend/openai-agents/factory.ts +66 -28
- package/src/backend/openai-agents/handler/events.ts +170 -0
- package/src/backend/openai-agents/handler/index.ts +13 -0
- package/src/backend/openai-agents/handler/message.ts +527 -0
- package/src/backend/openai-agents/handler/state.ts +13 -0
- package/src/backend/openai-agents/index.ts +22 -4
- package/src/backend/openai-agents/init.ts +51 -88
- package/src/backend/openai-agents/mcp-pool.ts +252 -0
- package/src/backend/openai-agents/models.ts +118 -11
- package/src/backend/openai-agents/session.ts +277 -0
- package/src/backend/openai-agents/state.ts +82 -11
- package/src/backend/opencode/factory.ts +54 -25
- package/src/backend/opencode/handler/index.ts +13 -0
- package/src/backend/opencode/handler/message.ts +325 -0
- package/src/backend/opencode/handler/state.ts +14 -0
- package/src/backend/opencode/handler/turn.ts +304 -0
- package/src/backend/opencode/index.ts +2 -2
- package/src/backend/opencode/model-provider.ts +26 -168
- package/src/backend/opencode/models/index.ts +51 -0
- package/src/backend/opencode/one-shot.ts +20 -137
- package/src/backend/opencode/server.ts +12 -11
- package/src/backend/remote-server/client.ts +13 -6
- package/src/backend/remote-server/events.ts +59 -9
- package/src/backend/remote-server/lifecycle.ts +3 -1
- package/src/backend/remote-server/mcp.ts +52 -19
- package/src/backend/remote-server/model-catalog/catalog.ts +270 -0
- package/src/backend/remote-server/model-catalog/index.ts +100 -0
- package/src/backend/remote-server/model-catalog/presentation.ts +298 -0
- package/src/backend/remote-server/model-catalog/provider.ts +193 -0
- package/src/backend/remote-server/model-catalog/resolve.ts +225 -0
- package/src/backend/remote-server/model-catalog/types.ts +136 -0
- package/src/backend/remote-server/one-shot.ts +225 -0
- package/src/backend/remote-server/session-helpers.ts +3 -4
- package/src/backend/remote-server/state.ts +1 -1
- package/src/backend/shared/delivery-contract.ts +161 -0
- package/src/backend/shared/delivery.ts +49 -0
- package/src/backend/shared/flow-violation.ts +38 -7
- package/src/backend/shared/frontends.ts +66 -0
- package/src/backend/shared/handle-retry.ts +244 -0
- package/src/backend/shared/handler-to-events.ts +186 -0
- package/src/backend/shared/handler-types.ts +57 -0
- package/src/backend/shared/index.ts +51 -5
- package/src/backend/shared/metrics.ts +199 -0
- package/src/backend/shared/model-retry.ts +1 -1
- package/src/backend/shared/prompt-format.ts +69 -0
- package/src/backend/shared/session-name.ts +1 -1
- package/src/backend/shared/stream-state.ts +36 -2
- package/src/backend/shared/system-prompt.ts +200 -23
- package/src/bootstrap.ts +338 -38
- package/src/cli/chat.ts +49 -0
- package/src/cli/config-view.ts +100 -0
- package/src/cli/config.ts +105 -0
- package/src/cli/context.ts +23 -0
- package/src/cli/daemon.ts +95 -0
- package/src/cli/doctor.ts +53 -0
- package/src/cli/index.ts +139 -0
- package/src/cli/logs.ts +60 -0
- package/src/cli/menu.ts +104 -0
- package/src/cli/setup.ts +455 -0
- package/src/cli/status.ts +75 -0
- package/src/cli.ts +20 -1211
- package/src/core/agent-runtime/README.md +173 -0
- package/src/{backend/registry.ts → core/agent-runtime/backend-registry.ts} +15 -6
- package/src/core/agent-runtime/capabilities.ts +330 -0
- package/src/core/agent-runtime/contract-tests.ts +378 -0
- package/src/core/agent-runtime/events.ts +291 -0
- package/src/core/agent-runtime/index.ts +62 -0
- package/src/core/agent-runtime/model-ref.ts +130 -0
- package/src/core/background/cron.ts +469 -0
- package/src/core/{dream.ts → background/dream.ts} +118 -53
- package/src/core/background/heartbeat/agent.ts +333 -0
- package/src/core/background/heartbeat/index.ts +25 -0
- package/src/core/background/heartbeat/scheduler.ts +211 -0
- package/src/core/background/heartbeat/state.ts +153 -0
- package/src/core/background/index.ts +21 -0
- package/src/core/background/isolated-agent.ts +115 -0
- package/src/core/background/job-health.ts +140 -0
- package/src/core/background/job-oneshot.ts +165 -0
- package/src/core/background/job-prompt.ts +54 -0
- package/src/core/{pulse.ts → background/pulse.ts} +6 -5
- package/src/core/background/triggers/command.ts +60 -0
- package/src/core/background/triggers/exit.ts +202 -0
- package/src/core/background/triggers/index.ts +38 -0
- package/src/core/background/triggers/output.ts +142 -0
- package/src/core/background/triggers/resume.ts +127 -0
- package/src/core/background/triggers/spawn.ts +282 -0
- package/src/core/background/triggers/state.ts +52 -0
- package/src/core/constants.ts +57 -28
- package/src/core/daemon/control.ts +229 -0
- package/src/core/daemon/discovery.ts +169 -0
- package/src/core/daemon/pidfile.ts +98 -0
- package/src/core/doctor.ts +459 -0
- package/src/core/engine/backend-controller/holders.ts +17 -0
- package/src/core/engine/backend-controller/index.ts +71 -0
- package/src/core/engine/backend-controller/legacy.ts +111 -0
- package/src/core/engine/backend-controller/pool.ts +296 -0
- package/src/core/engine/backend-controller/rebind.ts +205 -0
- package/src/core/engine/backend-controller/state.ts +150 -0
- package/src/core/engine/backend-controller/types.ts +51 -0
- package/src/core/engine/dispatcher.ts +68 -0
- package/src/core/engine/gateway-actions/cron.ts +518 -0
- package/src/core/engine/gateway-actions/fetch-url.ts +120 -0
- package/src/core/engine/gateway-actions/goals.ts +178 -0
- package/src/core/engine/gateway-actions/history.ts +53 -0
- package/src/core/engine/gateway-actions/index.ts +60 -0
- package/src/core/engine/gateway-actions/models.ts +120 -0
- package/src/core/engine/gateway-actions/plugins.ts +83 -0
- package/src/core/engine/gateway-actions/scripts.ts +127 -0
- package/src/core/engine/gateway-actions/shared.ts +66 -0
- package/src/core/engine/gateway-actions/skills.ts +120 -0
- package/src/core/engine/gateway-actions/triggers.ts +237 -0
- package/src/core/engine/gateway-actions/types.ts +18 -0
- package/src/core/{gateway.ts → engine/gateway.ts} +223 -60
- package/src/core/engine/index.ts +20 -0
- package/src/core/engine/model-audit.ts +121 -0
- package/src/core/errors.ts +68 -6
- package/src/core/mcp-hub/children.ts +254 -0
- package/src/core/mcp-hub/index.ts +335 -0
- package/src/core/mcp-hub/proxy-server.ts +45 -0
- package/src/core/mcp-hub/talon-server.ts +71 -0
- package/src/core/memory/retrieval.ts +90 -0
- package/src/core/models/active-model.ts +441 -0
- package/src/core/{models.ts → models/catalog.ts} +6 -0
- package/src/core/models/index.ts +17 -0
- package/src/core/models/reasoning-levels.ts +57 -0
- package/src/core/plugin/actions.ts +32 -0
- package/src/core/plugin/builtins.ts +162 -0
- package/src/core/plugin/index.ts +37 -0
- package/src/core/plugin/loader.ts +289 -0
- package/src/core/plugin/mcp.ts +86 -0
- package/src/core/plugin/registry.ts +124 -0
- package/src/core/plugin/types.ts +126 -0
- package/src/core/prompt/assemble.ts +273 -0
- package/src/core/prompt/disk-prompts.ts +41 -0
- package/src/core/prompt/embed.ts +107 -0
- package/src/core/prompt/embedded-prompts.ts +85 -0
- package/src/core/prompt/index.ts +30 -0
- package/src/core/prompt/templates.ts +124 -0
- package/src/core/prompt/workspace-listing.ts +91 -0
- package/src/core/scripting/lua-runner.ts +210 -0
- package/src/core/scripts/runner.ts +125 -0
- package/src/core/soul/README.md +110 -0
- package/src/core/soul/RESEARCH.md +98 -0
- package/src/core/soul/associative.ts +98 -0
- package/src/core/soul/centrality.ts +98 -0
- package/src/core/soul/cluster.ts +83 -0
- package/src/core/soul/compiler.ts +207 -0
- package/src/core/soul/consolidate.ts +179 -0
- package/src/core/soul/critic.ts +159 -0
- package/src/core/soul/dag.ts +265 -0
- package/src/core/soul/delta.ts +123 -0
- package/src/core/soul/drift.ts +99 -0
- package/src/core/soul/embedder.ts +129 -0
- package/src/core/soul/emergent-critic.ts +96 -0
- package/src/core/soul/forgetting.ts +131 -0
- package/src/core/soul/governance.ts +93 -0
- package/src/core/soul/hash.ts +97 -0
- package/src/core/soul/hdc.ts +154 -0
- package/src/core/soul/index.ts +140 -0
- package/src/core/soul/kernel.ts +540 -0
- package/src/core/soul/lattice.ts +103 -0
- package/src/core/soul/lens.ts +110 -0
- package/src/core/soul/projector.ts +240 -0
- package/src/core/soul/reflect.ts +170 -0
- package/src/core/soul/reflex.ts +164 -0
- package/src/core/soul/retrieve.ts +146 -0
- package/src/core/soul/salience.ts +142 -0
- package/src/core/soul/service.ts +204 -0
- package/src/core/soul/settings.ts +47 -0
- package/src/core/soul/signals.ts +117 -0
- package/src/core/soul/talon-embedder.ts +80 -0
- package/src/core/soul/taps.ts +199 -0
- package/src/core/soul/types.ts +298 -0
- package/src/core/soul/valence.ts +83 -0
- package/src/core/tools/goals.ts +117 -0
- package/src/core/tools/index.ts +10 -0
- package/src/core/tools/mcp-env.ts +94 -0
- package/src/core/tools/mcp-server.ts +27 -4
- package/src/core/tools/messaging.ts +122 -29
- package/src/core/tools/models.ts +39 -0
- package/src/core/tools/scheduling.ts +133 -14
- package/src/core/tools/schemas.ts +16 -4
- package/src/core/tools/scripts.ts +78 -0
- package/src/core/tools/skills.ts +90 -0
- package/src/core/tools/stickers.ts +1 -1
- package/src/core/tools/triggers.ts +19 -3
- package/src/core/tools/types.ts +6 -6
- package/src/core/types.ts +62 -112
- package/src/core/update/self-update.ts +269 -0
- package/src/core/weaver/index.ts +10 -0
- package/src/core/weaver/loom.ts +125 -0
- package/src/core/weaver/thread-session.ts +51 -0
- package/src/core/weaver/thread.ts +196 -0
- package/src/core/weaver/weaver.ts +357 -0
- package/src/frontend/discord/actions/chat-info.ts +268 -0
- package/src/frontend/discord/actions/index.ts +77 -0
- package/src/frontend/discord/actions/media.ts +190 -0
- package/src/frontend/discord/actions/messaging.ts +277 -0
- package/src/frontend/discord/actions/shared.ts +100 -0
- package/src/frontend/discord/actions/types.ts +33 -0
- package/src/frontend/discord/admin.ts +13 -23
- package/src/frontend/discord/callbacks/autocomplete.ts +55 -0
- package/src/frontend/discord/{callbacks.ts → callbacks/components.ts} +150 -190
- package/src/frontend/discord/callbacks/index.ts +14 -0
- package/src/frontend/discord/callbacks/modals.ts +76 -0
- package/src/frontend/discord/callbacks/shared.ts +22 -0
- package/src/frontend/discord/commands/admin.ts +88 -0
- package/src/frontend/discord/commands/definitions.ts +200 -0
- package/src/frontend/discord/commands/index.ts +19 -0
- package/src/frontend/discord/commands/info.ts +106 -0
- package/src/frontend/discord/commands/router.ts +149 -0
- package/src/frontend/discord/commands/session.ts +89 -0
- package/src/frontend/discord/commands/settings.ts +405 -0
- package/src/frontend/discord/commands/shared.ts +50 -0
- package/src/frontend/discord/formatting.ts +8 -34
- package/src/frontend/discord/handlers/access.ts +248 -0
- package/src/frontend/discord/handlers/context.ts +161 -0
- package/src/frontend/discord/handlers/delivery.ts +94 -0
- package/src/frontend/discord/handlers/index.ts +34 -0
- package/src/frontend/discord/handlers/messages.ts +120 -0
- package/src/frontend/discord/handlers/queue.ts +135 -0
- package/src/frontend/discord/handlers/registry.ts +29 -0
- package/src/frontend/discord/handlers/state.ts +94 -0
- package/src/frontend/discord/helpers.ts +46 -57
- package/src/frontend/discord/index.ts +10 -7
- package/src/frontend/discord/middleware.ts +2 -2
- package/src/frontend/native/actions.ts +133 -0
- package/src/frontend/native/chats.ts +146 -0
- package/src/frontend/native/discovery.ts +50 -0
- package/src/frontend/native/index.ts +1227 -0
- package/src/frontend/native/logs.ts +95 -0
- package/src/frontend/native/protocol.ts +253 -0
- package/src/frontend/native/server.ts +612 -0
- package/src/frontend/native/settings.ts +205 -0
- package/src/frontend/native/turn-meta.ts +43 -0
- package/src/frontend/shared/format.ts +51 -0
- package/src/frontend/shared/reasoning-levels.ts +46 -0
- package/src/frontend/shared/session-status.ts +189 -0
- package/src/frontend/shared/status-context.ts +125 -0
- package/src/frontend/teams/actions.ts +7 -5
- package/src/frontend/teams/formatting.ts +7 -18
- package/src/frontend/teams/graph.ts +2 -1
- package/src/frontend/teams/index.ts +76 -54
- package/src/frontend/telegram/actions/chat-info.ts +323 -0
- package/src/frontend/telegram/actions/index.ts +68 -0
- package/src/frontend/telegram/actions/media.ts +224 -0
- package/src/frontend/telegram/actions/messaging.ts +295 -0
- package/src/frontend/telegram/actions/shared.ts +58 -0
- package/src/frontend/telegram/actions/types.ts +31 -0
- package/src/frontend/telegram/admin.ts +12 -22
- package/src/frontend/telegram/callbacks/effort.ts +68 -0
- package/src/frontend/telegram/callbacks/index.ts +68 -0
- package/src/frontend/telegram/callbacks/model.ts +311 -0
- package/src/frontend/telegram/callbacks/pulse.ts +50 -0
- package/src/frontend/telegram/callbacks/settings.ts +177 -0
- package/src/frontend/telegram/callbacks/shared.ts +71 -0
- package/src/frontend/telegram/commands/admin.ts +226 -0
- package/src/frontend/telegram/commands/definitions.ts +60 -0
- package/src/frontend/telegram/commands/index.ts +39 -0
- package/src/frontend/telegram/commands/info.ts +125 -0
- package/src/frontend/telegram/commands/session.ts +77 -0
- package/src/frontend/telegram/commands/settings.ts +345 -0
- package/src/frontend/telegram/commands/state.ts +34 -0
- package/src/frontend/telegram/formatting.ts +14 -25
- package/src/frontend/telegram/handlers/access.ts +287 -0
- package/src/frontend/telegram/handlers/context.ts +210 -0
- package/src/frontend/telegram/handlers/delivery.ts +236 -0
- package/src/frontend/telegram/handlers/index.ts +39 -0
- package/src/frontend/telegram/handlers/messages.ts +477 -0
- package/src/frontend/telegram/handlers/queue.ts +241 -0
- package/src/frontend/telegram/handlers/state.ts +75 -0
- package/src/frontend/telegram/helpers/diagnostics.ts +188 -0
- package/src/frontend/telegram/helpers/format.ts +41 -0
- package/src/frontend/telegram/helpers/index.ts +13 -0
- package/src/frontend/telegram/{helpers.ts → helpers/menu.ts} +209 -210
- package/src/frontend/telegram/index.ts +24 -27
- package/src/frontend/telegram/middleware.ts +16 -3
- package/src/frontend/telegram/model-callbacks.ts +14 -0
- package/src/frontend/telegram/model-menu.ts +317 -0
- package/src/frontend/telegram/sticker-library.ts +178 -0
- package/src/frontend/telegram/userbot.ts +2 -44
- package/src/frontend/terminal/commands.ts +47 -41
- package/src/frontend/terminal/index.ts +46 -31
- package/src/frontend/terminal/input.ts +5 -0
- package/src/frontend/terminal/renderer.ts +4 -2
- package/src/index.ts +13 -171
- package/src/login.ts +3 -2
- package/src/native/blake3-wasm-bytes.ts +7 -0
- package/src/native/blake3.ts +234 -0
- package/src/native/htmlents-wasm-bytes.ts +7 -0
- package/src/native/htmlents.ts +56 -0
- package/src/native/prelude.d.mts +168 -0
- package/src/native/prelude.mjs +1561 -0
- package/src/native/registry.ts +142 -0
- package/src/native/runtime.ts +164 -0
- package/src/native/scheduler-core/gleam.d.mts +2 -0
- package/src/native/scheduler-core/gleam.mjs +1 -0
- package/src/native/scheduler-core/scheduler_core.d.mts +76 -0
- package/src/native/scheduler-core/scheduler_core.mjs +252 -0
- package/src/native/scheduler-core.ts +124 -0
- package/src/native/sqlguard-wasm-bytes.ts +7 -0
- package/src/native/sqlguard.ts +107 -0
- package/src/native/strsim-wasm-bytes.ts +7 -0
- package/src/native/strsim.ts +98 -0
- package/src/native/textops-wasm-bytes.ts +7 -0
- package/src/native/textops.ts +83 -0
- package/src/native/warden.ts +287 -0
- package/src/plugins/github/index.ts +1 -1
- package/src/plugins/mempalace/index.ts +1 -1
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/chat-settings.ts +335 -85
- package/src/storage/cron-store.ts +266 -109
- package/src/storage/daily-log.ts +15 -3
- package/src/storage/db.ts +206 -0
- package/src/storage/goal-store.ts +186 -0
- package/src/storage/history.ts +137 -188
- package/src/storage/kv.ts +52 -0
- package/src/storage/legacy-import.ts +47 -0
- package/src/storage/media-index.ts +118 -61
- package/src/storage/repositories/chat-settings-repo.ts +60 -0
- package/src/storage/repositories/cron-repo.ts +128 -0
- package/src/storage/repositories/goals-repo.ts +122 -0
- package/src/storage/repositories/history-repo.ts +207 -0
- package/src/storage/repositories/kv-repo.ts +25 -0
- package/src/storage/repositories/media-index-repo.ts +147 -0
- package/src/storage/repositories/scripts-repo.ts +81 -0
- package/src/storage/repositories/sessions-repo.ts +129 -0
- package/src/storage/repositories/triggers-repo.ts +158 -0
- package/src/storage/repositories/turn-meta-repo.ts +32 -0
- package/src/storage/scheduled-store.ts +84 -0
- package/src/storage/script-store.ts +198 -0
- package/src/storage/sessions.ts +227 -102
- package/src/storage/skill-store.ts +325 -0
- package/src/storage/sql/README.md +18 -0
- package/src/storage/sql/chat-settings.sql +11 -0
- package/src/storage/sql/cron.sql +40 -0
- package/src/storage/sql/db.sql +11 -0
- package/src/storage/sql/embed.ts +118 -0
- package/src/storage/sql/goals.sql +41 -0
- package/src/storage/sql/history.sql +83 -0
- package/src/storage/sql/kv.sql +11 -0
- package/src/storage/sql/media-index.sql +45 -0
- package/src/storage/sql/schema.sql +233 -0
- package/src/storage/sql/scripts.sql +28 -0
- package/src/storage/sql/sessions.sql +22 -0
- package/src/storage/sql/statements.generated.ts +500 -0
- package/src/storage/sql/triggers.sql +69 -0
- package/src/storage/sql/turn-meta.sql +24 -0
- package/src/storage/sticker-store.ts +141 -0
- package/src/storage/trigger-store.ts +104 -126
- package/src/storage/turn-meta.ts +118 -0
- package/src/util/chat-id.ts +30 -0
- package/src/util/config.ts +313 -195
- package/src/util/fs-path.ts +32 -0
- package/src/util/log.ts +53 -31
- package/src/util/mcp-launcher.ts +351 -33
- package/src/util/paths.ts +45 -17
- package/src/util/tail-file.ts +24 -0
- package/src/util/workspace.ts +161 -10
- package/tsconfig.json +1 -0
- package/src/backend/claude-sdk/models.ts +0 -501
- package/src/backend/codex/handler.ts +0 -595
- package/src/backend/kilo/handler.ts +0 -707
- package/src/backend/kilo/models.ts +0 -762
- package/src/backend/openai-agents/handler.ts +0 -539
- package/src/backend/openai-agents/mcp.ts +0 -139
- package/src/backend/opencode/handler.ts +0 -663
- package/src/backend/opencode/models.ts +0 -741
- package/src/core/cron.ts +0 -184
- package/src/core/dispatcher.ts +0 -144
- package/src/core/gateway-actions.ts +0 -603
- package/src/core/heartbeat.ts +0 -575
- package/src/core/plugin.ts +0 -776
- package/src/core/triggers.ts +0 -640
- package/src/frontend/discord/actions.ts +0 -729
- package/src/frontend/discord/commands.ts +0 -1036
- package/src/frontend/discord/handlers.ts +0 -798
- package/src/frontend/telegram/actions.ts +0 -731
- package/src/frontend/telegram/callbacks.ts +0 -419
- package/src/frontend/telegram/commands.ts +0 -642
- package/src/frontend/telegram/handlers.ts +0 -1352
- package/src/util/mcp-launcher.mjs +0 -140
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat scheduling + lifecycle — init, the cadence timer (Gleam
|
|
3
|
+
* scheduler-core driven, restart/suspend-safe), the one-at-a-time run guard,
|
|
4
|
+
* and the force/await/status public API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log, logError, logWarn } from "../../../util/log.js";
|
|
8
|
+
import {
|
|
9
|
+
catchupRunCount,
|
|
10
|
+
missedRunCount,
|
|
11
|
+
nextDueMs,
|
|
12
|
+
} from "../../../native/scheduler-core.js";
|
|
13
|
+
import {
|
|
14
|
+
hb,
|
|
15
|
+
readHeartbeatState,
|
|
16
|
+
writeHeartbeatState,
|
|
17
|
+
type HeartbeatConfig,
|
|
18
|
+
type HeartbeatState,
|
|
19
|
+
} from "./state.js";
|
|
20
|
+
import { HeartbeatTimeoutError, runHeartbeatAgent } from "./agent.js";
|
|
21
|
+
|
|
22
|
+
const STARTUP_DELAY_MS = 5 * 60 * 1000; // 5-minute delay before first run
|
|
23
|
+
const DUE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
24
|
+
|
|
25
|
+
export function initHeartbeat(cfg: HeartbeatConfig): void {
|
|
26
|
+
hb.config = cfg;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start the heartbeat timer. First run happens after a 5-minute startup delay,
|
|
31
|
+
* then repeats at the configured interval.
|
|
32
|
+
*/
|
|
33
|
+
export function startHeartbeatTimer(intervalMinutes: number): void {
|
|
34
|
+
if (hb.timer || hb.startupTimer) return; // already running or scheduled
|
|
35
|
+
|
|
36
|
+
if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) {
|
|
37
|
+
logWarn(
|
|
38
|
+
"heartbeat",
|
|
39
|
+
`Refusing to start heartbeat timer with invalid intervalMinutes: ${intervalMinutes}`,
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hb.intervalMinutesRef = intervalMinutes;
|
|
45
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
46
|
+
log(
|
|
47
|
+
"heartbeat",
|
|
48
|
+
`Starting heartbeat timer (every ${intervalMinutes}min, first due check in 5min)`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
hb.startupTimer = setTimeout(() => {
|
|
52
|
+
hb.startupTimer = null;
|
|
53
|
+
runIfDue(intervalMs, true);
|
|
54
|
+
// Due checks every minute instead of one fixed setInterval(intervalMs):
|
|
55
|
+
// the cadence is computed from persisted last_run via the Gleam scheduler
|
|
56
|
+
// core, so it survives process restarts (a quick restart no longer resets
|
|
57
|
+
// the phase or double-fires) and system suspends (any number of missed
|
|
58
|
+
// fire times collapses into one catch-up run).
|
|
59
|
+
hb.timer = setInterval(() => {
|
|
60
|
+
runIfDue(intervalMs, false);
|
|
61
|
+
}, DUE_CHECK_INTERVAL_MS);
|
|
62
|
+
}, STARTUP_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fire the heartbeat when its cadence says one (or more) runs are due. Decision
|
|
67
|
+
* logic is the Gleam scheduler core: `missedRunCount` counts fire times in
|
|
68
|
+
* (last_run, now], and the "once" catch-up policy collapses them into one run.
|
|
69
|
+
*/
|
|
70
|
+
function runIfDue(intervalMs: number, startup: boolean): void {
|
|
71
|
+
if (hb.running) return;
|
|
72
|
+
const lastRun = readHeartbeatState()?.last_run ?? 0;
|
|
73
|
+
if (lastRun <= 0) {
|
|
74
|
+
// Never ran on this install — fire now to establish the cadence.
|
|
75
|
+
executeHeartbeat("auto").catch(() => {});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const missed = missedRunCount(lastRun, intervalMs, now);
|
|
80
|
+
if (catchupRunCount(missed, "once", 1) > 0) {
|
|
81
|
+
executeHeartbeat("auto").catch(() => {});
|
|
82
|
+
} else if (startup) {
|
|
83
|
+
log(
|
|
84
|
+
"heartbeat",
|
|
85
|
+
`Cadence restored from state — next heartbeat due ${new Date(nextDueMs(lastRun, intervalMs, now)).toISOString()}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Stop the heartbeat timer. Does not wait for in-flight runs. Use
|
|
92
|
+
* awaitCurrentRun() after this to wait for a running heartbeat to finish.
|
|
93
|
+
*/
|
|
94
|
+
export function stopHeartbeatTimer(): void {
|
|
95
|
+
if (hb.startupTimer) {
|
|
96
|
+
clearTimeout(hb.startupTimer);
|
|
97
|
+
hb.startupTimer = null;
|
|
98
|
+
}
|
|
99
|
+
if (hb.timer) {
|
|
100
|
+
clearInterval(hb.timer);
|
|
101
|
+
hb.timer = null;
|
|
102
|
+
log("heartbeat", "Heartbeat timer stopped");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wait for any in-flight heartbeat run to complete.
|
|
108
|
+
* Call after stopHeartbeatTimer() during graceful shutdown.
|
|
109
|
+
*/
|
|
110
|
+
export async function awaitCurrentRun(timeoutMs = 10_000): Promise<void> {
|
|
111
|
+
if (hb.currentRunPromise) {
|
|
112
|
+
log("heartbeat", "Waiting for in-flight heartbeat to complete...");
|
|
113
|
+
try {
|
|
114
|
+
await Promise.race([
|
|
115
|
+
hb.currentRunPromise,
|
|
116
|
+
new Promise<void>((resolve) => {
|
|
117
|
+
const t = setTimeout(() => {
|
|
118
|
+
logWarn(
|
|
119
|
+
"heartbeat",
|
|
120
|
+
"In-flight heartbeat did not finish within shutdown budget, proceeding",
|
|
121
|
+
);
|
|
122
|
+
resolve();
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
t.unref();
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
} catch {
|
|
128
|
+
// Already logged in executeHeartbeat
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Force a heartbeat run immediately. Resolves when the heartbeat completes.
|
|
135
|
+
* Throws if a heartbeat is already running.
|
|
136
|
+
*/
|
|
137
|
+
export async function forceHeartbeat(): Promise<void> {
|
|
138
|
+
if (hb.running) throw new Error("Heartbeat already running");
|
|
139
|
+
await executeHeartbeat("forced");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Get the current heartbeat status. */
|
|
143
|
+
export function getHeartbeatStatus(): HeartbeatState | null {
|
|
144
|
+
return readHeartbeatState();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeHeartbeat(trigger: "auto" | "forced"): Promise<void> {
|
|
148
|
+
if (hb.running) return;
|
|
149
|
+
|
|
150
|
+
const state = readHeartbeatState();
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const previousRunCount = state?.run_count ?? 0;
|
|
153
|
+
const previousLastRun = state?.last_run ?? 0;
|
|
154
|
+
|
|
155
|
+
hb.running = true;
|
|
156
|
+
// Mark as running with last_started, but preserve last_run from previous run
|
|
157
|
+
writeHeartbeatState({
|
|
158
|
+
last_run: previousLastRun,
|
|
159
|
+
last_started: now,
|
|
160
|
+
status: "running",
|
|
161
|
+
run_count: previousRunCount,
|
|
162
|
+
});
|
|
163
|
+
log(
|
|
164
|
+
"heartbeat",
|
|
165
|
+
`${trigger === "forced" ? "Force-triggering" : "Triggering"} heartbeat #${previousRunCount + 1} (last run: ${previousLastRun ? new Date(previousLastRun).toISOString() : "never"})`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const run = (async () => {
|
|
169
|
+
try {
|
|
170
|
+
const heartbeatLogPath = await runHeartbeatAgent(
|
|
171
|
+
previousLastRun,
|
|
172
|
+
previousRunCount + 1,
|
|
173
|
+
);
|
|
174
|
+
// Only update last_run and increment run_count on success
|
|
175
|
+
writeHeartbeatState({
|
|
176
|
+
last_run: Date.now(),
|
|
177
|
+
last_started: now,
|
|
178
|
+
status: "idle",
|
|
179
|
+
run_count: previousRunCount + 1,
|
|
180
|
+
});
|
|
181
|
+
log(
|
|
182
|
+
"heartbeat",
|
|
183
|
+
`Heartbeat #${previousRunCount + 1} complete (${trigger}), log: ${heartbeatLogPath}`,
|
|
184
|
+
);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
logError(
|
|
187
|
+
"heartbeat",
|
|
188
|
+
`Heartbeat #${previousRunCount + 1} failed (${trigger})`,
|
|
189
|
+
err,
|
|
190
|
+
);
|
|
191
|
+
// Timeouts consume the full hour budget — advance state so the next
|
|
192
|
+
// heartbeat sees a fresh window and a bumped run_count instead of
|
|
193
|
+
// re-triggering against the same `last_run` forever. Non-timeout errors
|
|
194
|
+
// retry from the previous successful run (no budget consumed).
|
|
195
|
+
const isTimeout = err instanceof HeartbeatTimeoutError;
|
|
196
|
+
writeHeartbeatState({
|
|
197
|
+
last_run: isTimeout ? Date.now() : previousLastRun,
|
|
198
|
+
last_started: now,
|
|
199
|
+
status: "idle",
|
|
200
|
+
run_count: isTimeout ? previousRunCount + 1 : previousRunCount,
|
|
201
|
+
});
|
|
202
|
+
if (trigger === "forced") throw err;
|
|
203
|
+
} finally {
|
|
204
|
+
hb.running = false;
|
|
205
|
+
hb.currentRunPromise = null;
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
|
|
209
|
+
hb.currentRunPromise = run;
|
|
210
|
+
await run;
|
|
211
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat shared state — the in-process run guard + timers + config ref,
|
|
3
|
+
* the persisted state-file shape, and its read/write helpers.
|
|
4
|
+
*
|
|
5
|
+
* The scheduler and agent submodules both read/write this state, so the
|
|
6
|
+
* mutable fields live on a single `hb` holder object imported by both.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { files as pathFiles } from "../../../util/paths.js";
|
|
10
|
+
import { kvGet, kvSet } from "../../../storage/kv.js";
|
|
11
|
+
import { importLegacyJson } from "../../../storage/legacy-import.js";
|
|
12
|
+
import type { Backend } from "../../agent-runtime/capabilities.js";
|
|
13
|
+
|
|
14
|
+
export type HeartbeatState = {
|
|
15
|
+
/** Unix millisecond timestamp of the last successfully completed heartbeat run. */
|
|
16
|
+
last_run: number;
|
|
17
|
+
/** Human-readable ISO timestamp of the last successfully completed heartbeat run. */
|
|
18
|
+
last_run_at?: string;
|
|
19
|
+
/** Unix millisecond timestamp of the last time a heartbeat was started (success or failure). */
|
|
20
|
+
last_started?: number;
|
|
21
|
+
/** "idle" when no heartbeat is running, "running" while one is active. */
|
|
22
|
+
status: "idle" | "running";
|
|
23
|
+
/** Total number of successfully completed heartbeat runs. */
|
|
24
|
+
run_count: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type HeartbeatConfig = {
|
|
28
|
+
model?: string;
|
|
29
|
+
heartbeatModel?: string;
|
|
30
|
+
workspace?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Accessor for the active backend — invoked each time a heartbeat fires so
|
|
33
|
+
* backend hot-swaps performed by the controller take effect on the next
|
|
34
|
+
* heartbeat without an `initHeartbeat` recall.
|
|
35
|
+
*/
|
|
36
|
+
getBackend?: () => Backend | null;
|
|
37
|
+
/**
|
|
38
|
+
* Non-terminal frontends present at startup. Used to render the outbound
|
|
39
|
+
* messaging section of the heartbeat system prompt. Empty for terminal-only
|
|
40
|
+
* deployments.
|
|
41
|
+
*/
|
|
42
|
+
frontends?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* MemPalace presence flag — when true, the prompt tells the agent to recall
|
|
45
|
+
* palace context before working a goal and store learnings afterwards.
|
|
46
|
+
*/
|
|
47
|
+
mempalace?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* kv key owning the persisted heartbeat state. The blob used to live at
|
|
52
|
+
* `pathFiles.heartbeatState` as hand-rolled JSON; it now rides the shared
|
|
53
|
+
* `kv` table so it inherits transactional writes and TALON_DB_PATH test
|
|
54
|
+
* isolation like the rest of the unified storage layer.
|
|
55
|
+
*/
|
|
56
|
+
const HEARTBEAT_STATE_KEY = "heartbeat.state";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mutable heartbeat runtime state, shared across submodules:
|
|
60
|
+
* - `running` / `currentRunPromise` — the one-at-a-time run guard
|
|
61
|
+
* - `timer` / `startupTimer` — the cadence timers
|
|
62
|
+
* - `intervalMinutesRef` — interval captured from startHeartbeatTimer
|
|
63
|
+
* - `config` — injected via initHeartbeat
|
|
64
|
+
* - `logFileSequence` — monotonic per-run log filename counter
|
|
65
|
+
*/
|
|
66
|
+
export const hb: {
|
|
67
|
+
running: boolean;
|
|
68
|
+
currentRunPromise: Promise<void> | null;
|
|
69
|
+
timer: ReturnType<typeof setInterval> | null;
|
|
70
|
+
startupTimer: ReturnType<typeof setTimeout> | null;
|
|
71
|
+
intervalMinutesRef: number;
|
|
72
|
+
config: HeartbeatConfig | null;
|
|
73
|
+
logFileSequence: number;
|
|
74
|
+
} = {
|
|
75
|
+
running: false,
|
|
76
|
+
currentRunPromise: null,
|
|
77
|
+
timer: null,
|
|
78
|
+
startupTimer: null,
|
|
79
|
+
intervalMinutesRef: 60,
|
|
80
|
+
config: null,
|
|
81
|
+
logFileSequence: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ── State-file I/O ───────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function normalizeHeartbeatState(parsed: unknown): HeartbeatState | null {
|
|
87
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
88
|
+
|
|
89
|
+
const candidate = parsed as Record<string, unknown>;
|
|
90
|
+
const { last_run, last_run_at, last_started, status, run_count } = candidate;
|
|
91
|
+
|
|
92
|
+
if (typeof last_run !== "number" || !Number.isFinite(last_run)) return null;
|
|
93
|
+
if (typeof run_count !== "number" || !Number.isFinite(run_count)) return null;
|
|
94
|
+
if (status !== "idle" && status !== "running") return null;
|
|
95
|
+
if (last_run_at !== undefined && typeof last_run_at !== "string") return null;
|
|
96
|
+
if (
|
|
97
|
+
last_started !== undefined &&
|
|
98
|
+
(typeof last_started !== "number" || !Number.isFinite(last_started))
|
|
99
|
+
) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
last_run,
|
|
105
|
+
run_count,
|
|
106
|
+
status,
|
|
107
|
+
...(last_run_at !== undefined ? { last_run_at } : {}),
|
|
108
|
+
...(last_started !== undefined ? { last_started } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fold the pre-SQLite JSON file into the kv store exactly once per
|
|
114
|
+
* process. Runs lazily on the first read rather than at boot so a
|
|
115
|
+
* heartbeat-free deployment never pays for it. Gated + idempotent via
|
|
116
|
+
* the module-level `imported` flag; the rename to `<file>.imported`
|
|
117
|
+
* inside importLegacyJson stops it re-running across restarts.
|
|
118
|
+
*/
|
|
119
|
+
let imported = false;
|
|
120
|
+
function importLegacyStateOnce(): void {
|
|
121
|
+
if (imported) return;
|
|
122
|
+
imported = true;
|
|
123
|
+
importLegacyJson({
|
|
124
|
+
path: pathFiles.heartbeatState,
|
|
125
|
+
category: "heartbeat",
|
|
126
|
+
what: "heartbeat state",
|
|
127
|
+
ingest: (data) => {
|
|
128
|
+
const normalized = normalizeHeartbeatState(data);
|
|
129
|
+
if (!normalized) return 0;
|
|
130
|
+
kvSet(HEARTBEAT_STATE_KEY, normalized);
|
|
131
|
+
return 1;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function readHeartbeatState(): HeartbeatState | null {
|
|
137
|
+
importLegacyStateOnce();
|
|
138
|
+
return normalizeHeartbeatState(kvGet(HEARTBEAT_STATE_KEY));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function writeHeartbeatState(state: HeartbeatState): void {
|
|
142
|
+
// Re-derive last_run_at from last_run so the persisted ISO stamp can
|
|
143
|
+
// never drift from the millisecond field; omit it on the sentinel
|
|
144
|
+
// last_run === 0 (never-run) to match the pre-SQLite file format.
|
|
145
|
+
const { last_run_at: _lastRunAt, ...rest } = state;
|
|
146
|
+
const enriched: HeartbeatState = {
|
|
147
|
+
...rest,
|
|
148
|
+
...(state.last_run !== 0
|
|
149
|
+
? { last_run_at: new Date(state.last_run).toISOString() }
|
|
150
|
+
: {}),
|
|
151
|
+
};
|
|
152
|
+
kvSet(HEARTBEAT_STATE_KEY, enriched);
|
|
153
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background agents — everything that runs without a user message.
|
|
3
|
+
*
|
|
4
|
+
* - `heartbeat` — periodic autonomous agent pass ("anything need
|
|
5
|
+
* doing?"), prompted by prompts/heartbeat.md.
|
|
6
|
+
* - `dream` — memory-consolidation agent (daily notes → memory,
|
|
7
|
+
* mempalace mining), prompted by prompts/dream.md.
|
|
8
|
+
* - `pulse` — lightweight liveness/typing-indicator ticker.
|
|
9
|
+
* - `cron` — persistent user-scheduled jobs (message / query).
|
|
10
|
+
* - `triggers` — user-authored watcher scripts that fire wake-ups.
|
|
11
|
+
*
|
|
12
|
+
* All of these drive the backend through the same `BackgroundRunner`
|
|
13
|
+
* surface (core/agent-runtime/capabilities.ts) — they never talk to a
|
|
14
|
+
* concrete backend directly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export * from "./heartbeat/index.js";
|
|
18
|
+
export * from "./dream.js";
|
|
19
|
+
export * from "./pulse.js";
|
|
20
|
+
export * from "./cron.js";
|
|
21
|
+
export * from "./triggers/index.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic isolated one-shot agent runner.
|
|
3
|
+
*
|
|
4
|
+
* Runs a `background.runOneShotAgent` under a hard timeout with the same
|
|
5
|
+
* abort → bounded-grace → (optional) orphan-eviction discipline heartbeat uses,
|
|
6
|
+
* so any unattended one-shot (heartbeat, dream, decoupled jobs) gets robust
|
|
7
|
+
* cancellation instead of hand-rolling it per caller.
|
|
8
|
+
*
|
|
9
|
+
* Flow: race the agent against a timeout. On timeout, abort the controller and
|
|
10
|
+
* give the backend a bounded grace window to honour it; if it doesn't, optionally
|
|
11
|
+
* ask the backend to evict orphan subprocesses (Linux /proc sweep). Eviction is
|
|
12
|
+
* opt-in via `evictLabel` because it matches subprocesses by an env tag — a
|
|
13
|
+
* caller that shares a tag with another context (e.g. heartbeat) should leave it
|
|
14
|
+
* unset to avoid sweeping the other context's subprocesses.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { BackgroundRunner } from "../agent-runtime/capabilities.js";
|
|
18
|
+
import type { OneShotAgentParams } from "../types.js";
|
|
19
|
+
import { logWarn, logError } from "../../util/log.js";
|
|
20
|
+
|
|
21
|
+
/** Default bounded grace after an abort before giving up on the backend. */
|
|
22
|
+
export const DEFAULT_ABORT_GRACE_MS = 30 * 1000;
|
|
23
|
+
|
|
24
|
+
export interface IsolatedRunOptions {
|
|
25
|
+
readonly background: BackgroundRunner;
|
|
26
|
+
/** Fully-built one-shot params (must carry an `abortController`). */
|
|
27
|
+
readonly params: OneShotAgentParams;
|
|
28
|
+
/** Hard timeout before the run is aborted. */
|
|
29
|
+
readonly timeoutMs: number;
|
|
30
|
+
/** Bounded grace for the backend to honour the abort (default 30s). */
|
|
31
|
+
readonly abortGraceMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* When set, evict orphan subprocesses carrying this context tag if the backend
|
|
34
|
+
* ignores the abort. Leave unset when the tag is shared with another context.
|
|
35
|
+
*/
|
|
36
|
+
readonly evictLabel?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Resolves to the value, or the string "timed_out" if `ms` elapses first. */
|
|
40
|
+
async function raceWithTimeout<T>(
|
|
41
|
+
promise: Promise<T>,
|
|
42
|
+
ms: number,
|
|
43
|
+
): Promise<T | "timed_out"> {
|
|
44
|
+
let handle: ReturnType<typeof setTimeout> | undefined;
|
|
45
|
+
const timeout = new Promise<"timed_out">((resolve) => {
|
|
46
|
+
handle = setTimeout(() => resolve("timed_out"), ms);
|
|
47
|
+
handle.unref();
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
return await Promise.race([promise, timeout]);
|
|
51
|
+
} finally {
|
|
52
|
+
if (handle) clearTimeout(handle);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Run the one-shot under a hard timeout. Throws on timeout (after the grace
|
|
58
|
+
* window) and re-throws any agent error.
|
|
59
|
+
*/
|
|
60
|
+
export async function runIsolatedAgent(
|
|
61
|
+
opts: IsolatedRunOptions,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const { background, params, timeoutMs } = opts;
|
|
64
|
+
const graceMs = opts.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS;
|
|
65
|
+
|
|
66
|
+
let timedOut = false;
|
|
67
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
const agentPromise = background.runOneShotAgent(params);
|
|
69
|
+
|
|
70
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
71
|
+
timer = setTimeout(() => {
|
|
72
|
+
timedOut = true;
|
|
73
|
+
try {
|
|
74
|
+
params.abortController.abort();
|
|
75
|
+
} catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
reject(new Error(`isolated agent timed out after ${timeoutMs}ms`));
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
timer.unref();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await Promise.race([agentPromise, timeoutPromise]);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Snapshot + clear before any await so a late timer can't reclassify a
|
|
87
|
+
// non-timeout failure as a timeout (heartbeat learned this the hard way).
|
|
88
|
+
const wasTimeout = timedOut;
|
|
89
|
+
if (timer) {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
timer = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (wasTimeout) {
|
|
94
|
+
const settled = await raceWithTimeout(
|
|
95
|
+
agentPromise.catch(() => "settled"),
|
|
96
|
+
graceMs,
|
|
97
|
+
);
|
|
98
|
+
if (settled === "timed_out" && opts.evictLabel) {
|
|
99
|
+
const evict = background.evictOrphanSubprocesses;
|
|
100
|
+
if (evict) {
|
|
101
|
+
evict(opts.evictLabel).catch((sweepErr: unknown) => {
|
|
102
|
+
logError("triggers", "orphan subprocess sweep failed", sweepErr);
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
logWarn("triggers", "backend ignored abort and has no eviction hook");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
await agentPromise.catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
if (timer) clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-job health tracking for the cron scheduler — circuit breakers
|
|
3
|
+
* with escalating cooldowns, built on the Gleam scheduler decision
|
|
4
|
+
* core (src/native/scheduler-core.ts).
|
|
5
|
+
*
|
|
6
|
+
* A job that keeps throwing stops being dispatched every tick: after
|
|
7
|
+
* `threshold` consecutive failures its breaker opens and runs are
|
|
8
|
+
* blocked while it cools down. Cooldown length comes from the core's
|
|
9
|
+
* deterministic backoff (escalating with each re-open, jittered per
|
|
10
|
+
* job so herds de-correlate). After cooling down, one probe run is
|
|
11
|
+
* allowed (half-open); success closes the breaker, failure re-opens it
|
|
12
|
+
* with a longer cooldown.
|
|
13
|
+
*
|
|
14
|
+
* State is in-memory by design — a restart gives every job a clean
|
|
15
|
+
* slate, which is the behavior you want after fixing whatever made the
|
|
16
|
+
* job fail.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
backoffDelayMs,
|
|
21
|
+
breakerAllowsRun,
|
|
22
|
+
CLOSED_BREAKER,
|
|
23
|
+
stepBreaker,
|
|
24
|
+
type Breaker,
|
|
25
|
+
} from "../../native/scheduler-core.js";
|
|
26
|
+
|
|
27
|
+
export type JobHealthOptions = {
|
|
28
|
+
/** Consecutive failures before the breaker opens. */
|
|
29
|
+
threshold: number;
|
|
30
|
+
/** Cooldown after the first open. */
|
|
31
|
+
baseCooldownMs: number;
|
|
32
|
+
/** Cooldown ceiling for repeatedly re-opening jobs. */
|
|
33
|
+
maxCooldownMs: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type JobHealth = {
|
|
37
|
+
breaker: Breaker;
|
|
38
|
+
/** Times the breaker has opened since the last success — drives the
|
|
39
|
+
* cooldown escalation (the core's breaker resets its own failure
|
|
40
|
+
* count when it opens). */
|
|
41
|
+
opens: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const health = new Map<string, JobHealth>();
|
|
45
|
+
|
|
46
|
+
function get(jobId: string): JobHealth {
|
|
47
|
+
let h = health.get(jobId);
|
|
48
|
+
if (!h) {
|
|
49
|
+
h = { breaker: CLOSED_BREAKER, opens: 0 };
|
|
50
|
+
health.set(jobId, h);
|
|
51
|
+
}
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Deterministic per-job jitter seed (djb2 over the id). */
|
|
56
|
+
function jobSeed(jobId: string): number {
|
|
57
|
+
let hash = 5381;
|
|
58
|
+
for (let i = 0; i < jobId.length; i++) {
|
|
59
|
+
hash = (hash * 33 + jobId.charCodeAt(i)) | 0;
|
|
60
|
+
}
|
|
61
|
+
return Math.abs(hash);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cooldownMs(
|
|
65
|
+
jobId: string,
|
|
66
|
+
h: JobHealth,
|
|
67
|
+
opts: JobHealthOptions,
|
|
68
|
+
): number {
|
|
69
|
+
return backoffDelayMs(Math.max(1, h.opens), {
|
|
70
|
+
baseMs: opts.baseCooldownMs,
|
|
71
|
+
capMs: opts.maxCooldownMs,
|
|
72
|
+
seed: jobSeed(jobId) + h.opens,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* May this job run now? Also advances a cooled-down open breaker to
|
|
78
|
+
* half-open (the tick event), so the answer flips to true exactly when
|
|
79
|
+
* the cooldown elapses.
|
|
80
|
+
*/
|
|
81
|
+
export function jobAllowsRun(
|
|
82
|
+
jobId: string,
|
|
83
|
+
nowMs: number,
|
|
84
|
+
opts: JobHealthOptions,
|
|
85
|
+
): boolean {
|
|
86
|
+
const h = get(jobId);
|
|
87
|
+
h.breaker = stepBreaker(h.breaker, "tick", nowMs, {
|
|
88
|
+
threshold: opts.threshold,
|
|
89
|
+
cooldownMs: cooldownMs(jobId, h, opts),
|
|
90
|
+
});
|
|
91
|
+
return breakerAllowsRun(h.breaker);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Record a successful run — closes the breaker, resets escalation. */
|
|
95
|
+
export function recordJobSuccess(
|
|
96
|
+
jobId: string,
|
|
97
|
+
nowMs: number,
|
|
98
|
+
opts: JobHealthOptions,
|
|
99
|
+
): void {
|
|
100
|
+
const h = get(jobId);
|
|
101
|
+
h.breaker = stepBreaker(h.breaker, "success", nowMs, {
|
|
102
|
+
threshold: opts.threshold,
|
|
103
|
+
cooldownMs: cooldownMs(jobId, h, opts),
|
|
104
|
+
});
|
|
105
|
+
h.opens = 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Record a failed run. Returns the cooldown in ms when this failure
|
|
110
|
+
* opened the breaker (caller logs it), else null.
|
|
111
|
+
*/
|
|
112
|
+
export function recordJobFailure(
|
|
113
|
+
jobId: string,
|
|
114
|
+
nowMs: number,
|
|
115
|
+
opts: JobHealthOptions,
|
|
116
|
+
): number | null {
|
|
117
|
+
const h = get(jobId);
|
|
118
|
+
const wasOpen = h.breaker.state === "open";
|
|
119
|
+
h.breaker = stepBreaker(h.breaker, "failure", nowMs, {
|
|
120
|
+
threshold: opts.threshold,
|
|
121
|
+
cooldownMs: cooldownMs(jobId, h, opts),
|
|
122
|
+
});
|
|
123
|
+
if (h.breaker.state === "open" && !wasOpen) {
|
|
124
|
+
h.opens += 1;
|
|
125
|
+
return cooldownMs(jobId, h, opts);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Drop tracking for jobs that no longer exist (deleted cron jobs). */
|
|
131
|
+
export function pruneJobHealth(liveIds: ReadonlySet<string>): void {
|
|
132
|
+
for (const id of health.keys()) {
|
|
133
|
+
if (!liveIds.has(id)) health.delete(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Test hook: forget everything. */
|
|
138
|
+
export function resetJobHealth(): void {
|
|
139
|
+
health.clear();
|
|
140
|
+
}
|