@otto-assistant/otto 0.1.1 → 0.7.15
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/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-account-identity.js +62 -0
- package/dist/anthropic-account-identity.test.js +38 -0
- package/dist/anthropic-auth-plugin.js +917 -0
- package/dist/anthropic-auth-state.js +303 -0
- package/dist/anthropic-auth-state.test.js +150 -0
- package/dist/bin.js +152 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/channel-management.js +259 -0
- package/dist/cli-parsing.test.js +142 -0
- package/dist/cli-send-thread.e2e.test.js +353 -0
- package/dist/cli-telegram-options.test.js +99 -0
- package/dist/cli.js +4210 -568
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-dir.js +124 -0
- package/dist/commands/add-dir.test.js +126 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +355 -0
- package/dist/commands/ask-question.js +320 -0
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +121 -0
- package/dist/commands/cli-commands-group-a.test.js +728 -0
- package/dist/commands/cli-commands-group-b.test.js +695 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/discord-commands-group-a.test.js +621 -0
- package/dist/commands/discord-commands-group-b.test.js +595 -0
- package/dist/commands/discord-commands-group-c.test.js +739 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +262 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +887 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +162 -0
- package/dist/commands/model-variant.js +366 -0
- package/dist/commands/model.js +794 -0
- package/dist/commands/new-worktree.js +465 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +223 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/thread-deletion-sync.js +50 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +139 -0
- package/dist/commands/upgrade.js +48 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/vscode.js +269 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +468 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +100 -255
- package/dist/context-awareness-plugin.js +340 -0
- package/dist/context-awareness-plugin.test.js +126 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1355 -0
- package/dist/db.js +260 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1124 -0
- package/dist/discord-command-registration.js +567 -0
- package/dist/discord-urls.js +82 -0
- package/dist/discord-utils.js +616 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +491 -0
- package/dist/format-tables.test.js +478 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +485 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +58 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +254 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +251 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +420 -0
- package/dist/ipc-polling.js +327 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/ipc-utils.js +18 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +171 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +264 -0
- package/dist/memory-overview-plugin.js +128 -0
- package/dist/message-finish-field.e2e.test.js +168 -0
- package/dist/message-formatting.js +415 -0
- package/dist/message-formatting.test.js +115 -0
- package/dist/message-preprocessing.js +359 -0
- package/dist/onboarding-tutorial.js +163 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +131 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +388 -0
- package/dist/opencode-interrupt-plugin.test.js +463 -0
- package/dist/opencode.js +1117 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/otto-digital-twin.e2e.test.js +161 -0
- package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
- package/dist/otto-opencode-plugin.js +21 -0
- package/dist/otto-opencode-plugin.test.js +98 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +68 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +790 -0
- package/dist/queue-advanced-footer.e2e.test.js +481 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +256 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +514 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +475 -0
- package/dist/session-handler/event-stream-state.test.js +632 -0
- package/dist/session-handler/model-utils.js +147 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +131 -0
- package/dist/session-handler/thread-session-runtime.js +3390 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +92 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +19 -0
- package/dist/subagent-rate-limit-plugin.js +175 -0
- package/dist/system-message.js +702 -0
- package/dist/system-message.test.js +697 -0
- package/dist/task-runner.js +530 -0
- package/dist/task-schedule.js +213 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +313 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +1111 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +156 -0
- package/dist/utils.js +172 -0
- package/dist/utils.test.js +130 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +456 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +171 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +311 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +991 -0
- package/dist/worktrees.test.js +415 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +90 -38
- package/schema.prisma +303 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +38 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
- package/skills/new-skill/SKILL.md +237 -0
- package/skills/npm-package/SKILL.md +617 -0
- package/skills/opensrc/SKILL.md +78 -0
- package/skills/otto-publish/SKILL.md +61 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/profano/SKILL.md +16 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +28 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +98 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +49 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +979 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-account-identity.test.ts +52 -0
- package/src/anthropic-account-identity.ts +77 -0
- package/src/anthropic-auth-plugin.ts +1139 -0
- package/src/anthropic-auth-state.test.ts +187 -0
- package/src/anthropic-auth-state.ts +386 -0
- package/src/bin.ts +182 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/channel-management.ts +376 -0
- package/src/cli-parsing.test.ts +197 -0
- package/src/cli-send-thread.e2e.test.ts +463 -0
- package/src/cli-telegram-options.test.ts +114 -0
- package/src/cli.ts +5718 -580
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-dir.test.ts +154 -0
- package/src/commands/add-dir.ts +175 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +496 -0
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +455 -0
- package/src/commands/btw.ts +184 -0
- package/src/commands/cli-commands-group-a.test.ts +837 -0
- package/src/commands/cli-commands-group-b.test.ts +800 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/discord-commands-group-a.test.ts +751 -0
- package/src/commands/discord-commands-group-b.test.ts +648 -0
- package/src/commands/discord-commands-group-c.test.ts +882 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +386 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1175 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +226 -0
- package/src/commands/model-variant.ts +485 -0
- package/src/commands/model.ts +1078 -0
- package/src/commands/new-worktree.ts +645 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +293 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/thread-deletion-sync.ts +80 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +174 -0
- package/src/commands/upgrade.ts +59 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/vscode.ts +342 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +645 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +103 -339
- package/src/context-awareness-plugin.test.ts +144 -0
- package/src/context-awareness-plugin.ts +469 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1949 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +295 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1505 -0
- package/src/discord-command-registration.ts +752 -0
- package/src/discord-urls.ts +89 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +846 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +515 -0
- package/src/format-tables.ts +718 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +644 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +770 -0
- package/src/generated/enums.ts +98 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2394 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1700 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +299 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +610 -0
- package/src/ipc-polling.ts +427 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/ipc-utils.ts +29 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +215 -0
- package/src/markdown.test.ts +315 -0
- package/src/markdown.ts +410 -0
- package/src/memory-overview-plugin.ts +163 -0
- package/src/message-finish-field.e2e.test.ts +195 -0
- package/src/message-formatting.test.ts +126 -0
- package/src/message-formatting.ts +535 -0
- package/src/message-preprocessing.ts +488 -0
- package/src/onboarding-tutorial.ts +167 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +191 -0
- package/src/opencode-interrupt-plugin.test.ts +682 -0
- package/src/opencode-interrupt-plugin.ts +507 -0
- package/src/opencode.ts +1453 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/otto-digital-twin.e2e.test.ts +199 -0
- package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
- package/src/otto-opencode-plugin.test.ts +108 -0
- package/src/otto-opencode-plugin.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +84 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +877 -0
- package/src/queue-advanced-footer.e2e.test.ts +591 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +327 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +651 -0
- package/src/schema.sql +174 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +99 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +717 -0
- package/src/session-handler/event-stream-state.ts +706 -0
- package/src/session-handler/model-utils.ts +217 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +247 -0
- package/src/session-handler/thread-session-runtime.ts +4440 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +130 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +139 -0
- package/src/subagent-rate-limit-plugin.ts +218 -0
- package/src/system-message.test.ts +710 -0
- package/src/system-message.ts +814 -0
- package/src/task-runner.ts +725 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +317 -0
- package/src/test-utils.ts +451 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1350 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +185 -0
- package/src/utils.test.ts +155 -0
- package/src/utils.ts +265 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +638 -0
- package/src/wait-session.ts +273 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +396 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +489 -0
- package/src/worktrees.ts +1370 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -39
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -202
- package/dist/config.test.js.map +0 -1
- package/dist/detect.d.ts +0 -9
- package/dist/detect.d.ts.map +0 -1
- package/dist/detect.js +0 -40
- package/dist/detect.js.map +0 -1
- package/dist/detect.test.d.ts +0 -2
- package/dist/detect.test.d.ts.map +0 -1
- package/dist/detect.test.js +0 -26
- package/dist/detect.test.js.map +0 -1
- package/dist/docker.d.ts +0 -7
- package/dist/docker.d.ts.map +0 -1
- package/dist/docker.js +0 -17
- package/dist/docker.js.map +0 -1
- package/dist/docker.test.d.ts +0 -2
- package/dist/docker.test.d.ts.map +0 -1
- package/dist/docker.test.js +0 -12
- package/dist/docker.test.js.map +0 -1
- package/dist/health.d.ts +0 -31
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -117
- package/dist/health.js.map +0 -1
- package/dist/health.test.d.ts +0 -2
- package/dist/health.test.d.ts.map +0 -1
- package/dist/health.test.js +0 -52
- package/dist/health.test.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/installer.d.ts +0 -10
- package/dist/installer.d.ts.map +0 -1
- package/dist/installer.js +0 -50
- package/dist/installer.js.map +0 -1
- package/dist/installer.test.d.ts +0 -2
- package/dist/installer.test.d.ts.map +0 -1
- package/dist/installer.test.js +0 -43
- package/dist/installer.test.js.map +0 -1
- package/dist/lifecycle.d.ts +0 -10
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js +0 -45
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -2
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -20
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -30
- package/dist/manifest.js.map +0 -1
- package/dist/skills-baseline.d.ts +0 -7
- package/dist/skills-baseline.d.ts.map +0 -1
- package/dist/skills-baseline.js +0 -9
- package/dist/skills-baseline.js.map +0 -1
- package/dist/skills.d.ts +0 -110
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js +0 -429
- package/dist/skills.js.map +0 -1
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -416
- package/dist/skills.test.js.map +0 -1
- package/dist/sync.d.ts +0 -10
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -39
- package/dist/sync.js.map +0 -1
- package/dist/tenant.d.ts +0 -13
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js +0 -105
- package/dist/tenant.js.map +0 -1
- package/dist/tenant.test.d.ts +0 -2
- package/dist/tenant.test.d.ts.map +0 -1
- package/dist/tenant.test.js +0 -37
- package/dist/tenant.test.js.map +0 -1
- package/src/config.test.ts +0 -237
- package/src/detect.test.ts +0 -29
- package/src/detect.ts +0 -52
- package/src/docker.test.ts +0 -12
- package/src/docker.ts +0 -23
- package/src/health.test.ts +0 -61
- package/src/health.ts +0 -158
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -62
- package/src/installer.test.ts +0 -52
- package/src/installer.ts +0 -62
- package/src/lifecycle.test.ts +0 -23
- package/src/lifecycle.ts +0 -49
- package/src/manifest.ts +0 -42
- package/src/skills-baseline.ts +0 -14
- package/src/skills.test.ts +0 -503
- package/src/skills.ts +0 -512
- package/src/sync.ts +0 -53
- package/src/tenant.test.ts +0 -49
- package/src/tenant.ts +0 -120
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// Scheduled task runner for executing due `send --send-at` jobs in the bot process.
|
|
2
|
+
import { Client, Routes } from "discord.js";
|
|
3
|
+
import { createDiscordRest } from "./discord-urls.js";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { claimScheduledTaskRunning, createIpcRequest, getIpcRequestById, getBotTokenWithMode, getDuePlannedScheduledTasks, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, markScheduledTaskOneShotCompleted, recoverStaleRunningScheduledTasks, setThreadSession, getAllTextChannelDirectories, } from "./database.js";
|
|
6
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from "./logger.js";
|
|
7
|
+
import { buildSessionPermissions, initializeOpencodeForDirectory, } from "./opencode.js";
|
|
8
|
+
import { notifyError } from "./sentry.js";
|
|
9
|
+
import { getNextCronRun, getPromptPreview, parseScheduledTaskPayload, } from "./task-schedule.js";
|
|
10
|
+
const taskLogger = createLogger(LogPrefix.TASK);
|
|
11
|
+
async function waitForIpcRequestCompletion({ requestId, timeoutMs = 4_000, pollMs = 100, }) {
|
|
12
|
+
const startedAt = Date.now();
|
|
13
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
14
|
+
const row = await getIpcRequestById({ id: requestId });
|
|
15
|
+
if (row?.status === "completed") {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
await new Promise((resolve) => {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
resolve();
|
|
21
|
+
}, pollMs);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return new Error(`Timed out waiting for IPC request ${requestId} completion`);
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === "object" && value !== null;
|
|
28
|
+
}
|
|
29
|
+
function parseMessageId(value) {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
return new Error("Discord response is not an object");
|
|
32
|
+
}
|
|
33
|
+
if (typeof value.id !== "string") {
|
|
34
|
+
return new Error("Discord response is missing message ID");
|
|
35
|
+
}
|
|
36
|
+
return value.id;
|
|
37
|
+
}
|
|
38
|
+
async function executeThreadScheduledTask({ rest, task, payload, }) {
|
|
39
|
+
const marker = {
|
|
40
|
+
start: true,
|
|
41
|
+
scheduledKind: task.schedule_kind,
|
|
42
|
+
scheduledTaskId: task.id,
|
|
43
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
44
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
45
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
46
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
47
|
+
...(payload.permissions?.length
|
|
48
|
+
? { permissions: payload.permissions }
|
|
49
|
+
: {}),
|
|
50
|
+
...(payload.injectionGuardPatterns?.length
|
|
51
|
+
? { injectionGuardPatterns: payload.injectionGuardPatterns }
|
|
52
|
+
: {}),
|
|
53
|
+
};
|
|
54
|
+
const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }];
|
|
55
|
+
// Newline between prefix and prompt so leading /command detection can
|
|
56
|
+
// find the command on its own line.
|
|
57
|
+
const prefixedPrompt = `» **otto-cli:**\n${payload.prompt}`;
|
|
58
|
+
// Agent-first path for silent mode: initialize opencode directly and IPC the response
|
|
59
|
+
if (payload.silentPrompt) {
|
|
60
|
+
const botRow = await getBotTokenWithMode();
|
|
61
|
+
const appId = botRow?.appId;
|
|
62
|
+
if (!appId) {
|
|
63
|
+
return new Error(`Cannot get bot appId for task ${task.id}`);
|
|
64
|
+
}
|
|
65
|
+
const projectDirectory = task.project_directory || `/home/ubuntu/.otto/projects/general`;
|
|
66
|
+
const prevCleanup = process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
|
|
67
|
+
process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = "1";
|
|
68
|
+
try {
|
|
69
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
70
|
+
if (getClient instanceof Error) {
|
|
71
|
+
return new Error(`Failed to initialize opencode for task ${task.id}`, {
|
|
72
|
+
cause: getClient,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const registeredProjectDirs = await getAllTextChannelDirectories();
|
|
76
|
+
const created = await getClient().session.create({
|
|
77
|
+
directory: projectDirectory,
|
|
78
|
+
permission: buildSessionPermissions({ directory: projectDirectory, extraAllowedDirectories: registeredProjectDirs }),
|
|
79
|
+
});
|
|
80
|
+
const sessionId = created.data?.id;
|
|
81
|
+
if (!sessionId) {
|
|
82
|
+
return new Error(`Failed to create opencode session for task ${task.id}`);
|
|
83
|
+
}
|
|
84
|
+
// Post invisible starter with marker embed, then delete it
|
|
85
|
+
const starterResult = await rest
|
|
86
|
+
.post(Routes.channelMessages(payload.threadId), {
|
|
87
|
+
body: { content: "", embeds: embed },
|
|
88
|
+
})
|
|
89
|
+
.catch((e) => {
|
|
90
|
+
return new Error(`Failed to post starter for task ${task.id}`, {
|
|
91
|
+
cause: e,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
if (starterResult instanceof Error)
|
|
95
|
+
return starterResult;
|
|
96
|
+
const starterId = parseMessageId(starterResult);
|
|
97
|
+
if (starterId instanceof Error)
|
|
98
|
+
return starterId;
|
|
99
|
+
// Keep starter message to preserve valid thread-first message in Discord UI.
|
|
100
|
+
// Persist thread -> session and IPC so bot streams the response
|
|
101
|
+
await setThreadSession(payload.threadId, sessionId);
|
|
102
|
+
const ipcRow = await createIpcRequest({
|
|
103
|
+
type: "start_thread_listener",
|
|
104
|
+
sessionId,
|
|
105
|
+
threadId: payload.threadId,
|
|
106
|
+
payload: JSON.stringify({
|
|
107
|
+
channelId: payload.threadId,
|
|
108
|
+
appId,
|
|
109
|
+
projectDirectory,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
const ipcReady = await waitForIpcRequestCompletion({
|
|
113
|
+
requestId: ipcRow.id,
|
|
114
|
+
});
|
|
115
|
+
if (ipcReady instanceof Error) {
|
|
116
|
+
return ipcReady;
|
|
117
|
+
}
|
|
118
|
+
// Submit prompt AFTER listener IPC request is created so short/fast
|
|
119
|
+
// model responses are not missed before subscription is active.
|
|
120
|
+
await getClient().session.promptAsync({
|
|
121
|
+
sessionID: sessionId,
|
|
122
|
+
directory: projectDirectory,
|
|
123
|
+
parts: [{ type: "text", text: payload.prompt }],
|
|
124
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
125
|
+
});
|
|
126
|
+
taskLogger.log(`[task ${task.id}] Agent-first thread session started (thread=${payload.threadId}, session=${sessionId})`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (prevCleanup === undefined) {
|
|
130
|
+
delete process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = prevCleanup;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Non-silent path: post prompt visibly
|
|
139
|
+
const postResult = await rest
|
|
140
|
+
.post(Routes.channelMessages(payload.threadId), {
|
|
141
|
+
body: {
|
|
142
|
+
content: prefixedPrompt,
|
|
143
|
+
embeds: embed,
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
return new Error(`Failed to post scheduled thread task ${task.id}`, {
|
|
148
|
+
cause: error,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
if (postResult instanceof Error) {
|
|
152
|
+
return postResult;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function executeChannelScheduledTask({ rest, discordClient, task, payload, }) {
|
|
156
|
+
const marker = payload.notifyOnly
|
|
157
|
+
? undefined
|
|
158
|
+
: {
|
|
159
|
+
start: true,
|
|
160
|
+
scheduledKind: task.schedule_kind,
|
|
161
|
+
scheduledTaskId: task.id,
|
|
162
|
+
...(payload.worktreeName ? { worktree: payload.worktreeName } : {}),
|
|
163
|
+
...(payload.cwd ? { cwd: payload.cwd } : {}),
|
|
164
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
165
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
166
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
167
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
168
|
+
...(payload.permissions?.length
|
|
169
|
+
? { permissions: payload.permissions }
|
|
170
|
+
: {}),
|
|
171
|
+
...(payload.injectionGuardPatterns?.length
|
|
172
|
+
? { injectionGuardPatterns: payload.injectionGuardPatterns }
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
175
|
+
const embeds = marker
|
|
176
|
+
? [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
|
|
177
|
+
: undefined;
|
|
178
|
+
const threadName = (payload.name || getPromptPreview(payload.prompt)).slice(0, 100);
|
|
179
|
+
/**
|
|
180
|
+
* Agent-first path (silent, non-notify): the opencode session is created
|
|
181
|
+
* here and the prompt is submitted via SDK. No Discord message shows the
|
|
182
|
+
* user's prompt — the bot's IPC listener streams the response directly.
|
|
183
|
+
*/
|
|
184
|
+
if (payload.silentPrompt && !payload.notifyOnly) {
|
|
185
|
+
// 1. Get bot appId from stored credentials and project directory from task
|
|
186
|
+
const botRow = await getBotTokenWithMode();
|
|
187
|
+
const appId = botRow?.appId;
|
|
188
|
+
if (!appId) {
|
|
189
|
+
return new Error(`Cannot get bot appId for task ${task.id}`);
|
|
190
|
+
}
|
|
191
|
+
const projectDirectory = task.project_directory || `/home/ubuntu/.otto/projects/general`;
|
|
192
|
+
// 2. Prevent CLI exit from killing the opencode server we are about to start
|
|
193
|
+
const prevCleanup = process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
|
|
194
|
+
process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = "1";
|
|
195
|
+
try {
|
|
196
|
+
// 3. Initialize opencode (starts server if not already running)
|
|
197
|
+
taskLogger.log(`[task ${task.id}] Initializing opencode for ${projectDirectory}`);
|
|
198
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
199
|
+
if (getClient instanceof Error) {
|
|
200
|
+
return new Error(`Failed to initialize opencode for task ${task.id}`, {
|
|
201
|
+
cause: getClient,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// 4. Create session and queue the prompt
|
|
205
|
+
taskLogger.log(`[task ${task.id}] Creating opencode session`);
|
|
206
|
+
const registeredProjectDirs2 = await getAllTextChannelDirectories();
|
|
207
|
+
const created = await getClient().session.create({
|
|
208
|
+
directory: projectDirectory,
|
|
209
|
+
permission: buildSessionPermissions({ directory: projectDirectory, extraAllowedDirectories: registeredProjectDirs2 }),
|
|
210
|
+
});
|
|
211
|
+
const sessionId = created.data?.id;
|
|
212
|
+
if (!sessionId) {
|
|
213
|
+
return new Error(`Failed to create opencode session for task ${task.id}`);
|
|
214
|
+
}
|
|
215
|
+
// 5. Post an invisible starter message (marker embed only — no content, no attachment)
|
|
216
|
+
const starterResult = await rest
|
|
217
|
+
.post(Routes.channelMessages(payload.channelId), {
|
|
218
|
+
body: {
|
|
219
|
+
content: "",
|
|
220
|
+
embeds,
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
.catch((error) => {
|
|
224
|
+
return new Error(`Failed to create starter message for task ${task.id}`, {
|
|
225
|
+
cause: error,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
if (starterResult instanceof Error) {
|
|
229
|
+
return starterResult;
|
|
230
|
+
}
|
|
231
|
+
const starterMessageId = parseMessageId(starterResult);
|
|
232
|
+
if (starterMessageId instanceof Error) {
|
|
233
|
+
return new Error(`Invalid starter message response for task ${task.id}`, {
|
|
234
|
+
cause: starterMessageId,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// 6. Create thread from the invisible starter message
|
|
238
|
+
taskLogger.log(`[task ${task.id}] Creating thread`);
|
|
239
|
+
const threadResult = await rest
|
|
240
|
+
.post(Routes.threads(payload.channelId, starterMessageId), {
|
|
241
|
+
body: {
|
|
242
|
+
name: threadName,
|
|
243
|
+
auto_archive_duration: 1440,
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
return new Error(`Failed to create thread for task ${task.id}`, {
|
|
248
|
+
cause: error,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
if (threadResult instanceof Error) {
|
|
252
|
+
return threadResult;
|
|
253
|
+
}
|
|
254
|
+
const threadIdResult = parseMessageId(threadResult);
|
|
255
|
+
if (threadIdResult instanceof Error) {
|
|
256
|
+
return new Error(`Invalid thread response for task ${task.id}`, {
|
|
257
|
+
cause: threadIdResult,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// 7. Persist thread -> session mapping so future messages route to this session
|
|
261
|
+
await setThreadSession(threadIdResult, sessionId);
|
|
262
|
+
// 8. Create IPC request so the bot's listener picks up this session
|
|
263
|
+
const ipcRow = await createIpcRequest({
|
|
264
|
+
type: "start_thread_listener",
|
|
265
|
+
sessionId,
|
|
266
|
+
threadId: threadIdResult,
|
|
267
|
+
payload: JSON.stringify({
|
|
268
|
+
channelId: payload.channelId,
|
|
269
|
+
appId,
|
|
270
|
+
projectDirectory,
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
const ipcReady = await waitForIpcRequestCompletion({
|
|
274
|
+
requestId: ipcRow.id,
|
|
275
|
+
});
|
|
276
|
+
if (ipcReady instanceof Error) {
|
|
277
|
+
return ipcReady;
|
|
278
|
+
}
|
|
279
|
+
// Submit prompt AFTER listener IPC request is created so short/fast
|
|
280
|
+
// model responses are not missed before subscription is active.
|
|
281
|
+
await getClient().session.promptAsync({
|
|
282
|
+
sessionID: sessionId,
|
|
283
|
+
directory: projectDirectory,
|
|
284
|
+
parts: [{ type: "text", text: payload.prompt }],
|
|
285
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
286
|
+
});
|
|
287
|
+
// 9. Add user to thread if specified
|
|
288
|
+
if (payload.userId) {
|
|
289
|
+
await rest
|
|
290
|
+
.put(Routes.threadMembers(threadIdResult, payload.userId))
|
|
291
|
+
.catch(() => { }); // Best-effort
|
|
292
|
+
}
|
|
293
|
+
taskLogger.log(`[task ${task.id}] Agent-first scheduled session started (thread=${threadIdResult}, session=${sessionId})`);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
// Restore the env var so other task executions are unaffected
|
|
297
|
+
if (prevCleanup === undefined) {
|
|
298
|
+
delete process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = prevCleanup;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Non-silent / notify-only path: post the prompt visibly and let the bot handle it
|
|
307
|
+
const starterResult = await rest
|
|
308
|
+
.post(Routes.channelMessages(payload.channelId), {
|
|
309
|
+
body: {
|
|
310
|
+
content: payload.notifyOnly ? "" : payload.prompt,
|
|
311
|
+
embeds,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
.catch((error) => {
|
|
315
|
+
return new Error(`Failed to create starter message for task ${task.id}`, {
|
|
316
|
+
cause: error,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
if (starterResult instanceof Error) {
|
|
320
|
+
return starterResult;
|
|
321
|
+
}
|
|
322
|
+
const starterMessageId = parseMessageId(starterResult);
|
|
323
|
+
if (starterMessageId instanceof Error) {
|
|
324
|
+
return new Error(`Invalid starter message response for task ${task.id}`, {
|
|
325
|
+
cause: starterMessageId,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const threadResult = await rest
|
|
329
|
+
.post(Routes.threads(payload.channelId, starterMessageId), {
|
|
330
|
+
body: {
|
|
331
|
+
name: threadName,
|
|
332
|
+
auto_archive_duration: 1440,
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
return new Error(`Failed to create thread for task ${task.id}`, {
|
|
337
|
+
cause: error,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
if (threadResult instanceof Error) {
|
|
341
|
+
return threadResult;
|
|
342
|
+
}
|
|
343
|
+
if (!payload.userId) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const threadIdResult = parseMessageId(threadResult);
|
|
347
|
+
if (threadIdResult instanceof Error) {
|
|
348
|
+
return new Error(`Invalid thread response for task ${task.id}`, {
|
|
349
|
+
cause: threadIdResult,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const addMemberResult = await rest
|
|
353
|
+
.put(Routes.threadMembers(threadIdResult, payload.userId))
|
|
354
|
+
.catch((error) => {
|
|
355
|
+
return new Error(`Failed to add user to scheduled thread for task ${task.id}`, { cause: error });
|
|
356
|
+
});
|
|
357
|
+
if (addMemberResult instanceof Error) {
|
|
358
|
+
return addMemberResult;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function executeScheduledTask({ rest, discordClient, task, }) {
|
|
362
|
+
const payloadResult = parseScheduledTaskPayload(task.payload_json);
|
|
363
|
+
if (payloadResult instanceof Error) {
|
|
364
|
+
return new Error(`Task ${task.id} has invalid payload`, {
|
|
365
|
+
cause: payloadResult,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (payloadResult.kind === "thread") {
|
|
369
|
+
return executeThreadScheduledTask({
|
|
370
|
+
rest,
|
|
371
|
+
task,
|
|
372
|
+
payload: payloadResult,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return executeChannelScheduledTask({
|
|
376
|
+
rest,
|
|
377
|
+
discordClient,
|
|
378
|
+
task,
|
|
379
|
+
payload: payloadResult,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function finalizeSuccessfulTask({ task, completedAt, }) {
|
|
383
|
+
if (task.schedule_kind === "at") {
|
|
384
|
+
await markScheduledTaskOneShotCompleted({ taskId: task.id, completedAt });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!task.cron_expr) {
|
|
388
|
+
await markScheduledTaskFailed({
|
|
389
|
+
taskId: task.id,
|
|
390
|
+
failedAt: completedAt,
|
|
391
|
+
errorMessage: "Missing cron expression on cron task",
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// Use stored timezone, falling back to UTC (not machine local) for consistency
|
|
396
|
+
const timezone = task.timezone || "UTC";
|
|
397
|
+
const nextRunResult = getNextCronRun({
|
|
398
|
+
cronExpr: task.cron_expr,
|
|
399
|
+
timezone,
|
|
400
|
+
from: completedAt,
|
|
401
|
+
});
|
|
402
|
+
if (nextRunResult instanceof Error) {
|
|
403
|
+
await markScheduledTaskFailed({
|
|
404
|
+
taskId: task.id,
|
|
405
|
+
failedAt: completedAt,
|
|
406
|
+
errorMessage: nextRunResult.message,
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await markScheduledTaskCronRescheduled({
|
|
411
|
+
taskId: task.id,
|
|
412
|
+
completedAt,
|
|
413
|
+
nextRunAt: nextRunResult,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function finalizeFailedTask({ task, failedAt, error, }) {
|
|
417
|
+
if (task.schedule_kind === "cron" && task.cron_expr) {
|
|
418
|
+
// Use stored timezone, falling back to UTC (not machine local) for consistency
|
|
419
|
+
const timezone = task.timezone || "UTC";
|
|
420
|
+
const nextRunResult = getNextCronRun({
|
|
421
|
+
cronExpr: task.cron_expr,
|
|
422
|
+
timezone,
|
|
423
|
+
from: failedAt,
|
|
424
|
+
});
|
|
425
|
+
if (!(nextRunResult instanceof Error)) {
|
|
426
|
+
await markScheduledTaskCronRetry({
|
|
427
|
+
taskId: task.id,
|
|
428
|
+
failedAt,
|
|
429
|
+
errorMessage: error.message,
|
|
430
|
+
nextRunAt: nextRunResult,
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await markScheduledTaskFailed({
|
|
436
|
+
taskId: task.id,
|
|
437
|
+
failedAt,
|
|
438
|
+
errorMessage: error.message,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async function processDueTask({ rest, discordClient, task, }) {
|
|
442
|
+
const startedAt = new Date();
|
|
443
|
+
const claimed = await claimScheduledTaskRunning({
|
|
444
|
+
taskId: task.id,
|
|
445
|
+
startedAt,
|
|
446
|
+
});
|
|
447
|
+
if (!claimed) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const executeResult = await executeScheduledTask({
|
|
451
|
+
rest,
|
|
452
|
+
discordClient,
|
|
453
|
+
task,
|
|
454
|
+
});
|
|
455
|
+
const finishedAt = new Date();
|
|
456
|
+
if (executeResult instanceof Error) {
|
|
457
|
+
taskLogger.warn(`[task-runner] task ${task.id} failed: ${formatErrorWithStack(executeResult)}`);
|
|
458
|
+
await finalizeFailedTask({
|
|
459
|
+
task,
|
|
460
|
+
failedAt: finishedAt,
|
|
461
|
+
error: executeResult,
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
await finalizeSuccessfulTask({ task, completedAt: finishedAt });
|
|
466
|
+
}
|
|
467
|
+
async function runTaskRunnerTick({ rest, discordClient, staleRunningMs, dueBatchSize, }) {
|
|
468
|
+
const staleBefore = new Date(Date.now() - staleRunningMs);
|
|
469
|
+
const recoveredCount = await recoverStaleRunningScheduledTasks({
|
|
470
|
+
staleBefore,
|
|
471
|
+
});
|
|
472
|
+
if (recoveredCount > 0) {
|
|
473
|
+
taskLogger.warn(`[task-runner] Recovered ${recoveredCount} stale running task(s)`);
|
|
474
|
+
}
|
|
475
|
+
const dueTasks = await getDuePlannedScheduledTasks({
|
|
476
|
+
now: new Date(),
|
|
477
|
+
limit: dueBatchSize,
|
|
478
|
+
});
|
|
479
|
+
await dueTasks.reduce(async (previous, task) => {
|
|
480
|
+
await previous;
|
|
481
|
+
await processDueTask({ rest, discordClient, task });
|
|
482
|
+
}, Promise.resolve());
|
|
483
|
+
}
|
|
484
|
+
export function startTaskRunner({ token, discordClient, pollIntervalMs = 5_000, staleRunningMs = 120_000, dueBatchSize = 20, }) {
|
|
485
|
+
const rest = createDiscordRest(token);
|
|
486
|
+
let stopped = false;
|
|
487
|
+
let ticking = false;
|
|
488
|
+
let tickPromise = null;
|
|
489
|
+
const tick = async () => {
|
|
490
|
+
if (stopped || ticking) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
ticking = true;
|
|
494
|
+
const currentTickPromise = runTaskRunnerTick({
|
|
495
|
+
rest,
|
|
496
|
+
discordClient,
|
|
497
|
+
staleRunningMs,
|
|
498
|
+
dueBatchSize,
|
|
499
|
+
}).catch((error) => {
|
|
500
|
+
return new Error("Task runner tick failed", { cause: error });
|
|
501
|
+
});
|
|
502
|
+
tickPromise = currentTickPromise.then(() => {
|
|
503
|
+
return;
|
|
504
|
+
});
|
|
505
|
+
const runResult = await currentTickPromise;
|
|
506
|
+
if (runResult instanceof Error) {
|
|
507
|
+
taskLogger.error(`[task-runner] ${formatErrorWithStack(runResult)}`);
|
|
508
|
+
void notifyError(runResult, "Task runner tick failed");
|
|
509
|
+
}
|
|
510
|
+
ticking = false;
|
|
511
|
+
tickPromise = null;
|
|
512
|
+
};
|
|
513
|
+
const timer = setInterval(() => {
|
|
514
|
+
void tick();
|
|
515
|
+
}, pollIntervalMs);
|
|
516
|
+
void tick();
|
|
517
|
+
taskLogger.log(`[task-runner] started (interval=${pollIntervalMs}ms)`);
|
|
518
|
+
return async () => {
|
|
519
|
+
if (stopped) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
stopped = true;
|
|
523
|
+
clearInterval(timer);
|
|
524
|
+
if (tickPromise) {
|
|
525
|
+
await tickPromise;
|
|
526
|
+
tickPromise = null;
|
|
527
|
+
}
|
|
528
|
+
taskLogger.log("[task-runner] stopped");
|
|
529
|
+
};
|
|
530
|
+
}
|