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,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-message-type handlers — text, photo, document, voice, sticker, video,
|
|
3
|
+
* animation, audio, video-note, and callback queries. Each validates access /
|
|
4
|
+
* rate limits, builds a prompt, and enqueues (or runs the agent directly for
|
|
5
|
+
* callback queries).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Bot, Context } from "grammy";
|
|
9
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
10
|
+
import { escapeHtml } from "../formatting.js";
|
|
11
|
+
import { friendlyMessage } from "../../../core/errors.js";
|
|
12
|
+
import { setMessageFilePath } from "../../../storage/history.js";
|
|
13
|
+
import { addMedia } from "../../../storage/media-index.js";
|
|
14
|
+
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
15
|
+
import { logError } from "../../../util/log.js";
|
|
16
|
+
import { recordMessageSignal } from "../../../core/soul/taps.js";
|
|
17
|
+
import {
|
|
18
|
+
getSenderName,
|
|
19
|
+
getReplyContext,
|
|
20
|
+
getForwardContext,
|
|
21
|
+
downloadReplyPhoto,
|
|
22
|
+
downloadTelegramFile,
|
|
23
|
+
} from "./context.js";
|
|
24
|
+
import { shouldHandleInGroup, isAccessAllowed } from "./access.js";
|
|
25
|
+
import { enqueueMessage, isUserRateLimited } from "./queue.js";
|
|
26
|
+
import { ensurePackSaved } from "../sticker-library.js";
|
|
27
|
+
import { processAndReply, sendHtml } from "./delivery.js";
|
|
28
|
+
|
|
29
|
+
// ── Shared media handler ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
type MediaDescriptor = {
|
|
32
|
+
/** Human-readable media type for prompt (e.g. "photo", "video", "voice message"). */
|
|
33
|
+
type: string;
|
|
34
|
+
/** File ID to download from Telegram. */
|
|
35
|
+
fileId: string;
|
|
36
|
+
/** File name for saving locally. */
|
|
37
|
+
fileName: string;
|
|
38
|
+
/** Extra prompt lines describing the media. */
|
|
39
|
+
promptLines: string[];
|
|
40
|
+
/** Caption from the message, if any. */
|
|
41
|
+
caption?: string;
|
|
42
|
+
/** Optional file size check (reject if too large). */
|
|
43
|
+
fileSize?: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Shared handler for all downloadable media types (photo, document, voice, video, animation).
|
|
48
|
+
* Extracts forward/reply context, downloads the file, builds a prompt, and enqueues.
|
|
49
|
+
*/
|
|
50
|
+
async function handleMediaMessage(
|
|
51
|
+
ctx: Context,
|
|
52
|
+
bot: Bot,
|
|
53
|
+
config: TalonConfig,
|
|
54
|
+
media: MediaDescriptor,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
if (!ctx.message || !ctx.chat) return;
|
|
57
|
+
if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
|
|
58
|
+
|
|
59
|
+
const chatId = String(ctx.chat.id);
|
|
60
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
61
|
+
const sender = getSenderName(ctx.from);
|
|
62
|
+
const senderUsername = ctx.from?.username;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// File size check
|
|
66
|
+
if (media.fileSize && media.fileSize > 20 * 1024 * 1024) {
|
|
67
|
+
await sendHtml(
|
|
68
|
+
bot,
|
|
69
|
+
ctx.chat.id,
|
|
70
|
+
"File too large (max 20MB).",
|
|
71
|
+
ctx.message.message_id,
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const savedPath = await downloadTelegramFile(
|
|
77
|
+
bot,
|
|
78
|
+
config,
|
|
79
|
+
media.fileId,
|
|
80
|
+
media.fileName,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Store file path in history + media index
|
|
84
|
+
setMessageFilePath(chatId, ctx.message.message_id, savedPath);
|
|
85
|
+
addMedia({
|
|
86
|
+
chatId,
|
|
87
|
+
msgId: ctx.message.message_id,
|
|
88
|
+
senderName: sender,
|
|
89
|
+
type: media.type as
|
|
90
|
+
| "photo"
|
|
91
|
+
| "document"
|
|
92
|
+
| "voice"
|
|
93
|
+
| "video"
|
|
94
|
+
| "animation"
|
|
95
|
+
| "audio"
|
|
96
|
+
| "sticker",
|
|
97
|
+
filePath: savedPath,
|
|
98
|
+
caption: media.caption,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const fwdCtx = getForwardContext(
|
|
103
|
+
ctx.message as Parameters<typeof getForwardContext>[0],
|
|
104
|
+
);
|
|
105
|
+
const replyCtx = getReplyContext(
|
|
106
|
+
ctx.message.reply_to_message as Parameters<typeof getReplyContext>[0],
|
|
107
|
+
ctx.me.id,
|
|
108
|
+
(ctx.message as { quote?: { text?: string; is_manual?: boolean } }).quote,
|
|
109
|
+
);
|
|
110
|
+
const replyPhotoCtx = await downloadReplyPhoto(
|
|
111
|
+
ctx.message.reply_to_message as Parameters<typeof downloadReplyPhoto>[0],
|
|
112
|
+
bot,
|
|
113
|
+
config,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const promptParts = [
|
|
117
|
+
fwdCtx,
|
|
118
|
+
replyCtx,
|
|
119
|
+
replyPhotoCtx,
|
|
120
|
+
...media.promptLines.map((l) => l.replace("${savedPath}", savedPath)),
|
|
121
|
+
media.caption ? `Caption: ${media.caption}` : "",
|
|
122
|
+
].filter(Boolean);
|
|
123
|
+
|
|
124
|
+
const prompt = promptParts.join("\n");
|
|
125
|
+
|
|
126
|
+
enqueueMessage(bot, config, chatId, ctx.chat.id, {
|
|
127
|
+
prompt,
|
|
128
|
+
replyToId: ctx.message.message_id,
|
|
129
|
+
messageId: ctx.message.message_id,
|
|
130
|
+
senderName: sender,
|
|
131
|
+
senderUsername,
|
|
132
|
+
senderId: ctx.from?.id,
|
|
133
|
+
isGroup,
|
|
134
|
+
chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logError(
|
|
138
|
+
"bot",
|
|
139
|
+
`[${chatId}] ${media.type} error (${sender}): ${err instanceof Error ? err.message : err}`,
|
|
140
|
+
);
|
|
141
|
+
await sendHtml(
|
|
142
|
+
bot,
|
|
143
|
+
ctx.chat.id,
|
|
144
|
+
escapeHtml(friendlyMessage(err)),
|
|
145
|
+
ctx.message.message_id,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Text message handler ────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export async function handleTextMessage(
|
|
153
|
+
ctx: Context,
|
|
154
|
+
bot: Bot,
|
|
155
|
+
config: TalonConfig,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
158
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
159
|
+
if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
|
|
160
|
+
|
|
161
|
+
const chatId = String(ctx.chat.id);
|
|
162
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
163
|
+
const sender = getSenderName(ctx.from);
|
|
164
|
+
const senderUsername = ctx.from?.username;
|
|
165
|
+
|
|
166
|
+
const replyCtx = getReplyContext(
|
|
167
|
+
ctx.message.reply_to_message as Parameters<typeof getReplyContext>[0],
|
|
168
|
+
ctx.me.id,
|
|
169
|
+
(ctx.message as { quote?: { text?: string; is_manual?: boolean } }).quote,
|
|
170
|
+
);
|
|
171
|
+
const replyPhotoCtx = await downloadReplyPhoto(
|
|
172
|
+
ctx.message.reply_to_message as Parameters<typeof downloadReplyPhoto>[0],
|
|
173
|
+
bot,
|
|
174
|
+
config,
|
|
175
|
+
);
|
|
176
|
+
const fwdCtx = getForwardContext(
|
|
177
|
+
ctx.message as Parameters<typeof getForwardContext>[0],
|
|
178
|
+
);
|
|
179
|
+
const prompt = fwdCtx + replyCtx + replyPhotoCtx + (ctx.message.text ?? "");
|
|
180
|
+
|
|
181
|
+
// Soul tap: a message reaching here is addressed to Talon (DM, mention, or
|
|
182
|
+
// reply). If it reads as a standing directive or a correction, record it as
|
|
183
|
+
// evidence. No-op unless the soul is enabled.
|
|
184
|
+
recordMessageSignal({
|
|
185
|
+
text: ctx.message.text ?? "",
|
|
186
|
+
actor: sender,
|
|
187
|
+
addressedToBot: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
enqueueMessage(bot, config, chatId, ctx.chat.id, {
|
|
191
|
+
prompt,
|
|
192
|
+
replyToId: ctx.message.message_id,
|
|
193
|
+
messageId: ctx.message.message_id,
|
|
194
|
+
senderName: sender,
|
|
195
|
+
senderUsername,
|
|
196
|
+
senderId: ctx.from?.id,
|
|
197
|
+
isGroup,
|
|
198
|
+
chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function handlePhotoMessage(
|
|
203
|
+
ctx: Context,
|
|
204
|
+
bot: Bot,
|
|
205
|
+
config: TalonConfig,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
208
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
209
|
+
|
|
210
|
+
const photos = ctx.message.photo;
|
|
211
|
+
if (!photos?.length) return;
|
|
212
|
+
const bestPhoto = photos[photos.length - 1];
|
|
213
|
+
const caption = ctx.message.caption || "";
|
|
214
|
+
|
|
215
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
216
|
+
type: "photo",
|
|
217
|
+
fileId: bestPhoto.file_id,
|
|
218
|
+
fileName: `photo_${bestPhoto.file_unique_id}.jpg`,
|
|
219
|
+
promptLines: [
|
|
220
|
+
"User sent a photo saved to: ${savedPath}",
|
|
221
|
+
"Read this file to view it. If you need to reference this image in future turns, re-read the file — image data does not persist between turns.",
|
|
222
|
+
],
|
|
223
|
+
caption,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function handleDocumentMessage(
|
|
228
|
+
ctx: Context,
|
|
229
|
+
bot: Bot,
|
|
230
|
+
config: TalonConfig,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
233
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
234
|
+
|
|
235
|
+
const doc = ctx.message.document;
|
|
236
|
+
if (!doc) return;
|
|
237
|
+
|
|
238
|
+
const fileName = doc.file_name || `doc_${doc.file_unique_id}`;
|
|
239
|
+
const caption = ctx.message.caption || "";
|
|
240
|
+
|
|
241
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
242
|
+
type: "document",
|
|
243
|
+
fileId: doc.file_id,
|
|
244
|
+
fileName,
|
|
245
|
+
fileSize: doc.file_size,
|
|
246
|
+
promptLines: [
|
|
247
|
+
`User sent a document: "${fileName}" (${doc.mime_type || "unknown"}).`,
|
|
248
|
+
"Saved to: ${savedPath}",
|
|
249
|
+
"Read and process this file.",
|
|
250
|
+
],
|
|
251
|
+
caption,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function handleVoiceMessage(
|
|
256
|
+
ctx: Context,
|
|
257
|
+
bot: Bot,
|
|
258
|
+
config: TalonConfig,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
261
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
262
|
+
|
|
263
|
+
const voice = ctx.message.voice;
|
|
264
|
+
if (!voice) return;
|
|
265
|
+
|
|
266
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
267
|
+
type: "voice",
|
|
268
|
+
fileId: voice.file_id,
|
|
269
|
+
fileName: `voice_${voice.file_unique_id}.ogg`,
|
|
270
|
+
promptLines: [
|
|
271
|
+
`User sent a voice message (${voice.duration}s).`,
|
|
272
|
+
"Audio saved to: ${savedPath}. You cannot transcribe audio — acknowledge it and respond based on context.",
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function handleStickerMessage(
|
|
278
|
+
ctx: Context,
|
|
279
|
+
bot: Bot,
|
|
280
|
+
config: TalonConfig,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
283
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
284
|
+
|
|
285
|
+
const chatId = String(ctx.chat.id);
|
|
286
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
287
|
+
const sender = getSenderName(ctx.from);
|
|
288
|
+
const senderUsername = ctx.from?.username;
|
|
289
|
+
|
|
290
|
+
const sticker = ctx.message.sticker;
|
|
291
|
+
if (!sticker) return;
|
|
292
|
+
|
|
293
|
+
const emoji = sticker.emoji || "";
|
|
294
|
+
const setName = sticker.set_name || "";
|
|
295
|
+
|
|
296
|
+
// Grow the sticker library organically: any pack a user sends from
|
|
297
|
+
// gets saved in the background (skipped if already saved), so the
|
|
298
|
+
// model can later send from it by emoji without any browsing.
|
|
299
|
+
if (setName) void ensurePackSaved(bot, setName);
|
|
300
|
+
|
|
301
|
+
const prompt = [
|
|
302
|
+
`User sent a sticker: ${emoji}`,
|
|
303
|
+
`Sticker file_id: ${sticker.file_id}`,
|
|
304
|
+
setName ? `Sticker set: ${setName} (saved to your sticker library)` : "",
|
|
305
|
+
sticker.is_animated
|
|
306
|
+
? "(animated)"
|
|
307
|
+
: sticker.is_video
|
|
308
|
+
? "(video sticker)"
|
|
309
|
+
: "",
|
|
310
|
+
'You can send it back via send(type="sticker", file_id=...) — or send any sticker from your library by emoji.',
|
|
311
|
+
]
|
|
312
|
+
.filter(Boolean)
|
|
313
|
+
.join("\n");
|
|
314
|
+
|
|
315
|
+
enqueueMessage(bot, config, chatId, ctx.chat.id, {
|
|
316
|
+
prompt,
|
|
317
|
+
replyToId: ctx.message.message_id,
|
|
318
|
+
messageId: ctx.message.message_id,
|
|
319
|
+
senderName: sender,
|
|
320
|
+
senderUsername,
|
|
321
|
+
senderId: ctx.from?.id,
|
|
322
|
+
isGroup,
|
|
323
|
+
chatTitle: isGroup ? (ctx.chat as { title?: string }).title : undefined,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function handleVideoMessage(
|
|
328
|
+
ctx: Context,
|
|
329
|
+
bot: Bot,
|
|
330
|
+
config: TalonConfig,
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
333
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
334
|
+
|
|
335
|
+
const video = ctx.message.video;
|
|
336
|
+
if (!video) return;
|
|
337
|
+
|
|
338
|
+
const fileName = video.file_name || `video_${video.file_unique_id}.mp4`;
|
|
339
|
+
const caption = ctx.message.caption || "";
|
|
340
|
+
|
|
341
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
342
|
+
type: "video",
|
|
343
|
+
fileId: video.file_id,
|
|
344
|
+
fileName,
|
|
345
|
+
promptLines: [
|
|
346
|
+
`User sent a video: "${fileName}" (${video.duration}s, ${video.width}x${video.height}).`,
|
|
347
|
+
"Saved to: ${savedPath}",
|
|
348
|
+
],
|
|
349
|
+
caption,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function handleAnimationMessage(
|
|
354
|
+
ctx: Context,
|
|
355
|
+
bot: Bot,
|
|
356
|
+
config: TalonConfig,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
359
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
360
|
+
|
|
361
|
+
const anim = ctx.message.animation;
|
|
362
|
+
if (!anim) return;
|
|
363
|
+
|
|
364
|
+
const fileName = anim.file_name || `animation_${anim.file_unique_id}.mp4`;
|
|
365
|
+
const caption = ctx.message.caption || "";
|
|
366
|
+
|
|
367
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
368
|
+
type: "animation",
|
|
369
|
+
fileId: anim.file_id,
|
|
370
|
+
fileName,
|
|
371
|
+
promptLines: [
|
|
372
|
+
`User sent a GIF/animation: "${fileName}" (${anim.duration}s).`,
|
|
373
|
+
"Saved to: ${savedPath}",
|
|
374
|
+
`file_id: ${anim.file_id} — re-send it anytime with send(type="animation", file_id=...).`,
|
|
375
|
+
],
|
|
376
|
+
caption,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function handleAudioMessage(
|
|
381
|
+
ctx: Context,
|
|
382
|
+
bot: Bot,
|
|
383
|
+
config: TalonConfig,
|
|
384
|
+
): Promise<void> {
|
|
385
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
386
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
387
|
+
if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
|
|
388
|
+
|
|
389
|
+
const audio = ctx.message.audio;
|
|
390
|
+
if (!audio) return;
|
|
391
|
+
|
|
392
|
+
const title = audio.title || audio.file_name || "audio";
|
|
393
|
+
const performer = audio.performer ? ` by ${audio.performer}` : "";
|
|
394
|
+
const fileName = audio.file_name || `audio_${audio.file_unique_id}.mp3`;
|
|
395
|
+
const caption = ctx.message.caption || "";
|
|
396
|
+
|
|
397
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
398
|
+
type: "audio",
|
|
399
|
+
fileId: audio.file_id,
|
|
400
|
+
fileName,
|
|
401
|
+
fileSize: audio.file_size,
|
|
402
|
+
promptLines: [
|
|
403
|
+
`User sent an audio file: "${title}"${performer} (${audio.duration}s).`,
|
|
404
|
+
"Saved to: ${savedPath}",
|
|
405
|
+
],
|
|
406
|
+
caption,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function handleVideoNoteMessage(
|
|
411
|
+
ctx: Context,
|
|
412
|
+
bot: Bot,
|
|
413
|
+
config: TalonConfig,
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
if (!ctx.message || !ctx.chat || !shouldHandleInGroup(ctx)) return;
|
|
416
|
+
if (!(await isAccessAllowed(ctx, bot))) return;
|
|
417
|
+
if (ctx.from?.id && isUserRateLimited(ctx.from.id)) return;
|
|
418
|
+
|
|
419
|
+
const videoNote = ctx.message.video_note;
|
|
420
|
+
if (!videoNote) return;
|
|
421
|
+
|
|
422
|
+
await handleMediaMessage(ctx, bot, config, {
|
|
423
|
+
type: "video note",
|
|
424
|
+
fileId: videoNote.file_id,
|
|
425
|
+
fileName: `videonote_${videoNote.file_unique_id}.mp4`,
|
|
426
|
+
fileSize: videoNote.file_size,
|
|
427
|
+
promptLines: [
|
|
428
|
+
`User sent a round video note (${videoNote.duration}s).`,
|
|
429
|
+
"Saved to: ${savedPath}",
|
|
430
|
+
],
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function handleCallbackQuery(
|
|
435
|
+
ctx: Context,
|
|
436
|
+
bot: Bot,
|
|
437
|
+
config: TalonConfig,
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
if (!ctx.callbackQuery || !("data" in ctx.callbackQuery)) return;
|
|
440
|
+
|
|
441
|
+
const chatId = String(ctx.chat?.id ?? ctx.from?.id);
|
|
442
|
+
const numericChatId = ctx.chat?.id ?? ctx.from?.id ?? 0;
|
|
443
|
+
const isGroup = ctx.chat?.type === "group" || ctx.chat?.type === "supergroup";
|
|
444
|
+
const sender = getSenderName(ctx.from);
|
|
445
|
+
const callbackData = ctx.callbackQuery.data;
|
|
446
|
+
|
|
447
|
+
// Acknowledge the callback immediately
|
|
448
|
+
await ctx.answerCallbackQuery().catch(() => {});
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const prompt = `[Button pressed] User clicked inline button with callback data: "${callbackData}"`;
|
|
452
|
+
const replyToId = ctx.callbackQuery.message?.message_id ?? 0;
|
|
453
|
+
|
|
454
|
+
const chatTitle = isGroup
|
|
455
|
+
? (ctx.chat as { title?: string })?.title
|
|
456
|
+
: undefined;
|
|
457
|
+
appendDailyLog(sender, `Button: ${callbackData}`, { chatTitle });
|
|
458
|
+
|
|
459
|
+
await processAndReply({
|
|
460
|
+
bot,
|
|
461
|
+
config,
|
|
462
|
+
chatId,
|
|
463
|
+
numericChatId,
|
|
464
|
+
replyToId,
|
|
465
|
+
messageId: replyToId,
|
|
466
|
+
prompt,
|
|
467
|
+
senderName: sender,
|
|
468
|
+
isGroup,
|
|
469
|
+
chatTitle,
|
|
470
|
+
});
|
|
471
|
+
} catch (err) {
|
|
472
|
+
logError(
|
|
473
|
+
"bot",
|
|
474
|
+
`[${chatId}] Callback error (${sender}): ${err instanceof Error ? err.message : err}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message queue + per-user rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Debounces rapid-fire messages per chat (concatenating them into a single
|
|
5
|
+
* agent turn), tracks per-user rate limits, and surfaces a "messages were
|
|
6
|
+
* skipped" notice for groups.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Bot } from "grammy";
|
|
10
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
11
|
+
import { escapeHtml } from "../formatting.js";
|
|
12
|
+
import { classify, friendlyMessage } from "../../../core/errors.js";
|
|
13
|
+
import {
|
|
14
|
+
getRecentHistory,
|
|
15
|
+
type HistoryMessage,
|
|
16
|
+
} from "../../../storage/history.js";
|
|
17
|
+
import { recordMessageProcessed, recordError } from "../../../util/watchdog.js";
|
|
18
|
+
import { log, logError, logWarn, logDebug } from "../../../util/log.js";
|
|
19
|
+
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
20
|
+
import { processAndReply, sendHtml } from "./delivery.js";
|
|
21
|
+
import {
|
|
22
|
+
messageQueues,
|
|
23
|
+
lastHandledMessageIdByChat,
|
|
24
|
+
userMessageTimestamps,
|
|
25
|
+
DEBOUNCE_MS,
|
|
26
|
+
MAX_QUEUED_PER_CHAT,
|
|
27
|
+
RATE_LIMIT_WINDOW_MS,
|
|
28
|
+
RATE_LIMIT_MAX_MESSAGES,
|
|
29
|
+
type QueuedMessage,
|
|
30
|
+
} from "./state.js";
|
|
31
|
+
|
|
32
|
+
export function isUserRateLimited(senderId: number): boolean {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
let timestamps = userMessageTimestamps.get(senderId);
|
|
35
|
+
if (!timestamps) {
|
|
36
|
+
timestamps = [];
|
|
37
|
+
userMessageTimestamps.set(senderId, timestamps);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Remove old entries outside the window
|
|
41
|
+
while (timestamps.length > 0 && timestamps[0] < now - RATE_LIMIT_WINDOW_MS) {
|
|
42
|
+
timestamps.shift();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (timestamps.length >= RATE_LIMIT_MAX_MESSAGES) {
|
|
46
|
+
logDebug("bot", `Rate-limited user ${senderId}`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
timestamps.push(now);
|
|
51
|
+
|
|
52
|
+
// Evict stale entries — remove users who haven't messaged in 10+ minutes
|
|
53
|
+
if (userMessageTimestamps.size > 5_000) {
|
|
54
|
+
const cutoff = now - 10 * 60_000;
|
|
55
|
+
for (const [userId, ts] of userMessageTimestamps) {
|
|
56
|
+
if (ts.length === 0 || ts[ts.length - 1] < cutoff) {
|
|
57
|
+
userMessageTimestamps.delete(userId);
|
|
58
|
+
}
|
|
59
|
+
if (userMessageTimestamps.size <= 2_500) break; // evict down to half
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enqueue a message for processing. If another message arrives within DEBOUNCE_MS,
|
|
68
|
+
* they are concatenated and sent as a single query to avoid duplicate SDK spawns.
|
|
69
|
+
* Queued messages get a hourglass reaction to indicate they've been seen.
|
|
70
|
+
*/
|
|
71
|
+
export function enqueueMessage(
|
|
72
|
+
bot: Bot,
|
|
73
|
+
config: TalonConfig,
|
|
74
|
+
chatId: string,
|
|
75
|
+
numericChatId: number,
|
|
76
|
+
msg: QueuedMessage,
|
|
77
|
+
): void {
|
|
78
|
+
const existing = messageQueues.get(chatId);
|
|
79
|
+
if (existing) {
|
|
80
|
+
if (existing.messages.length >= MAX_QUEUED_PER_CHAT) return; // drop excess
|
|
81
|
+
existing.messages.push(msg);
|
|
82
|
+
// Show hourglass reaction on the queued message to indicate it's been seen
|
|
83
|
+
bot.api
|
|
84
|
+
.setMessageReaction(numericChatId, msg.messageId, [
|
|
85
|
+
{
|
|
86
|
+
type: "emoji",
|
|
87
|
+
emoji: "⏳" as "👍" /* grammY wants union type */,
|
|
88
|
+
},
|
|
89
|
+
])
|
|
90
|
+
.catch(() => {});
|
|
91
|
+
existing.queuedReactionMsgIds.push(msg.messageId);
|
|
92
|
+
clearTimeout(existing.timer);
|
|
93
|
+
existing.timer = setTimeout(() => flushQueue(chatId), DEBOUNCE_MS);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entry = {
|
|
98
|
+
messages: [msg],
|
|
99
|
+
timer: setTimeout(() => flushQueue(chatId), DEBOUNCE_MS),
|
|
100
|
+
bot,
|
|
101
|
+
config,
|
|
102
|
+
numericChatId,
|
|
103
|
+
queuedReactionMsgIds: [] as number[],
|
|
104
|
+
};
|
|
105
|
+
messageQueues.set(chatId, entry);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function flushQueue(chatId: string): Promise<void> {
|
|
109
|
+
const entry = messageQueues.get(chatId);
|
|
110
|
+
if (!entry) return;
|
|
111
|
+
messageQueues.delete(chatId);
|
|
112
|
+
|
|
113
|
+
const { messages, bot, config, numericChatId, queuedReactionMsgIds } = entry;
|
|
114
|
+
|
|
115
|
+
// Clear hourglass reactions on queued messages now that we're processing
|
|
116
|
+
for (const msgId of queuedReactionMsgIds) {
|
|
117
|
+
bot.api.setMessageReaction(numericChatId, msgId, []).catch((err) => {
|
|
118
|
+
logWarn(
|
|
119
|
+
"bot",
|
|
120
|
+
`Failed to clear reaction on msg ${msgId}: ${err instanceof Error ? err.message : err}`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Use last message's metadata for reply context
|
|
126
|
+
const first = messages[0];
|
|
127
|
+
const last = messages[messages.length - 1];
|
|
128
|
+
|
|
129
|
+
// Concatenate prompts (with newlines between them if multiple)
|
|
130
|
+
const combinedPrompt =
|
|
131
|
+
messages.length === 1
|
|
132
|
+
? messages[0].prompt
|
|
133
|
+
: messages.map((m) => m.prompt).join("\n\n");
|
|
134
|
+
const groupGapContext = buildGroupGapContextNotice({
|
|
135
|
+
isGroup: last.isGroup,
|
|
136
|
+
chatId,
|
|
137
|
+
firstQueuedMessageId: first.messageId,
|
|
138
|
+
lastHandledMessageId: lastHandledMessageIdByChat.get(chatId),
|
|
139
|
+
});
|
|
140
|
+
const promptWithContext = groupGapContext
|
|
141
|
+
? `${groupGapContext}\n\n${combinedPrompt}`
|
|
142
|
+
: combinedPrompt;
|
|
143
|
+
|
|
144
|
+
const chatContext = {
|
|
145
|
+
chatTitle: last.chatTitle,
|
|
146
|
+
username: last.senderUsername,
|
|
147
|
+
};
|
|
148
|
+
appendDailyLog(last.senderName, promptWithContext, chatContext);
|
|
149
|
+
|
|
150
|
+
const runTurn = () =>
|
|
151
|
+
processAndReply({
|
|
152
|
+
bot,
|
|
153
|
+
config,
|
|
154
|
+
chatId,
|
|
155
|
+
numericChatId,
|
|
156
|
+
replyToId: last.replyToId,
|
|
157
|
+
messageId: last.messageId,
|
|
158
|
+
prompt: promptWithContext,
|
|
159
|
+
senderName: last.senderName,
|
|
160
|
+
isGroup: last.isGroup,
|
|
161
|
+
senderUsername: last.senderUsername,
|
|
162
|
+
senderId: last.senderId,
|
|
163
|
+
chatTitle: last.chatTitle,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await runTurn();
|
|
168
|
+
lastHandledMessageIdByChat.set(chatId, last.messageId);
|
|
169
|
+
recordMessageProcessed();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const classified = classify(err);
|
|
172
|
+
const chatType = last.isGroup ? "group" : "DM";
|
|
173
|
+
const promptPreview = promptWithContext.slice(0, 100).replace(/\n/g, " ");
|
|
174
|
+
logError(
|
|
175
|
+
"bot",
|
|
176
|
+
`[${chatId}] [${chatType}] [${last.senderName}] ${classified.reason}: ${classified.message} | prompt: "${promptPreview}"`,
|
|
177
|
+
);
|
|
178
|
+
recordError(classified.message);
|
|
179
|
+
|
|
180
|
+
// Retry once for transient errors (rate_limit, overloaded, network)
|
|
181
|
+
if (classified.retryable) {
|
|
182
|
+
const delayMs = classified.retryAfterMs ?? 2000;
|
|
183
|
+
log(
|
|
184
|
+
"bot",
|
|
185
|
+
`[${chatId}] Retrying after ${classified.reason} (${delayMs}ms)...`,
|
|
186
|
+
);
|
|
187
|
+
try {
|
|
188
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
189
|
+
await runTurn();
|
|
190
|
+
lastHandledMessageIdByChat.set(chatId, last.messageId);
|
|
191
|
+
recordMessageProcessed();
|
|
192
|
+
return;
|
|
193
|
+
} catch (retryErr) {
|
|
194
|
+
const retryClassified = classify(retryErr);
|
|
195
|
+
logError(
|
|
196
|
+
"bot",
|
|
197
|
+
`[${chatId}] [${chatType}] Retry failed: ${retryClassified.message}`,
|
|
198
|
+
);
|
|
199
|
+
await sendHtml(
|
|
200
|
+
bot,
|
|
201
|
+
numericChatId,
|
|
202
|
+
escapeHtml(friendlyMessage(retryClassified)),
|
|
203
|
+
last.replyToId,
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await sendHtml(
|
|
210
|
+
bot,
|
|
211
|
+
numericChatId,
|
|
212
|
+
escapeHtml(friendlyMessage(classified)),
|
|
213
|
+
last.replyToId,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function buildGroupGapContextNotice(inputs: {
|
|
219
|
+
isGroup: boolean;
|
|
220
|
+
chatId: string;
|
|
221
|
+
firstQueuedMessageId: number;
|
|
222
|
+
lastHandledMessageId?: number;
|
|
223
|
+
history?: HistoryMessage[];
|
|
224
|
+
}): string {
|
|
225
|
+
if (!inputs.isGroup || inputs.lastHandledMessageId === undefined) return "";
|
|
226
|
+
|
|
227
|
+
const history = inputs.history ?? getRecentHistory(inputs.chatId, 100);
|
|
228
|
+
const interveningCount = history.filter(
|
|
229
|
+
(m) =>
|
|
230
|
+
m.msgId > inputs.lastHandledMessageId! &&
|
|
231
|
+
m.msgId < inputs.firstQueuedMessageId,
|
|
232
|
+
).length;
|
|
233
|
+
if (interveningCount === 0) return "";
|
|
234
|
+
|
|
235
|
+
const noun = interveningCount === 1 ? "message" : "messages";
|
|
236
|
+
return (
|
|
237
|
+
`[Group context notice: ${interveningCount} ${noun} were sent in this ` +
|
|
238
|
+
"chat since your last handled turn. If this prompt is vague, ambiguous, " +
|
|
239
|
+
"or asks what you think, use read_chat_history before answering.]"
|
|
240
|
+
);
|
|
241
|
+
}
|