@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,107 @@
|
|
|
1
|
+
// Shared apply_patch text parsing utilities.
|
|
2
|
+
// Used by diff-patch-plugin.ts (file path extraction for snapshots) and
|
|
3
|
+
// message-formatting.ts (per-file addition/deletion counts for Discord display).
|
|
4
|
+
//
|
|
5
|
+
// The apply_patch tool uses three path header formats:
|
|
6
|
+
// *** Add File: path — new file
|
|
7
|
+
// *** Update File: path — existing file edit
|
|
8
|
+
// *** Delete File: path — file removal
|
|
9
|
+
// *** Move to: path — rename destination
|
|
10
|
+
// --- a/path / +++ b/path — unified diff headers (fallback)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract all file paths referenced in a patchText string.
|
|
14
|
+
* Handles custom apply_patch headers, move targets, and unified diff headers.
|
|
15
|
+
* Returns deduplicated paths.
|
|
16
|
+
*/
|
|
17
|
+
export function extractPatchFilePaths(patchText: string): string[] {
|
|
18
|
+
const custom = [
|
|
19
|
+
...patchText.matchAll(
|
|
20
|
+
/^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm,
|
|
21
|
+
),
|
|
22
|
+
].map((m) => {
|
|
23
|
+
return (m[1] ?? '').trim()
|
|
24
|
+
})
|
|
25
|
+
const moved = [
|
|
26
|
+
...patchText.matchAll(/^\*\*\* Move to:\s+(.+)$/gm),
|
|
27
|
+
].map((m) => {
|
|
28
|
+
return (m[1] ?? '').trim()
|
|
29
|
+
})
|
|
30
|
+
const unified = [
|
|
31
|
+
...patchText.matchAll(/^(?:---|\+\+\+) [ab]\/(.+)$/gm),
|
|
32
|
+
].map((m) => {
|
|
33
|
+
return (m[1] ?? '').trim()
|
|
34
|
+
})
|
|
35
|
+
const all = [...custom, ...moved, ...unified].filter(Boolean)
|
|
36
|
+
return all.filter((v, i, a) => {
|
|
37
|
+
return a.indexOf(v) === i
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a patchText string and count additions/deletions per file.
|
|
43
|
+
* Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
|
|
44
|
+
* with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
|
|
45
|
+
*/
|
|
46
|
+
export function parsePatchFileCounts(
|
|
47
|
+
patchText: string,
|
|
48
|
+
): Map<string, { additions: number; deletions: number }> {
|
|
49
|
+
const counts = new Map<string, { additions: number; deletions: number }>()
|
|
50
|
+
const lines = patchText.split('\n')
|
|
51
|
+
let currentFile = ''
|
|
52
|
+
let currentType = ''
|
|
53
|
+
let inHunk = false
|
|
54
|
+
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
|
|
57
|
+
const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
|
|
58
|
+
const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
|
|
59
|
+
|
|
60
|
+
if (addMatch || updateMatch || deleteMatch) {
|
|
61
|
+
const match = addMatch || updateMatch || deleteMatch
|
|
62
|
+
currentFile = (match?.[1] ?? '').trim()
|
|
63
|
+
currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
|
|
64
|
+
counts.set(currentFile, { additions: 0, deletions: 0 })
|
|
65
|
+
inHunk = false
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (line.startsWith('@@')) {
|
|
70
|
+
inHunk = true
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (line.startsWith('*** ')) {
|
|
75
|
+
inHunk = false
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!currentFile) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entry = counts.get(currentFile)
|
|
84
|
+
if (!entry) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (currentType === 'add') {
|
|
89
|
+
// all content lines in Add File are additions
|
|
90
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
91
|
+
entry.additions++
|
|
92
|
+
}
|
|
93
|
+
} else if (currentType === 'delete') {
|
|
94
|
+
// all content lines in Delete File are deletions
|
|
95
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
96
|
+
entry.deletions++
|
|
97
|
+
}
|
|
98
|
+
} else if (inHunk) {
|
|
99
|
+
if (line.startsWith('+')) {
|
|
100
|
+
entry.additions++
|
|
101
|
+
} else if (line.startsWith('-')) {
|
|
102
|
+
entry.deletions++
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return counts
|
|
107
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import util from 'node:util'
|
|
4
|
+
import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js'
|
|
5
|
+
|
|
6
|
+
let pluginLogFilePath: string | null = null
|
|
7
|
+
|
|
8
|
+
export function setPluginLogFilePath(dataDir: string): void {
|
|
9
|
+
pluginLogFilePath = path.join(dataDir, 'otto.log')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatArg(arg: unknown): string {
|
|
13
|
+
if (typeof arg === 'string') {
|
|
14
|
+
return sanitizeSensitiveText(arg, { redactPaths: false })
|
|
15
|
+
}
|
|
16
|
+
const safeArg = sanitizeUnknownValue(arg, { redactPaths: false })
|
|
17
|
+
return util.inspect(safeArg, { colors: false, depth: 4 })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatPluginErrorWithStack(error: unknown): string {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
return sanitizeSensitiveText(
|
|
23
|
+
error.stack ?? `${error.name}: ${error.message}`,
|
|
24
|
+
{ redactPaths: false },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
if (typeof error === 'string') {
|
|
28
|
+
return sanitizeSensitiveText(error, { redactPaths: false })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const safeError = sanitizeUnknownValue(error, { redactPaths: false })
|
|
32
|
+
return sanitizeSensitiveText(util.inspect(safeError, { colors: false, depth: 4 }), {
|
|
33
|
+
redactPaths: false,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeToFile(level: string, prefix: string, args: unknown[]) {
|
|
38
|
+
if (!pluginLogFilePath) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const timestamp = new Date().toISOString()
|
|
42
|
+
const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`
|
|
43
|
+
try {
|
|
44
|
+
fs.appendFileSync(pluginLogFilePath, message)
|
|
45
|
+
} catch {
|
|
46
|
+
// Plugin logging must never break the OpenCode plugin process.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createPluginLogger(prefix: string) {
|
|
51
|
+
return {
|
|
52
|
+
log: (...args: unknown[]) => {
|
|
53
|
+
writeToFile('LOG', prefix, args)
|
|
54
|
+
},
|
|
55
|
+
info: (...args: unknown[]) => {
|
|
56
|
+
writeToFile('INFO', prefix, args)
|
|
57
|
+
},
|
|
58
|
+
warn: (...args: unknown[]) => {
|
|
59
|
+
writeToFile('WARN', prefix, args)
|
|
60
|
+
},
|
|
61
|
+
error: (...args: unknown[]) => {
|
|
62
|
+
writeToFile('ERROR', prefix, args)
|
|
63
|
+
},
|
|
64
|
+
debug: (...args: unknown[]) => {
|
|
65
|
+
writeToFile('DEBUG', prefix, args)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Append a session ID marker at the end of a toast message so the bot-side
|
|
71
|
+
// handleTuiToast can route the toast to the correct Discord thread.
|
|
72
|
+
// Without this marker the toast is silently dropped.
|
|
73
|
+
export function appendToastSessionMarker({
|
|
74
|
+
message,
|
|
75
|
+
sessionId,
|
|
76
|
+
}: {
|
|
77
|
+
message: string
|
|
78
|
+
sessionId: string | undefined
|
|
79
|
+
}): string {
|
|
80
|
+
if (!sessionId) {
|
|
81
|
+
return message
|
|
82
|
+
}
|
|
83
|
+
return `${message} ${sessionId}`
|
|
84
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Sensitive data redaction helpers for logs and telemetry payloads.
|
|
2
|
+
// Redacts common secrets, identifiers, emails, and can optionally redact paths.
|
|
3
|
+
|
|
4
|
+
const CORE_SENSITIVE_REPLACEMENTS: Array<{
|
|
5
|
+
pattern: RegExp
|
|
6
|
+
replacement: string
|
|
7
|
+
}> = [
|
|
8
|
+
{
|
|
9
|
+
pattern: /\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
|
|
10
|
+
replacement: 'Bearer [REDACTED]',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /\bsk-[A-Za-z0-9]{16,}\b/g,
|
|
14
|
+
replacement: '[REDACTED_OPENAI_KEY]',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g,
|
|
18
|
+
replacement: '[REDACTED_GOOGLE_KEY]',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
22
|
+
replacement: '[REDACTED_GITHUB_TOKEN]',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern:
|
|
26
|
+
/([?&](?:token|api[_-]?key|key|secret|password|authorization)=)[^&\s]+/gi,
|
|
27
|
+
replacement: '$1[REDACTED]',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern:
|
|
31
|
+
/(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*")([^"]+)(")/gi,
|
|
32
|
+
replacement: '$1[REDACTED]$3',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern:
|
|
36
|
+
/(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*)([^\s,;]+)/gi,
|
|
37
|
+
replacement: '$1[REDACTED]',
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const PATH_REPLACEMENTS: Array<{
|
|
42
|
+
pattern: RegExp
|
|
43
|
+
replacement: string
|
|
44
|
+
}> = [
|
|
45
|
+
{
|
|
46
|
+
pattern: /\/(?:Users|home)\/[^/\s]+\/[^\s'"`)]*/g,
|
|
47
|
+
replacement: '[REDACTED_PATH]',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: /[A-Za-z]:\\[^\s'"`)]*/g,
|
|
51
|
+
replacement: '[REDACTED_PATH]',
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
export function sanitizeSensitiveText(
|
|
56
|
+
value: string,
|
|
57
|
+
{ redactPaths = false }: { redactPaths?: boolean } = {},
|
|
58
|
+
): string {
|
|
59
|
+
const replacements = redactPaths
|
|
60
|
+
? [...CORE_SENSITIVE_REPLACEMENTS, ...PATH_REPLACEMENTS]
|
|
61
|
+
: CORE_SENSITIVE_REPLACEMENTS
|
|
62
|
+
return replacements.reduce((current, entry) => {
|
|
63
|
+
return current.replace(entry.pattern, entry.replacement)
|
|
64
|
+
}, value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sanitizeUnknownValue(
|
|
68
|
+
value: unknown,
|
|
69
|
+
{
|
|
70
|
+
depth = 0,
|
|
71
|
+
seen = new WeakSet<object>(),
|
|
72
|
+
redactPaths = false,
|
|
73
|
+
}: {
|
|
74
|
+
depth?: number
|
|
75
|
+
seen?: WeakSet<object>
|
|
76
|
+
redactPaths?: boolean
|
|
77
|
+
} = {},
|
|
78
|
+
): unknown {
|
|
79
|
+
if (depth > 8) {
|
|
80
|
+
return '[REDACTED_DEPTH_LIMIT]'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
return sanitizeSensitiveText(value, { redactPaths })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
typeof value === 'number' ||
|
|
89
|
+
typeof value === 'boolean' ||
|
|
90
|
+
value === null ||
|
|
91
|
+
value === undefined
|
|
92
|
+
) {
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value instanceof Date) {
|
|
97
|
+
return value.toISOString()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (value instanceof Error) {
|
|
101
|
+
const sanitizedStack = value.stack
|
|
102
|
+
? sanitizeSensitiveText(value.stack, { redactPaths })
|
|
103
|
+
: undefined
|
|
104
|
+
return {
|
|
105
|
+
name: value.name,
|
|
106
|
+
message: sanitizeSensitiveText(value.message, { redactPaths }),
|
|
107
|
+
stack: sanitizedStack,
|
|
108
|
+
cause: sanitizeUnknownValue(value.cause, {
|
|
109
|
+
depth: depth + 1,
|
|
110
|
+
seen,
|
|
111
|
+
redactPaths,
|
|
112
|
+
}),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map((item) => {
|
|
118
|
+
return sanitizeUnknownValue(item, { depth: depth + 1, seen, redactPaths })
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof value === 'object') {
|
|
123
|
+
if (seen.has(value)) {
|
|
124
|
+
return '[REDACTED_CIRCULAR]'
|
|
125
|
+
}
|
|
126
|
+
seen.add(value)
|
|
127
|
+
|
|
128
|
+
const sanitizedEntries = Object.entries(value).map(([key, entryValue]) => {
|
|
129
|
+
return [
|
|
130
|
+
key,
|
|
131
|
+
sanitizeUnknownValue(entryValue, {
|
|
132
|
+
depth: depth + 1,
|
|
133
|
+
seen,
|
|
134
|
+
redactPaths,
|
|
135
|
+
}),
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
return Object.fromEntries(sanitizedEntries)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return sanitizeSensitiveText(String(value), { redactPaths })
|
|
142
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// E2e tests for abort, model-switch, and retry scenarios.
|
|
2
|
+
// Split from thread-queue-advanced.e2e.test.ts for parallelization.
|
|
3
|
+
|
|
4
|
+
import { describe, test, expect } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
setupQueueAdvancedSuite,
|
|
7
|
+
TEST_USER_ID,
|
|
8
|
+
} from './queue-advanced-e2e-setup.js'
|
|
9
|
+
import {
|
|
10
|
+
getRuntime,
|
|
11
|
+
} from './session-handler/thread-session-runtime.js'
|
|
12
|
+
import { getThreadState } from './session-handler/thread-runtime-state.js'
|
|
13
|
+
import { setSessionModel } from './database.js'
|
|
14
|
+
import {
|
|
15
|
+
waitForFooterMessage,
|
|
16
|
+
waitForBotMessageContaining,
|
|
17
|
+
waitForBotReplyAfterUserMessage,
|
|
18
|
+
} from './test-utils.js'
|
|
19
|
+
|
|
20
|
+
const TEXT_CHANNEL_ID = '200000000000001003'
|
|
21
|
+
|
|
22
|
+
const e2eTest = describe
|
|
23
|
+
|
|
24
|
+
e2eTest('queue advanced: abort and retry', () => {
|
|
25
|
+
const ctx = setupQueueAdvancedSuite({
|
|
26
|
+
channelId: TEXT_CHANNEL_ID,
|
|
27
|
+
channelName: 'qa-abort-e2e',
|
|
28
|
+
dirName: 'qa-abort-e2e',
|
|
29
|
+
username: 'queue-advanced-tester',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test(
|
|
33
|
+
'slow tool call (sleep) gets aborted by explicit abort, then queue continues',
|
|
34
|
+
async () => {
|
|
35
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
36
|
+
content: 'Reply with exactly: oscar',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
40
|
+
timeout: 4_000,
|
|
41
|
+
predicate: (t) => {
|
|
42
|
+
return t.name === 'Reply with exactly: oscar'
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const th = ctx.discord.thread(thread.id)
|
|
47
|
+
const firstReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
48
|
+
expect(firstReply.content.trim().length).toBeGreaterThan(0)
|
|
49
|
+
|
|
50
|
+
// Wait for the first completion footer so it lands in a deterministic position
|
|
51
|
+
await waitForFooterMessage({
|
|
52
|
+
discord: ctx.discord,
|
|
53
|
+
threadId: thread.id,
|
|
54
|
+
timeout: 4_000,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const before = await th.getMessages()
|
|
58
|
+
const beforeBotCount = before.filter((m) => {
|
|
59
|
+
return m.author.id === ctx.discord.botUserId
|
|
60
|
+
}).length
|
|
61
|
+
|
|
62
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
63
|
+
content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// The matcher emits "starting sleep 100" text before the long delay.
|
|
67
|
+
// Wait for it to land in Discord BEFORE aborting so the message is in a
|
|
68
|
+
// deterministic position and the abort produces no further stray messages.
|
|
69
|
+
await waitForBotMessageContaining({
|
|
70
|
+
discord: ctx.discord,
|
|
71
|
+
threadId: thread.id,
|
|
72
|
+
userId: TEST_USER_ID,
|
|
73
|
+
text: 'starting sleep',
|
|
74
|
+
afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
75
|
+
timeout: 4_000,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const runtime = getRuntime(thread.id)
|
|
79
|
+
expect(runtime).toBeDefined()
|
|
80
|
+
if (!runtime) {
|
|
81
|
+
throw new Error('Expected runtime to exist for explicit-abort test')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
runtime.abortActiveRun('test-explicit-abort')
|
|
85
|
+
|
|
86
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
87
|
+
content: 'Reply with exactly: papa',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const after = await waitForBotReplyAfterUserMessage({
|
|
91
|
+
discord: ctx.discord,
|
|
92
|
+
threadId: thread.id,
|
|
93
|
+
userId: TEST_USER_ID,
|
|
94
|
+
userMessageIncludes: 'papa',
|
|
95
|
+
timeout: 8_000,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const afterBotMessages = after.filter((m) => {
|
|
99
|
+
return m.author.id === ctx.discord.botUserId
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await waitForFooterMessage({
|
|
103
|
+
discord: ctx.discord,
|
|
104
|
+
threadId: thread.id,
|
|
105
|
+
timeout: 8_000,
|
|
106
|
+
afterMessageIncludes: 'papa',
|
|
107
|
+
afterAuthorId: TEST_USER_ID,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Assert ordering invariants instead of exact snapshot — the papa reply
|
|
111
|
+
// and footer can interleave non-deterministically.
|
|
112
|
+
const timeline = await th.text()
|
|
113
|
+
expect(timeline).toContain('Reply with exactly: oscar')
|
|
114
|
+
expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER')
|
|
115
|
+
expect(timeline).toContain('⬥ starting sleep 100')
|
|
116
|
+
expect(timeline).toContain('Reply with exactly: papa')
|
|
117
|
+
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
118
|
+
// oscar comes before the sleep marker, sleep before papa
|
|
119
|
+
const oscarIdx = timeline.indexOf('oscar')
|
|
120
|
+
const sleepIdx = timeline.indexOf('PLUGIN_TIMEOUT_SLEEP_MARKER')
|
|
121
|
+
const papaIdx = timeline.indexOf('papa')
|
|
122
|
+
expect(oscarIdx).toBeLessThan(sleepIdx)
|
|
123
|
+
expect(sleepIdx).toBeLessThan(papaIdx)
|
|
124
|
+
expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
|
|
125
|
+
|
|
126
|
+
const sleepToolIndex = after.findIndex((m) => {
|
|
127
|
+
return (
|
|
128
|
+
m.author.id === TEST_USER_ID &&
|
|
129
|
+
m.content.includes('PLUGIN_TIMEOUT_SLEEP_MARKER')
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
expect(sleepToolIndex).toBeGreaterThan(-1)
|
|
133
|
+
|
|
134
|
+
const userPapaIndex = after.findIndex((m) => {
|
|
135
|
+
return m.author.id === TEST_USER_ID && m.content.includes('papa')
|
|
136
|
+
})
|
|
137
|
+
expect(userPapaIndex).toBeGreaterThan(-1)
|
|
138
|
+
expect(sleepToolIndex).toBeLessThan(userPapaIndex)
|
|
139
|
+
const lastBotIndex = after.findLastIndex((m) => {
|
|
140
|
+
return m.author.id === ctx.discord.botUserId
|
|
141
|
+
})
|
|
142
|
+
expect(userPapaIndex).toBeLessThan(lastBotIndex)
|
|
143
|
+
},
|
|
144
|
+
12_000,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
test(
|
|
148
|
+
'explicit abort emits MessageAbortedError and does not emit footer',
|
|
149
|
+
async () => {
|
|
150
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
151
|
+
content: 'Reply with exactly: abort-no-footer-setup',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
155
|
+
timeout: 4_000,
|
|
156
|
+
predicate: (t) => {
|
|
157
|
+
return t.name === 'Reply with exactly: abort-no-footer-setup'
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const th = ctx.discord.thread(thread.id)
|
|
162
|
+
await th.waitForBotReply({ timeout: 4_000 })
|
|
163
|
+
|
|
164
|
+
await waitForBotMessageContaining({
|
|
165
|
+
discord: ctx.discord,
|
|
166
|
+
threadId: thread.id,
|
|
167
|
+
userId: TEST_USER_ID,
|
|
168
|
+
text: '⋅',
|
|
169
|
+
timeout: 4_000,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
173
|
+
content: 'SLOW_ABORT_MARKER run long response',
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const runtime = getRuntime(thread.id)
|
|
177
|
+
expect(runtime).toBeDefined()
|
|
178
|
+
if (!runtime) {
|
|
179
|
+
throw new Error('Expected runtime to exist for abort no-footer test')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const beforeAbortMessages = await th.getMessages()
|
|
183
|
+
const baselineCount = beforeAbortMessages.length
|
|
184
|
+
|
|
185
|
+
runtime.abortActiveRun('test-no-footer-on-abort')
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < 10; i++) {
|
|
188
|
+
await new Promise((resolve) => {
|
|
189
|
+
setTimeout(resolve, 20)
|
|
190
|
+
})
|
|
191
|
+
const msgs = await th.getMessages()
|
|
192
|
+
const newMsgs = msgs.slice(baselineCount)
|
|
193
|
+
const hasFooter = newMsgs.some((m) => {
|
|
194
|
+
return m.author.id === ctx.discord.botUserId
|
|
195
|
+
&& m.content.startsWith('*')
|
|
196
|
+
&& m.content.includes('⋅')
|
|
197
|
+
})
|
|
198
|
+
expect(hasFooter).toBe(false)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
202
|
+
"--- from: user (queue-advanced-tester)
|
|
203
|
+
Reply with exactly: abort-no-footer-setup
|
|
204
|
+
--- from: assistant (TestBot)
|
|
205
|
+
⬥ ok
|
|
206
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
207
|
+
--- from: user (queue-advanced-tester)
|
|
208
|
+
SLOW_ABORT_MARKER run long response"
|
|
209
|
+
`)
|
|
210
|
+
},
|
|
211
|
+
10_000,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
test.skip(
|
|
215
|
+
'explicit abort stale-idle window: follow-up prompt still gets assistant text',
|
|
216
|
+
async () => {
|
|
217
|
+
const setupPrompt = 'Reply with exactly: race-setup-1'
|
|
218
|
+
const raceFinalPrompt = 'Reply with exactly: race-final-1'
|
|
219
|
+
|
|
220
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
221
|
+
content: setupPrompt,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
225
|
+
timeout: 4_000,
|
|
226
|
+
predicate: (t) => {
|
|
227
|
+
return t.name === setupPrompt
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const th = ctx.discord.thread(thread.id)
|
|
232
|
+
const setupReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
233
|
+
expect(setupReply.content.trim().length).toBeGreaterThan(0)
|
|
234
|
+
|
|
235
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
236
|
+
content: 'SLOW_ABORT_MARKER run long response',
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const runtime = getRuntime(thread.id)
|
|
240
|
+
expect(runtime).toBeDefined()
|
|
241
|
+
if (!runtime) {
|
|
242
|
+
throw new Error('Expected runtime to exist for race abort scenario')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
runtime.abortActiveRun('test-race-abort')
|
|
246
|
+
|
|
247
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
248
|
+
content: raceFinalPrompt,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
await waitForBotReplyAfterUserMessage({
|
|
252
|
+
discord: ctx.discord,
|
|
253
|
+
threadId: thread.id,
|
|
254
|
+
userId: TEST_USER_ID,
|
|
255
|
+
userMessageIncludes: raceFinalPrompt,
|
|
256
|
+
timeout: 4_000,
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
8_000,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
test(
|
|
263
|
+
'model switch mid-session aborts and restarts from same session history',
|
|
264
|
+
async () => {
|
|
265
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
266
|
+
content: 'Reply with exactly: retry-setup',
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
270
|
+
timeout: 4_000,
|
|
271
|
+
predicate: (t) => {
|
|
272
|
+
return t.name === 'Reply with exactly: retry-setup'
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const th = ctx.discord.thread(thread.id)
|
|
277
|
+
const firstReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
278
|
+
expect(firstReply.content.trim().length).toBeGreaterThan(0)
|
|
279
|
+
|
|
280
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
281
|
+
content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
await waitForBotMessageContaining({
|
|
285
|
+
discord: ctx.discord,
|
|
286
|
+
threadId: thread.id,
|
|
287
|
+
userId: TEST_USER_ID,
|
|
288
|
+
text: 'starting sleep',
|
|
289
|
+
afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
290
|
+
timeout: 4_000,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const sessionId = getThreadState(thread.id)?.sessionId
|
|
294
|
+
expect(sessionId).toBeDefined()
|
|
295
|
+
if (!sessionId) {
|
|
296
|
+
throw new Error('Expected active session id for model switch test')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await setSessionModel({
|
|
300
|
+
sessionId,
|
|
301
|
+
modelId: 'deterministic-provider/deterministic-v3',
|
|
302
|
+
variant: null,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const runtime = getRuntime(thread.id)
|
|
306
|
+
expect(runtime).toBeDefined()
|
|
307
|
+
if (!runtime) {
|
|
308
|
+
throw new Error('Expected runtime to exist for model switch test')
|
|
309
|
+
}
|
|
310
|
+
const retried = await runtime.retryLastUserPrompt()
|
|
311
|
+
expect(retried).toBe(true)
|
|
312
|
+
|
|
313
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
314
|
+
content: 'Reply with exactly: model-switch-followup',
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
await waitForBotReplyAfterUserMessage({
|
|
318
|
+
discord: ctx.discord,
|
|
319
|
+
threadId: thread.id,
|
|
320
|
+
userId: TEST_USER_ID,
|
|
321
|
+
userMessageIncludes: 'model-switch-followup',
|
|
322
|
+
timeout: 4_000,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Wait for potential footer to arrive (race between step-finish interrupt
|
|
326
|
+
// and model switch settling means footer may or may not appear).
|
|
327
|
+
await new Promise((resolve) => {
|
|
328
|
+
setTimeout(resolve, 200)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const text = await th.text()
|
|
332
|
+
// The follow-up reply ("ok") must be present with deterministic-v3
|
|
333
|
+
expect(text).toContain('Reply with exactly: model-switch-followup')
|
|
334
|
+
expect(text).toContain('⬥ ok')
|
|
335
|
+
// The old sleep text should be visible from the first turn
|
|
336
|
+
expect(text).toContain('starting sleep 100')
|
|
337
|
+
},
|
|
338
|
+
10_000,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
test(
|
|
342
|
+
'abortActiveRun settles correctly during long-running request',
|
|
343
|
+
async () => {
|
|
344
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
345
|
+
content: 'Reply with exactly: force-abort-setup',
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
349
|
+
timeout: 4_000,
|
|
350
|
+
predicate: (t) => {
|
|
351
|
+
return t.name === 'Reply with exactly: force-abort-setup'
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const th = ctx.discord.thread(thread.id)
|
|
356
|
+
const setupReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
357
|
+
expect(setupReply.content.trim().length).toBeGreaterThan(0)
|
|
358
|
+
|
|
359
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
360
|
+
content: 'SLOW_ABORT_MARKER run long response',
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const runtime = getRuntime(thread.id)
|
|
364
|
+
expect(runtime).toBeDefined()
|
|
365
|
+
if (!runtime) {
|
|
366
|
+
throw new Error('Expected runtime to exist for forced-abort test')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
runtime.abortActiveRun('force-abort-test')
|
|
370
|
+
|
|
371
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
372
|
+
"--- from: user (queue-advanced-tester)
|
|
373
|
+
Reply with exactly: force-abort-setup
|
|
374
|
+
--- from: assistant (TestBot)
|
|
375
|
+
⬥ ok
|
|
376
|
+
--- from: user (queue-advanced-tester)
|
|
377
|
+
SLOW_ABORT_MARKER run long response"
|
|
378
|
+
`)
|
|
379
|
+
},
|
|
380
|
+
10_000,
|
|
381
|
+
)
|
|
382
|
+
})
|